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#[derive(Clone, Default, Debug, Hash, PartialOrd, Ord, Eq, PartialEq)]
46pub struct URL {
47 pub did: DID,
48 pub parameters: Option<URLParameters>,
49}
50
51#[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) = ¶ms.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) = ¶ms.service {
140 ret += &("service=".to_string() + service);
141 ret += "&";
142 }
143
144 if let Some(relative_ref) = ¶ms.relative_ref {
145 ret += &("relativeRef=".to_string() + &url_encoded(relative_ref));
146 ret += "&";
147 }
148
149 if let Some(version_id) = ¶ms.version_id {
150 ret += &("versionId=".to_string() + version_id);
151 ret += "&";
152 }
153
154 if let Some(version_time) = ¶ms.version_time {
155 ret += &("versionTime=".to_string() + &version_time.to_string());
156 ret += "&";
157 }
158
159 if let Some(hash_link) = ¶ms.hash_link {
160 ret += &("hl=".to_string() + hash_link);
161 ret += "&";
162 }
163
164 if let Some(extra_query) = ¶ms.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) = ¶ms.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 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 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 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}