A better Rust ATProto crate
1//! Utilities for parsing and working with NSIDs and refs
2
3/// Parsed NSID components for easier manipulation
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct NsidPath<'a> {
6 nsid: &'a str,
7 segments: Vec<&'a str>,
8}
9
10impl<'a> NsidPath<'a> {
11 /// Parse an NSID into its component segments
12 pub fn parse(nsid: &'a str) -> Self {
13 let segments: Vec<&str> = nsid.split('.').collect();
14 Self { nsid, segments }
15 }
16
17 /// Get the namespace (first two segments joined with '.')
18 /// Returns "com.atproto" from "com.atproto.repo.strongRef"
19 pub fn namespace(&self) -> String {
20 if self.segments.len() >= 2 {
21 format!("{}.{}", self.segments[0], self.segments[1])
22 } else {
23 self.nsid.to_string()
24 }
25 }
26
27 /// Get the last segment of the NSID
28 pub fn last_segment(&self) -> &str {
29 self.segments.last().copied().unwrap_or(self.nsid)
30 }
31
32 /// Get all segments except the last
33 pub fn parent_segments(&self) -> &[&str] {
34 if self.segments.is_empty() {
35 &[]
36 } else {
37 &self.segments[..self.segments.len() - 1]
38 }
39 }
40
41 /// Check if this is a "defs" NSID (ends with "defs")
42 pub fn is_defs(&self) -> bool {
43 self.last_segment() == "defs"
44 }
45
46 /// Get all segments
47 pub fn segments(&self) -> &[&str] {
48 &self.segments
49 }
50
51 /// Get the original NSID string
52 pub fn as_str(&self) -> &str {
53 self.nsid
54 }
55
56 /// Get number of segments
57 pub fn len(&self) -> usize {
58 self.segments.len()
59 }
60
61 /// Check if empty (should not happen with valid NSIDs)
62 pub fn is_empty(&self) -> bool {
63 self.segments.is_empty()
64 }
65}
66
67/// Parsed reference with NSID and optional fragment
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct RefPath<'a> {
70 nsid: &'a str,
71 def: &'a str,
72}
73
74impl<'a> RefPath<'a> {
75 /// Parse a reference string, normalizing it based on current NSID context
76 pub fn parse(ref_str: &'a str, current_nsid: Option<&'a str>) -> Self {
77 if let Some(fragment) = ref_str.strip_prefix('#') {
78 // Local ref: #option → use current_nsid
79 let nsid = current_nsid.unwrap_or("");
80 Self {
81 nsid,
82 def: fragment,
83 }
84 } else if let Some((nsid, def)) = ref_str.split_once('#') {
85 // Full ref with fragment: nsid#def
86 Self { nsid, def }
87 } else {
88 // Full ref without fragment: nsid (implicit "main")
89 Self {
90 nsid: ref_str,
91 def: "main",
92 }
93 }
94 }
95
96 /// Get the NSID portion of the ref
97 pub fn nsid(&self) -> &str {
98 self.nsid
99 }
100
101 /// Get the def name (fragment) portion of the ref
102 pub fn def(&self) -> &str {
103 self.def
104 }
105
106 /// Check if this is a local ref (was parsed from #fragment)
107 pub fn is_local(&self, current_nsid: &str) -> bool {
108 self.nsid == current_nsid && self.def != "main"
109 }
110
111 /// Get the full ref string (nsid#def)
112 pub fn full_ref(&self) -> String {
113 if self.def == "main" {
114 self.nsid.to_string()
115 } else {
116 format!("{}#{}", self.nsid, self.def)
117 }
118 }
119
120 /// Normalize a local ref by prepending the current NSID if needed
121 /// Returns the normalized ref string suitable for corpus lookup
122 pub fn normalize(ref_str: &str, current_nsid: &str) -> String {
123 if ref_str.starts_with('#') {
124 format!("{}{}", current_nsid, ref_str)
125 } else {
126 ref_str.to_string()
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_nsid_path_parse() {
137 let path = NsidPath::parse("com.atproto.repo.strongRef");
138 assert_eq!(path.segments(), &["com", "atproto", "repo", "strongRef"]);
139 assert_eq!(path.namespace(), "com.atproto");
140 assert_eq!(path.last_segment(), "strongRef");
141 assert_eq!(path.parent_segments(), &["com", "atproto", "repo"]);
142 assert!(!path.is_defs());
143 }
144
145 #[test]
146 fn test_nsid_path_defs() {
147 let path = NsidPath::parse("com.atproto.label.defs");
148 assert!(path.is_defs());
149 assert_eq!(path.last_segment(), "defs");
150 }
151
152 #[test]
153 fn test_ref_path_local() {
154 let ref_path = RefPath::parse("#option", Some("com.example.foo"));
155 assert_eq!(ref_path.nsid(), "com.example.foo");
156 assert_eq!(ref_path.def(), "option");
157 assert!(ref_path.is_local("com.example.foo"));
158 assert_eq!(ref_path.full_ref(), "com.example.foo#option");
159 }
160
161 #[test]
162 fn test_ref_path_with_fragment() {
163 let ref_path = RefPath::parse("com.example.foo#bar", None);
164 assert_eq!(ref_path.nsid(), "com.example.foo");
165 assert_eq!(ref_path.def(), "bar");
166 assert!(!ref_path.is_local("com.other.baz"));
167 assert_eq!(ref_path.full_ref(), "com.example.foo#bar");
168 }
169
170 #[test]
171 fn test_ref_path_implicit_main() {
172 let ref_path = RefPath::parse("com.example.foo", None);
173 assert_eq!(ref_path.nsid(), "com.example.foo");
174 assert_eq!(ref_path.def(), "main");
175 assert_eq!(ref_path.full_ref(), "com.example.foo");
176 }
177
178 #[test]
179 fn test_ref_path_normalize() {
180 assert_eq!(
181 RefPath::normalize("#option", "com.example.foo"),
182 "com.example.foo#option"
183 );
184 assert_eq!(
185 RefPath::normalize("com.other.bar#baz", "com.example.foo"),
186 "com.other.bar#baz"
187 );
188 }
189}