A better Rust ATProto crate
at main 189 lines 5.6 kB view raw
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}