atproto-lexicon#
AT Protocol lexicon resolution and validation library for Rust.
Overview#
This 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:
- Convert NSID to DNS name with
_lexiconprefix - Perform DNS TXT lookup to get the authoritative DID
- Resolve the DID to get the DID document
- Extract PDS endpoint from the DID document
- Make XRPC call to fetch the lexicon schema
Features#
- Lexicon Resolution: Resolve NSIDs to their schema definitions via DNS and XRPC
- Recursive Resolution: Automatically resolve referenced lexicons with configurable depth limits
- NSID Validation: Comprehensive validation of Namespace Identifiers
- Reference Extraction: Extract and resolve lexicon references including fragment-only references
- Context-Aware Resolution: Handle fragment-only references using lexicon ID as context
- CLI Tool: Command-line interface for lexicon resolution
Installation#
Add this to your Cargo.toml:
[dependencies]
atproto-lexicon = "0.13.0"
Usage#
Basic Lexicon Resolution#
use atproto_lexicon::resolve::{DefaultLexiconResolver, LexiconResolver};
use atproto_identity::resolve::HickoryDnsResolver;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let http_client = reqwest::Client::new();
let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
let resolver = DefaultLexiconResolver::new(http_client, dns_resolver);
// Resolve a single lexicon
let lexicon = resolver.resolve("app.bsky.feed.post").await?;
println!("Resolved lexicon: {}", serde_json::to_string_pretty(&lexicon)?);
Ok(())
}
Recursive Resolution#
use atproto_lexicon::resolve_recursive::{RecursiveLexiconResolver, RecursiveResolverConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let http_client = reqwest::Client::new();
let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
let resolver = DefaultLexiconResolver::new(http_client, dns_resolver);
let config = RecursiveResolverConfig {
max_depth: 5, // Maximum recursion depth
include_entry: true, // Include the entry lexicon in results
};
let recursive_resolver = RecursiveLexiconResolver::with_config(resolver, config);
// Resolve a lexicon and all its dependencies
let lexicons = recursive_resolver.resolve_recursive("app.bsky.feed.post").await?;
for (nsid, schema) in lexicons {
println!("Resolved {}: {} bytes", nsid,
serde_json::to_string(&schema)?.len());
}
Ok(())
}
NSID Validation#
use atproto_lexicon::validation::{
is_valid_nsid, parse_nsid, nsid_to_dns_name, absolute
};
// Validate NSIDs
assert!(is_valid_nsid("app.bsky.feed.post"));
assert!(!is_valid_nsid("invalid"));
// Parse NSID components
let parts = parse_nsid("app.bsky.feed.post#reply", None)?;
assert_eq!(parts.parts, vec!["app", "bsky", "feed", "post"]);
assert_eq!(parts.fragment, Some("reply".to_string()));
// Convert parsed NSID back to string using Display trait
println!("Parsed NSID: {}", parts); // Outputs: app.bsky.feed.post#reply
// Convert NSID to DNS name for resolution
let dns_name = nsid_to_dns_name("app.bsky.feed.post")?;
assert_eq!(dns_name, "_lexicon.feed.bsky.app");
// Make fragment-only references absolute
assert_eq!(absolute("app.bsky.feed.post", "#reply"), "app.bsky.feed.post#reply");
assert_eq!(absolute("app.bsky.feed.post", "com.example.other"), "com.example.other");
Extract Lexicon References#
use atproto_lexicon::resolve_recursive::extract_lexicon_references;
use serde_json::json;
let schema = json!({
"lexicon": 1,
"id": "app.bsky.feed.post",
"defs": {
"main": {
"type": "record",
"record": {
"type": "object",
"properties": {
"embed": {
"type": "union",
"refs": [
{ "type": "ref", "ref": "app.bsky.embed.images" },
{ "type": "ref", "ref": "#localref" } // Fragment reference
]
}
}
}
}
}
});
let references = extract_lexicon_references(&schema);
// References will include:
// - "app.bsky.embed.images" (external reference)
// - "app.bsky.feed.post" (from #localref using the lexicon's id as context)
CLI Tool#
The crate includes a command-line tool for lexicon resolution:
# Build with CLI support
cargo build --features clap --bin atproto-lexicon-resolve
# Resolve a single lexicon
cargo run --features clap --bin atproto-lexicon-resolve -- app.bsky.feed.post
# Pretty print the output
cargo run --features clap --bin atproto-lexicon-resolve -- --pretty app.bsky.feed.post
# Recursively resolve all referenced lexicons
cargo run --features clap --bin atproto-lexicon-resolve -- --recursive app.bsky.feed.post
# Limit recursion depth
cargo run --features clap --bin atproto-lexicon-resolve -- --recursive --max-depth 3 app.bsky.feed.post
# Show dependency graph
cargo run --features clap --bin atproto-lexicon-resolve -- --recursive --show-deps app.bsky.feed.post
# List only direct references
cargo run --features clap --bin atproto-lexicon-resolve -- --list-refs app.bsky.feed.post
Module Structure#
errors: Structured error types for all lexicon operationsresolve: Core lexicon resolution implementation following AT Protocol specificationresolve_recursive: Recursive resolution with dependency tracking and cycle detectionvalidation: NSID validation, parsing, and helper functions
Key Types#
NsidParts#
Represents a parsed NSID with its component parts and optional fragment:
parts: Vector of NSID components (e.g.,["app", "bsky", "feed", "post"])fragment: Optional fragment identifier (e.g.,"reply"for#reply)
Implements Display trait for converting back to string format.
RecursiveResolverConfig#
Configuration for recursive resolution:
max_depth: Maximum recursion depth (default: 10)include_entry: Whether to include the entry lexicon in results (default: true)
RecursiveResolutionResult#
Detailed results from recursive resolution:
lexicons: HashMap of resolved lexicons by NSIDfailed: Set of NSIDs that couldn't be resolveddependencies: Dependency graph showing which lexicons reference which
Features#
- Fragment-Only Reference Resolution: Automatically resolves fragment-only references (e.g.,
#localref) using the lexicon'sidfield as context - Union Type Support: Extracts references from both
refobjects anduniontypes withrefsarrays - DNS-based Discovery: Implements the AT Protocol DNS-based lexicon discovery mechanism
- Cycle Detection: Prevents infinite recursion when resolving circular dependencies
- Validation: Comprehensive NSID validation following AT Protocol specifications
Error Handling#
The library uses structured error types following the project convention error-atproto-lexicon-<domain>-<number>:
LexiconResolveError: Resolution errors (no DIDs found, invalid DID format, PDS errors)LexiconValidationError: NSID format and validation errorsLexiconSchemaError: Schema structure and parsing errorsLexiconRecursiveError: Errors specific to recursive resolution
All errors implement the Error trait and provide detailed context about failures.
Dependencies#
atproto-identity: For DID resolution and DNS operationsatproto-client: For XRPC communicationserde_json: For JSON schema handlingasync-trait: For async trait definitionstracing: For structured logging
License#
This project is part of the atproto-identity-rs workspace. See the root LICENSE file for details.
Contributing#
Contributions are welcome! Please feel free to submit a Pull Request.