+158
crates/atproto-lexicon/src/errors.rs
+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
+1
crates/atproto-lexicon/src/lib.rs
+16
-10
crates/atproto-lexicon/src/resolve.rs
+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
+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
+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
}