A human-friendly DSL for ATProto Lexicons

Update docs

+234 -16
+137
mlf-lexicon-fetcher/README.md
··· 1 + # mlf-lexicon-fetcher 2 + 3 + ATProto Lexicon fetcher with DNS resolution and HTTP client. 4 + 5 + Resolves lexicon NSIDs to DIDs via DNS TXT records and fetches lexicon JSON from ATProto repositories. 6 + 7 + ## Features 8 + 9 + - **DNS Resolution**: Resolves NSIDs to DIDs using DNS TXT records (RFC spec) 10 + - **HTTP Fetching**: Fetches lexicons from ATProto repositories via XRPC 11 + - **Pattern Matching**: Supports wildcard patterns (`.*` and `._`) 12 + - **Network Optimization**: Groups similar NSIDs to reduce HTTP requests 13 + - **Lockfile Support**: Fetches from known DIDs, bypassing DNS resolution 14 + - **Testable**: Mock DNS and HTTP clients for testing 15 + 16 + ## Usage 17 + 18 + ### Basic Fetching 19 + 20 + ```rust 21 + use mlf_lexicon_fetcher::ProductionLexiconFetcher; 22 + 23 + #[tokio::main] 24 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 25 + // Create a production fetcher (real DNS + HTTP) 26 + let fetcher = ProductionLexiconFetcher::production().await?; 27 + 28 + // Fetch a single lexicon with metadata 29 + let result = fetcher.fetch_with_metadata("app.bsky.feed.post").await?; 30 + 31 + for lexicon in result.lexicons { 32 + println!("NSID: {}", lexicon.nsid); 33 + println!("DID: {}", lexicon.did); 34 + println!("Lexicon: {}", serde_json::to_string_pretty(&lexicon.lexicon)?); 35 + } 36 + 37 + Ok(()) 38 + } 39 + ``` 40 + 41 + ### Pattern Matching 42 + 43 + ```rust 44 + // Fetch all lexicons under a namespace 45 + let result = fetcher.fetch_with_metadata("app.bsky.feed.*").await?; 46 + println!("Fetched {} lexicons", result.lexicons.len()); 47 + 48 + // Fetch direct children only 49 + let result = fetcher.fetch_with_metadata("app.bsky._").await?; 50 + ``` 51 + 52 + ### Optimized Batch Fetching 53 + 54 + ```rust 55 + use mlf_lexicon_fetcher::ProductionLexiconFetcher; 56 + 57 + // Fetch many NSIDs with automatic optimization 58 + let nsids = vec![ 59 + "app.bsky.actor.profile".to_string(), 60 + "app.bsky.actor.defs".to_string(), 61 + "app.bsky.feed.post".to_string(), 62 + "app.bsky.feed.like".to_string(), 63 + ]; 64 + 65 + // Automatically groups into patterns like "app.bsky.actor.*" and "app.bsky.feed.*" 66 + let results = fetcher.fetch_many_optimized(&nsids).await?; 67 + ``` 68 + 69 + ### Lockfile Mode (Skip DNS) 70 + 71 + ```rust 72 + // When you already have the DID (e.g., from a lockfile) 73 + let result = fetcher.fetch_from_did_with_metadata( 74 + "did:plc:abc123", 75 + "app.bsky.feed.post" 76 + ).await?; 77 + ``` 78 + 79 + ### Advanced: Pattern Optimization 80 + 81 + ```rust 82 + use std::collections::HashSet; 83 + use mlf_lexicon_fetcher::optimize_fetch_patterns; 84 + 85 + let nsids: HashSet<String> = vec![ 86 + "app.bsky.actor.foo".to_string(), 87 + "app.bsky.actor.bar".to_string(), 88 + "app.bsky.feed.post".to_string(), 89 + ].into_iter().collect(); 90 + 91 + // Returns: ["app.bsky.actor.*", "app.bsky.feed.post"] 92 + let optimized = optimize_fetch_patterns(&nsids); 93 + ``` 94 + 95 + ## API 96 + 97 + ### Main Types 98 + 99 + - `ProductionLexiconFetcher` - Ready-to-use fetcher with real DNS and HTTP 100 + - `LexiconFetcher<D, H>` - Generic fetcher (for custom DNS/HTTP implementations) 101 + - `FetchResult` - Contains fetched lexicons with metadata (NSID, DID, JSON) 102 + 103 + ### Key Methods 104 + 105 + - `fetch_with_metadata(nsid)` - Fetch single/pattern, returns metadata 106 + - `fetch_many(nsids)` - Fetch multiple NSIDs sequentially 107 + - `fetch_many_optimized(nsids)` - Fetch multiple NSIDs with pattern optimization 108 + - `fetch_from_did_with_metadata(did, nsid)` - Skip DNS, fetch from known DID 109 + 110 + ### Utilities 111 + 112 + - `optimize_fetch_patterns(nsids)` - Group NSIDs into minimal patterns 113 + - `parse_nsid(nsid)` - Parse NSID into authority and name segments 114 + - `construct_dns_name(authority, name)` - Build DNS TXT record name 115 + 116 + ## Testing 117 + 118 + Use mock implementations for testing: 119 + 120 + ```rust 121 + use mlf_lexicon_fetcher::{LexiconFetcher, MockDnsResolver, MockHttpClient}; 122 + 123 + let mut dns = MockDnsResolver::new(); 124 + dns.add_record("app.bsky", "feed", "did:plc:test".to_string()); 125 + 126 + let mut http = MockHttpClient::new(); 127 + http.add_lexicon( 128 + "app.bsky.feed.post".to_string(), 129 + serde_json::json!({"lexicon": 1, "id": "app.bsky.feed.post"}) 130 + ); 131 + 132 + let fetcher = LexiconFetcher::new(dns, http); 133 + ``` 134 + 135 + ## License 136 + 137 + MIT
+75 -13
mlf-lexicon-fetcher/examples/usage.rs
··· 5 5 6 6 #[tokio::main] 7 7 async fn main() -> Result<(), Box<dyn std::error::Error>> { 8 - // Example 1: Fetch a single lexicon 9 - println!("=== Example 1: Fetch Single Lexicon ==="); 8 + // Example 1: Fetch with metadata (NEW!) 9 + println!("=== Example 1: Fetch with Metadata ==="); 10 10 11 11 let mut dns_resolver = MockDnsResolver::new(); 12 12 dns_resolver.add_record("place.stream", "chat.profile", "did:plc:test123".to_string()); ··· 28 28 29 29 let fetcher = LexiconFetcher::new(dns_resolver, http_client); 30 30 31 - match fetcher.fetch("place.stream.chat.profile").await { 32 - Ok(lexicon) => { 33 - println!("Successfully fetched lexicon:"); 34 - println!("{}", serde_json::to_string_pretty(&lexicon)?); 31 + // New API returns metadata (DID, NSID, lexicon) 32 + match fetcher.fetch_with_metadata("place.stream.chat.profile").await { 33 + Ok(result) => { 34 + println!("Successfully fetched {} lexicon(s):", result.lexicons.len()); 35 + for fetched in result.lexicons { 36 + println!(" NSID: {}", fetched.nsid); 37 + println!(" DID: {}", fetched.did); 38 + println!(" Lexicon: {}", serde_json::to_string_pretty(&fetched.lexicon)?); 39 + } 35 40 } 36 41 Err(e) => eprintln!("Error: {}", e), 37 42 } 38 43 39 - // Example 2: Fetch multiple lexicons with a pattern 40 - println!("\n=== Example 2: Fetch Multiple Lexicons with Pattern ==="); 44 + // Example 2: Fetch multiple with pattern 45 + println!("\n=== Example 2: Fetch Multiple with Pattern ==="); 41 46 42 47 let mut dns_resolver2 = MockDnsResolver::new(); 43 48 dns_resolver2.add_record("app.bsky", "feed", "did:plc:bsky123".to_string()); ··· 58 63 59 64 let fetcher2 = LexiconFetcher::new(dns_resolver2, http_client2); 60 65 61 - match fetcher2.fetch_pattern("app.bsky.feed.*").await { 62 - Ok(lexicons) => { 63 - println!("Successfully fetched {} lexicons:", lexicons.len()); 64 - for (nsid, _lexicon) in lexicons { 65 - println!(" - {}", nsid); 66 + match fetcher2.fetch_with_metadata("app.bsky.feed.*").await { 67 + Ok(result) => { 68 + println!("Successfully fetched {} lexicons:", result.lexicons.len()); 69 + for fetched in result.lexicons { 70 + println!(" - {} (from {})", fetched.nsid, fetched.did); 71 + } 72 + } 73 + Err(e) => eprintln!("Error: {}", e), 74 + } 75 + 76 + // Example 3: Optimized batch fetching (NEW!) 77 + println!("\n=== Example 3: Optimized Batch Fetching ==="); 78 + 79 + let mut dns_resolver3 = MockDnsResolver::new(); 80 + dns_resolver3.add_record("app.bsky", "actor", "did:plc:bsky123".to_string()); 81 + 82 + let mut http_client3 = MockHttpClient::new(); 83 + http_client3.add_lexicon( 84 + "app.bsky.actor.profile".to_string(), 85 + json!({"lexicon": 1, "id": "app.bsky.actor.profile"}), 86 + ); 87 + http_client3.add_lexicon( 88 + "app.bsky.actor.defs".to_string(), 89 + json!({"lexicon": 1, "id": "app.bsky.actor.defs"}), 90 + ); 91 + 92 + let fetcher3 = LexiconFetcher::new(dns_resolver3, http_client3); 93 + 94 + let nsids = vec![ 95 + "app.bsky.actor.profile".to_string(), 96 + "app.bsky.actor.defs".to_string(), 97 + ]; 98 + 99 + // Automatically optimizes to "app.bsky.actor.*" pattern 100 + match fetcher3.fetch_many_optimized(&nsids).await { 101 + Ok(results) => { 102 + println!("Optimized fetch completed:"); 103 + for result in results { 104 + println!(" Batch of {} lexicons", result.lexicons.len()); 105 + } 106 + } 107 + Err(e) => eprintln!("Error: {}", e), 108 + } 109 + 110 + // Example 4: Fetch from known DID (NEW!) 111 + println!("\n=== Example 4: Fetch from Known DID (Lockfile Mode) ==="); 112 + 113 + let dns_resolver4 = MockDnsResolver::new(); 114 + let mut http_client4 = MockHttpClient::new(); 115 + http_client4.add_lexicon( 116 + "app.bsky.feed.post".to_string(), 117 + json!({"lexicon": 1, "id": "app.bsky.feed.post"}), 118 + ); 119 + 120 + let fetcher4 = LexiconFetcher::new(dns_resolver4, http_client4); 121 + 122 + // Skip DNS resolution when DID is already known (e.g., from lockfile) 123 + match fetcher4.fetch_from_did_with_metadata("did:plc:bsky123", "app.bsky.feed.post").await { 124 + Ok(result) => { 125 + println!("Fetched from known DID:"); 126 + for fetched in result.lexicons { 127 + println!(" - {} (bypassed DNS)", fetched.nsid); 66 128 } 67 129 } 68 130 Err(e) => eprintln!("Error: {}", e),
+22 -3
website/content/docs/cli/07-fetch.md
··· 31 31 **Arguments:** 32 32 - `[NSID]` - Optional NSID or pattern to fetch: 33 33 - Specific lexicon: `com.example.forum.post` 34 - - Wildcard pattern: `com.example.forum.*` 34 + - All descendants: `com.example.forum.*` (matches `post`, `comment`, `comment.reply`, etc.) 35 + - Direct children only: `com.example.forum._` (matches `post`, `comment`, but NOT `comment.reply`) 35 36 - Real-world example: `app.bsky.feed.*` 36 37 37 38 **Options:** ··· 183 184 184 185 This downloads only the `com.example.forum.post` lexicon. 185 186 186 - ### Fetch with Wildcard 187 + ### Fetch with Wildcards 187 188 189 + **All descendants (`.*`):** 188 190 ```bash 189 191 mlf fetch com.example.forum.* 190 192 ``` 193 + Downloads all lexicons under the `com.example.forum` namespace (recursive). 191 194 192 - This downloads all lexicons under the `com.example.forum` namespace. 195 + **Example:** If the namespace contains: 196 + - `com.example.forum.post` 197 + - `com.example.forum.comment` 198 + - `com.example.forum.comment.reply` 199 + 200 + The `.*` pattern fetches **all three**. 201 + 202 + **Direct children only (`._`):** 203 + ```bash 204 + mlf fetch com.example.forum._ 205 + ``` 206 + Downloads only direct children of `com.example.forum` (non-recursive). 207 + 208 + Using the same example namespace, the `._` pattern fetches: 209 + - `com.example.forum.post` ✓ 210 + - `com.example.forum.comment` ✓ 211 + - `com.example.forum.comment.reply` ✗ (skipped, not a direct child) 193 212 194 213 ### Fetch and Save 195 214