xdid_core/
did_url.rs

1use std::{fmt::Display, str::FromStr};
2
3use anyhow::bail;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    did::Did,
8    uri::{Segment, is_segment},
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct DidUrl {
13    pub did: Did,
14    /// [DID path](https://www.w3.org/TR/did-core/#path). `path-abempty` component from
15    /// [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.3).
16    pub path_abempty: String,
17    /// [DID query](https://www.w3.org/TR/did-core/#query). `query` component from
18    /// [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.3).
19    pub query: Option<String>,
20    /// [DID fragment](https://www.w3.org/TR/did-core/#fragment). `fragment` component from
21    /// [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.3).
22    pub fragment: Option<String>,
23}
24
25impl Serialize for DidUrl {
26    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
27    where
28        S: serde::Serializer,
29    {
30        let v = self.to_string();
31        serializer.serialize_str(&v)
32    }
33}
34
35impl<'de> Deserialize<'de> for DidUrl {
36    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37    where
38        D: serde::Deserializer<'de>,
39    {
40        let s = String::deserialize(deserializer)?;
41        Self::from_str(&s).map_err(|_| serde::de::Error::custom("parse err"))
42    }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct RelativeDidUrl {
47    pub path: RelativeDidUrlPath,
48    /// [DID query](https://www.w3.org/TR/did-core/#query) ([RFC 3986 - 3.4. Query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4))
49    pub query: Option<String>,
50    /// [DID fragment](https://www.w3.org/TR/did-core/#fragment) ([RFC 3986 - 3.5. Fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5))
51    pub fragment: Option<String>,
52}
53
54impl Display for RelativeDidUrl {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        let path = self.path.to_string();
57        let query = match &self.query {
58            Some(q) => format!("?{q}"),
59            None => String::new(),
60        };
61        let fragment = match &self.fragment {
62            Some(f) => format!("#{f}"),
63            None => String::new(),
64        };
65        f.write_fmt(format_args!("{path}{query}{fragment}"))
66    }
67}
68
69impl FromStr for RelativeDidUrl {
70    type Err = anyhow::Error;
71
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        let (path, query, fragment) = match s.split_once('?') {
74            Some((path, rest)) => match rest.split_once('#') {
75                Some((query, fragment)) => (path, Some(query), Some(fragment)),
76                None => (path, Some(rest), None),
77            },
78            None => match s.split_once('#') {
79                Some((path, fragment)) => (path, None, Some(fragment)),
80                None => (s, None, None),
81            },
82        };
83
84        Ok(Self {
85            path: RelativeDidUrlPath::from_str(path)?,
86            query: query.map(|s| s.to_string()),
87            fragment: fragment.map(|s| s.to_string()),
88        })
89    }
90}
91
92impl Serialize for RelativeDidUrl {
93    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94    where
95        S: serde::Serializer,
96    {
97        let v = self.to_string();
98        serializer.serialize_str(&v)
99    }
100}
101
102impl<'de> Deserialize<'de> for RelativeDidUrl {
103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104    where
105        D: serde::Deserializer<'de>,
106    {
107        let s = String::deserialize(deserializer)?;
108        Self::from_str(&s).map_err(|_| serde::de::Error::custom("parse err"))
109    }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum RelativeDidUrlPath {
114    /// Absolute-path reference. `path-absolute` from [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)
115    Absolute(String),
116    /// Relative-path reference. `path-noscheme` from [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)
117    NoScheme(String),
118    /// Empty path. `path-empty` from [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)
119    Empty,
120}
121
122impl Display for RelativeDidUrlPath {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        let data = match self {
125            Self::Absolute(s) | Self::NoScheme(s) => s.as_str(),
126            Self::Empty => "",
127        };
128        f.write_str(data)
129    }
130}
131
132impl FromStr for RelativeDidUrlPath {
133    type Err = anyhow::Error;
134
135    fn from_str(path: &str) -> Result<Self, Self::Err> {
136        if path.is_empty() {
137            return Ok(Self::Empty);
138        }
139        if path.starts_with('/') {
140            // path-absolute = "/" [ segment-nz *( "/" segment ) ]
141            if path.len() >= 2 && path.chars().nth(1) == Some('/') {
142                bail!("double slash at start")
143            }
144
145            if !path
146                .split('/')
147                .skip(1)
148                .all(|v| is_segment(v, Segment::Base))
149            {
150                bail!("invalid segment")
151            }
152
153            Ok(Self::Absolute(path.to_string()))
154        } else {
155            // path-noscheme = segment-nz-nc *( "/" segment )
156            if !path.split('/').all(|v| is_segment(v, Segment::NzNc)) {
157                bail!("invalid segment")
158            }
159
160            Ok(Self::NoScheme(path.to_string()))
161        }
162    }
163}
164
165impl DidUrl {
166    /// Attempts to convert the [DidUrl] into a [RelativeDidUrl].
167    pub fn to_relative(&self) -> Option<RelativeDidUrl> {
168        Some(RelativeDidUrl {
169            path: match RelativeDidUrlPath::from_str(&self.path_abempty) {
170                Ok(v) => v,
171                Err(_) => return None,
172            },
173            fragment: self.fragment.clone(),
174            query: self.query.clone(),
175        })
176    }
177}
178
179impl Display for DidUrl {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        let mut url = format!("{}{}", self.did, self.path_abempty);
182
183        if let Some(ref query) = self.query {
184            url.push('?');
185            url.push_str(query);
186        }
187
188        if let Some(ref fragment) = self.fragment {
189            url.push('#');
190            url.push_str(fragment);
191        }
192
193        f.write_str(&url)
194    }
195}
196
197impl FromStr for DidUrl {
198    type Err = anyhow::Error;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        let (did_str, _) = s.split_once('/').unwrap_or_else(|| {
202            s.split_once('?')
203                .unwrap_or_else(|| s.split_once('#').unwrap_or((s, "")))
204        });
205
206        let did = Did::from_str(did_str)?;
207
208        let mut path_abempty = String::new();
209        let mut query = None;
210        let mut fragment = None;
211
212        let mut rest = s.strip_prefix(did_str).unwrap();
213        if let Some((before_fragment, frag)) = rest.split_once('#') {
214            fragment = Some(frag.to_string());
215            rest = before_fragment;
216        }
217
218        if let Some((before_query, qry)) = rest.split_once('?') {
219            query = Some(qry.to_string());
220            rest = before_query;
221        }
222
223        path_abempty.push_str(rest);
224
225        // path-abempty  = *( "/" segment )
226        if !path_abempty.is_empty() {
227            if !path_abempty.starts_with('/') {
228                bail!("path_abempty does not start with slash")
229            }
230
231            if !path_abempty
232                .split('/')
233                .all(|v| is_segment(v, Segment::Base))
234            {
235                bail!("invalid path_abempty segment")
236            }
237        }
238
239        Ok(DidUrl {
240            did,
241            path_abempty,
242            query,
243            fragment,
244        })
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_did_url_full() {
254        let did_url = DidUrl {
255            did: Did::from_str("did:example:123").unwrap(),
256            path_abempty: "/path/to/resource".to_string(),
257            query: Some("key=value".to_string()),
258            fragment: Some("section".to_string()),
259        };
260
261        let serialized = did_url.to_string();
262        assert_eq!(
263            serialized,
264            "did:example:123/path/to/resource?key=value#section"
265        );
266
267        let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
268        assert_eq!(deserialized, did_url);
269    }
270
271    #[test]
272    fn test_did_url_no_path() {
273        let did_url = DidUrl {
274            did: Did::from_str("did:example:123").unwrap(),
275            path_abempty: "".to_string(),
276            query: Some("key=value".to_string()),
277            fragment: Some("section".to_string()),
278        };
279
280        let serialized = did_url.to_string();
281        assert_eq!(serialized, "did:example:123?key=value#section");
282
283        let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
284        assert_eq!(deserialized, did_url);
285    }
286
287    #[test]
288    fn test_did_url_no_query() {
289        let did_url = DidUrl {
290            did: Did::from_str("did:example:123").unwrap(),
291            path_abempty: "/path/to/resource".to_string(),
292            query: None,
293            fragment: Some("section".to_string()),
294        };
295
296        let serialized = did_url.to_string();
297        assert_eq!(serialized, "did:example:123/path/to/resource#section");
298
299        let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
300        assert_eq!(deserialized, did_url);
301    }
302
303    #[test]
304    fn test_did_url_no_fragment() {
305        let did_url = DidUrl {
306            did: Did::from_str("did:example:123").unwrap(),
307            path_abempty: "/path/to/resource".to_string(),
308            query: Some("key=value".to_string()),
309            fragment: None,
310        };
311
312        let serialized = did_url.to_string();
313        assert_eq!(serialized, "did:example:123/path/to/resource?key=value");
314
315        let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
316        assert_eq!(deserialized, did_url);
317    }
318
319    #[test]
320    fn test_did_url_none() {
321        let did_url = DidUrl {
322            did: Did::from_str("did:example:123").unwrap(),
323            path_abempty: "".to_string(),
324            query: None,
325            fragment: None,
326        };
327
328        let serialized = did_url.to_string();
329        assert_eq!(serialized, "did:example:123");
330
331        let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
332        assert_eq!(deserialized, did_url);
333    }
334}