A library for ATProtocol identities.

feature: Adding facets to lexicon community calendar events

+205
crates/atproto-record/src/lexicon/app_bsky_richtext_facet.rs
···
··· 1 + //! AT Protocol rich text facet types. 2 + //! 3 + //! This module provides types for annotating rich text content with semantic 4 + //! meaning, based on the `app.bsky.richtext.facet` lexicon. Facets enable 5 + //! mentions, links, hashtags, and other structured metadata to be attached 6 + //! to specific byte ranges within text content. 7 + //! 8 + //! # Overview 9 + //! 10 + //! Facets consist of: 11 + //! - A byte range (start/end indices in UTF-8 encoded text) 12 + //! - One or more features (mention, link, tag) that apply to that range 13 + //! 14 + //! # Example 15 + //! 16 + //! ```ignore 17 + //! use atproto_record::lexicon::app::bsky::richtext::facet::{Facet, ByteSlice, FacetFeature, Mention}; 18 + //! 19 + //! // Create a mention facet for "@alice.bsky.social" 20 + //! let facet = Facet { 21 + //! index: ByteSlice { byte_start: 0, byte_end: 19 }, 22 + //! features: vec![ 23 + //! FacetFeature::Mention(Mention { 24 + //! did: "did:plc:alice123".to_string(), 25 + //! }) 26 + //! ], 27 + //! }; 28 + //! ``` 29 + 30 + use serde::{Deserialize, Serialize}; 31 + 32 + /// Byte range specification for facet features. 33 + /// 34 + /// Specifies the sub-string range a facet feature applies to using 35 + /// zero-indexed byte offsets in UTF-8 encoded text. Start index is 36 + /// inclusive, end index is exclusive. 37 + /// 38 + /// # Example 39 + /// 40 + /// ```ignore 41 + /// use atproto_record::lexicon::app::bsky::richtext::facet::ByteSlice; 42 + /// 43 + /// // Represents bytes 0-5 of the text 44 + /// let slice = ByteSlice { 45 + /// byte_start: 0, 46 + /// byte_end: 5, 47 + /// }; 48 + /// ``` 49 + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 50 + #[serde(rename_all = "camelCase")] 51 + pub struct ByteSlice { 52 + /// Starting byte index (inclusive) 53 + pub byte_start: usize, 54 + 55 + /// Ending byte index (exclusive) 56 + pub byte_end: usize, 57 + } 58 + 59 + /// Mention facet feature for referencing another account. 60 + /// 61 + /// The text content typically displays a handle with '@' prefix (e.g., "@alice.bsky.social"), 62 + /// but the facet reference must use the account's DID for stable identification. 63 + /// 64 + /// # Example 65 + /// 66 + /// ```ignore 67 + /// use atproto_record::lexicon::app::bsky::richtext::facet::Mention; 68 + /// 69 + /// let mention = Mention { 70 + /// did: "did:plc:alice123".to_string(), 71 + /// }; 72 + /// ``` 73 + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 74 + pub struct Mention { 75 + /// DID of the mentioned account 76 + pub did: String, 77 + } 78 + 79 + /// Link facet feature for URL references. 80 + /// 81 + /// The text content may be simplified or truncated for display purposes, 82 + /// but the facet reference should contain the complete, valid URL. 83 + /// 84 + /// # Example 85 + /// 86 + /// ```ignore 87 + /// use atproto_record::lexicon::app::bsky::richtext::facet::Link; 88 + /// 89 + /// let link = Link { 90 + /// uri: "https://example.com/full/path".to_string(), 91 + /// }; 92 + /// ``` 93 + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 94 + pub struct Link { 95 + /// Complete URI/URL for the link 96 + pub uri: String, 97 + } 98 + 99 + /// Tag facet feature for hashtags. 100 + /// 101 + /// The text content typically includes a '#' prefix for display, 102 + /// but the facet reference should contain only the tag text without the prefix. 103 + /// 104 + /// # Example 105 + /// 106 + /// ```ignore 107 + /// use atproto_record::lexicon::app::bsky::richtext::facet::Tag; 108 + /// 109 + /// // For text "#atproto", store just "atproto" 110 + /// let tag = Tag { 111 + /// tag: "atproto".to_string(), 112 + /// }; 113 + /// ``` 114 + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 115 + pub struct Tag { 116 + /// Tag text without '#' prefix 117 + pub tag: String, 118 + } 119 + 120 + /// Discriminated union of facet feature types. 121 + /// 122 + /// Represents the different types of semantic annotations that can be 123 + /// applied to text ranges. Each variant corresponds to a specific lexicon 124 + /// type in the `app.bsky.richtext.facet` namespace. 125 + /// 126 + /// # Example 127 + /// 128 + /// ```ignore 129 + /// use atproto_record::lexicon::app::bsky::richtext::facet::{FacetFeature, Mention, Link, Tag}; 130 + /// 131 + /// // Create different feature types 132 + /// let mention = FacetFeature::Mention(Mention { 133 + /// did: "did:plc:alice123".to_string(), 134 + /// }); 135 + /// 136 + /// let link = FacetFeature::Link(Link { 137 + /// uri: "https://example.com".to_string(), 138 + /// }); 139 + /// 140 + /// let tag = FacetFeature::Tag(Tag { 141 + /// tag: "rust".to_string(), 142 + /// }); 143 + /// ``` 144 + #[derive(Serialize, Deserialize, Clone, PartialEq)] 145 + #[cfg_attr(debug_assertions, derive(Debug))] 146 + #[serde(tag = "$type")] 147 + pub enum FacetFeature { 148 + /// Account mention feature 149 + #[serde(rename = "app.bsky.richtext.facet#mention")] 150 + Mention(Mention), 151 + 152 + /// URL link feature 153 + #[serde(rename = "app.bsky.richtext.facet#link")] 154 + Link(Link), 155 + 156 + /// Hashtag feature 157 + #[serde(rename = "app.bsky.richtext.facet#tag")] 158 + Tag(Tag), 159 + } 160 + 161 + /// Rich text facet annotation. 162 + /// 163 + /// Associates one or more semantic features with a specific byte range 164 + /// within text content. Multiple features can apply to the same range 165 + /// (e.g., a URL that is also a hashtag). 166 + /// 167 + /// # Example 168 + /// 169 + /// ```ignore 170 + /// use atproto_record::lexicon::app::bsky::richtext::facet::{ 171 + /// Facet, ByteSlice, FacetFeature, Mention, Link 172 + /// }; 173 + /// 174 + /// // Annotate "@alice.bsky.social" at bytes 0-19 175 + /// let facet = Facet { 176 + /// index: ByteSlice { byte_start: 0, byte_end: 19 }, 177 + /// features: vec![ 178 + /// FacetFeature::Mention(Mention { 179 + /// did: "did:plc:alice123".to_string(), 180 + /// }), 181 + /// ], 182 + /// }; 183 + /// 184 + /// // Multiple features for the same range 185 + /// let multi_facet = Facet { 186 + /// index: ByteSlice { byte_start: 20, byte_end: 35 }, 187 + /// features: vec![ 188 + /// FacetFeature::Link(Link { 189 + /// uri: "https://example.com".to_string(), 190 + /// }), 191 + /// FacetFeature::Tag(Tag { 192 + /// tag: "example".to_string(), 193 + /// }), 194 + /// ], 195 + /// }; 196 + /// ``` 197 + #[derive(Serialize, Deserialize, Clone, PartialEq)] 198 + #[cfg_attr(debug_assertions, derive(Debug))] 199 + pub struct Facet { 200 + /// Byte range this facet applies to 201 + pub index: ByteSlice, 202 + 203 + /// Semantic features applied to this range 204 + pub features: Vec<FacetFeature>, 205 + }
+19 -68
crates/atproto-record/src/lexicon/community_lexicon_attestation.rs
··· 30 /// 31 /// // Inline signature 32 /// let inline = SignatureOrRef::Inline(create_typed_signature( 33 - /// "did:plc:issuer".to_string(), 34 /// Bytes { bytes: b"signature".to_vec() }, 35 /// )); 36 /// ··· 55 56 /// Cryptographic signature structure. 57 /// 58 - /// Represents a signature created by an issuer (identified by DID) over 59 - /// some data. The signature can be used to verify authenticity, authorization, 60 - /// or other properties of the signed content. 61 /// 62 /// # Fields 63 /// 64 - /// - `issuer`: DID of the entity that created the signature 65 /// - `signature`: The actual signature bytes 66 /// - `extra`: Additional fields that may be present in the signature 67 /// ··· 73 /// use std::collections::HashMap; 74 /// 75 /// let sig = Signature { 76 - /// issuer: "did:plc:example".to_string(), 77 /// signature: Bytes { bytes: b"signature_bytes".to_vec() }, 78 /// extra: HashMap::new(), 79 /// }; ··· 81 #[derive(Deserialize, Serialize, Clone, PartialEq)] 82 #[cfg_attr(debug_assertions, derive(Debug))] 83 pub struct Signature { 84 - /// DID of the entity that created this signature 85 - pub issuer: String, 86 - 87 /// The cryptographic signature bytes 88 pub signature: Bytes, 89 ··· 116 /// 117 /// # Arguments 118 /// 119 - /// * `issuer` - DID of the signature issuer 120 /// * `signature` - The signature bytes 121 /// 122 /// # Example ··· 126 /// use atproto_record::lexicon::Bytes; 127 /// 128 /// let sig = create_typed_signature( 129 - /// "did:plc:issuer".to_string(), 130 /// Bytes { bytes: b"sig_data".to_vec() }, 131 /// ); 132 /// ``` 133 - pub fn create_typed_signature(issuer: String, signature: Bytes) -> TypedSignature { 134 TypedLexicon::new(Signature { 135 - issuer, 136 signature, 137 extra: HashMap::new(), 138 }) ··· 150 let json_str = r#"{ 151 "$type": "community.lexicon.attestation.signature", 152 "issuedAt": "2025-08-19T20:17:17.133Z", 153 - "issuer": "did:web:acudo-dev.smokesignal.tools", 154 "signature": { 155 "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 156 } ··· 160 let typed_sig_result: Result<TypedSignature, _> = serde_json::from_str(json_str); 161 match &typed_sig_result { 162 Ok(sig) => { 163 - println!("TypedSignature OK: issuer={}", sig.inner.issuer); 164 - assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools"); 165 } 166 Err(e) => { 167 eprintln!("TypedSignature deserialization error: {}", e); ··· 172 let sig_or_ref_result: Result<SignatureOrRef, _> = serde_json::from_str(json_str); 173 match &sig_or_ref_result { 174 Ok(SignatureOrRef::Inline(sig)) => { 175 - println!("SignatureOrRef OK (Inline): issuer={}", sig.inner.issuer); 176 - assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools"); 177 } 178 Ok(SignatureOrRef::Reference(_)) => { 179 panic!("Expected Inline signature, got Reference"); ··· 186 // Try without $type field 187 let json_no_type = r#"{ 188 "issuedAt": "2025-08-19T20:17:17.133Z", 189 - "issuer": "did:web:acudo-dev.smokesignal.tools", 190 "signature": { 191 "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 192 } ··· 195 let no_type_result: Result<Signature, _> = serde_json::from_str(json_no_type); 196 match &no_type_result { 197 Ok(sig) => { 198 - println!("Signature (no type) OK: issuer={}", sig.issuer); 199 - assert_eq!(sig.issuer, "did:web:acudo-dev.smokesignal.tools"); 200 assert_eq!(sig.signature.bytes.len(), 64); 201 202 // Now wrap it in TypedLexicon and try as SignatureOrRef ··· 220 fn test_signature_deserialization() { 221 let json_str = r#"{ 222 "$type": "community.lexicon.attestation.signature", 223 - "issuer": "did:plc:test123", 224 "signature": {"$bytes": "dGVzdCBzaWduYXR1cmU="} 225 }"#; 226 227 let signature: Signature = serde_json::from_str(json_str).unwrap(); 228 229 - assert_eq!(signature.issuer, "did:plc:test123"); 230 assert_eq!(signature.signature.bytes, b"test signature"); 231 // The $type field will be captured in extra due to #[serde(flatten)] 232 assert_eq!(signature.extra.len(), 1); ··· 237 fn test_signature_deserialization_with_extra_fields() { 238 let json_str = r#"{ 239 "$type": "community.lexicon.attestation.signature", 240 - "issuer": "did:plc:test123", 241 "signature": {"$bytes": "dGVzdCBzaWduYXR1cmU="}, 242 "issuedAt": "2024-01-01T00:00:00.000Z", 243 "purpose": "verification" ··· 245 246 let signature: Signature = serde_json::from_str(json_str).unwrap(); 247 248 - assert_eq!(signature.issuer, "did:plc:test123"); 249 assert_eq!(signature.signature.bytes, b"test signature"); 250 // 3 extra fields: $type, issuedAt, purpose 251 assert_eq!(signature.extra.len(), 3); ··· 263 extra.insert("custom_field".to_string(), json!("custom_value")); 264 265 let signature = Signature { 266 - issuer: "did:plc:serializer".to_string(), 267 signature: Bytes { 268 bytes: b"hello world".to_vec(), 269 }, ··· 274 275 // Without custom Serialize impl, $type is not automatically added 276 assert!(!json.as_object().unwrap().contains_key("$type")); 277 - assert_eq!(json["issuer"], "did:plc:serializer"); 278 // "hello world" base64 encoded is "aGVsbG8gd29ybGQ=" 279 assert_eq!(json["signature"]["$bytes"], "aGVsbG8gd29ybGQ="); 280 assert_eq!(json["custom_field"], "custom_value"); ··· 283 #[test] 284 fn test_signature_round_trip() { 285 let original = Signature { 286 - issuer: "did:plc:roundtrip".to_string(), 287 signature: Bytes { 288 bytes: b"round trip test".to_vec(), 289 }, ··· 296 // Deserialize back 297 let deserialized: Signature = serde_json::from_str(&json).unwrap(); 298 299 - assert_eq!(original.issuer, deserialized.issuer); 300 assert_eq!(original.signature.bytes, deserialized.signature.bytes); 301 // Without the custom Serialize impl, no $type is added 302 // so the round-trip preserves the empty extra map ··· 317 extra.insert("tags".to_string(), json!(["tag1", "tag2", "tag3"])); 318 319 let signature = Signature { 320 - issuer: "did:plc:complex".to_string(), 321 signature: Bytes { 322 bytes: vec![0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA], 323 }, ··· 328 329 // Without custom Serialize impl, $type is not automatically added 330 assert!(!json.as_object().unwrap().contains_key("$type")); 331 - assert_eq!(json["issuer"], "did:plc:complex"); 332 assert_eq!(json["timestamp"], 1234567890); 333 assert_eq!(json["metadata"]["version"], "1.0"); 334 assert_eq!(json["metadata"]["algorithm"], "ES256"); ··· 338 #[test] 339 fn test_empty_signature() { 340 let signature = Signature { 341 - issuer: String::new(), 342 signature: Bytes { bytes: Vec::new() }, 343 extra: HashMap::new(), 344 }; ··· 347 348 // Without custom Serialize impl, $type is not automatically added 349 assert!(!json.as_object().unwrap().contains_key("$type")); 350 - assert_eq!(json["issuer"], ""); 351 assert_eq!(json["signature"]["$bytes"], ""); // Empty bytes encode to empty string 352 } 353 ··· 356 // Test with plain Vec<Signature> for basic signature serialization 357 let signatures: Vec<Signature> = vec![ 358 Signature { 359 - issuer: "did:plc:first".to_string(), 360 signature: Bytes { 361 bytes: b"first".to_vec(), 362 }, 363 extra: HashMap::new(), 364 }, 365 Signature { 366 - issuer: "did:plc:second".to_string(), 367 signature: Bytes { 368 bytes: b"second".to_vec(), 369 }, ··· 375 376 assert!(json.is_array()); 377 assert_eq!(json.as_array().unwrap().len(), 2); 378 - assert_eq!(json[0]["issuer"], "did:plc:first"); 379 - assert_eq!(json[1]["issuer"], "did:plc:second"); 380 } 381 382 #[test] ··· 384 // Test the new Signatures type with inline signatures 385 let signatures: Signatures = vec![ 386 SignatureOrRef::Inline(create_typed_signature( 387 - "did:plc:first".to_string(), 388 Bytes { 389 bytes: b"first".to_vec(), 390 }, 391 )), 392 SignatureOrRef::Inline(create_typed_signature( 393 - "did:plc:second".to_string(), 394 Bytes { 395 bytes: b"second".to_vec(), 396 }, ··· 402 assert!(json.is_array()); 403 assert_eq!(json.as_array().unwrap().len(), 2); 404 assert_eq!(json[0]["$type"], "community.lexicon.attestation.signature"); 405 - assert_eq!(json[0]["issuer"], "did:plc:first"); 406 assert_eq!(json[1]["$type"], "community.lexicon.attestation.signature"); 407 - assert_eq!(json[1]["issuer"], "did:plc:second"); 408 } 409 410 #[test] 411 fn test_typed_signature_serialization() { 412 let typed_sig = create_typed_signature( 413 - "did:plc:typed".to_string(), 414 Bytes { 415 bytes: b"typed signature".to_vec(), 416 }, ··· 419 let json = serde_json::to_value(&typed_sig).unwrap(); 420 421 assert_eq!(json["$type"], "community.lexicon.attestation.signature"); 422 - assert_eq!(json["issuer"], "did:plc:typed"); 423 // "typed signature" base64 encoded 424 assert_eq!(json["signature"]["$bytes"], "dHlwZWQgc2lnbmF0dXJl"); 425 } ··· 428 fn test_typed_signature_deserialization() { 429 let json = json!({ 430 "$type": "community.lexicon.attestation.signature", 431 - "issuer": "did:plc:typed", 432 "signature": {"$bytes": "dHlwZWQgc2lnbmF0dXJl"} 433 }); 434 435 let typed_sig: TypedSignature = serde_json::from_value(json).unwrap(); 436 437 - assert_eq!(typed_sig.inner.issuer, "did:plc:typed"); 438 assert_eq!(typed_sig.inner.signature.bytes, b"typed signature"); 439 assert!(typed_sig.has_type_field()); 440 assert!(typed_sig.validate().is_ok()); ··· 443 #[test] 444 fn test_typed_signature_without_type_field() { 445 let json = json!({ 446 - "issuer": "did:plc:notype", 447 "signature": {"$bytes": "bm8gdHlwZQ=="} // "no type" in base64 448 }); 449 450 let typed_sig: TypedSignature = serde_json::from_value(json).unwrap(); 451 452 - assert_eq!(typed_sig.inner.issuer, "did:plc:notype"); 453 assert_eq!(typed_sig.inner.signature.bytes, b"no type"); 454 assert!(!typed_sig.has_type_field()); 455 // Validation should still pass because type_required() returns false for Signature ··· 459 #[test] 460 fn test_typed_signature_with_extra_fields() { 461 let mut sig = Signature { 462 - issuer: "did:plc:extra".to_string(), 463 signature: Bytes { 464 bytes: b"extra test".to_vec(), 465 }, ··· 474 let json = serde_json::to_value(&typed_sig).unwrap(); 475 476 assert_eq!(json["$type"], "community.lexicon.attestation.signature"); 477 - assert_eq!(json["issuer"], "did:plc:extra"); 478 assert_eq!(json["customField"], "customValue"); 479 assert_eq!(json["timestamp"], 1234567890); 480 } ··· 482 #[test] 483 fn test_typed_signature_round_trip() { 484 let original = Signature { 485 - issuer: "did:plc:roundtrip2".to_string(), 486 signature: Bytes { 487 bytes: b"round trip typed".to_vec(), 488 }, ··· 494 let json = serde_json::to_string(&typed).unwrap(); 495 let deserialized: TypedSignature = serde_json::from_str(&json).unwrap(); 496 497 - assert_eq!(deserialized.inner.issuer, original.issuer); 498 assert_eq!(deserialized.inner.signature.bytes, original.signature.bytes); 499 assert!(deserialized.has_type_field()); 500 } ··· 503 fn test_typed_signatures_vec() { 504 let typed_sigs: Vec<TypedSignature> = vec![ 505 create_typed_signature( 506 - "did:plc:first".to_string(), 507 Bytes { 508 bytes: b"first".to_vec(), 509 }, 510 ), 511 create_typed_signature( 512 - "did:plc:second".to_string(), 513 Bytes { 514 bytes: b"second".to_vec(), 515 }, ··· 520 521 assert!(json.is_array()); 522 assert_eq!(json[0]["$type"], "community.lexicon.attestation.signature"); 523 - assert_eq!(json[0]["issuer"], "did:plc:first"); 524 assert_eq!(json[1]["$type"], "community.lexicon.attestation.signature"); 525 - assert_eq!(json[1]["issuer"], "did:plc:second"); 526 } 527 528 #[test] 529 fn test_plain_vs_typed_signature() { 530 // Plain Signature doesn't include $type field 531 let plain_sig = Signature { 532 - issuer: "did:plc:plain".to_string(), 533 signature: Bytes { 534 bytes: b"plain sig".to_vec(), 535 }, ··· 548 ); 549 550 // Both have the same core data 551 - assert_eq!(plain_json["issuer"], typed_json["issuer"]); 552 assert_eq!(plain_json["signature"], typed_json["signature"]); 553 } 554 ··· 556 fn test_signature_or_ref_inline() { 557 // Test inline signature 558 let inline_sig = create_typed_signature( 559 - "did:plc:inline".to_string(), 560 Bytes { 561 bytes: b"inline signature".to_vec(), 562 }, ··· 567 // Serialize 568 let json = serde_json::to_value(&sig_or_ref).unwrap(); 569 assert_eq!(json["$type"], "community.lexicon.attestation.signature"); 570 - assert_eq!(json["issuer"], "did:plc:inline"); 571 assert_eq!(json["signature"]["$bytes"], "aW5saW5lIHNpZ25hdHVyZQ=="); // "inline signature" in base64 572 573 // Deserialize 574 let deserialized: SignatureOrRef = serde_json::from_value(json.clone()).unwrap(); 575 match deserialized { 576 SignatureOrRef::Inline(sig) => { 577 - assert_eq!(sig.inner.issuer, "did:plc:inline"); 578 assert_eq!(sig.inner.signature.bytes, b"inline signature"); 579 } 580 _ => panic!("Expected inline signature"), ··· 621 let signatures: Signatures = vec![ 622 // Inline signature 623 SignatureOrRef::Inline(create_typed_signature( 624 - "did:plc:signer1".to_string(), 625 Bytes { 626 bytes: b"sig1".to_vec(), 627 }, ··· 633 })), 634 // Another inline signature 635 SignatureOrRef::Inline(create_typed_signature( 636 - "did:plc:signer3".to_string(), 637 Bytes { 638 bytes: b"sig3".to_vec(), 639 }, ··· 648 649 // First element should be inline signature 650 assert_eq!(array[0]["$type"], "community.lexicon.attestation.signature"); 651 - assert_eq!(array[0]["issuer"], "did:plc:signer1"); 652 653 // Second element should be reference 654 assert_eq!(array[1]["$type"], "com.atproto.repo.strongRef"); ··· 659 660 // Third element should be inline signature 661 assert_eq!(array[2]["$type"], "community.lexicon.attestation.signature"); 662 - assert_eq!(array[2]["issuer"], "did:plc:signer3"); 663 664 // Deserialize back 665 let deserialized: Signatures = serde_json::from_value(json).unwrap(); ··· 667 668 // Verify each element 669 match &deserialized[0] { 670 - SignatureOrRef::Inline(sig) => assert_eq!(sig.inner.issuer, "did:plc:signer1"), 671 _ => panic!("Expected inline signature at index 0"), 672 } 673 ··· 682 } 683 684 match &deserialized[2] { 685 - SignatureOrRef::Inline(sig) => assert_eq!(sig.inner.issuer, "did:plc:signer3"), 686 _ => panic!("Expected inline signature at index 2"), 687 } 688 } ··· 694 // Inline signature JSON 695 let inline_json = r#"{ 696 "$type": "community.lexicon.attestation.signature", 697 - "issuer": "did:plc:testinline", 698 "signature": {"$bytes": "aGVsbG8="} 699 }"#; 700 701 let inline_deser: SignatureOrRef = serde_json::from_str(inline_json).unwrap(); 702 match inline_deser { 703 SignatureOrRef::Inline(sig) => { 704 - assert_eq!(sig.inner.issuer, "did:plc:testinline"); 705 assert_eq!(sig.inner.signature.bytes, b"hello"); 706 } 707 _ => panic!("Expected inline signature"),
··· 30 /// 31 /// // Inline signature 32 /// let inline = SignatureOrRef::Inline(create_typed_signature( 33 /// Bytes { bytes: b"signature".to_vec() }, 34 /// )); 35 /// ··· 54 55 /// Cryptographic signature structure. 56 /// 57 + /// Represents a cryptographic signature over some data. The signature can be 58 + /// used to verify authenticity, authorization, or other properties of the 59 + /// signed content. 60 /// 61 /// # Fields 62 /// 63 /// - `signature`: The actual signature bytes 64 /// - `extra`: Additional fields that may be present in the signature 65 /// ··· 71 /// use std::collections::HashMap; 72 /// 73 /// let sig = Signature { 74 /// signature: Bytes { bytes: b"signature_bytes".to_vec() }, 75 /// extra: HashMap::new(), 76 /// }; ··· 78 #[derive(Deserialize, Serialize, Clone, PartialEq)] 79 #[cfg_attr(debug_assertions, derive(Debug))] 80 pub struct Signature { 81 /// The cryptographic signature bytes 82 pub signature: Bytes, 83 ··· 110 /// 111 /// # Arguments 112 /// 113 /// * `signature` - The signature bytes 114 /// 115 /// # Example ··· 119 /// use atproto_record::lexicon::Bytes; 120 /// 121 /// let sig = create_typed_signature( 122 /// Bytes { bytes: b"sig_data".to_vec() }, 123 /// ); 124 /// ``` 125 + pub fn create_typed_signature(signature: Bytes) -> TypedSignature { 126 TypedLexicon::new(Signature { 127 signature, 128 extra: HashMap::new(), 129 }) ··· 141 let json_str = r#"{ 142 "$type": "community.lexicon.attestation.signature", 143 "issuedAt": "2025-08-19T20:17:17.133Z", 144 "signature": { 145 "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 146 } ··· 150 let typed_sig_result: Result<TypedSignature, _> = serde_json::from_str(json_str); 151 match &typed_sig_result { 152 Ok(sig) => { 153 + println!("TypedSignature OK: signature bytes len={}", sig.inner.signature.bytes.len()); 154 + assert_eq!(sig.inner.signature.bytes.len(), 64); 155 } 156 Err(e) => { 157 eprintln!("TypedSignature deserialization error: {}", e); ··· 162 let sig_or_ref_result: Result<SignatureOrRef, _> = serde_json::from_str(json_str); 163 match &sig_or_ref_result { 164 Ok(SignatureOrRef::Inline(sig)) => { 165 + println!("SignatureOrRef OK (Inline): signature bytes len={}", sig.inner.signature.bytes.len()); 166 + assert_eq!(sig.inner.signature.bytes.len(), 64); 167 } 168 Ok(SignatureOrRef::Reference(_)) => { 169 panic!("Expected Inline signature, got Reference"); ··· 176 // Try without $type field 177 let json_no_type = r#"{ 178 "issuedAt": "2025-08-19T20:17:17.133Z", 179 "signature": { 180 "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 181 } ··· 184 let no_type_result: Result<Signature, _> = serde_json::from_str(json_no_type); 185 match &no_type_result { 186 Ok(sig) => { 187 + println!("Signature (no type) OK: signature bytes len={}", sig.signature.bytes.len()); 188 assert_eq!(sig.signature.bytes.len(), 64); 189 190 // Now wrap it in TypedLexicon and try as SignatureOrRef ··· 208 fn test_signature_deserialization() { 209 let json_str = r#"{ 210 "$type": "community.lexicon.attestation.signature", 211 "signature": {"$bytes": "dGVzdCBzaWduYXR1cmU="} 212 }"#; 213 214 let signature: Signature = serde_json::from_str(json_str).unwrap(); 215 216 assert_eq!(signature.signature.bytes, b"test signature"); 217 // The $type field will be captured in extra due to #[serde(flatten)] 218 assert_eq!(signature.extra.len(), 1); ··· 223 fn test_signature_deserialization_with_extra_fields() { 224 let json_str = r#"{ 225 "$type": "community.lexicon.attestation.signature", 226 "signature": {"$bytes": "dGVzdCBzaWduYXR1cmU="}, 227 "issuedAt": "2024-01-01T00:00:00.000Z", 228 "purpose": "verification" ··· 230 231 let signature: Signature = serde_json::from_str(json_str).unwrap(); 232 233 assert_eq!(signature.signature.bytes, b"test signature"); 234 // 3 extra fields: $type, issuedAt, purpose 235 assert_eq!(signature.extra.len(), 3); ··· 247 extra.insert("custom_field".to_string(), json!("custom_value")); 248 249 let signature = Signature { 250 signature: Bytes { 251 bytes: b"hello world".to_vec(), 252 }, ··· 257 258 // Without custom Serialize impl, $type is not automatically added 259 assert!(!json.as_object().unwrap().contains_key("$type")); 260 // "hello world" base64 encoded is "aGVsbG8gd29ybGQ=" 261 assert_eq!(json["signature"]["$bytes"], "aGVsbG8gd29ybGQ="); 262 assert_eq!(json["custom_field"], "custom_value"); ··· 265 #[test] 266 fn test_signature_round_trip() { 267 let original = Signature { 268 signature: Bytes { 269 bytes: b"round trip test".to_vec(), 270 }, ··· 277 // Deserialize back 278 let deserialized: Signature = serde_json::from_str(&json).unwrap(); 279 280 assert_eq!(original.signature.bytes, deserialized.signature.bytes); 281 // Without the custom Serialize impl, no $type is added 282 // so the round-trip preserves the empty extra map ··· 297 extra.insert("tags".to_string(), json!(["tag1", "tag2", "tag3"])); 298 299 let signature = Signature { 300 signature: Bytes { 301 bytes: vec![0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA], 302 }, ··· 307 308 // Without custom Serialize impl, $type is not automatically added 309 assert!(!json.as_object().unwrap().contains_key("$type")); 310 assert_eq!(json["timestamp"], 1234567890); 311 assert_eq!(json["metadata"]["version"], "1.0"); 312 assert_eq!(json["metadata"]["algorithm"], "ES256"); ··· 316 #[test] 317 fn test_empty_signature() { 318 let signature = Signature { 319 signature: Bytes { bytes: Vec::new() }, 320 extra: HashMap::new(), 321 }; ··· 324 325 // Without custom Serialize impl, $type is not automatically added 326 assert!(!json.as_object().unwrap().contains_key("$type")); 327 assert_eq!(json["signature"]["$bytes"], ""); // Empty bytes encode to empty string 328 } 329 ··· 332 // Test with plain Vec<Signature> for basic signature serialization 333 let signatures: Vec<Signature> = vec![ 334 Signature { 335 signature: Bytes { 336 bytes: b"first".to_vec(), 337 }, 338 extra: HashMap::new(), 339 }, 340 Signature { 341 signature: Bytes { 342 bytes: b"second".to_vec(), 343 }, ··· 349 350 assert!(json.is_array()); 351 assert_eq!(json.as_array().unwrap().len(), 2); 352 + assert_eq!(json[0]["signature"]["$bytes"], "Zmlyc3Q="); // "first" in base64 353 + assert_eq!(json[1]["signature"]["$bytes"], "c2Vjb25k"); // "second" in base64 354 } 355 356 #[test] ··· 358 // Test the new Signatures type with inline signatures 359 let signatures: Signatures = vec![ 360 SignatureOrRef::Inline(create_typed_signature( 361 Bytes { 362 bytes: b"first".to_vec(), 363 }, 364 )), 365 SignatureOrRef::Inline(create_typed_signature( 366 Bytes { 367 bytes: b"second".to_vec(), 368 }, ··· 374 assert!(json.is_array()); 375 assert_eq!(json.as_array().unwrap().len(), 2); 376 assert_eq!(json[0]["$type"], "community.lexicon.attestation.signature"); 377 + assert_eq!(json[0]["signature"]["$bytes"], "Zmlyc3Q="); // "first" in base64 378 assert_eq!(json[1]["$type"], "community.lexicon.attestation.signature"); 379 + assert_eq!(json[1]["signature"]["$bytes"], "c2Vjb25k"); // "second" in base64 380 } 381 382 #[test] 383 fn test_typed_signature_serialization() { 384 let typed_sig = create_typed_signature( 385 Bytes { 386 bytes: b"typed signature".to_vec(), 387 }, ··· 390 let json = serde_json::to_value(&typed_sig).unwrap(); 391 392 assert_eq!(json["$type"], "community.lexicon.attestation.signature"); 393 // "typed signature" base64 encoded 394 assert_eq!(json["signature"]["$bytes"], "dHlwZWQgc2lnbmF0dXJl"); 395 } ··· 398 fn test_typed_signature_deserialization() { 399 let json = json!({ 400 "$type": "community.lexicon.attestation.signature", 401 "signature": {"$bytes": "dHlwZWQgc2lnbmF0dXJl"} 402 }); 403 404 let typed_sig: TypedSignature = serde_json::from_value(json).unwrap(); 405 406 assert_eq!(typed_sig.inner.signature.bytes, b"typed signature"); 407 assert!(typed_sig.has_type_field()); 408 assert!(typed_sig.validate().is_ok()); ··· 411 #[test] 412 fn test_typed_signature_without_type_field() { 413 let json = json!({ 414 "signature": {"$bytes": "bm8gdHlwZQ=="} // "no type" in base64 415 }); 416 417 let typed_sig: TypedSignature = serde_json::from_value(json).unwrap(); 418 419 assert_eq!(typed_sig.inner.signature.bytes, b"no type"); 420 assert!(!typed_sig.has_type_field()); 421 // Validation should still pass because type_required() returns false for Signature ··· 425 #[test] 426 fn test_typed_signature_with_extra_fields() { 427 let mut sig = Signature { 428 signature: Bytes { 429 bytes: b"extra test".to_vec(), 430 }, ··· 439 let json = serde_json::to_value(&typed_sig).unwrap(); 440 441 assert_eq!(json["$type"], "community.lexicon.attestation.signature"); 442 assert_eq!(json["customField"], "customValue"); 443 assert_eq!(json["timestamp"], 1234567890); 444 } ··· 446 #[test] 447 fn test_typed_signature_round_trip() { 448 let original = Signature { 449 signature: Bytes { 450 bytes: b"round trip typed".to_vec(), 451 }, ··· 457 let json = serde_json::to_string(&typed).unwrap(); 458 let deserialized: TypedSignature = serde_json::from_str(&json).unwrap(); 459 460 assert_eq!(deserialized.inner.signature.bytes, original.signature.bytes); 461 assert!(deserialized.has_type_field()); 462 } ··· 465 fn test_typed_signatures_vec() { 466 let typed_sigs: Vec<TypedSignature> = vec![ 467 create_typed_signature( 468 Bytes { 469 bytes: b"first".to_vec(), 470 }, 471 ), 472 create_typed_signature( 473 Bytes { 474 bytes: b"second".to_vec(), 475 }, ··· 480 481 assert!(json.is_array()); 482 assert_eq!(json[0]["$type"], "community.lexicon.attestation.signature"); 483 + assert_eq!(json[0]["signature"]["$bytes"], "Zmlyc3Q="); // "first" in base64 484 assert_eq!(json[1]["$type"], "community.lexicon.attestation.signature"); 485 + assert_eq!(json[1]["signature"]["$bytes"], "c2Vjb25k"); // "second" in base64 486 } 487 488 #[test] 489 fn test_plain_vs_typed_signature() { 490 // Plain Signature doesn't include $type field 491 let plain_sig = Signature { 492 signature: Bytes { 493 bytes: b"plain sig".to_vec(), 494 }, ··· 507 ); 508 509 // Both have the same core data 510 assert_eq!(plain_json["signature"], typed_json["signature"]); 511 } 512 ··· 514 fn test_signature_or_ref_inline() { 515 // Test inline signature 516 let inline_sig = create_typed_signature( 517 Bytes { 518 bytes: b"inline signature".to_vec(), 519 }, ··· 524 // Serialize 525 let json = serde_json::to_value(&sig_or_ref).unwrap(); 526 assert_eq!(json["$type"], "community.lexicon.attestation.signature"); 527 assert_eq!(json["signature"]["$bytes"], "aW5saW5lIHNpZ25hdHVyZQ=="); // "inline signature" in base64 528 529 // Deserialize 530 let deserialized: SignatureOrRef = serde_json::from_value(json.clone()).unwrap(); 531 match deserialized { 532 SignatureOrRef::Inline(sig) => { 533 assert_eq!(sig.inner.signature.bytes, b"inline signature"); 534 } 535 _ => panic!("Expected inline signature"), ··· 576 let signatures: Signatures = vec![ 577 // Inline signature 578 SignatureOrRef::Inline(create_typed_signature( 579 Bytes { 580 bytes: b"sig1".to_vec(), 581 }, ··· 587 })), 588 // Another inline signature 589 SignatureOrRef::Inline(create_typed_signature( 590 Bytes { 591 bytes: b"sig3".to_vec(), 592 }, ··· 601 602 // First element should be inline signature 603 assert_eq!(array[0]["$type"], "community.lexicon.attestation.signature"); 604 + assert_eq!(array[0]["signature"]["$bytes"], "c2lnMQ=="); // "sig1" in base64 605 606 // Second element should be reference 607 assert_eq!(array[1]["$type"], "com.atproto.repo.strongRef"); ··· 612 613 // Third element should be inline signature 614 assert_eq!(array[2]["$type"], "community.lexicon.attestation.signature"); 615 + assert_eq!(array[2]["signature"]["$bytes"], "c2lnMw=="); // "sig3" in base64 616 617 // Deserialize back 618 let deserialized: Signatures = serde_json::from_value(json).unwrap(); ··· 620 621 // Verify each element 622 match &deserialized[0] { 623 + SignatureOrRef::Inline(sig) => assert_eq!(sig.inner.signature.bytes, b"sig1"), 624 _ => panic!("Expected inline signature at index 0"), 625 } 626 ··· 635 } 636 637 match &deserialized[2] { 638 + SignatureOrRef::Inline(sig) => assert_eq!(sig.inner.signature.bytes, b"sig3"), 639 _ => panic!("Expected inline signature at index 2"), 640 } 641 } ··· 647 // Inline signature JSON 648 let inline_json = r#"{ 649 "$type": "community.lexicon.attestation.signature", 650 "signature": {"$bytes": "aGVsbG8="} 651 }"#; 652 653 let inline_deser: SignatureOrRef = serde_json::from_str(inline_json).unwrap(); 654 match inline_deser { 655 SignatureOrRef::Inline(sig) => { 656 assert_eq!(sig.inner.signature.bytes, b"hello"); 657 } 658 _ => panic!("Expected inline signature"),
+1 -2
crates/atproto-record/src/lexicon/community_lexicon_badge.rs
··· 311 // The signature should be inline in this test 312 match sig_or_ref { 313 crate::lexicon::community_lexicon_attestation::SignatureOrRef::Inline(sig) => { 314 - assert_eq!(sig.issuer, "did:plc:issuer"); 315 // The bytes should match the decoded base64 value 316 // "dGVzdCBzaWduYXR1cmU=" decodes to "test signature" 317 - assert_eq!(sig.signature.bytes, b"test signature".to_vec()); 318 } 319 _ => panic!("Expected inline signature"), 320 }
··· 311 // The signature should be inline in this test 312 match sig_or_ref { 313 crate::lexicon::community_lexicon_attestation::SignatureOrRef::Inline(sig) => { 314 // The bytes should match the decoded base64 value 315 // "dGVzdCBzaWduYXR1cmU=" decodes to "test signature" 316 + assert_eq!(sig.inner.signature.bytes, b"test signature".to_vec()); 317 } 318 _ => panic!("Expected inline signature"), 319 }
+43 -9
crates/atproto-record/src/lexicon/community_lexicon_calendar_event.rs
··· 10 11 use crate::datetime::format as datetime_format; 12 use crate::datetime::optional_format as optional_datetime_format; 13 use crate::lexicon::TypedBlob; 14 use crate::lexicon::community::lexicon::location::Locations; 15 use crate::typed::{LexiconType, TypedLexicon}; 16 17 - /// The namespace identifier for events 18 pub const NSID: &str = "community.lexicon.calendar.event"; 19 20 /// Event status enumeration. ··· 65 Hybrid, 66 } 67 68 - /// The namespace identifier for named URIs 69 pub const NAMED_URI_NSID: &str = "community.lexicon.calendar.event#uri"; 70 71 /// Named URI structure. ··· 89 } 90 } 91 92 - /// Type alias for NamedUri with automatic $type field handling 93 pub type TypedNamedUri = TypedLexicon<NamedUri>; 94 95 - /// The namespace identifier for event links 96 pub const EVENT_LINK_NSID: &str = "community.lexicon.calendar.event#uri"; 97 98 /// Event link structure. ··· 116 } 117 } 118 119 - /// Type alias for EventLink with automatic $type field handling 120 pub type TypedEventLink = TypedLexicon<EventLink>; 121 122 - /// A vector of typed event links 123 pub type EventLinks = Vec<TypedEventLink>; 124 125 /// Aspect ratio for media content. ··· 134 pub height: u64, 135 } 136 137 - /// The namespace identifier for media 138 pub const MEDIA_NSID: &str = "community.lexicon.calendar.event#media"; 139 140 /// Media structure for event-related visual content. ··· 163 } 164 } 165 166 - /// Type alias for Media with automatic $type field handling 167 pub type TypedMedia = TypedLexicon<Media>; 168 169 - /// A vector of typed media items 170 pub type MediaList = Vec<TypedMedia>; 171 172 /// Calendar event structure. ··· 248 #[serde(skip_serializing_if = "Vec::is_empty", default)] 249 pub media: MediaList, 250 251 /// Extension fields for forward compatibility. 252 /// This catch-all allows unknown fields to be preserved and indexed 253 /// for potential future use without requiring re-indexing. ··· 312 locations: vec![], 313 uris: vec![], 314 media: vec![], 315 extra: HashMap::new(), 316 }; 317 ··· 466 locations: vec![], 467 uris: vec![TypedLexicon::new(event_link)], 468 media: vec![TypedLexicon::new(media)], 469 extra: HashMap::new(), 470 }; 471
··· 10 11 use crate::datetime::format as datetime_format; 12 use crate::datetime::optional_format as optional_datetime_format; 13 + use crate::lexicon::app::bsky::richtext::facet::Facet; 14 use crate::lexicon::TypedBlob; 15 use crate::lexicon::community::lexicon::location::Locations; 16 use crate::typed::{LexiconType, TypedLexicon}; 17 18 + /// Lexicon namespace identifier for calendar events. 19 + /// 20 + /// Used as the `$type` field value for event records in the AT Protocol. 21 pub const NSID: &str = "community.lexicon.calendar.event"; 22 23 /// Event status enumeration. ··· 68 Hybrid, 69 } 70 71 + /// Lexicon namespace identifier for named URIs in calendar events. 72 + /// 73 + /// Used as the `$type` field value for URI references associated with events. 74 pub const NAMED_URI_NSID: &str = "community.lexicon.calendar.event#uri"; 75 76 /// Named URI structure. ··· 94 } 95 } 96 97 + /// Type alias for NamedUri with automatic $type field handling. 98 + /// 99 + /// Wraps `NamedUri` in `TypedLexicon` to ensure proper serialization 100 + /// and deserialization of the `$type` field. 101 pub type TypedNamedUri = TypedLexicon<NamedUri>; 102 103 + /// Lexicon namespace identifier for event links. 104 + /// 105 + /// Used as the `$type` field value for event link references. 106 + /// Note: This shares the same NSID as `NAMED_URI_NSID` for compatibility. 107 pub const EVENT_LINK_NSID: &str = "community.lexicon.calendar.event#uri"; 108 109 /// Event link structure. ··· 127 } 128 } 129 130 + /// Type alias for EventLink with automatic $type field handling. 131 + /// 132 + /// Wraps `EventLink` in `TypedLexicon` to ensure proper serialization 133 + /// and deserialization of the `$type` field. 134 pub type TypedEventLink = TypedLexicon<EventLink>; 135 136 + /// Collection of typed event links. 137 + /// 138 + /// Represents multiple URI references associated with an event, 139 + /// such as registration pages, live streams, or related content. 140 pub type EventLinks = Vec<TypedEventLink>; 141 142 /// Aspect ratio for media content. ··· 151 pub height: u64, 152 } 153 154 + /// Lexicon namespace identifier for event media. 155 + /// 156 + /// Used as the `$type` field value for media attachments associated with events. 157 pub const MEDIA_NSID: &str = "community.lexicon.calendar.event#media"; 158 159 /// Media structure for event-related visual content. ··· 182 } 183 } 184 185 + /// Type alias for Media with automatic $type field handling. 186 + /// 187 + /// Wraps `Media` in `TypedLexicon` to ensure proper serialization 188 + /// and deserialization of the `$type` field. 189 pub type TypedMedia = TypedLexicon<Media>; 190 191 + /// Collection of typed media items. 192 + /// 193 + /// Represents multiple media attachments for an event, such as banners, 194 + /// posters, thumbnails, or promotional images. 195 pub type MediaList = Vec<TypedMedia>; 196 197 /// Calendar event structure. ··· 273 #[serde(skip_serializing_if = "Vec::is_empty", default)] 274 pub media: MediaList, 275 276 + /// Rich text facets for semantic annotations in description field. 277 + /// 278 + /// Enables mentions, links, and hashtags to be embedded in the event 279 + /// description text with proper semantic metadata. 280 + #[serde(skip_serializing_if = "Option::is_none")] 281 + pub facets: Option<Vec<Facet>>, 282 + 283 /// Extension fields for forward compatibility. 284 /// This catch-all allows unknown fields to be preserved and indexed 285 /// for potential future use without requiring re-indexing. ··· 344 locations: vec![], 345 uris: vec![], 346 media: vec![], 347 + facets: None, 348 extra: HashMap::new(), 349 }; 350 ··· 499 locations: vec![], 500 uris: vec![TypedLexicon::new(event_link)], 501 media: vec![TypedLexicon::new(media)], 502 + facets: None, 503 extra: HashMap::new(), 504 }; 505
-3
crates/atproto-record/src/lexicon/community_lexicon_calendar_rsvp.rs
··· 294 assert_eq!(typed_rsvp.inner.signatures.len(), 1); 295 match &typed_rsvp.inner.signatures[0] { 296 SignatureOrRef::Inline(sig) => { 297 - assert_eq!(sig.inner.issuer, "did:plc:issuer"); 298 assert_eq!(sig.inner.signature.bytes, b"test signature"); 299 } 300 _ => panic!("Expected inline signature"), ··· 364 assert_eq!(typed_rsvp.inner.signatures.len(), 1); 365 match &typed_rsvp.inner.signatures[0] { 366 SignatureOrRef::Inline(sig) => { 367 - assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools"); 368 - 369 // Verify the issuedAt field if present 370 if let Some(issued_at_value) = sig.inner.extra.get("issuedAt") { 371 assert_eq!(issued_at_value, "2025-08-19T20:17:17.133Z");
··· 294 assert_eq!(typed_rsvp.inner.signatures.len(), 1); 295 match &typed_rsvp.inner.signatures[0] { 296 SignatureOrRef::Inline(sig) => { 297 assert_eq!(sig.inner.signature.bytes, b"test signature"); 298 } 299 _ => panic!("Expected inline signature"), ··· 363 assert_eq!(typed_rsvp.inner.signatures.len(), 1); 364 match &typed_rsvp.inner.signatures[0] { 365 SignatureOrRef::Inline(sig) => { 366 // Verify the issuedAt field if present 367 if let Some(issued_at_value) = sig.inner.extra.get("issuedAt") { 368 assert_eq!(issued_at_value, "2025-08-19T20:17:17.133Z");
+22
crates/atproto-record/src/lexicon/mod.rs
··· 37 mod community_lexicon_calendar_event; 38 mod community_lexicon_calendar_rsvp; 39 mod community_lexicon_location; 40 mod primatives; 41 42 pub use primatives::*; 43 44 /// AT Protocol core types namespace 45 pub mod com {
··· 37 mod community_lexicon_calendar_event; 38 mod community_lexicon_calendar_rsvp; 39 mod community_lexicon_location; 40 + mod app_bsky_richtext_facet; 41 mod primatives; 42 43 + // Re-export primitive types for convenience 44 pub use primatives::*; 45 + 46 + /// Bluesky application namespace. 47 + /// 48 + /// Contains lexicon types specific to the Bluesky application, 49 + /// including rich text formatting and social features. 50 + pub mod app { 51 + /// Bluesky namespace. 52 + pub mod bsky { 53 + /// Rich text formatting types. 54 + pub mod richtext { 55 + /// Facet types for semantic text annotations. 56 + /// 57 + /// Provides types for mentions, links, hashtags, and other 58 + /// structured metadata that can be attached to text content. 59 + pub mod facet { 60 + pub use crate::lexicon::app_bsky_richtext_facet::*; 61 + } 62 + } 63 + } 64 + } 65 66 /// AT Protocol core types namespace 67 pub mod com {