1use once_cell::sync::Lazy;
7use regex::Regex;
8use std::fmt;
9use std::str::FromStr;
10
11const MAX_DID_LENGTH: usize = 2048;
13
14static DID_REGEX: Lazy<Regex> =
15 Lazy::new(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap());
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct Did(String);
22
23#[derive(Debug, Clone, thiserror::Error)]
25#[error("Invalid DID: {reason}")]
26pub struct InvalidDidError {
27 pub reason: String,
28}
29
30impl Did {
31 pub fn new(s: &str) -> Result<Self, InvalidDidError> {
33 ensure_valid_did(s)?;
34 Ok(Did(s.to_string()))
35 }
36
37 pub fn is_valid(s: &str) -> bool {
39 ensure_valid_did(s).is_ok()
40 }
41
42 pub fn method(&self) -> &str {
44 self.0.split(':').nth(1).unwrap()
46 }
47
48 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52
53 pub fn into_inner(self) -> String {
55 self.0
56 }
57}
58
59fn ensure_valid_did(s: &str) -> Result<(), InvalidDidError> {
60 let err = |reason: &str| InvalidDidError {
61 reason: reason.to_string(),
62 };
63
64 if s.len() > MAX_DID_LENGTH {
65 return Err(err(&format!(
66 "DID is too long ({} chars, max {})",
67 s.len(),
68 MAX_DID_LENGTH
69 )));
70 }
71
72 if !DID_REGEX.is_match(s) {
73 if !s.starts_with("did:") {
75 return Err(err("DID requires \"did:\" prefix"));
76 }
77 if s.ends_with(':') || s.ends_with('%') {
78 return Err(err("DID cannot end with ':' or '%'"));
79 }
80 let parts: Vec<&str> = s.splitn(4, ':').collect();
81 if parts.len() < 3 {
82 return Err(err(
83 "DID requires prefix, method, and method-specific content",
84 ));
85 }
86 if parts[1].is_empty() || !parts[1].chars().all(|c| c.is_ascii_lowercase()) {
87 return Err(err("DID method must be lowercase letters only"));
88 }
89 return Err(err("DID contains invalid characters"));
90 }
91
92 Ok(())
93}
94
95impl fmt::Display for Did {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(&self.0)
98 }
99}
100
101impl FromStr for Did {
102 type Err = InvalidDidError;
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 Did::new(s)
105 }
106}
107
108impl AsRef<str> for Did {
109 fn as_ref(&self) -> &str {
110 &self.0
111 }
112}
113
114impl serde::Serialize for Did {
115 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
116 self.0.serialize(serializer)
117 }
118}
119
120impl<'de> serde::Deserialize<'de> for Did {
121 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
122 let s = String::deserialize(deserializer)?;
123 Did::new(&s).map_err(serde::de::Error::custom)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn valid_dids() {
133 let cases = [
134 "did:plc:asdf123",
135 "did:web:example.com",
136 "did:method:val:two",
137 "did:m:v",
138 "did:method:%3A",
139 "did:method:val-two",
140 "did:method:val_two",
141 "did:method:val.two",
142 ];
143 for did in &cases {
144 assert!(Did::new(did).is_ok(), "should be valid: {did}");
145 }
146 }
147
148 #[test]
149 fn invalid_dids() {
150 let cases = [
151 ("", "empty"),
152 ("did:", "no method"),
153 ("did:m:", "ends with colon"),
154 ("did:m:%", "ends with percent"),
155 ("DID:method:val", "uppercase prefix"),
156 ("did:UPPER:val", "uppercase method"),
157 ("did:m:v!v", "invalid character"),
158 ("randomstring", "no prefix"),
159 ("did:method:", "ends with colon"),
160 ];
161 for (input, desc) in &cases {
162 assert!(
163 Did::new(input).is_err(),
164 "should be invalid ({desc}): {input}"
165 );
166 }
167 }
168
169 #[test]
170 fn method_extraction() {
171 let did = Did::new("did:plc:asdf123").unwrap();
172 assert_eq!(did.method(), "plc");
173
174 let did = Did::new("did:web:example.com").unwrap();
175 assert_eq!(did.method(), "web");
176 }
177
178 #[test]
179 fn serde_roundtrip() {
180 let did = Did::new("did:plc:asdf123").unwrap();
181 let json = serde_json::to_string(&did).unwrap();
182 assert_eq!(json, "\"did:plc:asdf123\"");
183 let parsed: Did = serde_json::from_str(&json).unwrap();
184 assert_eq!(parsed, did);
185 }
186
187 #[test]
188 fn max_length() {
189 let long_did = format!("did:m:{}", "a".repeat(MAX_DID_LENGTH));
190 assert!(Did::new(&long_did).is_err());
191 }
192}