did_toolkit/
url.rs

1use crate::{
2    did::DID,
3    string::{method_id_encoded, url_decoded, url_encoded, validate_method_name},
4    time::VersionTime,
5};
6use anyhow::anyhow;
7use serde::{de::Visitor, Deserialize, Serialize};
8use std::{collections::BTreeMap, fmt::Display};
9
10/// DID URL handling, including parsing, (de)-serialization, and manipulation according to
11/// <https://www.w3.org/TR/did-core/#did-url-syntax>.
12///
13/// DID URLs are nothing like hypertext URLs and it is strongly cautioned that you do not treat
14/// them as such.
15///
16/// The struct includes a [DID] as well as optional [URLParameters] to extend the [DID]. Converting
17/// to string, formatting for display, or serialization will cause the URL to be generated.
18///
19/// ```
20/// use did_toolkit::prelude::*;
21///
22/// let url = URL::parse("did:mymethod:alice/path?service=foo#fragment").unwrap();
23/// assert_eq!(url, URL {
24///     did: DID::parse("did:mymethod:alice").unwrap(),
25///     parameters: Some(URLParameters{
26///         path: Some("path".as_bytes().to_vec()),
27///         fragment: Some("fragment".as_bytes().to_vec()),
28///         service: Some("foo".to_string()),
29///         ..Default::default()
30///     })
31/// });
32/// let url = URL {
33///     did: DID::parse("did:mymethod:bob").unwrap(),
34///     parameters: Some(URLParameters{
35///         path: Some("path".as_bytes().to_vec()),
36///         fragment: Some("fragment".as_bytes().to_vec()),
37///         service: Some("bar".to_string()),
38///         version_id: Some("1.0".to_string()),
39///         ..Default::default()
40///     })
41/// };
42///
43/// assert_eq!(url.to_string(), "did:mymethod:bob/path?service=bar&versionId=1.0#fragment");
44/// ```
45#[derive(Clone, Default, Debug, Hash, PartialOrd, Ord, Eq, PartialEq)]
46pub struct URL {
47    pub did: DID,
48    pub parameters: Option<URLParameters>,
49}
50
51/// A struct to encapsulate URL parameters. All members of this struct are optional, liberal use of
52/// `..Default::default()` is recommended to couch the extra fields.
53///
54/// Many parts of this struct are concatenated into the query string, which has unique escaping
55/// rules for each special parameter (see <https://www.w3.org/TR/did-core/#did-parameters>). These
56/// are handled according to spec and may take [String] or [`Vec<u8>`] depending on needs. Query members
57/// that do not match a special field are stuffed in the `extra_query` bucket.
58#[derive(Clone, Default, Debug, Hash, PartialOrd, Ord, Eq, PartialEq)]
59pub struct URLParameters {
60    pub path: Option<Vec<u8>>,
61    pub fragment: Option<Vec<u8>>,
62    pub service: Option<String>,
63    pub relative_ref: Option<Vec<u8>>,
64    pub version_id: Option<String>,
65    pub version_time: Option<VersionTime>,
66    pub hash_link: Option<String>,
67    pub extra_query: Option<BTreeMap<Vec<u8>, Vec<u8>>>,
68}
69
70impl Serialize for URL {
71    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72    where
73        S: serde::Serializer,
74    {
75        serializer.serialize_str(&self.to_string())
76    }
77}
78
79struct URLVisitor;
80
81impl Visitor<'_> for URLVisitor {
82    type Value = URL;
83
84    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
85        formatter.write_str("Expecting a decentralized identity URL")
86    }
87
88    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
89    where
90        E: serde::de::Error,
91    {
92        match URL::parse(&v) {
93            Ok(url) => Ok(url),
94            Err(e) => Err(E::custom(e)),
95        }
96    }
97
98    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
99    where
100        E: serde::de::Error,
101    {
102        match URL::parse(v) {
103            Ok(url) => Ok(url),
104            Err(e) => Err(E::custom(e)),
105        }
106    }
107}
108
109impl<'de> Deserialize<'de> for URL {
110    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111    where
112        D: serde::Deserializer<'de>,
113    {
114        deserializer.deserialize_any(URLVisitor)
115    }
116}
117
118impl Display for URL {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        let mut ret = String::from("did:");
121
122        ret += &url_encoded(&self.did.name);
123        ret += &(":".to_string() + &method_id_encoded(&self.did.id));
124
125        if let Some(params) = &self.parameters {
126            if let Some(path) = &params.path {
127                ret += &("/".to_string() + &url_encoded(path));
128            }
129
130            if params.service.is_some()
131                || params.relative_ref.is_some()
132                || params.version_id.is_some()
133                || params.version_time.is_some()
134                || params.hash_link.is_some()
135                || params.extra_query.is_some()
136            {
137                ret += "?";
138
139                if let Some(service) = &params.service {
140                    ret += &("service=".to_string() + service);
141                    ret += "&";
142                }
143
144                if let Some(relative_ref) = &params.relative_ref {
145                    ret += &("relativeRef=".to_string() + &url_encoded(relative_ref));
146                    ret += "&";
147                }
148
149                if let Some(version_id) = &params.version_id {
150                    ret += &("versionId=".to_string() + version_id);
151                    ret += "&";
152                }
153
154                if let Some(version_time) = &params.version_time {
155                    ret += &("versionTime=".to_string() + &version_time.to_string());
156                    ret += "&";
157                }
158
159                if let Some(hash_link) = &params.hash_link {
160                    ret += &("hl=".to_string() + hash_link);
161                    ret += "&";
162                }
163
164                if let Some(extra_query) = &params.extra_query {
165                    for (key, value) in extra_query.iter() {
166                        ret += &format!("{}={}&", url_encoded(key), url_encoded(value));
167                    }
168                }
169
170                ret = match ret.strip_suffix('&') {
171                    Some(ret) => ret.to_string(),
172                    None => ret,
173                };
174            }
175
176            if let Some(fragment) = &params.fragment {
177                ret += &("#".to_string() + &url_encoded(fragment));
178            }
179        }
180
181        f.write_str(&ret)
182    }
183}
184
185#[inline]
186fn before(s: &str, left: char, right: char) -> bool {
187    for c in s.chars() {
188        if c == left {
189            return true;
190        } else if c == right {
191            return false;
192        }
193    }
194
195    false
196}
197
198impl URL {
199    /// Parse a DID URL from string. See [URL] for more information.
200    pub fn parse(s: &str) -> Result<Self, anyhow::Error> {
201        match s.strip_prefix("did:") {
202            Some(s) => match s.split_once(':') {
203                Some((method_name, right)) => {
204                    if !before(right, '?', '/') && !before(right, '#', '/') {
205                        match right.split_once('/') {
206                            Some((method_id, path)) => Self::match_path(
207                                method_name.as_bytes(),
208                                method_id.as_bytes(),
209                                path.as_bytes(),
210                            ),
211                            None => Self::split_query(method_name.as_bytes(), right),
212                        }
213                    } else if before(right, '?', '#') {
214                        Self::split_query(method_name.as_bytes(), right)
215                    } else {
216                        Self::split_fragment(method_name.as_bytes(), right)
217                    }
218                }
219                None => return Err(anyhow!("DID did not contain method specific ID")),
220            },
221            None => return Err(anyhow!("DID did not start with `did:` scheme")),
222        }
223    }
224
225    /// Parse and join a DID URL. If you want to join a URL from [URLParameters], see [DID::join].
226    pub fn join(&self, s: &str) -> Result<Self, anyhow::Error> {
227        if s.is_empty() {
228            return Err(anyhow!("relative DID URL is empty"));
229        }
230
231        match s.chars().next().unwrap() {
232            '/' => Self::match_path(&self.did.name, &self.did.id, &s.as_bytes()[1..]),
233            '?' => Self::match_query(&self.did.name, &self.did.id, None, &s.as_bytes()[1..]),
234            '#' => {
235                Self::match_fragment(&self.did.name, &self.did.id, None, None, &s.as_bytes()[1..])
236            }
237            _ => Err(anyhow!("DID URL is not relative or is malformed")),
238        }
239    }
240
241    /// Converts to the underlying [DID].
242    pub fn to_did(&self) -> DID {
243        DID {
244            name: self.did.name.clone(),
245            id: self.did.id.clone(),
246        }
247    }
248
249    #[inline]
250    fn split_query(method_name: &[u8], right: &str) -> Result<Self, anyhow::Error> {
251        match right.split_once('?') {
252            Some((method_id, query)) => {
253                Self::match_query(method_name, method_id.as_bytes(), None, query.as_bytes())
254            }
255            None => Self::split_fragment(method_name, right),
256        }
257    }
258
259    #[inline]
260    fn split_fragment(method_name: &[u8], right: &str) -> Result<Self, anyhow::Error> {
261        match right.split_once('#') {
262            Some((method_id, fragment)) => Self::match_fragment(
263                method_name,
264                method_id.as_bytes(),
265                None,
266                None,
267                fragment.as_bytes(),
268            ),
269            None => {
270                validate_method_name(method_name)?;
271
272                Ok(URL {
273                    did: DID {
274                        name: url_decoded(method_name),
275                        id: url_decoded(right.as_bytes()),
276                    },
277                    ..Default::default()
278                })
279            }
280        }
281    }
282
283    #[inline]
284    fn match_path(
285        method_name: &[u8],
286        method_id: &[u8],
287        left: &[u8],
288    ) -> Result<Self, anyhow::Error> {
289        let item = String::from_utf8_lossy(left);
290
291        if !before(&item, '#', '?') {
292            match item.split_once('?') {
293                Some((path, query)) => Self::match_query(
294                    method_name,
295                    method_id,
296                    Some(path.as_bytes()),
297                    query.as_bytes(),
298                ),
299                None => match item.split_once('#') {
300                    Some((path, fragment)) => Self::match_fragment(
301                        method_name,
302                        method_id,
303                        Some(path.as_bytes()),
304                        None,
305                        fragment.as_bytes(),
306                    ),
307                    None => {
308                        validate_method_name(method_name)?;
309
310                        Ok(URL {
311                            did: DID {
312                                name: url_decoded(method_name),
313                                id: url_decoded(method_id),
314                            },
315                            parameters: Some(URLParameters {
316                                path: Some(url_decoded(left)),
317                                ..Default::default()
318                            }),
319                        })
320                    }
321                },
322            }
323        } else {
324            match item.split_once('#') {
325                Some((path, fragment)) => Self::match_fragment(
326                    method_name,
327                    method_id,
328                    Some(path.as_bytes()),
329                    None,
330                    fragment.as_bytes(),
331                ),
332                None => {
333                    validate_method_name(method_name)?;
334
335                    Ok(URL {
336                        did: DID {
337                            name: url_decoded(method_name),
338                            id: url_decoded(method_id),
339                        },
340                        parameters: Some(URLParameters {
341                            path: Some(url_decoded(left)),
342                            ..Default::default()
343                        }),
344                    })
345                }
346            }
347        }
348    }
349
350    #[inline]
351    fn match_fragment(
352        method_name: &[u8],
353        method_id: &[u8],
354        path: Option<&[u8]>,
355        query: Option<&[u8]>,
356        fragment: &[u8],
357    ) -> Result<Self, anyhow::Error> {
358        validate_method_name(method_name)?;
359
360        let mut url = URL {
361            did: DID {
362                name: url_decoded(method_name),
363                id: url_decoded(method_id),
364            },
365            parameters: Some(URLParameters {
366                fragment: Some(url_decoded(fragment)),
367                path: path.map(url_decoded),
368                ..Default::default()
369            }),
370        };
371
372        if query.is_some() {
373            url.parse_query(query.unwrap())?;
374        }
375
376        Ok(url)
377    }
378
379    #[inline]
380    fn match_query(
381        method_name: &[u8],
382        method_id: &[u8],
383        path: Option<&[u8]>,
384        query: &[u8],
385    ) -> Result<Self, anyhow::Error> {
386        let item = String::from_utf8_lossy(query);
387
388        match item.split_once('#') {
389            Some((query, fragment)) => Self::match_fragment(
390                method_name,
391                method_id,
392                path,
393                Some(query.as_bytes()),
394                fragment.as_bytes(),
395            ),
396            None => {
397                validate_method_name(method_name)?;
398
399                let mut url = URL {
400                    did: DID {
401                        name: url_decoded(method_name),
402                        id: url_decoded(method_id),
403                    },
404                    parameters: Some(URLParameters {
405                        path: path.map(url_decoded),
406                        ..Default::default()
407                    }),
408                };
409
410                url.parse_query(query)?;
411                Ok(url)
412            }
413        }
414    }
415
416    #[inline]
417    fn match_fixed_query_params(
418        &mut self,
419        left: &[u8],
420        right: &[u8],
421        extra_query: &mut BTreeMap<Vec<u8>, Vec<u8>>,
422    ) -> Result<(), anyhow::Error> {
423        if self.parameters.is_none() {
424            self.parameters = Some(Default::default());
425        }
426
427        let mut params = self.parameters.clone().unwrap();
428        let item = String::from_utf8(left.to_vec())?;
429
430        match item.as_str() {
431            "service" => params.service = Some(String::from_utf8(right.to_vec())?),
432            "relativeRef" => {
433                params.relative_ref = Some(url_decoded(right));
434            }
435            "versionId" => params.version_id = Some(String::from_utf8(right.to_vec())?),
436            "versionTime" => {
437                params.version_time = Some(VersionTime::parse(&String::from_utf8(right.to_vec())?)?)
438            }
439            "hl" => params.hash_link = Some(String::from_utf8(right.to_vec())?),
440            _ => {
441                extra_query.insert(url_decoded(left), url_decoded(right));
442            }
443        }
444
445        self.parameters = Some(params);
446
447        Ok(())
448    }
449
450    #[inline]
451    fn parse_query(&mut self, query: &[u8]) -> Result<(), anyhow::Error> {
452        let mut extra_query = BTreeMap::new();
453
454        let item = String::from_utf8(query.to_vec())?;
455
456        if !item.contains('&') {
457            match item.split_once('=') {
458                Some((left, right)) => {
459                    self.match_fixed_query_params(
460                        left.as_bytes(),
461                        right.as_bytes(),
462                        &mut extra_query,
463                    )?;
464                }
465                None => {
466                    extra_query.insert(url_decoded(query), Default::default());
467                }
468            }
469        } else {
470            for part in item.split('&') {
471                match part.split_once('=') {
472                    Some((left, right)) => {
473                        self.match_fixed_query_params(
474                            left.as_bytes(),
475                            right.as_bytes(),
476                            &mut extra_query,
477                        )?;
478                    }
479                    None => {
480                        extra_query.insert(url_decoded(part.as_bytes()), Default::default());
481                    }
482                }
483            }
484        }
485
486        if !extra_query.is_empty() {
487            if self.parameters.is_none() {
488                self.parameters = Some(Default::default());
489            }
490
491            let mut params = self.parameters.clone().unwrap();
492            params.extra_query = Some(extra_query.clone());
493            self.parameters = Some(params);
494        }
495
496        Ok(())
497    }
498}
499
500mod tests {
501    #[test]
502    fn test_join() {
503        use super::URL;
504        use crate::did::DID;
505
506        let url = URL {
507            did: DID {
508                name: "abcdef".into(),
509                id: "123456".into(),
510            },
511            ..Default::default()
512        };
513
514        assert!(url.join("").is_err());
515
516        assert_eq!(
517            url.join("#fragment").unwrap().to_string(),
518            "did:abcdef:123456#fragment"
519        );
520
521        assert_eq!(
522            url.join("?service=frobnik").unwrap().to_string(),
523            "did:abcdef:123456?service=frobnik"
524        );
525
526        assert_eq!(
527            url.join("?service=frobnik#fragment").unwrap().to_string(),
528            "did:abcdef:123456?service=frobnik#fragment"
529        );
530
531        assert_eq!(
532            url.join("/path?service=frobnik#fragment")
533                .unwrap()
534                .to_string(),
535            "did:abcdef:123456/path?service=frobnik#fragment"
536        );
537    }
538
539    #[test]
540    fn test_to_string() {
541        use super::{URLParameters, URL};
542        use crate::did::DID;
543        use crate::time::VersionTime;
544        use std::collections::BTreeMap;
545        use time::OffsetDateTime;
546
547        let url = URL {
548            did: DID {
549                name: "abcdef".into(),
550                id: "123456".into(),
551            },
552            ..Default::default()
553        };
554
555        assert_eq!(url.to_string(), "did:abcdef:123456");
556
557        let url = URL {
558            did: DID {
559                name: "abcdef".into(),
560                id: "123456".into(),
561            },
562            parameters: Some(URLParameters {
563                path: Some("path".into()),
564                ..Default::default()
565            }),
566        };
567
568        assert_eq!(url.to_string(), "did:abcdef:123456/path");
569
570        let url = URL {
571            did: DID {
572                name: "abcdef".into(),
573                id: "123456".into(),
574            },
575            parameters: Some(URLParameters {
576                fragment: Some("fragment".into()),
577                ..Default::default()
578            }),
579        };
580
581        assert_eq!(url.to_string(), "did:abcdef:123456#fragment");
582
583        let url = URL {
584            did: DID {
585                name: "abcdef".into(),
586                id: "123456".into(),
587            },
588            parameters: Some(URLParameters {
589                path: Some("path".into()),
590                fragment: Some("fragment".into()),
591                ..Default::default()
592            }),
593        };
594
595        assert_eq!(url.to_string(), "did:abcdef:123456/path#fragment");
596
597        let url = URL {
598            did: DID {
599                name: "abcdef".into(),
600                id: "123456".into(),
601            },
602            parameters: Some(URLParameters {
603                service: Some("frobnik".into()),
604                ..Default::default()
605            }),
606        };
607
608        assert_eq!(url.to_string(), "did:abcdef:123456?service=frobnik");
609
610        let url = URL {
611            did: DID {
612                name: "abcdef".into(),
613                id: "123456".into(),
614            },
615            parameters: Some(URLParameters {
616                service: Some("frobnik".into()),
617                relative_ref: Some("/ref".into()),
618                ..Default::default()
619            }),
620        };
621
622        assert_eq!(
623            url.to_string(),
624            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref"
625        );
626
627        let url = URL {
628            did: DID {
629                name: "abcdef".into(),
630                id: "123456".into(),
631            },
632            parameters: Some(URLParameters {
633                service: Some("frobnik".into()),
634                relative_ref: Some("/ref".into()),
635                version_id: Some("1".into()),
636                ..Default::default()
637            }),
638        };
639
640        assert_eq!(
641            url.to_string(),
642            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1"
643        );
644
645        let url = URL {
646            did: DID {
647                name: "abcdef".into(),
648                id: "123456".into(),
649            },
650            parameters: Some(URLParameters {
651                service: Some("frobnik".into()),
652                relative_ref: Some("/ref".into()),
653                version_id: Some("1".into()),
654                hash_link: Some("myhash".into()),
655                ..Default::default()
656            }),
657        };
658
659        assert_eq!(
660            url.to_string(),
661            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash",
662        );
663
664        let mut map = BTreeMap::new();
665        map.insert("extra".into(), "parameter".into());
666
667        let url = URL {
668            did: DID {
669                name: "abcdef".into(),
670                id: "123456".into(),
671            },
672            parameters: Some(URLParameters {
673                service: Some("frobnik".into()),
674                relative_ref: Some("/ref".into()),
675                version_id: Some("1".into()),
676                hash_link: Some("myhash".into()),
677                extra_query: Some(map),
678                ..Default::default()
679            }),
680        };
681
682        assert_eq!(
683            url.to_string(),
684            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash&extra=parameter",
685        );
686
687        let mut map = BTreeMap::new();
688        map.insert("extra".into(), "".into());
689
690        let url = URL {
691            did: DID {
692                name: "abcdef".into(),
693                id: "123456".into(),
694            },
695            parameters: Some(URLParameters {
696                service: Some("frobnik".into()),
697                relative_ref: Some("/ref".into()),
698                version_id: Some("1".into()),
699                hash_link: Some("myhash".into()),
700                extra_query: Some(map),
701                ..Default::default()
702            }),
703        };
704
705        assert_eq!(
706            url.to_string(),
707            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash&extra=",
708        );
709
710        let mut map = BTreeMap::new();
711        map.insert("extra".into(), "parameter".into());
712
713        let url = URL {
714            did: DID {
715                name: "abcdef".into(),
716                id: "123456".into(),
717            },
718            parameters: Some(URLParameters {
719                extra_query: Some(map),
720                ..Default::default()
721            }),
722        };
723
724        assert_eq!(url.to_string(), "did:abcdef:123456?extra=parameter",);
725
726        let mut map = BTreeMap::new();
727        map.insert("extra".into(), "".into());
728
729        let url = URL {
730            did: DID {
731                name: "abcdef".into(),
732                id: "123456".into(),
733            },
734            parameters: Some(URLParameters {
735                extra_query: Some(map),
736                ..Default::default()
737            }),
738        };
739
740        assert_eq!(url.to_string(), "did:abcdef:123456?extra=",);
741
742        let url = URL {
743            did: DID {
744                name: "abcdef".into(),
745                id: "123456".into(),
746            },
747            parameters: Some(URLParameters {
748                path: Some("path".into()),
749                fragment: Some("fragment".into()),
750                service: Some("frobnik".into()),
751                relative_ref: Some("/ref".into()),
752                version_id: Some("1".into()),
753                hash_link: Some("myhash".into()),
754                ..Default::default()
755            }),
756        };
757
758        assert_eq!(
759            url.to_string(),
760            "did:abcdef:123456/path?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash#fragment",
761        );
762
763        let url = URL {
764            did: DID {
765                name: "abcdef".into(),
766                id: "123456".into(),
767            },
768            parameters: Some(URLParameters {
769                path: Some("path".into()),
770                fragment: Some("fragment".into()),
771                service: Some("frobnik".into()),
772                relative_ref: Some("/ref".into()),
773                version_id: Some("1".into()),
774                version_time: Some(VersionTime(
775                    OffsetDateTime::from_unix_timestamp(260690400).unwrap(),
776                )),
777                ..Default::default()
778            }),
779        };
780
781        assert_eq!(
782            url.to_string(),
783            "did:abcdef:123456/path?service=frobnik&relativeRef=%2Fref&versionId=1&versionTime=1978-04-06T06:00:00Z#fragment",
784        );
785
786        let url = URL {
787            did: DID {
788                name: "abcdef".into(),
789                id: "123456:mumble:foo".into(),
790            },
791            ..Default::default()
792        };
793
794        assert_eq!(url.to_string(), "did:abcdef:123456:mumble:foo");
795    }
796
797    #[test]
798    fn test_parse() {
799        use super::{URLParameters, URL};
800        use crate::did::DID;
801        use crate::time::VersionTime;
802        use std::collections::BTreeMap;
803        use time::OffsetDateTime;
804
805        assert!(URL::parse("").is_err());
806        assert!(URL::parse("frobnik").is_err());
807        assert!(URL::parse("did").is_err());
808        assert!(URL::parse("frobnik:").is_err());
809        assert!(URL::parse("did:").is_err());
810        assert!(URL::parse("did:abcdef").is_err());
811
812        let url = URL::parse("did:abcdef:123456").unwrap();
813        assert_eq!(
814            url,
815            URL {
816                did: DID {
817                    name: "abcdef".into(),
818                    id: "123456".into(),
819                },
820                ..Default::default()
821            }
822        );
823
824        let url = URL::parse("did:abcdef:123456/path").unwrap();
825        assert_eq!(
826            url,
827            URL {
828                did: DID {
829                    name: "abcdef".into(),
830                    id: "123456".into(),
831                },
832                parameters: Some(URLParameters {
833                    path: Some("path".into()),
834                    ..Default::default()
835                }),
836            }
837        );
838
839        let url = URL::parse("did:abcdef:123456#fragment").unwrap();
840        assert_eq!(
841            url,
842            URL {
843                did: DID {
844                    name: "abcdef".into(),
845                    id: "123456".into(),
846                },
847                parameters: Some(URLParameters {
848                    fragment: Some("fragment".into()),
849                    ..Default::default()
850                }),
851            }
852        );
853
854        let url = URL::parse("did:abcdef:123456/path#fragment").unwrap();
855        assert_eq!(
856            url,
857            URL {
858                did: DID {
859                    name: "abcdef".into(),
860                    id: "123456".into(),
861                },
862                parameters: Some(URLParameters {
863                    path: Some("path".into()),
864                    fragment: Some("fragment".into()),
865                    ..Default::default()
866                }),
867            }
868        );
869
870        let url = URL::parse("did:abcdef:123456?service=frobnik").unwrap();
871        assert_eq!(
872            url,
873            URL {
874                did: DID {
875                    name: "abcdef".into(),
876                    id: "123456".into(),
877                },
878                parameters: Some(URLParameters {
879                    service: Some("frobnik".into()),
880                    ..Default::default()
881                }),
882            }
883        );
884
885        let url = URL::parse("did:abcdef:123456?service=frobnik&relativeRef=%2Fref").unwrap();
886        assert_eq!(
887            url,
888            URL {
889                did: DID {
890                    name: "abcdef".into(),
891                    id: "123456".into(),
892                },
893                parameters: Some(URLParameters {
894                    service: Some("frobnik".into()),
895                    relative_ref: Some("/ref".into()),
896                    ..Default::default()
897                }),
898            }
899        );
900
901        let url =
902            URL::parse("did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1").unwrap();
903        assert_eq!(
904            url,
905            URL {
906                did: DID {
907                    name: "abcdef".into(),
908                    id: "123456".into(),
909                },
910                parameters: Some(URLParameters {
911                    service: Some("frobnik".into()),
912                    relative_ref: Some("/ref".into()),
913                    version_id: Some("1".into()),
914                    ..Default::default()
915                }),
916            }
917        );
918
919        let url = URL::parse(
920            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash",
921        )
922        .unwrap();
923        assert_eq!(
924            url,
925            URL {
926                did: DID {
927                    name: "abcdef".into(),
928                    id: "123456".into(),
929                },
930                parameters: Some(URLParameters {
931                    service: Some("frobnik".into()),
932                    relative_ref: Some("/ref".into()),
933                    version_id: Some("1".into()),
934                    hash_link: Some("myhash".into()),
935                    ..Default::default()
936                }),
937            }
938        );
939
940        let url = URL::parse(
941            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash&extra=parameter",
942        )
943        .unwrap();
944
945        let mut map = BTreeMap::new();
946        map.insert("extra".into(), "parameter".into());
947
948        assert_eq!(
949            url,
950            URL {
951                did: DID {
952                    name: "abcdef".into(),
953                    id: "123456".into(),
954                },
955                parameters: Some(URLParameters {
956                    service: Some("frobnik".into()),
957                    relative_ref: Some("/ref".into()),
958                    version_id: Some("1".into()),
959                    hash_link: Some("myhash".into()),
960                    extra_query: Some(map),
961                    ..Default::default()
962                }),
963            }
964        );
965
966        let url = URL::parse(
967            "did:abcdef:123456?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash&extra",
968        )
969        .unwrap();
970
971        let mut map = BTreeMap::new();
972        map.insert("extra".into(), "".into());
973
974        assert_eq!(
975            url,
976            URL {
977                did: DID {
978                    name: "abcdef".into(),
979                    id: "123456".into(),
980                },
981                parameters: Some(URLParameters {
982                    service: Some("frobnik".into()),
983                    relative_ref: Some("/ref".into()),
984                    version_id: Some("1".into()),
985                    hash_link: Some("myhash".into()),
986                    extra_query: Some(map),
987                    ..Default::default()
988                }),
989            }
990        );
991
992        let url = URL::parse("did:abcdef:123456?extra=parameter").unwrap();
993
994        let mut map = BTreeMap::new();
995        map.insert("extra".into(), "parameter".into());
996
997        assert_eq!(
998            url,
999            URL {
1000                did: DID {
1001                    name: "abcdef".into(),
1002                    id: "123456".into(),
1003                },
1004                parameters: Some(URLParameters {
1005                    extra_query: Some(map),
1006                    ..Default::default()
1007                }),
1008            }
1009        );
1010
1011        let url = URL::parse("did:abcdef:123456?extra").unwrap();
1012
1013        let mut map = BTreeMap::new();
1014        map.insert("extra".into(), "".into());
1015
1016        assert_eq!(
1017            url,
1018            URL {
1019                did: DID {
1020                    name: "abcdef".into(),
1021                    id: "123456".into(),
1022                },
1023                parameters: Some(URLParameters {
1024                    extra_query: Some(map),
1025                    ..Default::default()
1026                }),
1027            }
1028        );
1029
1030        let url = URL::parse(
1031            "did:abcdef:123456/path?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash#fragment",
1032        )
1033        .unwrap();
1034        assert_eq!(
1035            url,
1036            URL {
1037                did: DID {
1038                    name: "abcdef".into(),
1039                    id: "123456".into(),
1040                },
1041                parameters: Some(URLParameters {
1042                    path: Some("path".into()),
1043                    fragment: Some("fragment".into()),
1044                    service: Some("frobnik".into()),
1045                    relative_ref: Some("/ref".into()),
1046                    version_id: Some("1".into()),
1047                    hash_link: Some("myhash".into()),
1048                    ..Default::default()
1049                }),
1050            }
1051        );
1052
1053        assert!(URL::parse(
1054            "did:abcdef:123456/path?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash&versionTime=foo#fragment",
1055        )
1056        .is_err());
1057
1058        let url = URL::parse(
1059            "did:abcdef:123456/path?service=frobnik&relativeRef=%2Fref&versionId=1&hl=myhash&versionTime=1978-04-06T06:00:00Z#fragment",
1060        )
1061        .unwrap();
1062
1063        assert_eq!(
1064            url,
1065            URL {
1066                did: DID {
1067                    name: "abcdef".into(),
1068                    id: "123456".into(),
1069                },
1070                parameters: Some(URLParameters {
1071                    path: Some("path".into()),
1072                    fragment: Some("fragment".into()),
1073                    service: Some("frobnik".into()),
1074                    relative_ref: Some("/ref".into()),
1075                    version_id: Some("1".into()),
1076                    hash_link: Some("myhash".into()),
1077                    version_time: Some(VersionTime(
1078                        OffsetDateTime::from_unix_timestamp(260690400).unwrap()
1079                    )),
1080                    ..Default::default()
1081                }),
1082            }
1083        );
1084
1085        let url = URL::parse("did:abcdef:123456:mumble:foo").unwrap();
1086        assert_eq!(
1087            url,
1088            URL {
1089                did: DID {
1090                    name: "abcdef".into(),
1091                    id: "123456:mumble:foo".into(),
1092                },
1093                ..Default::default()
1094            }
1095        );
1096
1097        let url =
1098            URL::parse("did:example:123?service=agent&relativeRef=/credentials#degree").unwrap();
1099
1100        assert_eq!(
1101            url,
1102            URL {
1103                did: DID {
1104                    name: "example".into(),
1105                    id: "123".into(),
1106                },
1107                parameters: Some(URLParameters {
1108                    service: Some("agent".into()),
1109                    relative_ref: Some("/credentials".into()),
1110                    fragment: Some("degree".into()),
1111                    ..Default::default()
1112                }),
1113            }
1114        );
1115
1116        let url = URL::parse("did:example:123#/degree").unwrap();
1117        assert_eq!(
1118            url,
1119            URL {
1120                did: DID {
1121                    name: "example".into(),
1122                    id: "123".into(),
1123                },
1124                parameters: Some(URLParameters {
1125                    fragment: Some("/degree".into()),
1126                    ..Default::default()
1127                }),
1128            }
1129        );
1130
1131        let url = URL::parse("did:example:123#?degree").unwrap();
1132        assert_eq!(
1133            url,
1134            URL {
1135                did: DID {
1136                    name: "example".into(),
1137                    id: "123".into(),
1138                },
1139                parameters: Some(URLParameters {
1140                    fragment: Some("?degree".into()),
1141                    ..Default::default()
1142                }),
1143            }
1144        );
1145
1146        let url = URL::parse("did:example:123/path#?degree").unwrap();
1147        assert_eq!(
1148            url,
1149            URL {
1150                did: DID {
1151                    name: "example".into(),
1152                    id: "123".into(),
1153                },
1154                parameters: Some(URLParameters {
1155                    path: Some("path".into()),
1156                    fragment: Some("?degree".into()),
1157                    ..Default::default()
1158                }),
1159            }
1160        );
1161
1162        let url = URL::parse("did:example:123#?/degree").unwrap();
1163        assert_eq!(
1164            url,
1165            URL {
1166                did: DID {
1167                    name: "example".into(),
1168                    id: "123".into(),
1169                },
1170                parameters: Some(URLParameters {
1171                    fragment: Some("?/degree".into()),
1172                    ..Default::default()
1173                }),
1174            }
1175        );
1176
1177        let url = URL::parse("did:123456:123#?/degree").unwrap();
1178        assert_eq!(
1179            url,
1180            URL {
1181                did: DID {
1182                    name: "123456".into(),
1183                    id: "123".into(),
1184                },
1185                parameters: Some(URLParameters {
1186                    fragment: Some("?/degree".into()),
1187                    ..Default::default()
1188                }),
1189            }
1190        );
1191    }
1192
1193    #[test]
1194    fn test_serde() {
1195        use super::{URLParameters, URL};
1196        use crate::did::DID;
1197
1198        let url: [URL; 1] = serde_json::from_str(r#"["did:123456:123"]"#).unwrap();
1199        assert_eq!(
1200            url[0],
1201            URL {
1202                did: DID {
1203                    name: "123456".into(),
1204                    id: "123".into(),
1205                },
1206                ..Default::default()
1207            }
1208        );
1209
1210        assert_eq!(
1211            serde_json::to_string(&url).unwrap(),
1212            r#"["did:123456:123"]"#
1213        );
1214
1215        let url: [URL; 1] = serde_json::from_str(
1216            r#"["did:123456:123/path?service=foo&relativeRef=/ref#fragment"]"#,
1217        )
1218        .unwrap();
1219        assert_eq!(
1220            url[0],
1221            URL {
1222                did: DID {
1223                    name: "123456".into(),
1224                    id: "123".into(),
1225                },
1226                parameters: Some(URLParameters {
1227                    path: Some("path".into()),
1228                    service: Some("foo".into()),
1229                    relative_ref: Some("/ref".into()),
1230                    fragment: Some("fragment".into()),
1231                    ..Default::default()
1232                }),
1233            }
1234        );
1235
1236        assert_eq!(
1237            serde_json::to_string(&url).unwrap(),
1238            r#"["did:123456:123/path?service=foo&relativeRef=%2Fref#fragment"]"#,
1239        );
1240    }
1241}