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