dpp/identity/identity_public_key/contract_bounds/
mod.rs1use crate::identifier::Identifier;
2use crate::identity::identity_public_key::contract_bounds::ContractBounds::{
3 SingleContract, SingleContractDocumentType,
4};
5#[cfg(feature = "json-conversion")]
6use crate::serialization::JsonConvertible;
7#[cfg(feature = "value-conversion")]
8use crate::serialization::ValueConvertible;
9use crate::ProtocolError;
10use bincode::{Decode, Encode};
11use serde::{Deserialize, Serialize};
12
13pub type ContractBoundsType = u8;
14
15#[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
22#[repr(u8)]
23#[derive(
24 Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, Ord, PartialOrd, Hash,
25)]
26#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
27#[serde(tag = "type", rename_all = "camelCase")]
28pub enum ContractBounds {
29 #[serde(rename = "singleContract")]
31 SingleContract { id: Identifier } = 0,
32 #[serde(rename = "documentType", rename_all = "camelCase")]
34 SingleContractDocumentType {
35 id: Identifier,
36 document_type_name: String,
37 } = 1,
38 }
42
43impl ContractBounds {
44 pub fn new_from_type(
46 contract_bounds_type: u8,
47 identifier: Vec<u8>,
48 document_type: String,
49 ) -> Result<Self, ProtocolError> {
50 Ok(match contract_bounds_type {
51 0 => SingleContract {
52 id: Identifier::from_bytes(identifier.as_slice())?,
53 },
54 1 => SingleContractDocumentType {
55 id: Identifier::from_bytes(identifier.as_slice())?,
56 document_type_name: document_type,
57 },
58 _ => {
59 return Err(ProtocolError::InvalidKeyContractBoundsError(format!(
60 "unrecognized contract bounds type: {}",
61 contract_bounds_type
62 )))
63 }
64 })
65 }
66
67 pub fn contract_bounds_type(&self) -> ContractBoundsType {
69 match self {
70 SingleContract { .. } => 0,
71 SingleContractDocumentType { .. } => 1,
72 }
74 }
75
76 pub fn contract_bounds_type_from_str(str: &str) -> Result<ContractBoundsType, ProtocolError> {
77 match str {
78 "singleContract" => Ok(0),
79 "documentType" => Ok(1),
80 _ => Err(ProtocolError::DecodingError(String::from(
81 "Expected type to be one of none, singleContract or singleContractDocumentType",
82 ))),
83 }
84 }
85 pub fn contract_bounds_type_string(&self) -> &str {
87 match self {
88 SingleContract { .. } => "singleContract",
89 SingleContractDocumentType { .. } => "documentType",
90 }
92 }
93
94 pub fn identifier(&self) -> &Identifier {
96 match self {
97 SingleContract { id } => id,
98 SingleContractDocumentType { id, .. } => id,
99 }
101 }
102
103 pub fn document_type(&self) -> Option<&String> {
105 match self {
106 SingleContract { .. } => None,
107 SingleContractDocumentType {
108 document_type_name: document_type,
109 ..
110 } => Some(document_type),
111 }
113 }
114 }
165
166#[cfg(test)]
167mod core_tests {
168 use super::*;
169
170 #[test]
172 fn test_new_from_type_single_contract() {
173 let id_bytes = vec![0xAAu8; 32];
174 let bounds =
175 ContractBounds::new_from_type(0, id_bytes.clone(), "ignored".to_string()).unwrap();
176 assert!(matches!(bounds, ContractBounds::SingleContract { .. }));
177 assert_eq!(bounds.contract_bounds_type(), 0);
178 assert_eq!(bounds.contract_bounds_type_string(), "singleContract");
179 assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
180 assert!(bounds.document_type().is_none());
182 }
183
184 #[test]
185 fn test_new_from_type_single_contract_document_type() {
186 let id_bytes = vec![0xBBu8; 32];
187 let bounds = ContractBounds::new_from_type(1, id_bytes.clone(), "myDoc".to_string())
188 .expect("expected to construct SingleContractDocumentType");
189 assert!(matches!(
190 bounds,
191 ContractBounds::SingleContractDocumentType { .. }
192 ));
193 assert_eq!(bounds.contract_bounds_type(), 1);
194 assert_eq!(bounds.contract_bounds_type_string(), "documentType");
195 assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
196 assert_eq!(bounds.document_type().map(String::as_str), Some("myDoc"));
197 }
198
199 #[test]
201 fn test_new_from_type_unrecognized_type_returns_error() {
202 let id_bytes = vec![0xCCu8; 32];
203 let err = ContractBounds::new_from_type(99, id_bytes, "".to_string()).unwrap_err();
204 match err {
205 ProtocolError::InvalidKeyContractBoundsError(msg) => {
206 assert!(msg.contains("99"), "expected error message to mention 99");
207 }
208 other => panic!("expected InvalidKeyContractBoundsError, got {:?}", other),
209 }
210 }
211
212 #[test]
214 fn test_new_from_type_invalid_identifier_length_returns_error() {
215 let short = vec![0x01u8; 10];
217 assert!(ContractBounds::new_from_type(0, short, "".to_string()).is_err());
218 }
219
220 #[test]
222 fn test_contract_bounds_type_from_str_single_contract() {
223 assert_eq!(
224 ContractBounds::contract_bounds_type_from_str("singleContract").unwrap(),
225 0
226 );
227 }
228
229 #[test]
230 fn test_contract_bounds_type_from_str_document_type() {
231 assert_eq!(
232 ContractBounds::contract_bounds_type_from_str("documentType").unwrap(),
233 1
234 );
235 }
236
237 #[test]
238 fn test_contract_bounds_type_from_str_unknown_returns_error() {
239 let err = ContractBounds::contract_bounds_type_from_str("garbage").unwrap_err();
240 match err {
241 ProtocolError::DecodingError(_) => {}
242 other => panic!("expected ProtocolError::DecodingError, got {:?}", other),
243 }
244 }
245
246 #[test]
248 fn test_contract_bounds_equality_and_clone() {
249 let id = Identifier::from([0x11u8; 32]);
250 let a = ContractBounds::SingleContract { id };
251 let b = a.clone();
252 assert_eq!(a, b);
253
254 let different = ContractBounds::SingleContractDocumentType {
255 id,
256 document_type_name: "foo".to_string(),
257 };
258 assert_ne!(a, different);
259 }
260
261 #[test]
262 fn test_contract_bounds_type_string_roundtrip_with_from_str() {
263 let id_bytes = vec![0xD0u8; 32];
266 let sc = ContractBounds::new_from_type(0, id_bytes.clone(), "".to_string()).unwrap();
267 let sctd = ContractBounds::new_from_type(1, id_bytes, "docType".to_string()).unwrap();
268
269 assert_eq!(
270 ContractBounds::contract_bounds_type_from_str(sc.contract_bounds_type_string())
271 .unwrap(),
272 sc.contract_bounds_type()
273 );
274 assert_eq!(
275 ContractBounds::contract_bounds_type_from_str(sctd.contract_bounds_type_string())
276 .unwrap(),
277 sctd.contract_bounds_type()
278 );
279 }
280}
281
282#[cfg(all(test, feature = "json-conversion"))]
283mod tests {
284 use super::*;
285 use crate::serialization::JsonConvertible;
286
287 #[test]
288 fn contract_bounds_single_contract_json_round_trip() {
289 let id = Identifier::from([0xABu8; 32]);
290 let bounds = ContractBounds::SingleContract { id };
291
292 let json = bounds.to_json().expect("to_json should succeed");
293 assert!(
294 json["id"].is_string(),
295 "Identifier should be a base58 string, got: {:?}",
296 json["id"]
297 );
298
299 let expected_base58 = id.to_string(platform_value::string_encoding::Encoding::Base58);
300 assert_eq!(json["id"].as_str().unwrap(), expected_base58);
301
302 let restored = ContractBounds::from_json(json).expect("from_json should succeed");
303 assert_eq!(bounds, restored);
304 }
305
306 #[test]
307 fn contract_bounds_document_type_json_round_trip() {
308 let id = Identifier::from([0xCDu8; 32]);
309 let bounds = ContractBounds::SingleContractDocumentType {
310 id,
311 document_type_name: "myDocument".to_string(),
312 };
313
314 let json = bounds.to_json().expect("to_json should succeed");
315 assert!(json["id"].is_string());
316 assert_eq!(json["documentTypeName"].as_str().unwrap(), "myDocument");
317
318 let restored = ContractBounds::from_json(json).expect("from_json should succeed");
319 assert_eq!(bounds, restored);
320 }
321
322 #[test]
323 fn contract_bounds_value_round_trip() {
324 let id = Identifier::from([0x55u8; 32]);
325 let bounds = ContractBounds::SingleContractDocumentType {
326 id,
327 document_type_name: "note".to_string(),
328 };
329
330 let obj = bounds.to_object().expect("to_object should succeed");
331 let restored = ContractBounds::from_object(obj).expect("from_object should succeed");
332 assert_eq!(bounds, restored);
333 }
334}