Skip to main content

proto_blue_syntax/
at_identifier.rs

1//! AT Identifier (DID or Handle) validation and types.
2//!
3//! An AtIdentifier is either a DID or a Handle.
4
5use std::fmt;
6use std::str::FromStr;
7
8use crate::did::Did;
9use crate::handle::Handle;
10
11/// A validated AT identifier (either a DID or a Handle).
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum AtIdentifier {
14    /// A DID (e.g., `did:plc:asdf123`).
15    Did(Did),
16    /// A Handle (e.g., `alice.bsky.social`).
17    Handle(Handle),
18}
19
20/// Error returned when an AT identifier string is invalid.
21#[derive(Debug, Clone, thiserror::Error)]
22#[error("Invalid AT identifier: {reason}")]
23pub struct InvalidAtIdentifierError {
24    pub reason: String,
25}
26
27impl AtIdentifier {
28    /// Create a new `AtIdentifier` from a string, attempting DID first, then Handle.
29    pub fn new(s: &str) -> Result<Self, InvalidAtIdentifierError> {
30        if s.starts_with("did:") {
31            Did::new(s)
32                .map(AtIdentifier::Did)
33                .map_err(|e| InvalidAtIdentifierError {
34                    reason: e.to_string(),
35                })
36        } else {
37            Handle::new(s)
38                .map(AtIdentifier::Handle)
39                .map_err(|e| InvalidAtIdentifierError {
40                    reason: e.to_string(),
41                })
42        }
43    }
44
45    /// Check whether a string is a valid AT identifier.
46    pub fn is_valid(s: &str) -> bool {
47        AtIdentifier::new(s).is_ok()
48    }
49
50    /// Return the inner string representation.
51    pub fn as_str(&self) -> &str {
52        match self {
53            AtIdentifier::Did(d) => d.as_str(),
54            AtIdentifier::Handle(h) => h.as_str(),
55        }
56    }
57
58    /// Check if this is a DID.
59    pub fn is_did(&self) -> bool {
60        matches!(self, AtIdentifier::Did(_))
61    }
62
63    /// Check if this is a Handle.
64    pub fn is_handle(&self) -> bool {
65        matches!(self, AtIdentifier::Handle(_))
66    }
67}
68
69impl fmt::Display for AtIdentifier {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            AtIdentifier::Did(d) => d.fmt(f),
73            AtIdentifier::Handle(h) => h.fmt(f),
74        }
75    }
76}
77
78impl FromStr for AtIdentifier {
79    type Err = InvalidAtIdentifierError;
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        AtIdentifier::new(s)
82    }
83}
84
85impl From<Did> for AtIdentifier {
86    fn from(d: Did) -> Self {
87        AtIdentifier::Did(d)
88    }
89}
90
91impl From<Handle> for AtIdentifier {
92    fn from(h: Handle) -> Self {
93        AtIdentifier::Handle(h)
94    }
95}
96
97impl serde::Serialize for AtIdentifier {
98    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
99        self.as_str().serialize(serializer)
100    }
101}
102
103impl<'de> serde::Deserialize<'de> for AtIdentifier {
104    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
105        let s = String::deserialize(deserializer)?;
106        AtIdentifier::new(&s).map_err(serde::de::Error::custom)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parses_dids() {
116        let id = AtIdentifier::new("did:plc:asdf123").unwrap();
117        assert!(id.is_did());
118        assert!(!id.is_handle());
119    }
120
121    #[test]
122    fn parses_handles() {
123        let id = AtIdentifier::new("alice.bsky.social").unwrap();
124        assert!(id.is_handle());
125        assert!(!id.is_did());
126    }
127
128    #[test]
129    fn invalid() {
130        assert!(AtIdentifier::new("").is_err());
131        assert!(AtIdentifier::new("not-valid").is_err());
132    }
133
134    #[test]
135    fn display() {
136        let id = AtIdentifier::new("did:plc:asdf123").unwrap();
137        assert_eq!(id.to_string(), "did:plc:asdf123");
138
139        let id = AtIdentifier::new("alice.bsky.social").unwrap();
140        assert_eq!(id.to_string(), "alice.bsky.social");
141    }
142}