Skip to main content

auths_core/
keri_did.rs

1//! Newtype for `did:keri:<prefix>` identifiers.
2//!
3//! Parsing and validation happen at construction time so downstream code
4//! can rely on the invariant without re-parsing.
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10const PREFIX: &str = "did:keri:";
11
12/// A validated `did:keri:<prefix>` identifier.
13///
14/// The inner string always starts with `"did:keri:"` followed by a non-empty
15/// KERI prefix. Construction is fallible — use [`KeriDid::parse`] or
16/// [`TryFrom<String>`].
17///
18/// Usage:
19/// ```ignore
20/// let did = KeriDid::parse("did:keri:EXq5abc")?;
21/// assert_eq!(did.prefix(), "EXq5abc");
22/// assert_eq!(did.as_str(), "did:keri:EXq5abc");
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
25#[serde(try_from = "String", into = "String")]
26pub struct KeriDid(String);
27
28impl KeriDid {
29    /// Parse a `did:keri:` string, returning an error if the format is invalid.
30    ///
31    /// Args:
32    /// * `s`: A string that must start with `"did:keri:"` followed by a non-empty prefix.
33    ///
34    /// Usage:
35    /// ```ignore
36    /// let did = KeriDid::parse("did:keri:EXq5")?;
37    /// ```
38    pub fn parse(s: &str) -> Result<Self, KeriDidError> {
39        let keri_prefix = s.strip_prefix(PREFIX).ok_or(KeriDidError::MissingPrefix)?;
40        if keri_prefix.is_empty() {
41            return Err(KeriDidError::EmptyPrefix);
42        }
43        Ok(Self(s.to_string()))
44    }
45
46    /// Build a `KeriDid` from a raw KERI prefix (without the `did:keri:` scheme).
47    ///
48    /// Args:
49    /// * `prefix`: The bare KERI prefix string (e.g. `"EXq5abc"`).
50    ///
51    /// Usage:
52    /// ```ignore
53    /// let did = KeriDid::from_prefix("EXq5abc");
54    /// assert_eq!(did.as_str(), "did:keri:EXq5abc");
55    /// ```
56    pub fn from_prefix(prefix: &str) -> Result<Self, KeriDidError> {
57        if prefix.is_empty() {
58            return Err(KeriDidError::EmptyPrefix);
59        }
60        Ok(Self(format!("{}{}", PREFIX, prefix)))
61    }
62
63    /// Returns the KERI prefix portion (everything after `did:keri:`).
64    pub fn prefix(&self) -> &str {
65        // Safe: invariant established at construction
66        &self.0[PREFIX.len()..]
67    }
68
69    /// Returns the full DID string.
70    pub fn as_str(&self) -> &str {
71        &self.0
72    }
73}
74
75impl fmt::Display for KeriDid {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        f.write_str(&self.0)
78    }
79}
80
81impl AsRef<str> for KeriDid {
82    fn as_ref(&self) -> &str {
83        &self.0
84    }
85}
86
87impl From<KeriDid> for String {
88    fn from(did: KeriDid) -> Self {
89        did.0
90    }
91}
92
93impl TryFrom<String> for KeriDid {
94    type Error = KeriDidError;
95
96    fn try_from(s: String) -> Result<Self, Self::Error> {
97        let keri_prefix = s.strip_prefix(PREFIX).ok_or(KeriDidError::MissingPrefix)?;
98        if keri_prefix.is_empty() {
99            return Err(KeriDidError::EmptyPrefix);
100        }
101        Ok(Self(s))
102    }
103}
104
105impl TryFrom<&str> for KeriDid {
106    type Error = KeriDidError;
107
108    fn try_from(s: &str) -> Result<Self, Self::Error> {
109        Self::parse(s)
110    }
111}
112
113/// Error from parsing an invalid `did:keri:` string.
114#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
115pub enum KeriDidError {
116    /// The `did:keri:` prefix is absent.
117    #[error("not a did:keri: identifier")]
118    MissingPrefix,
119
120    /// The prefix portion is empty.
121    #[error("did:keri: prefix is empty")]
122    EmptyPrefix,
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn parse_valid() {
131        let did = KeriDid::parse("did:keri:EXq5abc123").unwrap();
132        assert_eq!(did.prefix(), "EXq5abc123");
133        assert_eq!(did.as_str(), "did:keri:EXq5abc123");
134        assert_eq!(did.to_string(), "did:keri:EXq5abc123");
135    }
136
137    #[test]
138    fn from_prefix_valid() {
139        let did = KeriDid::from_prefix("EXq5abc123").unwrap();
140        assert_eq!(did.as_str(), "did:keri:EXq5abc123");
141        assert_eq!(did.prefix(), "EXq5abc123");
142    }
143
144    #[test]
145    fn rejects_non_keri() {
146        assert_eq!(
147            KeriDid::parse("did:key:z6Mk123"),
148            Err(KeriDidError::MissingPrefix)
149        );
150    }
151
152    #[test]
153    fn rejects_empty_prefix() {
154        assert_eq!(KeriDid::parse("did:keri:"), Err(KeriDidError::EmptyPrefix));
155    }
156
157    #[test]
158    fn rejects_missing_scheme() {
159        assert_eq!(KeriDid::parse("EXq5abc"), Err(KeriDidError::MissingPrefix));
160    }
161
162    #[test]
163    fn from_prefix_rejects_empty() {
164        assert_eq!(KeriDid::from_prefix(""), Err(KeriDidError::EmptyPrefix));
165    }
166
167    #[test]
168    fn try_from_string() {
169        let did: KeriDid = "did:keri:EXq5".to_string().try_into().unwrap();
170        assert_eq!(did.prefix(), "EXq5");
171    }
172
173    #[test]
174    fn into_string() {
175        let did = KeriDid::parse("did:keri:EXq5").unwrap();
176        let s: String = did.into();
177        assert_eq!(s, "did:keri:EXq5");
178    }
179
180    #[test]
181    fn serde_roundtrip() {
182        let did = KeriDid::parse("did:keri:EXq5abc").unwrap();
183        let json = serde_json::to_string(&did).unwrap();
184        assert_eq!(json, r#""did:keri:EXq5abc""#);
185        let parsed: KeriDid = serde_json::from_str(&json).unwrap();
186        assert_eq!(parsed, did);
187    }
188
189    #[test]
190    fn serde_rejects_invalid() {
191        let result: Result<KeriDid, _> = serde_json::from_str(r#""did:key:z6Mk""#);
192        assert!(result.is_err());
193    }
194}