A library for ATProtocol identities.

chore: atproto-lexicon error management

Changed files
+202 -72
crates
+158
crates/atproto-lexicon/src/errors.rs
··· 1 + //! Error types for the atproto-lexicon crate. 2 + //! 3 + //! This module defines all error types for lexicon operations including resolution, 4 + //! validation, and schema processing. All errors follow the project's error naming 5 + //! convention using globally unique identifiers. 6 + //! 7 + //! ## Error Categories 8 + //! 9 + //! - **`LexiconResolveError`** (lexicon-resolve-1 to lexicon-resolve-6): Errors during lexicon resolution 10 + //! - **`LexiconValidationError`** (lexicon-validation-1 to lexicon-validation-8): Validation errors for NSIDs and schemas 11 + //! - **`LexiconSchemaError`** (lexicon-schema-1 to lexicon-schema-4): Schema parsing and structure errors 12 + //! 13 + //! ## Error Format 14 + //! 15 + //! All errors follow the format: `error-atproto-lexicon-<domain>-<number> <message>: <details>` 16 + 17 + use thiserror::Error; 18 + use atproto_identity::errors::ResolveError; 19 + 20 + /// Errors that can occur during lexicon resolution operations. 21 + #[derive(Debug, Error)] 22 + pub enum LexiconResolveError { 23 + /// No DIDs found when resolving a handle or identifier. 24 + #[error("error-atproto-lexicon-resolve-1 No DIDs found for resolution")] 25 + NoDIDsFound, 26 + 27 + /// Multiple DIDs found when expecting exactly one. 28 + #[error("error-atproto-lexicon-resolve-2 Multiple DIDs found: expected single DID")] 29 + MultipleDIDsFound, 30 + 31 + /// Invalid DID format encountered during resolution. 32 + #[error("error-atproto-lexicon-resolve-3 Invalid DID format: {did}")] 33 + InvalidDIDFormat { 34 + /// The invalid DID string 35 + did: String, 36 + }, 37 + 38 + /// No PDS endpoint found in DID document. 39 + #[error("error-atproto-lexicon-resolve-4 No PDS endpoint found in DID document")] 40 + NoPDSEndpoint, 41 + 42 + /// Failed to fetch lexicon from PDS. 43 + #[error("error-atproto-lexicon-resolve-5 Failed to fetch lexicon from PDS: {details}")] 44 + PDSFetchFailed { 45 + /// Details about the fetch failure 46 + details: String, 47 + }, 48 + 49 + /// Error response from PDS when fetching lexicon. 50 + #[error("error-atproto-lexicon-resolve-6 Error fetching lexicon for {nsid}: {message}")] 51 + PDSErrorResponse { 52 + /// The NSID being fetched 53 + nsid: String, 54 + /// Error message from PDS 55 + message: String, 56 + }, 57 + } 58 + 59 + /// Errors that can occur during lexicon validation. 60 + #[derive(Debug, Error)] 61 + pub enum LexiconValidationError { 62 + /// Invalid NSID format. 63 + #[error("error-atproto-lexicon-validation-1 Invalid NSID format: {details}")] 64 + InvalidNsidFormat { 65 + /// Details about what makes the NSID invalid 66 + details: String, 67 + }, 68 + 69 + /// Invalid reference format. 70 + #[error("error-atproto-lexicon-validation-2 Invalid reference format: {details}")] 71 + InvalidReferenceFormat { 72 + /// Details about what makes the reference invalid 73 + details: String, 74 + }, 75 + 76 + /// NSID has insufficient parts (requires at least 3). 77 + #[error("error-atproto-lexicon-validation-3 NSID must have at least 3 parts: {nsid}")] 78 + InsufficientNsidParts { 79 + /// The NSID with insufficient parts 80 + nsid: String, 81 + }, 82 + 83 + /// Cannot convert NSID to DNS name. 84 + #[error("error-atproto-lexicon-validation-4 Cannot convert NSID to DNS name: {nsid}")] 85 + InvalidDnsNameConversion { 86 + /// The NSID that couldn't be converted 87 + nsid: String, 88 + }, 89 + 90 + /// Empty NSID provided. 91 + #[error("error-atproto-lexicon-validation-5 Empty NSID provided")] 92 + EmptyNsid, 93 + 94 + /// NSID contains empty parts. 95 + #[error("error-atproto-lexicon-validation-6 NSID contains empty parts: {nsid}")] 96 + EmptyNsidParts { 97 + /// The NSID with empty parts 98 + nsid: String, 99 + }, 100 + 101 + /// Invalid NSID character. 102 + #[error("error-atproto-lexicon-validation-7 Invalid character in NSID: {details}")] 103 + InvalidNsidCharacter { 104 + /// Details about the invalid character 105 + details: String, 106 + }, 107 + 108 + /// Invalid NSID in schema ID field. 109 + #[error("error-atproto-lexicon-validation-8 Invalid NSID in schema ID field: {id}")] 110 + InvalidSchemaId { 111 + /// The invalid ID from the schema 112 + id: String, 113 + }, 114 + } 115 + 116 + /// Errors that can occur when processing lexicon schemas. 117 + #[derive(Debug, Error)] 118 + pub enum LexiconSchemaError { 119 + /// Lexicon schema must be an object. 120 + #[error("error-atproto-lexicon-schema-1 Lexicon schema must be an object")] 121 + NotAnObject, 122 + 123 + /// Missing required 'lexicon' version field. 124 + #[error("error-atproto-lexicon-schema-2 Missing 'lexicon' version field")] 125 + MissingLexiconVersion, 126 + 127 + /// Missing or invalid 'id' field. 128 + #[error("error-atproto-lexicon-schema-3 Missing or invalid 'id' field")] 129 + MissingOrInvalidId, 130 + 131 + /// Missing or invalid 'defs' field. 132 + #[error("error-atproto-lexicon-schema-4 Missing or invalid 'defs' field")] 133 + MissingOrInvalidDefs, 134 + } 135 + 136 + /// Errors specific to recursive lexicon resolution. 137 + #[derive(Debug, Error)] 138 + pub enum LexiconRecursiveError { 139 + /// Failed to resolve any lexicons during recursive resolution. 140 + #[error("error-atproto-lexicon-recursive-1 Failed to resolve any lexicons")] 141 + NoLexiconsResolved, 142 + } 143 + 144 + // Re-export the validation error for backwards compatibility during migration 145 + pub use LexiconValidationError as ValidationError; 146 + 147 + /// Implement conversion from ResolveError to LexiconResolveError. 148 + impl From<ResolveError> for LexiconResolveError { 149 + fn from(err: ResolveError) -> Self { 150 + match err { 151 + ResolveError::NoDIDsFound => LexiconResolveError::NoDIDsFound, 152 + ResolveError::MultipleDIDsFound => LexiconResolveError::MultipleDIDsFound, 153 + _ => LexiconResolveError::PDSFetchFailed { 154 + details: format!("DNS resolution error: {:?}", err), 155 + }, 156 + } 157 + } 158 + }
+1
crates/atproto-lexicon/src/lib.rs
··· 6 6 #![forbid(unsafe_code)] 7 7 #![warn(missing_docs)] 8 8 9 + pub mod errors; 9 10 pub mod resolve; 10 11 pub mod resolve_recursive; 11 12 pub mod validation;
+16 -10
crates/atproto-lexicon/src/resolve.rs
··· 10 10 //! 4. Extract PDS endpoint from DID document 11 11 //! 5. Make XRPC call to com.atproto.repo.getRecord to fetch lexicon 12 12 13 - use anyhow::{anyhow, Result}; 13 + use anyhow::Result; 14 14 use atproto_client::{ 15 15 client::Auth, 16 16 com::atproto::repo::{get_record, GetRecordResponse}, 17 17 }; 18 18 use atproto_identity::{ 19 - errors::ResolveError, 20 19 resolve::{DnsResolver, resolve_subject}, 21 20 }; 22 21 use serde_json::Value; 23 22 use tracing::instrument; 24 23 25 - use crate::validation; 24 + use crate::{errors::LexiconResolveError, validation}; 26 25 27 26 /// Trait for lexicon resolution implementations. 28 27 #[async_trait::async_trait] ··· 79 78 pub async fn resolve_lexicon_dns<R: DnsResolver + ?Sized>( 80 79 dns_resolver: &R, 81 80 lookup_dns: &str, 82 - ) -> Result<String, ResolveError> { 81 + ) -> Result<String, LexiconResolveError> { 83 82 let txt_records = dns_resolver 84 83 .resolve_txt(lookup_dns) 85 84 .await?; ··· 104 103 .collect(); 105 104 106 105 if dids.is_empty() { 107 - return Err(ResolveError::NoDIDsFound); 106 + return Err(LexiconResolveError::NoDIDsFound); 108 107 } 109 108 110 109 if dids.len() > 1 { 111 - return Err(ResolveError::MultipleDIDsFound); 110 + return Err(LexiconResolveError::MultipleDIDsFound); 112 111 } 113 112 114 113 Ok(dids[0].clone()) ··· 127 126 InputType::Web(did) => { 128 127 web::query(http_client, &did).await? 129 128 } 130 - _ => return Err(anyhow!("Invalid DID format: {}", did)), 129 + _ => return Err(LexiconResolveError::InvalidDIDFormat { 130 + did: did.to_string(), 131 + }.into()), 131 132 }; 132 133 133 134 // Extract PDS endpoint from service array ··· 137 138 } 138 139 } 139 140 140 - Err(anyhow!("No PDS endpoint found in DID document")) 141 + Err(LexiconResolveError::NoPDSEndpoint.into()) 141 142 } 142 143 143 144 /// Fetch lexicon schema from PDS using XRPC. ··· 164 165 None, 165 166 ) 166 167 .await 167 - .map_err(|e| anyhow!("Failed to fetch lexicon from PDS: {}", e))?; 168 + .map_err(|e| LexiconResolveError::PDSFetchFailed { 169 + details: e.to_string(), 170 + })?; 168 171 169 172 // Extract the value from the response 170 173 match response { 171 174 GetRecordResponse::Record { value, .. } => Ok(value), 172 175 GetRecordResponse::Error(err) => { 173 176 let msg = err.message.or(err.error_description).or(err.error).unwrap_or_else(|| "Unknown error".to_string()); 174 - Err(anyhow!("Error fetching lexicon for {}: {}", nsid, msg)) 177 + Err(LexiconResolveError::PDSErrorResponse { 178 + nsid: nsid.to_string(), 179 + message: msg, 180 + }.into()) 175 181 } 176 182 } 177 183 }
+3 -2
crates/atproto-lexicon/src/resolve_recursive.rs
··· 5 5 6 6 use std::collections::{HashMap, HashSet}; 7 7 8 - use anyhow::{anyhow, Result}; 8 + use anyhow::Result; 9 9 use serde_json::Value; 10 10 use tracing::instrument; 11 11 12 + use crate::errors::LexiconRecursiveError; 12 13 use crate::resolve::LexiconResolver; 13 14 use crate::validation::{absolute, extract_nsid_from_ref_object}; 14 15 ··· 132 133 } 133 134 134 135 if resolved.is_empty() && self.config.include_entry { 135 - return Err(anyhow!("Failed to resolve any lexicons")); 136 + return Err(LexiconRecursiveError::NoLexiconsResolved.into()); 136 137 } 137 138 138 139 Ok(resolved)
+24 -60
crates/atproto-lexicon/src/validation.rs
··· 4 4 5 5 use std::fmt; 6 6 7 - use anyhow::{anyhow, Result}; 7 + use anyhow::Result; 8 8 use serde_json::Value; 9 - use thiserror::Error; 10 9 11 - /// Errors that can occur during lexicon validation. 12 - #[derive(Error, Debug)] 13 - pub enum ValidationError { 14 - /// Invalid NSID format. 15 - #[error("Invalid NSID format: {0}")] 16 - InvalidNsidFormat(String), 17 - 18 - /// Invalid reference format. 19 - #[error("Invalid reference format: {0}")] 20 - InvalidReferenceFormat(String), 21 - 22 - /// NSID has too few parts. 23 - #[error("NSID must have at least 3 parts: {0}")] 24 - InsufficientNsidParts(String), 25 - 26 - /// Invalid DNS name conversion. 27 - #[error("Cannot convert NSID to DNS name: {0}")] 28 - InvalidDnsNameConversion(String), 29 - } 10 + use crate::errors::{LexiconSchemaError, LexiconValidationError}; 30 11 31 12 /// Components of a parsed NSID. 32 13 #[derive(Debug, Clone, PartialEq)] ··· 39 20 } 40 21 41 22 impl NsidParts { 42 - /// Serializes the NSID parts back to a string. 43 - /// 44 - /// Joins the parts with dots and appends the fragment with '#' if present. 45 - /// 46 - /// # Examples 47 - /// ``` 48 - /// use atproto_lexicon::validation::NsidParts; 49 - /// 50 - /// let parts = NsidParts { 51 - /// parts: vec!["app".to_string(), "bsky".to_string(), "feed".to_string(), "post".to_string()], 52 - /// fragment: None, 53 - /// }; 54 - /// assert_eq!(parts.to_string(), "app.bsky.feed.post"); 55 - /// 56 - /// let parts_with_fragment = NsidParts { 57 - /// parts: vec!["app".to_string(), "bsky".to_string(), "feed".to_string(), "post".to_string()], 58 - /// fragment: Some("reply".to_string()), 59 - /// }; 60 - /// assert_eq!(parts_with_fragment.to_string(), "app.bsky.feed.post#reply"); 61 - /// ``` 62 - pub fn to_string(&self) -> String { 63 - let base = self.parts.join("."); 64 - match &self.fragment { 65 - Some(fragment) => format!("{}#{}", base, fragment), 66 - None => base, 67 - } 68 - } 69 23 } 70 24 71 25 impl fmt::Display for NsidParts { 72 26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 - write!(f, "{}", self.to_string()) 27 + let base = self.parts.join("."); 28 + match &self.fragment { 29 + Some(fragment) => write!(f, "{}#{}", base, fragment), 30 + None => write!(f, "{}", base), 31 + } 74 32 } 75 33 } 76 34 ··· 228 186 229 187 // Parse the NSID part 230 188 if nsid_part.is_empty() { 231 - return Err(ValidationError::InvalidNsidFormat("Empty NSID".to_string()).into()); 189 + return Err(LexiconValidationError::EmptyNsid.into()); 232 190 } 233 191 234 192 let parts: Vec<String> = nsid_part.split('.').map(|s| s.to_string()).collect(); 235 193 236 194 // Validate parts (at least 3 components for a valid NSID) 237 195 if parts.len() < 3 { 238 - return Err(ValidationError::InsufficientNsidParts(nsid.to_string()).into()); 196 + return Err(LexiconValidationError::InsufficientNsidParts { 197 + nsid: nsid.to_string(), 198 + }.into()); 239 199 } 240 200 241 201 if parts.iter().any(|p| p.is_empty()) { 242 - return Err(ValidationError::InvalidNsidFormat(format!("NSID contains empty parts: {}", nsid)).into()); 202 + return Err(LexiconValidationError::EmptyNsidParts { 203 + nsid: nsid.to_string(), 204 + }.into()); 243 205 } 244 206 245 207 // Handle fragment ··· 269 231 270 232 // Need at least 3 parts for a valid NSID (authority + name + record_type) 271 233 if parsed.parts.len() < 3 { 272 - return Err(ValidationError::InvalidNsidFormat( 273 - format!("NSID must have at least 3 parts: {}", nsid) 274 - ).into()); 234 + return Err(LexiconValidationError::InsufficientNsidParts { 235 + nsid: nsid.to_string(), 236 + }.into()); 275 237 } 276 238 277 239 // Build DNS name: _lexicon.<name>.<reversed-authority> ··· 363 325 /// - Well-formed definitions 364 326 pub fn validate_lexicon_schema(schema: &Value) -> Result<()> { 365 327 let obj = schema.as_object() 366 - .ok_or_else(|| anyhow!("Lexicon schema must be an object"))?; 328 + .ok_or(LexiconSchemaError::NotAnObject)?; 367 329 368 330 // Check lexicon version 369 331 if !obj.contains_key("lexicon") { 370 - return Err(anyhow!("Missing 'lexicon' version field")); 332 + return Err(LexiconSchemaError::MissingLexiconVersion.into()); 371 333 } 372 334 373 335 // Check and validate ID 374 336 let id = obj.get("id") 375 337 .and_then(|v| v.as_str()) 376 - .ok_or_else(|| anyhow!("Missing or invalid 'id' field"))?; 338 + .ok_or(LexiconSchemaError::MissingOrInvalidId)?; 377 339 378 340 if !is_valid_nsid(id) { 379 - return Err(ValidationError::InvalidNsidFormat(id.to_string()).into()); 341 + return Err(LexiconValidationError::InvalidSchemaId { 342 + id: id.to_string(), 343 + }.into()); 380 344 } 381 345 382 346 // Check defs exists and is an object 383 347 obj.get("defs") 384 348 .and_then(|v| v.as_object()) 385 - .ok_or_else(|| anyhow!("Missing or invalid 'defs' field"))?; 349 + .ok_or(LexiconSchemaError::MissingOrInvalidDefs)?; 386 350 387 351 Ok(()) 388 352 }