baseid_did/
url.rs

1//! DID URL parsing per W3C DID Core 1.0.
2
3/// A parsed DID URL.
4///
5/// Format: `did:<method>:<method-specific-id>[/<path>][?<query>][#<fragment>]`
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DidUrl {
8    /// The full DID (without path/query/fragment).
9    pub did: String,
10    /// The DID method (e.g., "key", "web").
11    pub method: String,
12    /// The method-specific identifier.
13    pub method_id: String,
14    /// Optional path component.
15    pub path: Option<String>,
16    /// Optional query component.
17    pub query: Option<String>,
18    /// Optional fragment component.
19    pub fragment: Option<String>,
20}
21
22impl DidUrl {
23    /// Parse a DID URL string.
24    pub fn parse(input: &str) -> baseid_core::Result<Self> {
25        let without_fragment = if let Some((base, fragment)) = input.split_once('#') {
26            let fragment = Some(fragment.to_string());
27            (base, fragment)
28        } else {
29            (input, None)
30        };
31
32        let (base, fragment) = without_fragment;
33
34        let (base, query) = if let Some((b, q)) = base.split_once('?') {
35            (b, Some(q.to_string()))
36        } else {
37            (base, None)
38        };
39
40        let (did_part, path) = if let Some(idx) = base.find('/') {
41            (&base[..idx], Some(base[idx..].to_string()))
42        } else {
43            (base, None)
44        };
45
46        let parts: Vec<&str> = did_part.splitn(3, ':').collect();
47        if parts.len() < 3 || parts[0] != "did" {
48            return Err(baseid_core::error::DidError::InvalidDid.into());
49        }
50
51        Ok(Self {
52            did: did_part.to_string(),
53            method: parts[1].to_string(),
54            method_id: parts[2].to_string(),
55            path,
56            query,
57            fragment,
58        })
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn parse_simple_did() {
68        let url =
69            DidUrl::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
70        assert_eq!(url.method, "key");
71        assert_eq!(
72            url.method_id,
73            "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
74        );
75        assert!(url.path.is_none());
76        assert!(url.query.is_none());
77        assert!(url.fragment.is_none());
78    }
79
80    #[test]
81    fn parse_did_with_fragment() {
82        let url = DidUrl::parse("did:web:example.com#key-1").unwrap();
83        assert_eq!(url.method, "web");
84        assert_eq!(url.method_id, "example.com");
85        assert_eq!(url.fragment.as_deref(), Some("key-1"));
86    }
87
88    #[test]
89    fn reject_invalid_did() {
90        assert!(DidUrl::parse("not-a-did").is_err());
91        assert!(DidUrl::parse("did:").is_err());
92    }
93}