A library for ATProtocol identities.
1# atproto-lexicon
2
3AT Protocol lexicon resolution and validation library for Rust.
4
5## Overview
6
7This library provides functionality for resolving and validating AT Protocol lexicons, which define the schema and structure of AT Protocol data. It implements the full lexicon resolution chain as specified by the AT Protocol:
8
91. Convert NSID to DNS name with `_lexicon` prefix
102. Perform DNS TXT lookup to get the authoritative DID
113. Resolve the DID to get the DID document
124. Extract PDS endpoint from the DID document
135. Make XRPC call to fetch the lexicon schema
14
15## Features
16
17- **Lexicon Resolution**: Resolve NSIDs to their schema definitions via DNS and XRPC
18- **Recursive Resolution**: Automatically resolve referenced lexicons with configurable depth limits
19- **NSID Validation**: Comprehensive validation of Namespace Identifiers
20- **Reference Extraction**: Extract and resolve lexicon references including fragment-only references
21- **Context-Aware Resolution**: Handle fragment-only references using lexicon ID as context
22- **CLI Tool**: Command-line interface for lexicon resolution
23
24## Installation
25
26Add this to your `Cargo.toml`:
27
28```toml
29[dependencies]
30atproto-lexicon = "0.13.0"
31```
32
33## Usage
34
35### Basic Lexicon Resolution
36
37```rust
38use atproto_lexicon::resolve::{DefaultLexiconResolver, LexiconResolver};
39use atproto_identity::resolve::HickoryDnsResolver;
40
41#[tokio::main]
42async fn main() -> anyhow::Result<()> {
43 let http_client = reqwest::Client::new();
44 let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
45
46 let resolver = DefaultLexiconResolver::new(http_client, dns_resolver);
47
48 // Resolve a single lexicon
49 let lexicon = resolver.resolve("app.bsky.feed.post").await?;
50 println!("Resolved lexicon: {}", serde_json::to_string_pretty(&lexicon)?);
51
52 Ok(())
53}
54```
55
56### Recursive Resolution
57
58```rust
59use atproto_lexicon::resolve_recursive::{RecursiveLexiconResolver, RecursiveResolverConfig};
60
61#[tokio::main]
62async fn main() -> anyhow::Result<()> {
63 let http_client = reqwest::Client::new();
64 let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
65 let resolver = DefaultLexiconResolver::new(http_client, dns_resolver);
66
67 let config = RecursiveResolverConfig {
68 max_depth: 5, // Maximum recursion depth
69 include_entry: true, // Include the entry lexicon in results
70 };
71
72 let recursive_resolver = RecursiveLexiconResolver::with_config(resolver, config);
73
74 // Resolve a lexicon and all its dependencies
75 let lexicons = recursive_resolver.resolve_recursive("app.bsky.feed.post").await?;
76
77 for (nsid, schema) in lexicons {
78 println!("Resolved {}: {} bytes", nsid,
79 serde_json::to_string(&schema)?.len());
80 }
81
82 Ok(())
83}
84```
85
86### NSID Validation
87
88```rust
89use atproto_lexicon::validation::{
90 is_valid_nsid, parse_nsid, nsid_to_dns_name, absolute
91};
92
93// Validate NSIDs
94assert!(is_valid_nsid("app.bsky.feed.post"));
95assert!(!is_valid_nsid("invalid"));
96
97// Parse NSID components
98let parts = parse_nsid("app.bsky.feed.post#reply", None)?;
99assert_eq!(parts.parts, vec!["app", "bsky", "feed", "post"]);
100assert_eq!(parts.fragment, Some("reply".to_string()));
101
102// Convert parsed NSID back to string using Display trait
103println!("Parsed NSID: {}", parts); // Outputs: app.bsky.feed.post#reply
104
105// Convert NSID to DNS name for resolution
106let dns_name = nsid_to_dns_name("app.bsky.feed.post")?;
107assert_eq!(dns_name, "_lexicon.feed.bsky.app");
108
109// Make fragment-only references absolute
110assert_eq!(absolute("app.bsky.feed.post", "#reply"), "app.bsky.feed.post#reply");
111assert_eq!(absolute("app.bsky.feed.post", "com.example.other"), "com.example.other");
112```
113
114### Extract Lexicon References
115
116```rust
117use atproto_lexicon::resolve_recursive::extract_lexicon_references;
118use serde_json::json;
119
120let schema = json!({
121 "lexicon": 1,
122 "id": "app.bsky.feed.post",
123 "defs": {
124 "main": {
125 "type": "record",
126 "record": {
127 "type": "object",
128 "properties": {
129 "embed": {
130 "type": "union",
131 "refs": [
132 { "type": "ref", "ref": "app.bsky.embed.images" },
133 { "type": "ref", "ref": "#localref" } // Fragment reference
134 ]
135 }
136 }
137 }
138 }
139 }
140});
141
142let references = extract_lexicon_references(&schema);
143// References will include:
144// - "app.bsky.embed.images" (external reference)
145// - "app.bsky.feed.post" (from #localref using the lexicon's id as context)
146```
147
148## CLI Tool
149
150The crate includes a command-line tool for lexicon resolution:
151
152```bash
153# Build with CLI support
154cargo build --features clap --bin atproto-lexicon-resolve
155
156# Resolve a single lexicon
157cargo run --features clap --bin atproto-lexicon-resolve -- app.bsky.feed.post
158
159# Pretty print the output
160cargo run --features clap --bin atproto-lexicon-resolve -- --pretty app.bsky.feed.post
161
162# Recursively resolve all referenced lexicons
163cargo run --features clap --bin atproto-lexicon-resolve -- --recursive app.bsky.feed.post
164
165# Limit recursion depth
166cargo run --features clap --bin atproto-lexicon-resolve -- --recursive --max-depth 3 app.bsky.feed.post
167
168# Show dependency graph
169cargo run --features clap --bin atproto-lexicon-resolve -- --recursive --show-deps app.bsky.feed.post
170
171# List only direct references
172cargo run --features clap --bin atproto-lexicon-resolve -- --list-refs app.bsky.feed.post
173```
174
175## Module Structure
176
177- **`errors`**: Structured error types for all lexicon operations
178- **`resolve`**: Core lexicon resolution implementation following AT Protocol specification
179- **`resolve_recursive`**: Recursive resolution with dependency tracking and cycle detection
180- **`validation`**: NSID validation, parsing, and helper functions
181
182## Key Types
183
184### `NsidParts`
185Represents a parsed NSID with its component parts and optional fragment:
186- `parts`: Vector of NSID components (e.g., `["app", "bsky", "feed", "post"]`)
187- `fragment`: Optional fragment identifier (e.g., `"reply"` for `#reply`)
188
189Implements `Display` trait for converting back to string format.
190
191### `RecursiveResolverConfig`
192Configuration for recursive resolution:
193- `max_depth`: Maximum recursion depth (default: 10)
194- `include_entry`: Whether to include the entry lexicon in results (default: true)
195
196### `RecursiveResolutionResult`
197Detailed results from recursive resolution:
198- `lexicons`: HashMap of resolved lexicons by NSID
199- `failed`: Set of NSIDs that couldn't be resolved
200- `dependencies`: Dependency graph showing which lexicons reference which
201
202## Features
203
204- **Fragment-Only Reference Resolution**: Automatically resolves fragment-only references (e.g., `#localref`) using the lexicon's `id` field as context
205- **Union Type Support**: Extracts references from both `ref` objects and `union` types with `refs` arrays
206- **DNS-based Discovery**: Implements the AT Protocol DNS-based lexicon discovery mechanism
207- **Cycle Detection**: Prevents infinite recursion when resolving circular dependencies
208- **Validation**: Comprehensive NSID validation following AT Protocol specifications
209
210## Error Handling
211
212The library uses structured error types following the project convention `error-atproto-lexicon-<domain>-<number>`:
213
214- **`LexiconResolveError`**: Resolution errors (no DIDs found, invalid DID format, PDS errors)
215- **`LexiconValidationError`**: NSID format and validation errors
216- **`LexiconSchemaError`**: Schema structure and parsing errors
217- **`LexiconRecursiveError`**: Errors specific to recursive resolution
218
219All errors implement the `Error` trait and provide detailed context about failures.
220
221## Dependencies
222
223- `atproto-identity`: For DID resolution and DNS operations
224- `atproto-client`: For XRPC communication
225- `serde_json`: For JSON schema handling
226- `async-trait`: For async trait definitions
227- `tracing`: For structured logging
228
229## License
230
231This project is part of the atproto-identity-rs workspace. See the root LICENSE file for details.
232
233## Contributing
234
235Contributions are welcome! Please feel free to submit a Pull Request.