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    #[must_use]
47    pub fn is_valid(s: &str) -> bool {
48        Self::new(s).is_ok()
49    }
50
51    /// Return the inner string representation.
52    #[must_use]
53    pub fn as_str(&self) -> &str {
54        match self {
55            Self::Did(d) => d.as_str(),
56            Self::Handle(h) => h.as_str(),
57        }
58    }
59
60    /// Check if this is a DID.
61    #[must_use]
62    pub const fn is_did(&self) -> bool {
63        matches!(self, Self::Did(_))
64    }
65
66    /// Check if this is a Handle.
67    #[must_use]
68    pub const fn is_handle(&self) -> bool {
69        matches!(self, Self::Handle(_))
70    }
71}
72
73impl fmt::Display for AtIdentifier {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Did(d) => d.fmt(f),
77            Self::Handle(h) => h.fmt(f),
78        }
79    }
80}
81
82impl FromStr for AtIdentifier {
83    type Err = InvalidAtIdentifierError;
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        Self::new(s)
86    }
87}
88
89impl From<Did> for AtIdentifier {
90    fn from(d: Did) -> Self {
91        Self::Did(d)
92    }
93}
94
95impl From<Handle> for AtIdentifier {
96    fn from(h: Handle) -> Self {
97        Self::Handle(h)
98    }
99}
100
101impl serde::Serialize for AtIdentifier {
102    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
103        self.as_str().serialize(serializer)
104    }
105}
106
107impl<'de> serde::Deserialize<'de> for AtIdentifier {
108    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
109        let s = String::deserialize(deserializer)?;
110        Self::new(&s).map_err(serde::de::Error::custom)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn parses_dids() {
120        let id = AtIdentifier::new("did:plc:asdf123").unwrap();
121        assert!(id.is_did());
122        assert!(!id.is_handle());
123    }
124
125    #[test]
126    fn parses_handles() {
127        let id = AtIdentifier::new("alice.bsky.social").unwrap();
128        assert!(id.is_handle());
129        assert!(!id.is_did());
130    }
131
132    #[test]
133    fn invalid() {
134        assert!(AtIdentifier::new("").is_err());
135        assert!(AtIdentifier::new("not-valid").is_err());
136    }
137
138    #[test]
139    fn display() {
140        let id = AtIdentifier::new("did:plc:asdf123").unwrap();
141        assert_eq!(id.to_string(), "did:plc:asdf123");
142
143        let id = AtIdentifier::new("alice.bsky.social").unwrap();
144        assert_eq!(id.to_string(), "alice.bsky.social");
145    }
146}