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