A better Rust ATProto crate

lexicon corpus structure and test fixtures.

Orual c047f035 2760ad16

+15 -4
codegen_plan.md
··· 129 129 130 130 **Tasks**: 131 131 1. Create `LexiconCorpus` struct 132 - - `HashMap<SmolStr, LexiconDoc<'static>>` - NSID → doc 132 + - `BTreeMap<SmolStr, LexiconDoc<'static>>` - NSID → doc 133 133 - Methods: `load_from_dir()`, `get()`, `resolve_ref()` 134 134 2. Load all `.json` files from lexicon directory 135 135 3. Parse into `LexiconDoc` and insert into registry ··· 261 261 // ... fields 262 262 } 263 263 264 - impl Collection for Post<'_> { 264 + impl Collection for Post<'p> { 265 265 const NSID: &'static str = "app.bsky.feed.post"; 266 - type Record = Post<'static>; 266 + type Record = Post<'p>; 267 267 } 268 268 ``` 269 269 ··· 275 275 /// The NSID for this XRPC method 276 276 const NSID: &'static str; 277 277 278 - /// HTTP method (GET for queries, POST for procedures) 278 + /// XRPC method (query/GET, procedure/POST) 279 279 const METHOD: XrpcMethod; 280 280 281 281 /// Input encoding (MIME type, e.g., "application/json") ··· 290 290 291 291 /// Response output type 292 292 type Output: Deserialize<'x>; 293 + 294 + type Err: Error; 293 295 } 294 296 295 297 pub enum XrpcMethod { ··· 298 300 } 299 301 ``` 300 302 303 + 304 + 301 305 **Generated implementation:** 302 306 ```rust 303 307 pub struct GetAuthorFeedParams<'a> { ··· 319 323 320 324 type Params = Self; 321 325 type Output = GetAuthorFeedOutput<'static>; 326 + type Err = GetAuthorFeedError; 322 327 } 323 328 ``` 324 329 ··· 332 337 - Allows monomorphization (static dispatch) for performance 333 338 - Also supports `dyn XrpcRequest` for dynamic dispatch if needed 334 339 - Client code can be generic over `impl XrpcRequest` 340 + 341 + 342 + #### XRPC Errors 343 + Lexicons contain information on the kind of errors they can return. 344 + Trait contains an associated error type. Error enum with thiserror::Error and 345 + miette:Diagnostic derives and appropriate content generated based on lexicon info. 335 346 336 347 ### Subscriptions 337 348 WebSocket streams - defer for now. Will need separate trait with message types.
+34 -1
crates/jacquard-common/src/into_static.rs
··· 1 1 use std::borrow::Cow; 2 + use std::collections::BTreeMap; 2 3 use std::collections::HashMap; 3 4 use std::collections::HashSet; 4 5 use std::collections::VecDeque; ··· 67 68 } 68 69 69 70 impl_into_static_passthru!( 70 - String, u128, u64, u32, u16, u8, i128, i64, i32, i16, i8, bool, char, usize, isize, f32, f64 71 + String, 72 + u128, 73 + u64, 74 + u32, 75 + u16, 76 + u8, 77 + i128, 78 + i64, 79 + i32, 80 + i16, 81 + i8, 82 + bool, 83 + char, 84 + usize, 85 + isize, 86 + f32, 87 + f64, 88 + crate::smol_str::SmolStr 71 89 ); 72 90 73 91 impl<T: IntoStatic> IntoStatic for Box<T> { ··· 111 129 K::Output: Eq + Hash, 112 130 { 113 131 type Output = HashMap<K::Output, V::Output, S>; 132 + 133 + fn into_static(self) -> Self::Output { 134 + self.into_iter() 135 + .map(|(k, v)| (k.into_static(), v.into_static())) 136 + .collect() 137 + } 138 + } 139 + 140 + impl<K, V> IntoStatic for BTreeMap<K, V> 141 + where 142 + K: IntoStatic + Ord, 143 + V: IntoStatic, 144 + K::Output: Ord, 145 + { 146 + type Output = BTreeMap<K::Output, V::Output>; 114 147 115 148 fn into_static(self) -> Self::Output { 116 149 self.into_iter()
+7 -11
crates/jacquard-common/src/types/value/convert.rs
··· 1 - use core::{any::TypeId, fmt}; 2 - use std::{borrow::ToOwned, boxed::Box, collections::BTreeMap, vec::Vec}; 3 - 4 - use crate::{ 5 - CowStr, 6 - types::{ 7 - DataModelType, 8 - cid::Cid, 9 - string::AtprotoStr, 10 - value::{Array, Data, Object}, 11 - }, 1 + use crate::types::{ 2 + DataModelType, 3 + cid::Cid, 4 + string::AtprotoStr, 5 + value::{Array, Data, Object}, 12 6 }; 13 7 use bytes::Bytes; 8 + use core::{any::TypeId, fmt}; 14 9 use smol_str::SmolStr; 10 + use std::{borrow::ToOwned, boxed::Box, collections::BTreeMap, vec::Vec}; 15 11 16 12 /// Error used for converting from and into [`crate::types::value::Data`]. 17 13 #[derive(Clone, Debug)]
+162
crates/jacquard-lexicon/src/corpus.rs
··· 1 + use crate::lexicon::{LexiconDoc, LexUserType}; 2 + use jacquard_common::{into_static::IntoStatic, smol_str::SmolStr}; 3 + use std::collections::BTreeMap; 4 + use std::fs; 5 + use std::io; 6 + use std::path::Path; 7 + 8 + /// Registry of all loaded lexicons for reference resolution 9 + #[derive(Debug, Clone)] 10 + pub struct LexiconCorpus { 11 + /// Map from NSID to lexicon document 12 + docs: BTreeMap<SmolStr, LexiconDoc<'static>>, 13 + } 14 + 15 + impl LexiconCorpus { 16 + /// Create an empty corpus 17 + pub fn new() -> Self { 18 + Self { 19 + docs: BTreeMap::new(), 20 + } 21 + } 22 + 23 + /// Load all lexicons from a directory 24 + pub fn load_from_dir(path: impl AsRef<Path>) -> io::Result<Self> { 25 + let mut corpus = Self::new(); 26 + 27 + let schemas = crate::fs::find_schemas(path.as_ref())?; 28 + for schema_path in schemas { 29 + let content = fs::read_to_string(schema_path.as_ref())?; 30 + let doc: LexiconDoc = serde_json::from_str(&content).map_err(|e| { 31 + io::Error::new( 32 + io::ErrorKind::InvalidData, 33 + format!("Failed to parse {}: {}", schema_path.as_ref().display(), e), 34 + ) 35 + })?; 36 + 37 + let nsid = SmolStr::from(doc.id.to_string()); 38 + corpus.docs.insert(nsid, doc.into_static()); 39 + } 40 + 41 + Ok(corpus) 42 + } 43 + 44 + /// Get a lexicon document by NSID 45 + pub fn get(&self, nsid: &str) -> Option<&LexiconDoc<'static>> { 46 + self.docs.get(nsid) 47 + } 48 + 49 + /// Resolve a reference, handling fragments 50 + /// 51 + /// Examples: 52 + /// - `app.bsky.feed.post` → main def from that lexicon 53 + /// - `app.bsky.feed.post#replyRef` → replyRef def from that lexicon 54 + pub fn resolve_ref(&self, ref_str: &str) -> Option<(&LexiconDoc<'static>, &LexUserType<'static>)> { 55 + let (nsid, def_name) = if let Some((nsid, fragment)) = ref_str.split_once('#') { 56 + (nsid, fragment) 57 + } else { 58 + (ref_str, "main") 59 + }; 60 + 61 + let doc = self.get(nsid)?; 62 + let def = doc.defs.get(def_name)?; 63 + Some((doc, def)) 64 + } 65 + 66 + /// Check if a reference exists 67 + pub fn ref_exists(&self, ref_str: &str) -> bool { 68 + self.resolve_ref(ref_str).is_some() 69 + } 70 + 71 + /// Iterate over all documents 72 + pub fn iter(&self) -> impl Iterator<Item = (&SmolStr, &LexiconDoc<'static>)> { 73 + self.docs.iter() 74 + } 75 + 76 + /// Number of loaded lexicons 77 + pub fn len(&self) -> usize { 78 + self.docs.len() 79 + } 80 + 81 + /// Check if corpus is empty 82 + pub fn is_empty(&self) -> bool { 83 + self.docs.is_empty() 84 + } 85 + } 86 + 87 + impl Default for LexiconCorpus { 88 + fn default() -> Self { 89 + Self::new() 90 + } 91 + } 92 + 93 + #[cfg(test)] 94 + mod tests { 95 + use super::*; 96 + use crate::lexicon::LexUserType; 97 + 98 + #[test] 99 + fn test_empty_corpus() { 100 + let corpus = LexiconCorpus::new(); 101 + assert!(corpus.is_empty()); 102 + assert_eq!(corpus.len(), 0); 103 + } 104 + 105 + #[test] 106 + fn test_load_real_lexicons() { 107 + let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons") 108 + .expect("failed to load lexicons"); 109 + 110 + assert!(!corpus.is_empty()); 111 + assert_eq!(corpus.len(), 10); 112 + 113 + // Check that we loaded the expected lexicons 114 + assert!(corpus.get("app.bsky.feed.post").is_some()); 115 + assert!(corpus.get("app.bsky.feed.getAuthorFeed").is_some()); 116 + assert!(corpus.get("app.bsky.richtext.facet").is_some()); 117 + assert!(corpus.get("app.bsky.embed.images").is_some()); 118 + assert!(corpus.get("com.atproto.repo.strongRef").is_some()); 119 + assert!(corpus.get("com.atproto.label.defs").is_some()); 120 + } 121 + 122 + #[test] 123 + fn test_resolve_ref_without_fragment() { 124 + let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons") 125 + .expect("failed to load lexicons"); 126 + 127 + // Without fragment should resolve to main def 128 + let (doc, def) = corpus 129 + .resolve_ref("app.bsky.feed.post") 130 + .expect("should resolve"); 131 + assert_eq!(doc.id.as_ref(), "app.bsky.feed.post"); 132 + assert!(matches!(def, LexUserType::Record(_))); 133 + } 134 + 135 + #[test] 136 + fn test_resolve_ref_with_fragment() { 137 + let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons") 138 + .expect("failed to load lexicons"); 139 + 140 + // With fragment should resolve to specific def 141 + let (doc, def) = corpus 142 + .resolve_ref("app.bsky.richtext.facet#mention") 143 + .expect("should resolve"); 144 + assert_eq!(doc.id.as_ref(), "app.bsky.richtext.facet"); 145 + assert!(matches!(def, LexUserType::Object(_))); 146 + } 147 + 148 + #[test] 149 + fn test_ref_exists() { 150 + let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons") 151 + .expect("failed to load lexicons"); 152 + 153 + // Existing refs 154 + assert!(corpus.ref_exists("app.bsky.feed.post")); 155 + assert!(corpus.ref_exists("app.bsky.feed.post#main")); 156 + assert!(corpus.ref_exists("app.bsky.richtext.facet#mention")); 157 + 158 + // Non-existing refs 159 + assert!(!corpus.ref_exists("com.example.fake")); 160 + assert!(!corpus.ref_exists("app.bsky.feed.post#nonexistent")); 161 + } 162 + }
+433 -1
crates/jacquard-lexicon/src/lexicon.rs
··· 2 2 // https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lexicon.rs 3 3 // https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lib.rs 4 4 5 - use jacquard_common::{CowStr, smol_str::SmolStr, types::blob::MimeType}; 5 + use jacquard_common::{into_static::IntoStatic, smol_str::SmolStr, types::blob::MimeType, CowStr}; 6 6 use serde::{Deserialize, Serialize}; 7 7 use serde_repr::{Deserialize_repr, Serialize_repr}; 8 8 use serde_with::skip_serializing_none; ··· 402 402 CidLink(LexCidLink<'s>), 403 403 // lexUnknown 404 404 Unknown(LexUnknown<'s>), 405 + } 406 + 407 + // IntoStatic implementations for all lexicon types 408 + // These enable converting borrowed lexicon docs to owned 'static versions 409 + 410 + macro_rules! impl_into_static_for_lex_struct { 411 + ($($ty:ident),+ $(,)?) => { 412 + $( 413 + impl IntoStatic for $ty<'_> { 414 + type Output = $ty<'static>; 415 + 416 + fn into_static(self) -> Self::Output { 417 + let Self { 418 + $(description,)? 419 + ..$fields 420 + } = self; 421 + Self::Output { 422 + $(description: description.into_static(),)? 423 + ..$fields.into_static() 424 + } 425 + } 426 + } 427 + )+ 428 + }; 429 + } 430 + 431 + // Simpler approach: just clone and convert each field 432 + impl IntoStatic for Lexicon { 433 + type Output = Lexicon; 434 + fn into_static(self) -> Self::Output { 435 + self 436 + } 437 + } 438 + 439 + impl IntoStatic for LexStringFormat { 440 + type Output = LexStringFormat; 441 + fn into_static(self) -> Self::Output { 442 + self 443 + } 444 + } 445 + 446 + impl IntoStatic for LexiconDoc<'_> { 447 + type Output = LexiconDoc<'static>; 448 + fn into_static(self) -> Self::Output { 449 + LexiconDoc { 450 + lexicon: self.lexicon, 451 + id: self.id.into_static(), 452 + revision: self.revision, 453 + description: self.description.into_static(), 454 + defs: self.defs.into_static(), 455 + } 456 + } 457 + } 458 + 459 + impl IntoStatic for LexBoolean<'_> { 460 + type Output = LexBoolean<'static>; 461 + fn into_static(self) -> Self::Output { 462 + LexBoolean { 463 + description: self.description.into_static(), 464 + default: self.default, 465 + r#const: self.r#const, 466 + } 467 + } 468 + } 469 + 470 + impl IntoStatic for LexInteger<'_> { 471 + type Output = LexInteger<'static>; 472 + fn into_static(self) -> Self::Output { 473 + LexInteger { 474 + description: self.description.into_static(), 475 + default: self.default, 476 + minimum: self.minimum, 477 + maximum: self.maximum, 478 + r#enum: self.r#enum, 479 + r#const: self.r#const, 480 + } 481 + } 482 + } 483 + 484 + impl IntoStatic for LexString<'_> { 485 + type Output = LexString<'static>; 486 + fn into_static(self) -> Self::Output { 487 + LexString { 488 + description: self.description.into_static(), 489 + format: self.format, 490 + default: self.default.into_static(), 491 + min_length: self.min_length, 492 + max_length: self.max_length, 493 + min_graphemes: self.min_graphemes, 494 + max_graphemes: self.max_graphemes, 495 + r#enum: self.r#enum.into_static(), 496 + r#const: self.r#const.into_static(), 497 + known_values: self.known_values.into_static(), 498 + } 499 + } 500 + } 501 + 502 + impl IntoStatic for LexUnknown<'_> { 503 + type Output = LexUnknown<'static>; 504 + fn into_static(self) -> Self::Output { 505 + LexUnknown { 506 + description: self.description.into_static(), 507 + } 508 + } 509 + } 510 + 511 + impl IntoStatic for LexBytes<'_> { 512 + type Output = LexBytes<'static>; 513 + fn into_static(self) -> Self::Output { 514 + LexBytes { 515 + description: self.description.into_static(), 516 + max_length: self.max_length, 517 + min_length: self.min_length, 518 + } 519 + } 520 + } 521 + 522 + impl IntoStatic for LexCidLink<'_> { 523 + type Output = LexCidLink<'static>; 524 + fn into_static(self) -> Self::Output { 525 + LexCidLink { 526 + description: self.description.into_static(), 527 + } 528 + } 529 + } 530 + 531 + impl IntoStatic for LexRef<'_> { 532 + type Output = LexRef<'static>; 533 + fn into_static(self) -> Self::Output { 534 + LexRef { 535 + description: self.description.into_static(), 536 + r#ref: self.r#ref.into_static(), 537 + } 538 + } 539 + } 540 + 541 + impl IntoStatic for LexRefUnion<'_> { 542 + type Output = LexRefUnion<'static>; 543 + fn into_static(self) -> Self::Output { 544 + LexRefUnion { 545 + description: self.description.into_static(), 546 + refs: self.refs.into_static(), 547 + closed: self.closed, 548 + } 549 + } 550 + } 551 + 552 + impl IntoStatic for LexBlob<'_> { 553 + type Output = LexBlob<'static>; 554 + fn into_static(self) -> Self::Output { 555 + LexBlob { 556 + description: self.description.into_static(), 557 + accept: self.accept.into_static(), 558 + max_size: self.max_size, 559 + } 560 + } 561 + } 562 + 563 + impl IntoStatic for LexArrayItem<'_> { 564 + type Output = LexArrayItem<'static>; 565 + fn into_static(self) -> Self::Output { 566 + match self { 567 + Self::Boolean(x) => LexArrayItem::Boolean(x.into_static()), 568 + Self::Integer(x) => LexArrayItem::Integer(x.into_static()), 569 + Self::String(x) => LexArrayItem::String(x.into_static()), 570 + Self::Unknown(x) => LexArrayItem::Unknown(x.into_static()), 571 + Self::Bytes(x) => LexArrayItem::Bytes(x.into_static()), 572 + Self::CidLink(x) => LexArrayItem::CidLink(x.into_static()), 573 + Self::Blob(x) => LexArrayItem::Blob(x.into_static()), 574 + Self::Ref(x) => LexArrayItem::Ref(x.into_static()), 575 + Self::Union(x) => LexArrayItem::Union(x.into_static()), 576 + } 577 + } 578 + } 579 + 580 + impl IntoStatic for LexArray<'_> { 581 + type Output = LexArray<'static>; 582 + fn into_static(self) -> Self::Output { 583 + LexArray { 584 + description: self.description.into_static(), 585 + items: self.items.into_static(), 586 + min_length: self.min_length, 587 + max_length: self.max_length, 588 + } 589 + } 590 + } 591 + 592 + impl IntoStatic for LexPrimitiveArrayItem<'_> { 593 + type Output = LexPrimitiveArrayItem<'static>; 594 + fn into_static(self) -> Self::Output { 595 + match self { 596 + Self::Boolean(x) => LexPrimitiveArrayItem::Boolean(x.into_static()), 597 + Self::Integer(x) => LexPrimitiveArrayItem::Integer(x.into_static()), 598 + Self::String(x) => LexPrimitiveArrayItem::String(x.into_static()), 599 + Self::Unknown(x) => LexPrimitiveArrayItem::Unknown(x.into_static()), 600 + } 601 + } 602 + } 603 + 604 + impl IntoStatic for LexPrimitiveArray<'_> { 605 + type Output = LexPrimitiveArray<'static>; 606 + fn into_static(self) -> Self::Output { 607 + LexPrimitiveArray { 608 + description: self.description.into_static(), 609 + items: self.items.into_static(), 610 + min_length: self.min_length, 611 + max_length: self.max_length, 612 + } 613 + } 614 + } 615 + 616 + impl IntoStatic for LexToken<'_> { 617 + type Output = LexToken<'static>; 618 + fn into_static(self) -> Self::Output { 619 + LexToken { 620 + description: self.description.into_static(), 621 + } 622 + } 623 + } 624 + 625 + impl IntoStatic for LexObjectProperty<'_> { 626 + type Output = LexObjectProperty<'static>; 627 + fn into_static(self) -> Self::Output { 628 + match self { 629 + Self::Ref(x) => LexObjectProperty::Ref(x.into_static()), 630 + Self::Union(x) => LexObjectProperty::Union(x.into_static()), 631 + Self::Bytes(x) => LexObjectProperty::Bytes(x.into_static()), 632 + Self::CidLink(x) => LexObjectProperty::CidLink(x.into_static()), 633 + Self::Array(x) => LexObjectProperty::Array(x.into_static()), 634 + Self::Blob(x) => LexObjectProperty::Blob(x.into_static()), 635 + Self::Boolean(x) => LexObjectProperty::Boolean(x.into_static()), 636 + Self::Integer(x) => LexObjectProperty::Integer(x.into_static()), 637 + Self::String(x) => LexObjectProperty::String(x.into_static()), 638 + Self::Unknown(x) => LexObjectProperty::Unknown(x.into_static()), 639 + } 640 + } 641 + } 642 + 643 + impl IntoStatic for LexObject<'_> { 644 + type Output = LexObject<'static>; 645 + fn into_static(self) -> Self::Output { 646 + LexObject { 647 + description: self.description.into_static(), 648 + required: self.required, 649 + nullable: self.nullable, 650 + properties: self.properties.into_static(), 651 + } 652 + } 653 + } 654 + 655 + impl IntoStatic for LexXrpcParametersProperty<'_> { 656 + type Output = LexXrpcParametersProperty<'static>; 657 + fn into_static(self) -> Self::Output { 658 + match self { 659 + Self::Boolean(x) => LexXrpcParametersProperty::Boolean(x.into_static()), 660 + Self::Integer(x) => LexXrpcParametersProperty::Integer(x.into_static()), 661 + Self::String(x) => LexXrpcParametersProperty::String(x.into_static()), 662 + Self::Unknown(x) => LexXrpcParametersProperty::Unknown(x.into_static()), 663 + Self::Array(x) => LexXrpcParametersProperty::Array(x.into_static()), 664 + } 665 + } 666 + } 667 + 668 + impl IntoStatic for LexXrpcParameters<'_> { 669 + type Output = LexXrpcParameters<'static>; 670 + fn into_static(self) -> Self::Output { 671 + LexXrpcParameters { 672 + description: self.description.into_static(), 673 + required: self.required, 674 + properties: self.properties.into_static(), 675 + } 676 + } 677 + } 678 + 679 + impl IntoStatic for LexXrpcBodySchema<'_> { 680 + type Output = LexXrpcBodySchema<'static>; 681 + fn into_static(self) -> Self::Output { 682 + match self { 683 + Self::Ref(x) => LexXrpcBodySchema::Ref(x.into_static()), 684 + Self::Union(x) => LexXrpcBodySchema::Union(x.into_static()), 685 + Self::Object(x) => LexXrpcBodySchema::Object(x.into_static()), 686 + } 687 + } 688 + } 689 + 690 + impl IntoStatic for LexXrpcBody<'_> { 691 + type Output = LexXrpcBody<'static>; 692 + fn into_static(self) -> Self::Output { 693 + LexXrpcBody { 694 + description: self.description.into_static(), 695 + encoding: self.encoding.into_static(), 696 + schema: self.schema.into_static(), 697 + } 698 + } 699 + } 700 + 701 + impl IntoStatic for LexXrpcSubscriptionMessageSchema<'_> { 702 + type Output = LexXrpcSubscriptionMessageSchema<'static>; 703 + fn into_static(self) -> Self::Output { 704 + match self { 705 + Self::Ref(x) => LexXrpcSubscriptionMessageSchema::Ref(x.into_static()), 706 + Self::Union(x) => LexXrpcSubscriptionMessageSchema::Union(x.into_static()), 707 + Self::Object(x) => LexXrpcSubscriptionMessageSchema::Object(x.into_static()), 708 + } 709 + } 710 + } 711 + 712 + impl IntoStatic for LexXrpcSubscriptionMessage<'_> { 713 + type Output = LexXrpcSubscriptionMessage<'static>; 714 + fn into_static(self) -> Self::Output { 715 + LexXrpcSubscriptionMessage { 716 + description: self.description.into_static(), 717 + schema: self.schema.into_static(), 718 + } 719 + } 720 + } 721 + 722 + impl IntoStatic for LexXrpcError<'_> { 723 + type Output = LexXrpcError<'static>; 724 + fn into_static(self) -> Self::Output { 725 + LexXrpcError { 726 + description: self.description.into_static(), 727 + name: self.name.into_static(), 728 + } 729 + } 730 + } 731 + 732 + impl IntoStatic for LexXrpcQueryParameter<'_> { 733 + type Output = LexXrpcQueryParameter<'static>; 734 + fn into_static(self) -> Self::Output { 735 + match self { 736 + Self::Params(x) => LexXrpcQueryParameter::Params(x.into_static()), 737 + } 738 + } 739 + } 740 + 741 + impl IntoStatic for LexXrpcQuery<'_> { 742 + type Output = LexXrpcQuery<'static>; 743 + fn into_static(self) -> Self::Output { 744 + LexXrpcQuery { 745 + description: self.description.into_static(), 746 + parameters: self.parameters.into_static(), 747 + output: self.output.into_static(), 748 + errors: self.errors.into_static(), 749 + } 750 + } 751 + } 752 + 753 + impl IntoStatic for LexXrpcProcedureParameter<'_> { 754 + type Output = LexXrpcProcedureParameter<'static>; 755 + fn into_static(self) -> Self::Output { 756 + match self { 757 + Self::Params(x) => LexXrpcProcedureParameter::Params(x.into_static()), 758 + } 759 + } 760 + } 761 + 762 + impl IntoStatic for LexXrpcProcedure<'_> { 763 + type Output = LexXrpcProcedure<'static>; 764 + fn into_static(self) -> Self::Output { 765 + LexXrpcProcedure { 766 + description: self.description.into_static(), 767 + parameters: self.parameters.into_static(), 768 + input: self.input.into_static(), 769 + output: self.output.into_static(), 770 + errors: self.errors.into_static(), 771 + } 772 + } 773 + } 774 + 775 + impl IntoStatic for LexXrpcSubscriptionParameter<'_> { 776 + type Output = LexXrpcSubscriptionParameter<'static>; 777 + fn into_static(self) -> Self::Output { 778 + match self { 779 + Self::Params(x) => LexXrpcSubscriptionParameter::Params(x.into_static()), 780 + } 781 + } 782 + } 783 + 784 + impl IntoStatic for LexXrpcSubscription<'_> { 785 + type Output = LexXrpcSubscription<'static>; 786 + fn into_static(self) -> Self::Output { 787 + LexXrpcSubscription { 788 + description: self.description.into_static(), 789 + parameters: self.parameters.into_static(), 790 + message: self.message.into_static(), 791 + infos: self.infos.into_static(), 792 + errors: self.errors.into_static(), 793 + } 794 + } 795 + } 796 + 797 + impl IntoStatic for LexRecordRecord<'_> { 798 + type Output = LexRecordRecord<'static>; 799 + fn into_static(self) -> Self::Output { 800 + match self { 801 + Self::Object(x) => LexRecordRecord::Object(x.into_static()), 802 + } 803 + } 804 + } 805 + 806 + impl IntoStatic for LexRecord<'_> { 807 + type Output = LexRecord<'static>; 808 + fn into_static(self) -> Self::Output { 809 + LexRecord { 810 + description: self.description.into_static(), 811 + key: self.key.into_static(), 812 + record: self.record.into_static(), 813 + } 814 + } 815 + } 816 + 817 + impl IntoStatic for LexUserType<'_> { 818 + type Output = LexUserType<'static>; 819 + fn into_static(self) -> Self::Output { 820 + match self { 821 + Self::Record(x) => LexUserType::Record(x.into_static()), 822 + Self::XrpcQuery(x) => LexUserType::XrpcQuery(x.into_static()), 823 + Self::XrpcProcedure(x) => LexUserType::XrpcProcedure(x.into_static()), 824 + Self::XrpcSubscription(x) => LexUserType::XrpcSubscription(x.into_static()), 825 + Self::Blob(x) => LexUserType::Blob(x.into_static()), 826 + Self::Array(x) => LexUserType::Array(x.into_static()), 827 + Self::Token(x) => LexUserType::Token(x.into_static()), 828 + Self::Object(x) => LexUserType::Object(x.into_static()), 829 + Self::Boolean(x) => LexUserType::Boolean(x.into_static()), 830 + Self::Integer(x) => LexUserType::Integer(x.into_static()), 831 + Self::String(x) => LexUserType::String(x.into_static()), 832 + Self::Bytes(x) => LexUserType::Bytes(x.into_static()), 833 + Self::CidLink(x) => LexUserType::CidLink(x.into_static()), 834 + Self::Unknown(x) => LexUserType::Unknown(x.into_static()), 835 + } 836 + } 405 837 } 406 838 407 839 #[cfg(test)]
+1
crates/jacquard-lexicon/src/lib.rs
··· 1 + pub mod corpus; 1 2 pub mod fs; 2 3 pub mod lexicon; 3 4 pub mod output;
+156
crates/jacquard-lexicon/tests/fixtures/lexicons/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "label": { 6 + "type": "object", 7 + "description": "Metadata tag on an atproto resource (eg, repo or record).", 8 + "required": ["src", "uri", "val", "cts"], 9 + "properties": { 10 + "ver": { 11 + "type": "integer", 12 + "description": "The AT Protocol version of the label object." 13 + }, 14 + "src": { 15 + "type": "string", 16 + "format": "did", 17 + "description": "DID of the actor who created this label." 18 + }, 19 + "uri": { 20 + "type": "string", 21 + "format": "uri", 22 + "description": "AT URI of the record, repository (account), or other resource that this label applies to." 23 + }, 24 + "cid": { 25 + "type": "string", 26 + "format": "cid", 27 + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." 28 + }, 29 + "val": { 30 + "type": "string", 31 + "maxLength": 128, 32 + "description": "The short string name of the value or type of this label." 33 + }, 34 + "neg": { 35 + "type": "boolean", 36 + "description": "If true, this is a negation label, overwriting a previous label." 37 + }, 38 + "cts": { 39 + "type": "string", 40 + "format": "datetime", 41 + "description": "Timestamp when this label was created." 42 + }, 43 + "exp": { 44 + "type": "string", 45 + "format": "datetime", 46 + "description": "Timestamp at which this label expires (no longer applies)." 47 + }, 48 + "sig": { 49 + "type": "bytes", 50 + "description": "Signature of dag-cbor encoded label." 51 + } 52 + } 53 + }, 54 + "selfLabels": { 55 + "type": "object", 56 + "description": "Metadata tags on an atproto record, published by the author within the record.", 57 + "required": ["values"], 58 + "properties": { 59 + "values": { 60 + "type": "array", 61 + "items": { "type": "ref", "ref": "#selfLabel" }, 62 + "maxLength": 10 63 + } 64 + } 65 + }, 66 + "selfLabel": { 67 + "type": "object", 68 + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.", 69 + "required": ["val"], 70 + "properties": { 71 + "val": { 72 + "type": "string", 73 + "maxLength": 128, 74 + "description": "The short string name of the value or type of this label." 75 + } 76 + } 77 + }, 78 + "labelValueDefinition": { 79 + "type": "object", 80 + "description": "Declares a label value and its expected interpretations and behaviors.", 81 + "required": ["identifier", "severity", "blurs", "locales"], 82 + "properties": { 83 + "identifier": { 84 + "type": "string", 85 + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 86 + "maxLength": 100, 87 + "maxGraphemes": 100 88 + }, 89 + "severity": { 90 + "type": "string", 91 + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 92 + "knownValues": ["inform", "alert", "none"] 93 + }, 94 + "blurs": { 95 + "type": "string", 96 + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 97 + "knownValues": ["content", "media", "none"] 98 + }, 99 + "defaultSetting": { 100 + "type": "string", 101 + "description": "The default setting for this label.", 102 + "knownValues": ["ignore", "warn", "hide"], 103 + "default": "warn" 104 + }, 105 + "adultOnly": { 106 + "type": "boolean", 107 + "description": "Does the user need to have adult content enabled in order to configure this label?" 108 + }, 109 + "locales": { 110 + "type": "array", 111 + "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" } 112 + } 113 + } 114 + }, 115 + "labelValueDefinitionStrings": { 116 + "type": "object", 117 + "description": "Strings which describe the label in the UI, localized into a specific language.", 118 + "required": ["lang", "name", "description"], 119 + "properties": { 120 + "lang": { 121 + "type": "string", 122 + "description": "The code of the language these strings are written in.", 123 + "format": "language" 124 + }, 125 + "name": { 126 + "type": "string", 127 + "description": "A short human-readable name for the label.", 128 + "maxGraphemes": 64, 129 + "maxLength": 640 130 + }, 131 + "description": { 132 + "type": "string", 133 + "description": "A longer description of what the label means and why it might be applied.", 134 + "maxGraphemes": 10000, 135 + "maxLength": 100000 136 + } 137 + } 138 + }, 139 + "labelValue": { 140 + "type": "string", 141 + "knownValues": [ 142 + "!hide", 143 + "!no-promote", 144 + "!warn", 145 + "!no-unauthenticated", 146 + "dmca-violation", 147 + "doxxing", 148 + "porn", 149 + "sexual", 150 + "nudity", 151 + "nsfl", 152 + "gore" 153 + ] 154 + } 155 + } 156 + }
+51
crates/jacquard-lexicon/tests/fixtures/lexicons/external.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.external", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).", 8 + "required": ["external"], 9 + "properties": { 10 + "external": { 11 + "type": "ref", 12 + "ref": "#external" 13 + } 14 + } 15 + }, 16 + "external": { 17 + "type": "object", 18 + "required": ["uri", "title", "description"], 19 + "properties": { 20 + "uri": { "type": "string", "format": "uri" }, 21 + "title": { "type": "string" }, 22 + "description": { "type": "string" }, 23 + "thumb": { 24 + "type": "blob", 25 + "accept": ["image/*"], 26 + "maxSize": 1000000 27 + } 28 + } 29 + }, 30 + "view": { 31 + "type": "object", 32 + "required": ["external"], 33 + "properties": { 34 + "external": { 35 + "type": "ref", 36 + "ref": "#viewExternal" 37 + } 38 + } 39 + }, 40 + "viewExternal": { 41 + "type": "object", 42 + "required": ["uri", "title", "description"], 43 + "properties": { 44 + "uri": { "type": "string", "format": "uri" }, 45 + "title": { "type": "string" }, 46 + "description": { "type": "string" }, 47 + "thumb": { "type": "string", "format": "uri" } 48 + } 49 + } 50 + } 51 + }
+51
crates/jacquard-lexicon/tests/fixtures/lexicons/facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.richtext.facet", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "Annotation of a sub-string within rich text.", 8 + "required": ["index", "features"], 9 + "properties": { 10 + "index": { "type": "ref", "ref": "#byteSlice" }, 11 + "features": { 12 + "type": "array", 13 + "items": { "type": "union", "refs": ["#mention", "#link", "#tag"] } 14 + } 15 + } 16 + }, 17 + "mention": { 18 + "type": "object", 19 + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", 20 + "required": ["did"], 21 + "properties": { 22 + "did": { "type": "string", "format": "did" } 23 + } 24 + }, 25 + "link": { 26 + "type": "object", 27 + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", 28 + "required": ["uri"], 29 + "properties": { 30 + "uri": { "type": "string", "format": "uri" } 31 + } 32 + }, 33 + "tag": { 34 + "type": "object", 35 + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", 36 + "required": ["tag"], 37 + "properties": { 38 + "tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } 39 + } 40 + }, 41 + "byteSlice": { 42 + "type": "object", 43 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", 44 + "required": ["byteStart", "byteEnd"], 45 + "properties": { 46 + "byteStart": { "type": "integer", "minimum": 0 }, 47 + "byteEnd": { "type": "integer", "minimum": 0 } 48 + } 49 + } 50 + } 51 + }
+58
crates/jacquard-lexicon/tests/fixtures/lexicons/getAuthorFeed.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.getAuthorFeed", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { "type": "string", "format": "at-identifier" }, 13 + "limit": { 14 + "type": "integer", 15 + "minimum": 1, 16 + "maximum": 100, 17 + "default": 50 18 + }, 19 + "cursor": { "type": "string" }, 20 + "filter": { 21 + "type": "string", 22 + "description": "Combinations of post/repost types to include in response.", 23 + "knownValues": [ 24 + "posts_with_replies", 25 + "posts_no_replies", 26 + "posts_with_media", 27 + "posts_and_author_threads", 28 + "posts_with_video" 29 + ], 30 + "default": "posts_with_replies" 31 + }, 32 + "includePins": { 33 + "type": "boolean", 34 + "default": false 35 + } 36 + } 37 + }, 38 + "output": { 39 + "encoding": "application/json", 40 + "schema": { 41 + "type": "object", 42 + "required": ["feed"], 43 + "properties": { 44 + "cursor": { "type": "string" }, 45 + "feed": { 46 + "type": "array", 47 + "items": { 48 + "type": "ref", 49 + "ref": "app.bsky.feed.defs#feedViewPost" 50 + } 51 + } 52 + } 53 + } 54 + }, 55 + "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }] 56 + } 57 + } 58 + }
+72
crates/jacquard-lexicon/tests/fixtures/lexicons/images.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.images", 4 + "description": "A set of images embedded in a Bluesky record (eg, a post).", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["images"], 9 + "properties": { 10 + "images": { 11 + "type": "array", 12 + "items": { "type": "ref", "ref": "#image" }, 13 + "maxLength": 4 14 + } 15 + } 16 + }, 17 + "image": { 18 + "type": "object", 19 + "required": ["image", "alt"], 20 + "properties": { 21 + "image": { 22 + "type": "blob", 23 + "accept": ["image/*"], 24 + "maxSize": 1000000 25 + }, 26 + "alt": { 27 + "type": "string", 28 + "description": "Alt text description of the image, for accessibility." 29 + }, 30 + "aspectRatio": { 31 + "type": "ref", 32 + "ref": "app.bsky.embed.defs#aspectRatio" 33 + } 34 + } 35 + }, 36 + "view": { 37 + "type": "object", 38 + "required": ["images"], 39 + "properties": { 40 + "images": { 41 + "type": "array", 42 + "items": { "type": "ref", "ref": "#viewImage" }, 43 + "maxLength": 4 44 + } 45 + } 46 + }, 47 + "viewImage": { 48 + "type": "object", 49 + "required": ["thumb", "fullsize", "alt"], 50 + "properties": { 51 + "thumb": { 52 + "type": "string", 53 + "format": "uri", 54 + "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View." 55 + }, 56 + "fullsize": { 57 + "type": "string", 58 + "format": "uri", 59 + "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View." 60 + }, 61 + "alt": { 62 + "type": "string", 63 + "description": "Alt text description of the image, for accessibility." 64 + }, 65 + "aspectRatio": { 66 + "type": "ref", 67 + "ref": "app.bsky.embed.defs#aspectRatio" 68 + } 69 + } 70 + } 71 + } 72 + }
+96
crates/jacquard-lexicon/tests/fixtures/lexicons/post.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.post", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Record containing a Bluesky post.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["text", "createdAt"], 12 + "properties": { 13 + "text": { 14 + "type": "string", 15 + "maxLength": 3000, 16 + "maxGraphemes": 300, 17 + "description": "The primary post content. May be an empty string, if there are embeds." 18 + }, 19 + "entities": { 20 + "type": "array", 21 + "description": "DEPRECATED: replaced by app.bsky.richtext.facet.", 22 + "items": { "type": "ref", "ref": "#entity" } 23 + }, 24 + "facets": { 25 + "type": "array", 26 + "description": "Annotations of text (mentions, URLs, hashtags, etc)", 27 + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } 28 + }, 29 + "reply": { "type": "ref", "ref": "#replyRef" }, 30 + "embed": { 31 + "type": "union", 32 + "refs": [ 33 + "app.bsky.embed.images", 34 + "app.bsky.embed.video", 35 + "app.bsky.embed.external", 36 + "app.bsky.embed.record", 37 + "app.bsky.embed.recordWithMedia" 38 + ] 39 + }, 40 + "langs": { 41 + "type": "array", 42 + "description": "Indicates human language of post primary text content.", 43 + "maxLength": 3, 44 + "items": { "type": "string", "format": "language" } 45 + }, 46 + "labels": { 47 + "type": "union", 48 + "description": "Self-label values for this post. Effectively content warnings.", 49 + "refs": ["com.atproto.label.defs#selfLabels"] 50 + }, 51 + "tags": { 52 + "type": "array", 53 + "description": "Additional hashtags, in addition to any included in post text and facets.", 54 + "maxLength": 8, 55 + "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } 56 + }, 57 + "createdAt": { 58 + "type": "string", 59 + "format": "datetime", 60 + "description": "Client-declared timestamp when this post was originally created." 61 + } 62 + } 63 + } 64 + }, 65 + "replyRef": { 66 + "type": "object", 67 + "required": ["root", "parent"], 68 + "properties": { 69 + "root": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 70 + "parent": { "type": "ref", "ref": "com.atproto.repo.strongRef" } 71 + } 72 + }, 73 + "entity": { 74 + "type": "object", 75 + "description": "Deprecated: use facets instead.", 76 + "required": ["index", "type", "value"], 77 + "properties": { 78 + "index": { "type": "ref", "ref": "#textSlice" }, 79 + "type": { 80 + "type": "string", 81 + "description": "Expected values are 'mention' and 'link'." 82 + }, 83 + "value": { "type": "string" } 84 + } 85 + }, 86 + "textSlice": { 87 + "type": "object", 88 + "description": "Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.", 89 + "required": ["start", "end"], 90 + "properties": { 91 + "start": { "type": "integer", "minimum": 0 }, 92 + "end": { "type": "integer", "minimum": 0 } 93 + } 94 + } 95 + } 96 + }
+96
crates/jacquard-lexicon/tests/fixtures/lexicons/record.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.record", 4 + "description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["record"], 9 + "properties": { 10 + "record": { "type": "ref", "ref": "com.atproto.repo.strongRef" } 11 + } 12 + }, 13 + "view": { 14 + "type": "object", 15 + "required": ["record"], 16 + "properties": { 17 + "record": { 18 + "type": "union", 19 + "refs": [ 20 + "#viewRecord", 21 + "#viewNotFound", 22 + "#viewBlocked", 23 + "#viewDetached", 24 + "app.bsky.feed.defs#generatorView", 25 + "app.bsky.graph.defs#listView", 26 + "app.bsky.labeler.defs#labelerView", 27 + "app.bsky.graph.defs#starterPackViewBasic" 28 + ] 29 + } 30 + } 31 + }, 32 + "viewRecord": { 33 + "type": "object", 34 + "required": ["uri", "cid", "author", "value", "indexedAt"], 35 + "properties": { 36 + "uri": { "type": "string", "format": "at-uri" }, 37 + "cid": { "type": "string", "format": "cid" }, 38 + "author": { 39 + "type": "ref", 40 + "ref": "app.bsky.actor.defs#profileViewBasic" 41 + }, 42 + "value": { 43 + "type": "unknown", 44 + "description": "The record data itself." 45 + }, 46 + "labels": { 47 + "type": "array", 48 + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 49 + }, 50 + "replyCount": { "type": "integer" }, 51 + "repostCount": { "type": "integer" }, 52 + "likeCount": { "type": "integer" }, 53 + "quoteCount": { "type": "integer" }, 54 + "embeds": { 55 + "type": "array", 56 + "items": { 57 + "type": "union", 58 + "refs": [ 59 + "app.bsky.embed.images#view", 60 + "app.bsky.embed.video#view", 61 + "app.bsky.embed.external#view", 62 + "app.bsky.embed.record#view", 63 + "app.bsky.embed.recordWithMedia#view" 64 + ] 65 + } 66 + }, 67 + "indexedAt": { "type": "string", "format": "datetime" } 68 + } 69 + }, 70 + "viewNotFound": { 71 + "type": "object", 72 + "required": ["uri", "notFound"], 73 + "properties": { 74 + "uri": { "type": "string", "format": "at-uri" }, 75 + "notFound": { "type": "boolean", "const": true } 76 + } 77 + }, 78 + "viewBlocked": { 79 + "type": "object", 80 + "required": ["uri", "blocked", "author"], 81 + "properties": { 82 + "uri": { "type": "string", "format": "at-uri" }, 83 + "blocked": { "type": "boolean", "const": true }, 84 + "author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" } 85 + } 86 + }, 87 + "viewDetached": { 88 + "type": "object", 89 + "required": ["uri", "detached"], 90 + "properties": { 91 + "uri": { "type": "string", "format": "at-uri" }, 92 + "detached": { "type": "boolean", "const": true } 93 + } 94 + } 95 + } 96 + }
+43
crates/jacquard-lexicon/tests/fixtures/lexicons/recordWithMedia.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.recordWithMedia", 4 + "description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["record", "media"], 9 + "properties": { 10 + "record": { 11 + "type": "ref", 12 + "ref": "app.bsky.embed.record" 13 + }, 14 + "media": { 15 + "type": "union", 16 + "refs": [ 17 + "app.bsky.embed.images", 18 + "app.bsky.embed.video", 19 + "app.bsky.embed.external" 20 + ] 21 + } 22 + } 23 + }, 24 + "view": { 25 + "type": "object", 26 + "required": ["record", "media"], 27 + "properties": { 28 + "record": { 29 + "type": "ref", 30 + "ref": "app.bsky.embed.record#view" 31 + }, 32 + "media": { 33 + "type": "union", 34 + "refs": [ 35 + "app.bsky.embed.images#view", 36 + "app.bsky.embed.video#view", 37 + "app.bsky.embed.external#view" 38 + ] 39 + } 40 + } 41 + } 42 + } 43 + }
+15
crates/jacquard-lexicon/tests/fixtures/lexicons/strongRef.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.strongRef", 4 + "description": "A URI with a content-hash fingerprint.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["uri", "cid"], 9 + "properties": { 10 + "uri": { "type": "string", "format": "at-uri" }, 11 + "cid": { "type": "string", "format": "cid" } 12 + } 13 + } 14 + } 15 + }
+67
crates/jacquard-lexicon/tests/fixtures/lexicons/video.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.video", 4 + "description": "A video embedded in a Bluesky record (eg, a post).", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["video"], 9 + "properties": { 10 + "video": { 11 + "type": "blob", 12 + "description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb.", 13 + "accept": ["video/mp4"], 14 + "maxSize": 100000000 15 + }, 16 + "captions": { 17 + "type": "array", 18 + "items": { "type": "ref", "ref": "#caption" }, 19 + "maxLength": 20 20 + }, 21 + "alt": { 22 + "type": "string", 23 + "description": "Alt text description of the video, for accessibility.", 24 + "maxGraphemes": 1000, 25 + "maxLength": 10000 26 + }, 27 + "aspectRatio": { 28 + "type": "ref", 29 + "ref": "app.bsky.embed.defs#aspectRatio" 30 + } 31 + } 32 + }, 33 + "caption": { 34 + "type": "object", 35 + "required": ["lang", "file"], 36 + "properties": { 37 + "lang": { 38 + "type": "string", 39 + "format": "language" 40 + }, 41 + "file": { 42 + "type": "blob", 43 + "accept": ["text/vtt"], 44 + "maxSize": 20000 45 + } 46 + } 47 + }, 48 + "view": { 49 + "type": "object", 50 + "required": ["cid", "playlist"], 51 + "properties": { 52 + "cid": { "type": "string", "format": "cid" }, 53 + "playlist": { "type": "string", "format": "uri" }, 54 + "thumbnail": { "type": "string", "format": "uri" }, 55 + "alt": { 56 + "type": "string", 57 + "maxGraphemes": 1000, 58 + "maxLength": 10000 59 + }, 60 + "aspectRatio": { 61 + "type": "ref", 62 + "ref": "app.bsky.embed.defs#aspectRatio" 63 + } 64 + } 65 + } 66 + } 67 + }