identity_iota_core/did/
iota_did.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use core::convert::TryFrom;
5use core::fmt::Debug;
6use core::fmt::Display;
7use core::fmt::Formatter;
8use core::str::FromStr;
9
10use identity_core::common::KeyComparable;
11use identity_did::BaseDIDUrl;
12use identity_did::CoreDID;
13use identity_did::Error as DIDError;
14use identity_did::DID;
15use product_common::network_name::NetworkName;
16use ref_cast::ref_cast_custom;
17use ref_cast::RefCastCustom;
18use serde::Deserialize;
19use serde::Serialize;
20
21/// Alias for a `Result` with the error type [`DIDError`].
22type Result<T> = std::result::Result<T, DIDError>;
23
24/// A DID conforming to the IOTA DID method specification.
25///
26/// This is a thin wrapper around the [`DID`][`CoreDID`] type from the
27/// [`identity_did`][`identity_did`] crate.
28#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, RefCastCustom)]
29#[repr(transparent)]
30#[serde(into = "CoreDID", try_from = "CoreDID")]
31pub struct IotaDID(CoreDID);
32
33impl IotaDID {
34  /// The URL scheme for Decentralized Identifiers.
35  pub const SCHEME: &'static str = CoreDID::SCHEME;
36
37  /// The IOTA DID method name (`"iota"`).
38  pub const METHOD: &'static str = "iota";
39
40  /// The default network name (`"iota"`).
41  // TODO: replace this with main net chain ID
42  // as soon as IOTA rebased lands on mainnet.
43  pub const DEFAULT_NETWORK: &'static str = "iota";
44
45  /// The tag of the placeholder DID.
46  pub const PLACEHOLDER_TAG: &'static str = "0x0000000000000000000000000000000000000000000000000000000000000000";
47
48  /// The length of an identity's object id, which is a BLAKE2b-256 hash (32-bytes).
49  pub(crate) const TAG_BYTES_LEN: usize = 32;
50
51  /// Convert a `CoreDID` reference to an `IotaDID` reference without checking the referenced value.
52  ///  
53  /// # Warning
54  /// This method should only be called on [`CoreDIDs`](CoreDID) that
55  /// are known to satisfy the requirements of the IOTA UTXO specification.  
56  ///
57  /// # Memory safety
58  ///
59  /// The `ref-cast` crate ensures a memory safe implementation.  
60  #[ref_cast_custom]
61  pub(crate) const fn from_inner_ref_unchecked(did: &CoreDID) -> &Self;
62
63  // ===========================================================================
64  // Constructors
65  // ===========================================================================
66
67  /// Constructs a new [`IotaDID`] from a byte representation of the tag and the given
68  /// network name.
69  ///
70  /// See also [`IotaDID::placeholder`].
71  ///
72  /// # Example
73  ///
74  /// ```
75  /// # use identity_did::DID;
76  /// # use product_common::network_name::NetworkName;
77  /// # use identity_iota_core::IotaDID;
78  /// #
79  /// let did = IotaDID::new(&[1;32], &NetworkName::try_from("smr").unwrap());
80  /// assert_eq!(did.as_str(), "did:iota:smr:0x0101010101010101010101010101010101010101010101010101010101010101");
81  pub fn new(bytes: &[u8; Self::TAG_BYTES_LEN], network_name: &NetworkName) -> Self {
82    let tag: String = prefix_hex::encode(bytes);
83    let did: String = format!("did:{}:{}:{}", Self::METHOD, network_name, tag);
84
85    Self::parse(did).expect("DIDs constructed with new should be valid")
86  }
87
88  /// Constructs a new [`IotaDID`] from an identity's object id and the given `network_name`.
89  pub fn from_object_id(object_id: &str, network_name: &NetworkName) -> Self {
90    let did: String = format!("did:{}:{}:{}", Self::METHOD, network_name, object_id);
91    Self::parse(did).expect("DIDs constructed with new should be valid")
92  }
93
94  /// Creates a new placeholder [`IotaDID`] with the given network name.
95  ///
96  /// # Example
97  ///
98  /// ```
99  /// # use identity_did::DID;
100  /// # use product_common::network_name::NetworkName;
101  /// # use identity_iota_core::IotaDID;
102  /// #
103  /// let placeholder = IotaDID::placeholder(&NetworkName::try_from("smr").unwrap());
104  /// assert_eq!(placeholder.as_str(), "did:iota:smr:0x0000000000000000000000000000000000000000000000000000000000000000");
105  /// assert!(placeholder.is_placeholder());
106  pub fn placeholder(network_name: &NetworkName) -> Self {
107    Self::new(&[0; 32], network_name)
108  }
109
110  /// Returns whether this is the placeholder DID.
111  ///
112  /// # Example
113  ///
114  /// ```
115  /// # use identity_did::DID;
116  /// # use product_common::network_name::NetworkName;
117  /// # use identity_iota_core::IotaDID;
118  /// #
119  /// let placeholder = IotaDID::placeholder(&NetworkName::try_from("smr").unwrap());
120  /// assert!(placeholder.is_placeholder());
121  pub fn is_placeholder(&self) -> bool {
122    self.tag_str() == Self::PLACEHOLDER_TAG
123  }
124
125  /// Parses an [`IotaDID`] from the given `input`.
126  ///
127  /// # Errors
128  ///
129  /// Returns `Err` if the input does not conform to the [`IotaDID`] specification.
130  pub fn parse(input: impl AsRef<str>) -> Result<Self> {
131    CoreDID::parse(input.as_ref().to_lowercase()).and_then(Self::try_from_core)
132  }
133
134  /// Converts a [`CoreDID`] to a [`IotaDID`].
135  ///
136  /// # Errors
137  ///
138  /// Returns `Err` if the input does not conform to the [`IotaDID`] specification.
139  pub fn try_from_core(did: CoreDID) -> Result<Self> {
140    Self::check_validity(&did)?;
141
142    Ok(Self(Self::normalize(did)))
143  }
144
145  // ===========================================================================
146  // Properties
147  // ===========================================================================
148
149  /// Returns the IOTA `network` name of the `DID`.
150  pub fn network_str(&self) -> &str {
151    Self::denormalized_components(self.method_id()).0
152  }
153
154  /// Returns the tag of the `DID`, which is an identity's object id.
155  pub fn tag_str(&self) -> &str {
156    Self::denormalized_components(self.method_id()).1
157  }
158
159  // ===========================================================================
160  // Validation
161  // ===========================================================================
162
163  /// Checks if the given `DID` is syntactically valid according to the [`IotaDID`] method specification.
164  ///
165  /// # Errors
166  ///
167  /// Returns `Err` if the input is not a syntactically valid [`IotaDID`].
168  pub fn check_validity<D: DID>(did: &D) -> Result<()> {
169    Self::check_method(did)
170      .and_then(|_| Self::check_tag(did))
171      .and_then(|_| Self::check_network(did))
172  }
173
174  /// Returns a `bool` indicating if the given `DID` is valid according to the
175  /// [`IotaDID`] method specification.
176  ///
177  /// Equivalent to `IotaDID::check_validity(did).is_ok()`.
178  pub fn is_valid(did: &CoreDID) -> bool {
179    Self::check_validity(did).is_ok()
180  }
181
182  // ===========================================================================
183  // Helpers
184  // ===========================================================================
185
186  /// Checks if the given `DID` has a valid [`IotaDID`] `method` (i.e. `"iota"`).
187  ///
188  /// # Errors
189  ///
190  /// Returns `Err` if the input represents another method.
191  fn check_method<D: DID>(did: &D) -> Result<()> {
192    (did.method() == Self::METHOD)
193      .then_some(())
194      .ok_or(DIDError::InvalidMethodName)
195  }
196
197  /// Checks if the given `DID` has a valid [`IotaDID`] `method_id`.
198  ///
199  /// # Errors
200  ///
201  /// Returns `Err` if the input does not have a [`IotaDID`] compliant method id.
202  fn check_tag<D: DID>(did: &D) -> Result<()> {
203    let (_, tag) = Self::denormalized_components(did.method_id());
204
205    // Implicitly catches if there are too many segments (:) in the DID too.
206    prefix_hex::decode::<[u8; Self::TAG_BYTES_LEN]>(tag)
207      .map_err(|_| DIDError::InvalidMethodId)
208      .map(|_| ())
209  }
210
211  /// Checks if the given `DID` has a valid [`IotaDID`] network name.
212  ///
213  /// # Errors
214  ///
215  /// Returns `Err` if the input is not a valid network name according to the [`IotaDID`] method specification.
216  fn check_network<D: DID>(did: &D) -> Result<()> {
217    let (network_name, _) = Self::denormalized_components(did.method_id());
218    NetworkName::validate_network_name(network_name).map_err(|_| DIDError::Other("invalid network name"))
219  }
220
221  /// Normalizes the DID `method_id` by removing the default network segment if present.
222  ///
223  /// E.g.
224  /// - `"did:iota:main:123" -> "did:iota:123"` is normalized
225  /// - `"did:iota:dev:123" -> "did:iota:dev:123"` is unchanged
226  // TODO: Remove the lint once this bug in clippy has been fixed. Without to_owned a mutable reference will be aliased.
227  #[allow(clippy::unnecessary_to_owned)]
228  fn normalize(mut did: CoreDID) -> CoreDID {
229    let method_id = did.method_id();
230    let (network, tag) = Self::denormalized_components(method_id);
231    if tag.len() == method_id.len() || network != Self::DEFAULT_NETWORK {
232      did
233    } else {
234      did
235        .set_method_id(tag.to_owned())
236        .expect("normalizing a valid CoreDID should be Ok");
237      did
238    }
239  }
240
241  /// foo:bar -> (foo,bar)
242  /// foo:bar:baz -> (foo, bar:baz)
243  /// foo -> (IotaDID::DEFAULT_NETWORK.as_ref(), foo)
244  #[inline(always)]
245  fn denormalized_components(input: &str) -> (&str, &str) {
246    input
247      .find(':')
248      .map(|idx| input.split_at(idx))
249      .map(|(network, tail)| (network, &tail[1..]))
250      // Self::DEFAULT_NETWORK is built from a static reference so unwrapping is fine
251      .unwrap_or((Self::DEFAULT_NETWORK, input))
252  }
253}
254
255impl FromStr for IotaDID {
256  type Err = DIDError;
257
258  fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
259    Self::parse(s)
260  }
261}
262
263impl TryFrom<&str> for IotaDID {
264  type Error = DIDError;
265
266  fn try_from(other: &str) -> std::result::Result<Self, Self::Error> {
267    Self::parse(other)
268  }
269}
270
271impl TryFrom<String> for IotaDID {
272  type Error = DIDError;
273
274  fn try_from(other: String) -> std::result::Result<Self, Self::Error> {
275    Self::parse(other)
276  }
277}
278
279impl Display for IotaDID {
280  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
281    write!(f, "{}", self.0)
282  }
283}
284
285impl From<IotaDID> for CoreDID {
286  fn from(id: IotaDID) -> Self {
287    id.0
288  }
289}
290
291impl From<IotaDID> for String {
292  fn from(did: IotaDID) -> Self {
293    did.into_string()
294  }
295}
296
297impl TryFrom<CoreDID> for IotaDID {
298  type Error = DIDError;
299
300  fn try_from(value: CoreDID) -> std::result::Result<Self, Self::Error> {
301    Self::try_from_core(value)
302  }
303}
304
305impl TryFrom<BaseDIDUrl> for IotaDID {
306  type Error = DIDError;
307
308  fn try_from(other: BaseDIDUrl) -> Result<Self> {
309    let core_did: CoreDID = CoreDID::try_from(other)?;
310    Self::try_from(core_did)
311  }
312}
313
314impl AsRef<CoreDID> for IotaDID {
315  fn as_ref(&self) -> &CoreDID {
316    &self.0
317  }
318}
319
320impl KeyComparable for IotaDID {
321  type Key = CoreDID;
322
323  #[inline]
324  fn key(&self) -> &Self::Key {
325    self.as_ref()
326  }
327}
328
329#[cfg(test)]
330mod tests {
331  use identity_did::DIDUrl;
332  use once_cell::sync::Lazy;
333  use proptest::strategy::Strategy;
334  use proptest::*;
335
336  use super::*;
337
338  // ===========================================================================================================================
339  // Reusable constants and statics
340  // ===========================================================================================================================
341
342  const VALID_OBJECT_ID_STR: &str = "0xf29dd16310c2100fd1bf568b345fb1cc14d71caa3bd9b5ad735d2bd6d455ca3b";
343
344  const LEN_VALID_OBJECT_ID_STR: usize = VALID_OBJECT_ID_STR.len();
345
346  static VALID_IOTA_DID_STRING: Lazy<String> = Lazy::new(|| format!("did:{}:{}", IotaDID::METHOD, VALID_OBJECT_ID_STR));
347
348  // Rules are: at least one character, at most six characters and may only contain digits and/or lowercase ascii
349  // characters.
350  const VALID_NETWORK_NAMES: [&str; 13] = [
351    IotaDID::DEFAULT_NETWORK,
352    "main",
353    "dev",
354    "smr",
355    "rms",
356    "test",
357    "foo",
358    "foobar",
359    "123456",
360    "0",
361    "foo42",
362    "bar123",
363    "42foo",
364  ];
365
366  static VALID_IOTA_DID_STRINGS: Lazy<Vec<String>> = Lazy::new(|| {
367    let network_tag_to_did = |network, tag| format!("did:{}:{}:{}", IotaDID::METHOD, network, tag);
368
369    let valid_strings: Vec<String> = VALID_NETWORK_NAMES
370      .iter()
371      .flat_map(|network| {
372        [VALID_OBJECT_ID_STR, IotaDID::PLACEHOLDER_TAG]
373          .iter()
374          .map(move |tag| network_tag_to_did(network, tag))
375      })
376      .collect();
377
378    // in principle the previous binding is not necessary (we could have just returned the value),
379    // but let's just ensure that it contains the expected number of elements first.
380    assert_eq!(valid_strings.len(), 2 * VALID_NETWORK_NAMES.len());
381
382    valid_strings
383  });
384
385  // ===========================================================================================================================
386  // Test check_* methods
387  // ===========================================================================================================================
388
389  #[test]
390  fn invalid_check_method() {
391    let did_key_core: CoreDID = CoreDID::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
392    assert!(matches!(
393      IotaDID::check_method(&did_key_core),
394      Err(DIDError::InvalidMethodName)
395    ));
396  }
397
398  #[test]
399  fn valid_check_method() {
400    let did_iota_core: CoreDID = CoreDID::parse(&*VALID_IOTA_DID_STRING).unwrap();
401    assert!(IotaDID::check_method(&did_iota_core).is_ok());
402  }
403
404  #[test]
405  fn valid_check_network() {
406    let assert_check_network = |input: &str| {
407      let did_core: CoreDID =
408        CoreDID::parse(input).unwrap_or_else(|_| panic!("expected {input} to parse to a valid CoreDID"));
409      assert!(
410        IotaDID::check_network(&did_core).is_ok(),
411        "test: valid_check_network failed with input {input}",
412      );
413    };
414
415    for network_name in VALID_NETWORK_NAMES {
416      let did_string = format!("did:method:{network_name}:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
417      assert_check_network(&did_string);
418    }
419
420    assert_check_network("did:method:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
421  }
422
423  #[test]
424  fn invalid_check_network() {
425    // Loop over list of network names known to be invalid, attempt to create a CoreDID containing the given network
426    // name in the method_id sub-string and ensure that `IotaDID::check_network` fails. If the provided network
427    // name is in conflict with the DID Core spec itself then proceed to the next network name.
428
429    // Ensure that this test is robust to changes in the supplied list of network names, i.e. fail if none of the
430    // network names can be contained in a generic CoreDID.
431
432    let mut check_network_executed: bool = false;
433
434    const INVALID_NETWORK_NAMES: [&str; 10] = [
435      "Main",
436      "fOo",
437      "deV",
438      "féta",
439      "",
440      "  ",
441      "foo ",
442      " foo",
443      "123456789",
444      "foobar123",
445    ];
446    for network_name in INVALID_NETWORK_NAMES {
447      let did_string: String = format!("did:method:{network_name}:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
448      let did_core: CoreDID = {
449        match CoreDID::parse(did_string) {
450          Ok(did_core) => did_core,
451          Err(_) => continue,
452        }
453      };
454
455      assert!(matches!(IotaDID::check_network(&did_core), Err(DIDError::Other(_))));
456      check_network_executed = true;
457    }
458    assert!(
459      check_network_executed,
460      "IotaDID::check_network` should have been executed"
461    );
462  }
463
464  #[test]
465  fn valid_check_tag() {
466    for input in VALID_IOTA_DID_STRINGS.iter() {
467      let did_core: CoreDID = CoreDID::parse(input).unwrap();
468      assert!(
469        IotaDID::check_tag(&did_core).is_ok(),
470        "test: valid_check_method_id failed on input {input}"
471      );
472    }
473
474    // Should also work for DID's of the form: did:<method_name>:<valid_iota_network (or
475    // nothing/normalized)>:<object_id>
476    let did_other_string: String = format!("did:method:{VALID_OBJECT_ID_STR}");
477    let did_other_with_network: String = format!("did:method:test:{VALID_OBJECT_ID_STR}");
478    let did_other_core: CoreDID = CoreDID::parse(did_other_string).unwrap();
479    let did_other_with_network_core: CoreDID = CoreDID::parse(did_other_with_network).unwrap();
480
481    assert!(IotaDID::check_tag(&did_other_core).is_ok());
482    assert!(IotaDID::check_tag(&did_other_with_network_core).is_ok());
483  }
484
485  #[test]
486  fn invalid_check_tag() {
487    let invalid_method_id_strings = [
488      // Too many segments
489      format!("did:method:main:test:{VALID_OBJECT_ID_STR}"),
490      // Tag is not prefixed
491      format!("did:method:{}", &VALID_OBJECT_ID_STR.strip_prefix("0x").unwrap()),
492      // Tag is too long
493      format!(
494        "did:method:{}",
495        &VALID_OBJECT_ID_STR.chars().chain("a".chars()).collect::<String>()
496      ),
497      // Tag is too short (omit last character)
498      format!("did:method:main:{}", &VALID_OBJECT_ID_STR[..65]),
499    ];
500
501    for input in invalid_method_id_strings {
502      let did_core: CoreDID = CoreDID::parse(input).unwrap();
503      assert!(
504        matches!(IotaDID::check_tag(&did_core), Err(DIDError::InvalidMethodId)),
505        "{}",
506        did_core
507      );
508    }
509  }
510
511  // ===========================================================================================================================
512  // Test constructors
513  // ===========================================================================================================================
514
515  #[test]
516  fn placeholder_produces_a_did_with_expected_string_representation() {
517    assert_eq!(
518      IotaDID::placeholder(&NetworkName::try_from(IotaDID::DEFAULT_NETWORK).unwrap()).as_str(),
519      format!("did:{}:{}", IotaDID::METHOD, IotaDID::PLACEHOLDER_TAG)
520    );
521
522    for name in VALID_NETWORK_NAMES
523      .iter()
524      .filter(|name| *name != &IotaDID::DEFAULT_NETWORK)
525    {
526      let network_name: NetworkName = NetworkName::try_from(*name).unwrap();
527      let did: IotaDID = IotaDID::placeholder(&network_name);
528      assert_eq!(
529        did.as_str(),
530        format!("did:{}:{}:{}", IotaDID::METHOD, name, IotaDID::PLACEHOLDER_TAG)
531      );
532    }
533  }
534
535  #[test]
536  fn normalization_in_constructors() {
537    let did_with_default_network_string: String = format!(
538      "did:{}:{}:{}",
539      IotaDID::METHOD,
540      IotaDID::DEFAULT_NETWORK,
541      VALID_OBJECT_ID_STR
542    );
543    let expected_normalization_string_representation: String =
544      format!("did:{}:{}", IotaDID::METHOD, VALID_OBJECT_ID_STR);
545
546    assert_eq!(
547      IotaDID::parse(did_with_default_network_string).unwrap().as_str(),
548      expected_normalization_string_representation
549    );
550  }
551
552  #[test]
553  fn parse_valid() {
554    for did_str in VALID_IOTA_DID_STRINGS.iter() {
555      assert!(IotaDID::parse(did_str).is_ok());
556    }
557  }
558
559  #[test]
560  fn parse_invalid() {
561    let execute_assertions = |valid_object_id: &str| {
562      assert!(matches!(
563        IotaDID::parse(format!("dod:{}:{}", IotaDID::METHOD, valid_object_id)),
564        Err(DIDError::InvalidScheme)
565      ));
566
567      assert!(matches!(
568        IotaDID::parse(format!("did:key:{valid_object_id}")),
569        Err(DIDError::InvalidMethodName)
570      ));
571
572      // invalid network name (exceeded eight characters)
573      assert!(matches!(
574        IotaDID::parse(format!("did:{}:123456789:{}", IotaDID::METHOD, valid_object_id)),
575        Err(DIDError::Other(_))
576      ));
577
578      // invalid network name (contains non ascii character é)
579      assert!(matches!(
580        IotaDID::parse(format!("did:{}:féta:{}", IotaDID::METHOD, valid_object_id)),
581        Err(DIDError::InvalidMethodId)
582      ));
583
584      // invalid tag
585      assert!(matches!(
586        IotaDID::parse(format!("did:{}:", IotaDID::METHOD)),
587        Err(DIDError::InvalidMethodId)
588      ));
589
590      // too many segments in method_id
591      assert!(matches!(
592        IotaDID::parse(format!("did:{}:test:foo:{}", IotaDID::METHOD, valid_object_id)),
593        Err(DIDError::InvalidMethodId)
594      ));
595    };
596
597    execute_assertions(IotaDID::PLACEHOLDER_TAG);
598    execute_assertions(VALID_OBJECT_ID_STR);
599  }
600
601  // ===========================================================================================================================
602  // Test constructors with randomly generated input
603  // ===========================================================================================================================
604
605  #[cfg(feature = "iota-client")]
606  proptest! {
607    #[test]
608    fn property_based_new(bytes in proptest::prelude::any::<[u8;32]>()) {
609      for network_name in VALID_NETWORK_NAMES.iter().map(|name| NetworkName::try_from(*name).unwrap()) {
610        // check that this does not panic
611        IotaDID::new(&bytes, &network_name);
612      }
613    }
614  }
615
616  fn arbitrary_object_id_string_replica() -> impl Strategy<Value = String> {
617    proptest::string::string_regex(&format!("0x([a-f]|[0-9]){{{}}}", (LEN_VALID_OBJECT_ID_STR - 2)))
618      .expect("regex should be ok")
619  }
620
621  proptest! {
622    #[test]
623    fn valid_object_id_string_replicas(tag in arbitrary_object_id_string_replica()) {
624      let did : String = format!("did:{}:{}", IotaDID::METHOD, tag);
625      assert!(
626        IotaDID::parse(did).is_ok()
627      );
628    }
629  }
630
631  fn arbitrary_invalid_tag() -> impl Strategy<Value = String> {
632    proptest::string::string_regex("[[:^alpha:]|[a-z]|[1-9]]*")
633      .expect("regex should be ok")
634      .prop_map(|arb_string| {
635        if arb_string
636          .chars()
637          .all(|c| c.is_ascii_hexdigit() && c.is_ascii_lowercase())
638          && arb_string.len() == LEN_VALID_OBJECT_ID_STR
639          && arb_string.starts_with("0x")
640        {
641          // this means we are in the rare case of generating a valid string hence we replace the last 0 with the non
642          // ascii character é
643          let mut counter = 0;
644          arb_string
645            .chars()
646            .rev()
647            .map(|value| {
648              if value == '0' && counter == 0 {
649                counter += 1;
650                'é'
651              } else {
652                value
653              }
654            })
655            .collect::<String>()
656        } else {
657          arb_string
658        }
659      })
660  }
661
662  proptest! {
663    #[test]
664    fn invalid_tag_property_based_parse(tag in arbitrary_invalid_tag()) {
665      let did: String = format!("did:{}:{}", IotaDID::METHOD, tag);
666      assert!(
667        IotaDID::parse(did).is_err()
668      );
669    }
670  }
671
672  fn arbitrary_delimiter_mixed_in_prefix_hex() -> impl Strategy<Value = String> {
673    proptest::string::string_regex("0x([a-f]|[:]|[0-9])*").expect("regex should be ok")
674  }
675
676  proptest! {
677    #[test]
678    fn invalid_hex_mixed_with_delimiter(tag in arbitrary_delimiter_mixed_in_prefix_hex()) {
679      let did: String = format!("did:{}:{}", IotaDID::METHOD, tag);
680      assert!(IotaDID::parse(did).is_err());
681    }
682  }
683
684  // ===========================================================================================================================
685  // Test getters
686  // ===========================================================================================================================
687  #[test]
688  fn test_network() {
689    let execute_assertions = |valid_object_id: &str| {
690      let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_object_id).parse().unwrap();
691      assert_eq!(did.network_str(), IotaDID::DEFAULT_NETWORK);
692
693      let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)
694        .parse()
695        .unwrap();
696      assert_eq!(did.network_str(), "dev");
697
698      let did: IotaDID = format!("did:{}:test:{}", IotaDID::METHOD, valid_object_id)
699        .parse()
700        .unwrap();
701      assert_eq!(did.network_str(), "test");
702
703      let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)
704        .parse()
705        .unwrap();
706      assert_eq!(did.network_str(), "custom");
707    };
708
709    execute_assertions(IotaDID::PLACEHOLDER_TAG);
710    execute_assertions(VALID_OBJECT_ID_STR);
711  }
712
713  #[test]
714  fn test_tag() {
715    let execute_assertions = |valid_object_id: &str| {
716      let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_object_id).parse().unwrap();
717      assert_eq!(did.tag_str(), valid_object_id);
718
719      let did: IotaDID = format!(
720        "did:{}:{}:{}",
721        IotaDID::METHOD,
722        IotaDID::DEFAULT_NETWORK,
723        valid_object_id
724      )
725      .parse()
726      .unwrap();
727      assert_eq!(did.tag_str(), valid_object_id);
728
729      let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)
730        .parse()
731        .unwrap();
732      assert_eq!(did.tag_str(), valid_object_id);
733
734      let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)
735        .parse()
736        .unwrap();
737      assert_eq!(did.tag_str(), valid_object_id);
738    };
739    execute_assertions(IotaDID::PLACEHOLDER_TAG);
740    execute_assertions(VALID_OBJECT_ID_STR);
741  }
742
743  // ===========================================================================================================================
744  // Test DIDUrl
745  // ===========================================================================================================================
746
747  #[test]
748  fn test_parse_did_url_valid() {
749    let execute_assertions = |valid_object_id: &str| {
750      assert!(DIDUrl::parse(format!("did:{}:{}", IotaDID::METHOD, valid_object_id)).is_ok());
751      assert!(DIDUrl::parse(format!("did:{}:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
752      assert!(DIDUrl::parse(format!(
753        "did:{}:{}?somequery=somevalue",
754        IotaDID::METHOD,
755        valid_object_id
756      ))
757      .is_ok());
758      assert!(DIDUrl::parse(format!(
759        "did:{}:{}?somequery=somevalue#fragment",
760        IotaDID::METHOD,
761        valid_object_id
762      ))
763      .is_ok());
764
765      assert!(DIDUrl::parse(format!("did:{}:main:{}", IotaDID::METHOD, valid_object_id)).is_ok());
766      assert!(DIDUrl::parse(format!("did:{}:main:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
767      assert!(DIDUrl::parse(format!(
768        "did:{}:main:{}?somequery=somevalue",
769        IotaDID::METHOD,
770        valid_object_id
771      ))
772      .is_ok());
773      assert!(DIDUrl::parse(format!(
774        "did:{}:main:{}?somequery=somevalue#fragment",
775        IotaDID::METHOD,
776        valid_object_id
777      ))
778      .is_ok());
779
780      assert!(DIDUrl::parse(format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)).is_ok());
781      assert!(DIDUrl::parse(format!("did:{}:dev:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
782      assert!(DIDUrl::parse(format!(
783        "did:{}:dev:{}?somequery=somevalue",
784        IotaDID::METHOD,
785        valid_object_id
786      ))
787      .is_ok());
788      assert!(DIDUrl::parse(format!(
789        "did:{}:dev:{}?somequery=somevalue#fragment",
790        IotaDID::METHOD,
791        valid_object_id
792      ))
793      .is_ok());
794
795      assert!(DIDUrl::parse(format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)).is_ok());
796      assert!(DIDUrl::parse(format!("did:{}:custom:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok());
797      assert!(DIDUrl::parse(format!(
798        "did:{}:custom:{}?somequery=somevalue",
799        IotaDID::METHOD,
800        valid_object_id
801      ))
802      .is_ok());
803      assert!(DIDUrl::parse(format!(
804        "did:{}:custom:{}?somequery=somevalue#fragment",
805        IotaDID::METHOD,
806        valid_object_id
807      ))
808      .is_ok());
809    };
810    execute_assertions(IotaDID::PLACEHOLDER_TAG);
811    execute_assertions(VALID_OBJECT_ID_STR);
812  }
813
814  #[test]
815  fn valid_url_setters() {
816    let execute_assertions = |valid_object_id: &str| {
817      let mut did_url: DIDUrl = IotaDID::parse(format!("did:{}:{}", IotaDID::METHOD, valid_object_id))
818        .unwrap()
819        .into_url();
820
821      did_url.set_path(Some("/foo")).unwrap();
822      did_url.set_query(Some("diff=true")).unwrap();
823      did_url.set_fragment(Some("foo")).unwrap();
824
825      assert_eq!(did_url.path(), Some("/foo"));
826      assert_eq!(did_url.query(), Some("diff=true"));
827      assert_eq!(did_url.fragment(), Some("foo"));
828    };
829    execute_assertions(IotaDID::PLACEHOLDER_TAG);
830    execute_assertions(VALID_OBJECT_ID_STR);
831  }
832}