Skip to main content

jacquard_common/types/
ident.rs

1use crate::bos::{Bos, DefaultStr};
2use crate::types::handle::Handle;
3use crate::types::string::AtStrError;
4use crate::{
5    CowStr, IntoStatic,
6    types::did::{Did, validate_did},
7};
8use alloc::string::String;
9use alloc::string::ToString;
10use core::fmt;
11use core::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15/// AT Protocol identifier (either a DID or handle).
16///
17/// Represents the union of DIDs and handles, which can both be used to identify
18/// accounts in AT Protocol. DIDs are permanent identifiers, while handles are
19/// human-friendly and can change.
20///
21/// Automatically determines whether a string is a DID or a handle during parsing.
22#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
23#[serde(untagged)]
24#[serde(bound(
25    serialize = "S: Bos<str> + AsRef<str> + Serialize",
26    deserialize = "S: Bos<str> + AsRef<str> + Deserialize<'de>"
27))]
28pub enum AtIdentifier<S: Bos<str> + AsRef<str> = DefaultStr> {
29    /// DID variant
30    Did(Did<S>),
31    /// Handle variant
32    Handle(Handle<S>),
33}
34
35// ---------------------------------------------------------------------------
36// Core methods
37// ---------------------------------------------------------------------------
38
39impl<S: Bos<str> + AsRef<str>> AtIdentifier<S> {
40    /// Get the identifier as a string slice.
41    pub fn as_str(&self) -> &str {
42        match self {
43            AtIdentifier::Did(did) => did.as_str(),
44            AtIdentifier::Handle(handle) => handle.as_str(),
45        }
46    }
47}
48
49// ---------------------------------------------------------------------------
50// Generic construction
51// ---------------------------------------------------------------------------
52
53impl<S: Bos<str> + AsRef<str>> AtIdentifier<S> {
54    /// Fallible constructor, validates, wraps the input directly.
55    ///
56    /// Tries DID first, then handle. Rejects uppercase handles — use
57    /// `new_owned()` for case-insensitive construction.
58    pub fn new(ident: S) -> Result<Self, AtStrError> {
59        let s = ident.as_ref();
60        if validate_did(s).is_ok() {
61            Ok(AtIdentifier::Did(unsafe { Did::unchecked(ident) }))
62        } else {
63            Handle::new(ident).map(AtIdentifier::Handle)
64        }
65    }
66
67    /// Infallible constructor. Panics on invalid identifiers.
68    pub fn raw(ident: S) -> Self {
69        Self::new(ident).expect("valid identifier")
70    }
71
72    /// Infallible unchecked constructor.
73    ///
74    /// # Safety
75    ///
76    /// Validates DIDs, treats anything else as a valid handle.
77    pub unsafe fn unchecked(ident: S) -> Self {
78        if validate_did(ident.as_ref()).is_ok() {
79            AtIdentifier::Did(unsafe { Did::unchecked(ident) })
80        } else {
81            unsafe { AtIdentifier::Handle(Handle::unchecked(ident)) }
82        }
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Owned construction
88// ---------------------------------------------------------------------------
89
90impl<S: Bos<str> + AsRef<str> + FromStr> AtIdentifier<S> {
91    /// Fallible constructor, validates, takes ownership.
92    /// Strips prefixes and normalises handle case.
93    pub fn new_owned(ident: impl AsRef<str>) -> Result<Self, AtStrError> {
94        let ident = ident.as_ref();
95        if let Ok(did) = Did::new_owned(ident) {
96            Ok(AtIdentifier::Did(did))
97        } else {
98            Handle::new_owned(ident).map(AtIdentifier::Handle)
99        }
100    }
101
102    /// Fallible constructor for static strings.
103    pub fn new_static(ident: &'static str) -> Result<Self, AtStrError> {
104        if let Ok(did) = Did::new_static(ident) {
105            Ok(AtIdentifier::Did(did))
106        } else {
107            Handle::new_static(ident).map(AtIdentifier::Handle)
108        }
109    }
110}
111
112// ---------------------------------------------------------------------------
113// Trait impls
114// ---------------------------------------------------------------------------
115
116impl<S: Bos<str> + AsRef<str> + IntoStatic> IntoStatic for AtIdentifier<S>
117where
118    S::Output: Bos<str> + AsRef<str>,
119{
120    type Output = AtIdentifier<S::Output>;
121
122    fn into_static(self) -> Self::Output {
123        match self {
124            AtIdentifier::Did(did) => AtIdentifier::Did(did.into_static()),
125            AtIdentifier::Handle(handle) => AtIdentifier::Handle(handle.into_static()),
126        }
127    }
128}
129
130impl<S: Bos<str> + AsRef<str>> AtIdentifier<S> {
131    /// Convert to an `AtIdentifier` with a different backing type.
132    pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> AtIdentifier<B> {
133        match self {
134            AtIdentifier::Did(did) => AtIdentifier::Did(did.convert()),
135            AtIdentifier::Handle(handle) => AtIdentifier::Handle(handle.convert()),
136        }
137    }
138}
139
140impl<S: Bos<str> + AsRef<str>> From<Did<S>> for AtIdentifier<S> {
141    fn from(did: Did<S>) -> Self {
142        AtIdentifier::Did(did)
143    }
144}
145
146impl<S: Bos<str> + AsRef<str>> From<Handle<S>> for AtIdentifier<S> {
147    fn from(handle: Handle<S>) -> Self {
148        AtIdentifier::Handle(handle)
149    }
150}
151
152impl FromStr for AtIdentifier {
153    type Err = AtStrError;
154
155    fn from_str(s: &str) -> Result<Self, Self::Err> {
156        Self::new_owned(s)
157    }
158}
159
160impl FromStr for AtIdentifier<CowStr<'static>> {
161    type Err = AtStrError;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        Self::new_owned(s)
165    }
166}
167
168impl FromStr for AtIdentifier<String> {
169    type Err = AtStrError;
170
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        Self::new_owned(s)
173    }
174}
175
176impl<S: Bos<str> + AsRef<str>> fmt::Display for AtIdentifier<S> {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            AtIdentifier::Did(did) => did.fmt(f),
180            AtIdentifier::Handle(handle) => handle.fmt(f),
181        }
182    }
183}
184
185impl From<String> for AtIdentifier {
186    fn from(value: String) -> Self {
187        Self::new_owned(value).expect("valid identifier")
188    }
189}
190
191impl<'i> From<CowStr<'i>> for AtIdentifier<CowStr<'i>> {
192    fn from(value: CowStr<'i>) -> Self {
193        Self::new(value).expect("valid identifier")
194    }
195}
196
197impl<S: Bos<str> + AsRef<str>> From<AtIdentifier<S>> for String {
198    fn from(value: AtIdentifier<S>) -> Self {
199        value.as_str().to_string()
200    }
201}
202
203impl<S: Bos<str> + AsRef<str>> AsRef<str> for AtIdentifier<S> {
204    fn as_ref(&self) -> &str {
205        self.as_str()
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use smol_str::SmolStr;
212
213    use super::*;
214    use crate::cowstr::ToCowStr;
215
216    #[test]
217    fn parses_did() {
218        let ident = AtIdentifier::<&str>::new("did:plc:foo").unwrap();
219        assert!(matches!(ident, AtIdentifier::Did(_)));
220        assert_eq!(ident.as_str(), "did:plc:foo");
221    }
222
223    #[test]
224    fn parses_handle() {
225        let ident = AtIdentifier::<&str>::new("alice.test").unwrap();
226        assert!(matches!(ident, AtIdentifier::Handle(_)));
227        assert_eq!(ident.as_str(), "alice.test");
228    }
229
230    #[test]
231    fn did_takes_precedence() {
232        let ident = AtIdentifier::<&str>::new("did:web:alice.test").unwrap();
233        assert!(matches!(ident, AtIdentifier::Did(_)));
234    }
235
236    #[test]
237    fn from_types() {
238        let did = Did::<SmolStr>::new_owned("did:plc:foo").unwrap();
239        let ident: AtIdentifier<SmolStr> = did.into();
240        assert!(matches!(ident, AtIdentifier::Did(_)));
241
242        let handle = Handle::new("alice.test".to_cowstr()).unwrap();
243        let ident: AtIdentifier<CowStr> = handle.into();
244        assert!(matches!(ident, AtIdentifier::Handle(_)));
245    }
246
247    #[test]
248    fn owned_construction() {
249        let ident = AtIdentifier::<SmolStr>::new_owned("did:plc:foo").unwrap();
250        assert!(matches!(ident, AtIdentifier::Did(_)));
251
252        let ident = AtIdentifier::<SmolStr>::new_owned("alice.test").unwrap();
253        assert!(matches!(ident, AtIdentifier::Handle(_)));
254    }
255}