A better Rust ATProto crate
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 48 kB View raw
1// Forked from atrium-lexicon 2// https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lexicon.rs 3// https://github.com/atrium-rs/atrium/blob/main/lexicon/atrium-lex/src/lib.rs 4 5use jacquard_common::{ 6 CowStr, 7 deps::smol_str::SmolStr, 8 into_static::IntoStatic, 9 types::{ 10 blob::MimeType, 11 nsid::Nsid, 12 scope_primitives::{AccountAction, RepoAction}, 13 string::DidService, 14 }, 15}; 16use serde::{Deserialize, Serialize}; 17use serde_repr::{Deserialize_repr, Serialize_repr}; 18use serde_with::skip_serializing_none; 19use std::collections::{BTreeMap, HashMap}; 20use thiserror::Error; 21 22#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy, Default)] 23#[repr(u8)] 24pub enum Lexicon { 25 #[default] 26 Lexicon1 = 1, 27} 28#[skip_serializing_none] 29#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 30pub struct LexiconDoc<'s> { 31 pub lexicon: Lexicon, 32 #[serde(borrow)] 33 pub id: CowStr<'s>, 34 pub revision: Option<u32>, 35 pub description: Option<CowStr<'s>>, 36 pub defs: BTreeMap<SmolStr, LexUserType<'s>>, 37} 38 39// primitives 40 41#[skip_serializing_none] 42#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 43pub struct LexBoolean<'s> { 44 #[serde(borrow)] 45 pub description: Option<CowStr<'s>>, 46 pub default: Option<bool>, 47 pub r#const: Option<bool>, 48} 49 50/// The Lexicon type `integer`. 51/// 52/// Lexicon integers are [specified] as signed and 64-bit, which means that values will 53/// always fit in an `i64`. 54/// 55/// [specified]: https://atproto.com/specs/data-model#data-types 56#[skip_serializing_none] 57#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 58pub struct LexInteger<'s> { 59 #[serde(borrow)] 60 pub description: Option<CowStr<'s>>, 61 pub default: Option<i64>, 62 pub minimum: Option<i64>, 63 pub maximum: Option<i64>, 64 pub r#enum: Option<Vec<i64>>, 65 pub r#const: Option<i64>, 66} 67 68#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] 69#[serde(rename_all = "kebab-case")] 70pub enum LexStringFormat { 71 Datetime, 72 Uri, 73 AtUri, 74 Did, 75 Handle, 76 AtIdentifier, 77 Nsid, 78 Cid, 79 Language, 80 Tid, 81 RecordKey, 82} 83#[skip_serializing_none] 84#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 85#[serde(rename_all = "camelCase")] 86pub struct LexString<'s> { 87 #[serde(borrow)] 88 pub description: Option<CowStr<'s>>, 89 pub format: Option<LexStringFormat>, 90 pub default: Option<CowStr<'s>>, 91 pub min_length: Option<usize>, 92 pub max_length: Option<usize>, 93 pub min_graphemes: Option<usize>, 94 pub max_graphemes: Option<usize>, 95 pub r#enum: Option<Vec<CowStr<'s>>>, 96 pub r#const: Option<CowStr<'s>>, 97 pub known_values: Option<Vec<CowStr<'s>>>, 98} 99 100#[skip_serializing_none] 101#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 102pub struct LexUnknown<'s> { 103 #[serde(borrow)] 104 pub description: Option<CowStr<'s>>, 105} 106// ipld types 107 108#[skip_serializing_none] 109#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 110#[serde(rename_all = "camelCase")] 111pub struct LexBytes<'s> { 112 #[serde(borrow)] 113 pub description: Option<CowStr<'s>>, 114 pub max_length: Option<usize>, 115 pub min_length: Option<usize>, 116} 117 118#[skip_serializing_none] 119#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 120pub struct LexCidLink<'s> { 121 #[serde(borrow)] 122 pub description: Option<CowStr<'s>>, 123} 124 125// references 126 127#[skip_serializing_none] 128#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 129pub struct LexRef<'s> { 130 #[serde(borrow)] 131 pub description: Option<CowStr<'s>>, 132 pub r#ref: CowStr<'s>, 133} 134 135#[skip_serializing_none] 136#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 137pub struct LexRefUnion<'s> { 138 #[serde(borrow)] 139 pub description: Option<CowStr<'s>>, 140 pub refs: Vec<CowStr<'s>>, 141 pub closed: Option<bool>, 142} 143 144// blobs 145 146#[skip_serializing_none] 147#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 148#[serde(rename_all = "camelCase")] 149pub struct LexBlob<'s> { 150 #[serde(borrow)] 151 pub description: Option<CowStr<'s>>, 152 pub accept: Option<Vec<MimeType<CowStr<'s>>>>, 153 pub max_size: Option<usize>, 154} 155 156// complex types 157 158#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 159#[serde(tag = "type", rename_all = "kebab-case")] 160pub enum LexArrayItem<'s> { 161 // lexPrimitive 162 Boolean(LexBoolean<'s>), 163 Integer(LexInteger<'s>), 164 String(LexString<'s>), 165 Unknown(LexUnknown<'s>), 166 // lexIpldType 167 Bytes(LexBytes<'s>), 168 CidLink(LexCidLink<'s>), 169 // lexBlob 170 #[serde(borrow)] 171 Blob(LexBlob<'s>), 172 // lexObject 173 Object(LexObject<'s>), 174 // lexRefVariant 175 Ref(LexRef<'s>), 176 Union(LexRefUnion<'s>), 177} 178 179impl<'s> Default for LexArrayItem<'s> { 180 fn default() -> Self { 181 LexArrayItem::Unknown(LexUnknown::default()) 182 } 183} 184 185#[skip_serializing_none] 186#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 187#[serde(rename_all = "camelCase")] 188pub struct LexArray<'s> { 189 #[serde(borrow)] 190 pub description: Option<CowStr<'s>>, 191 pub items: LexArrayItem<'s>, 192 pub min_length: Option<usize>, 193 pub max_length: Option<usize>, 194} 195 196#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 197#[serde(tag = "type", rename_all = "lowercase")] 198pub enum LexPrimitiveArrayItem<'s> { 199 // lexPrimitive 200 #[serde(borrow)] 201 Boolean(LexBoolean<'s>), 202 Integer(LexInteger<'s>), 203 String(LexString<'s>), 204 Unknown(LexUnknown<'s>), 205} 206 207impl<'s> Default for LexPrimitiveArrayItem<'s> { 208 fn default() -> Self { 209 LexPrimitiveArrayItem::Unknown(LexUnknown::default()) 210 } 211} 212 213#[skip_serializing_none] 214#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 215#[serde(rename_all = "camelCase")] 216pub struct LexPrimitiveArray<'s> { 217 #[serde(borrow)] 218 pub description: Option<CowStr<'s>>, 219 pub items: LexPrimitiveArrayItem<'s>, 220 pub min_length: Option<usize>, 221 pub max_length: Option<usize>, 222} 223 224#[skip_serializing_none] 225#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 226pub struct LexToken<'s> { 227 #[serde(borrow)] 228 pub description: Option<CowStr<'s>>, 229} 230 231#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 232#[serde(tag = "type", rename_all = "kebab-case")] 233pub enum LexObjectProperty<'s> { 234 // lexRefVariant 235 #[serde(borrow)] 236 Ref(LexRef<'s>), 237 Union(LexRefUnion<'s>), 238 // lexIpldType 239 Bytes(LexBytes<'s>), 240 CidLink(LexCidLink<'s>), 241 // lexArray 242 Array(LexArray<'s>), 243 // lexBlob 244 Blob(LexBlob<'s>), 245 // lexObject (nested) 246 Object(LexObject<'s>), 247 // lexPrimitive 248 Boolean(LexBoolean<'s>), 249 Integer(LexInteger<'s>), 250 String(LexString<'s>), 251 Unknown(LexUnknown<'s>), 252} 253#[skip_serializing_none] 254#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 255pub struct LexObject<'s> { 256 #[serde(borrow)] 257 pub description: Option<CowStr<'s>>, 258 pub required: Option<Vec<SmolStr>>, 259 pub nullable: Option<Vec<SmolStr>>, 260 pub properties: BTreeMap<SmolStr, LexObjectProperty<'s>>, 261} 262 263// xrpc 264 265#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 266#[serde(tag = "type", rename_all = "lowercase")] 267pub enum LexXrpcParametersProperty<'s> { 268 // lexPrimitive 269 #[serde(borrow)] 270 Boolean(LexBoolean<'s>), 271 Integer(LexInteger<'s>), 272 String(LexString<'s>), 273 Unknown(LexUnknown<'s>), 274 // lexPrimitiveArray 275 Array(LexPrimitiveArray<'s>), 276} 277#[skip_serializing_none] 278#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 279pub struct LexXrpcParameters<'s> { 280 #[serde(borrow)] 281 pub description: Option<CowStr<'s>>, 282 pub required: Option<Vec<SmolStr>>, 283 pub properties: BTreeMap<SmolStr, LexXrpcParametersProperty<'s>>, 284} 285 286#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 287#[serde(tag = "type", rename_all = "lowercase")] 288pub enum LexXrpcBodySchema<'s> { 289 // lexRefVariant 290 #[serde(borrow)] 291 Ref(LexRef<'s>), 292 Union(LexRefUnion<'s>), 293 // lexObject 294 Object(LexObject<'s>), 295} 296#[skip_serializing_none] 297#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 298pub struct LexXrpcBody<'s> { 299 #[serde(borrow)] 300 pub description: Option<CowStr<'s>>, 301 pub encoding: CowStr<'s>, 302 pub schema: Option<LexXrpcBodySchema<'s>>, 303} 304 305#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 306#[serde(tag = "type", rename_all = "lowercase")] 307pub enum LexXrpcSubscriptionMessageSchema<'s> { 308 // lexRefVariant 309 #[serde(borrow)] 310 Ref(LexRef<'s>), 311 Union(LexRefUnion<'s>), 312 // lexObject 313 Object(LexObject<'s>), 314} 315#[skip_serializing_none] 316#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 317pub struct LexXrpcSubscriptionMessage<'s> { 318 #[serde(borrow)] 319 pub description: Option<CowStr<'s>>, 320 pub schema: Option<LexXrpcSubscriptionMessageSchema<'s>>, 321} 322 323#[skip_serializing_none] 324#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 325pub struct LexXrpcError<'s> { 326 #[serde(borrow)] 327 pub description: Option<CowStr<'s>>, 328 pub name: CowStr<'s>, 329} 330 331#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 332#[serde(tag = "type", rename_all = "lowercase")] 333pub enum LexXrpcQueryParameter<'s> { 334 #[serde(borrow)] 335 Params(LexXrpcParameters<'s>), 336} 337#[skip_serializing_none] 338#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 339pub struct LexXrpcQuery<'s> { 340 #[serde(borrow)] 341 pub description: Option<CowStr<'s>>, 342 pub parameters: Option<LexXrpcQueryParameter<'s>>, 343 pub output: Option<LexXrpcBody<'s>>, 344 pub errors: Option<Vec<LexXrpcError<'s>>>, 345} 346 347#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 348#[serde(tag = "type", rename_all = "lowercase")] 349pub enum LexXrpcProcedureParameter<'s> { 350 #[serde(borrow)] 351 Params(LexXrpcParameters<'s>), 352} 353#[skip_serializing_none] 354#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 355pub struct LexXrpcProcedure<'s> { 356 #[serde(borrow)] 357 pub description: Option<CowStr<'s>>, 358 pub parameters: Option<LexXrpcProcedureParameter<'s>>, 359 pub input: Option<LexXrpcBody<'s>>, 360 pub output: Option<LexXrpcBody<'s>>, 361 pub errors: Option<Vec<LexXrpcError<'s>>>, 362} 363 364#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 365#[serde(tag = "type", rename_all = "lowercase")] 366pub enum LexXrpcSubscriptionParameter<'s> { 367 #[serde(borrow)] 368 Params(LexXrpcParameters<'s>), 369} 370 371#[skip_serializing_none] 372#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 373pub struct LexXrpcSubscription<'s> { 374 #[serde(borrow)] 375 pub description: Option<CowStr<'s>>, 376 pub parameters: Option<LexXrpcSubscriptionParameter<'s>>, 377 pub message: Option<LexXrpcSubscriptionMessage<'s>>, 378 pub infos: Option<Vec<LexXrpcError<'s>>>, 379 pub errors: Option<Vec<LexXrpcError<'s>>>, 380} 381 382// database 383 384#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 385#[serde(tag = "type", rename_all = "lowercase")] 386pub enum LexRecordRecord<'s> { 387 #[serde(borrow)] 388 Object(LexObject<'s>), 389} 390 391impl<'s> Default for LexRecordRecord<'s> { 392 fn default() -> Self { 393 Self::Object(LexObject::default()) 394 } 395} 396 397#[skip_serializing_none] 398#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] 399pub struct LexRecord<'s> { 400 #[serde(borrow)] 401 pub description: Option<CowStr<'s>>, 402 pub key: Option<CowStr<'s>>, 403 pub record: LexRecordRecord<'s>, 404} 405 406// permission sets 407 408/// AT Protocol permission set lexicon type. 409/// 410/// Contains a `permissions` array where each entry is a `LexPermission` 411/// with `"type": "permission"` and a `"resource"` discriminator carrying 412/// typed fields (NSIDs, DIDs, MIME types, action enums). 413#[skip_serializing_none] 414#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 415pub struct LexPermissionSet<'s> { 416 #[serde(borrow)] 417 pub title: Option<CowStr<'s>>, 418 #[serde(default, rename = "title:lang")] 419 pub title_lang: Option<HashMap<CowStr<'s>, CowStr<'s>>>, 420 pub detail: Option<CowStr<'s>>, 421 #[serde(default, rename = "detail:lang")] 422 pub detail_lang: Option<HashMap<CowStr<'s>, CowStr<'s>>>, 423 pub permissions: Vec<LexPermission<'s>>, 424} 425 426/// A permission entry within a permission set. 427/// 428/// Single-variant enum: the `"type": "permission"` JSON tag selects the 429/// `Permission` variant, which wraps a `LexPermissionResource` discriminated 430/// by the `"resource"` field. 431#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 432#[serde(tag = "type", rename_all = "kebab-case")] 433pub enum LexPermission<'s> { 434 /// A permission entry. 435 Permission { 436 #[serde(flatten, borrow)] 437 resource: LexPermissionResource<'s>, 438 }, 439} 440 441/// Resource-specific permission data, discriminated by the `"resource"` field. 442#[skip_serializing_none] 443#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 444#[serde(tag = "resource", rename_all = "kebab-case")] 445pub enum LexPermissionResource<'s> { 446 /// Repository resource permission. 447 Repo { 448 /// Collection NSIDs this permission applies to. 449 #[serde(borrow)] 450 collection: Vec<Nsid<CowStr<'s>>>, 451 /// Permitted actions (create, update, delete). None = all actions. 452 #[serde(default)] 453 action: Option<Vec<RepoAction>>, 454 }, 455 /// RPC method permission. 456 Rpc { 457 /// Lexicon method NSIDs this permission applies to. 458 #[serde(borrow)] 459 lxm: Vec<Nsid<CowStr<'s>>>, 460 /// Audience DID with optional service fragment for inter-service auth. 461 #[serde(borrow, default)] 462 aud: Option<DidService<CowStr<'s>>>, 463 /// If true, inherits audience from the include scope's aud parameter. 464 #[serde(default, rename = "inheritAud")] 465 inherit_aud: Option<bool>, 466 }, 467 /// Blob resource permission. 468 Blob { 469 /// Accepted MIME type patterns. 470 #[serde(borrow)] 471 accept: Vec<MimeType<CowStr<'s>>>, 472 /// Maximum blob size in bytes. 473 #[serde(default)] 474 max_size: Option<u64>, 475 }, 476 /// Identity resource permission. 477 Identity { 478 /// Identity attribute (e.g., "handle"). 479 #[serde(borrow)] 480 attr: CowStr<'s>, 481 }, 482 /// Account resource permission. 483 Account { 484 /// Account attribute (e.g., "email"). 485 #[serde(borrow)] 486 attr: CowStr<'s>, 487 /// Permitted actions (read, manage). None = read. 488 #[serde(default)] 489 action: Option<Vec<AccountAction>>, 490 }, 491} 492 493/// Errors from permission set validation. 494#[derive(Debug, Error)] 495pub enum PermissionSetError { 496 #[error("permission set has empty permissions array")] 497 EmptyPermissions, 498 499 #[error("permission set {nsid} references out-of-namespace resource: {resource}")] 500 NamespaceViolation { nsid: String, resource: String }, 501} 502 503impl<'s> LexPermissionSet<'s> { 504 /// Validate the permission set against its owning NSID. 505 /// 506 /// Checks: 507 /// 1. Permissions array is non-empty 508 /// 2. All NSID-scoped resources (collection, lxm) are within the 509 /// owning NSID's namespace (first two segments) 510 pub fn validate(&self, owning_nsid: &str) -> Result<(), PermissionSetError> { 511 if self.permissions.is_empty() { 512 return Err(PermissionSetError::EmptyPermissions); 513 } 514 515 let namespace = { 516 let mut parts = owning_nsid.splitn(3, '.'); 517 match (parts.next(), parts.next()) { 518 (Some(a), Some(b)) => format!("{}.{}", a, b), 519 _ => owning_nsid.to_string(), 520 } 521 }; 522 523 for perm in &self.permissions { 524 let LexPermission::Permission { resource } = perm; 525 match resource { 526 LexPermissionResource::Repo { collection, .. } => { 527 for col in collection { 528 let col_str: &str = col.as_ref(); 529 if !col_str.starts_with(&format!("{}.", namespace)) { 530 return Err(PermissionSetError::NamespaceViolation { 531 nsid: owning_nsid.to_string(), 532 resource: col_str.to_string(), 533 }); 534 } 535 } 536 } 537 LexPermissionResource::Rpc { lxm, .. } => { 538 for l in lxm { 539 let lxm_str: &str = l.as_ref(); 540 if !lxm_str.starts_with(&format!("{}.", namespace)) { 541 return Err(PermissionSetError::NamespaceViolation { 542 nsid: owning_nsid.to_string(), 543 resource: lxm_str.to_string(), 544 }); 545 } 546 } 547 } 548 // Blob, Identity, Account don't have namespace-scoped NSID resources. 549 _ => {} 550 } 551 } 552 553 Ok(()) 554 } 555} 556 557// core 558 559#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] 560#[serde(tag = "type", rename_all = "kebab-case")] 561pub enum LexUserType<'s> { 562 // lexRecord 563 #[serde(borrow)] 564 Record(LexRecord<'s>), 565 // lexXrpcQuery 566 #[serde(rename = "query")] 567 XrpcQuery(LexXrpcQuery<'s>), 568 // lexXrpcProcedure 569 #[serde(rename = "procedure")] 570 XrpcProcedure(LexXrpcProcedure<'s>), 571 // lexXrpcSubscription 572 #[serde(rename = "subscription")] 573 XrpcSubscription(LexXrpcSubscription<'s>), 574 // lexBlob 575 Blob(LexBlob<'s>), 576 // lexArray 577 Array(LexArray<'s>), 578 // lexToken 579 Token(LexToken<'s>), 580 // lexObject 581 Object(LexObject<'s>), 582 // lexBoolean, 583 Boolean(LexBoolean<'s>), 584 // lexInteger, 585 Integer(LexInteger<'s>), 586 // lexString, 587 String(LexString<'s>), 588 // lexBytes 589 Bytes(LexBytes<'s>), 590 // lexCidLink 591 CidLink(LexCidLink<'s>), 592 // lexUnknown 593 Unknown(LexUnknown<'s>), 594 // lexRefUnion 595 Union(LexRefUnion<'s>), 596 // lexPermissionSet 597 #[serde(borrow)] 598 PermissionSet(LexPermissionSet<'s>), 599} 600 601// IntoStatic implementations for all lexicon types 602// These enable converting borrowed lexicon docs to owned 'static versions 603 604// Simpler approach: just clone and convert each field 605impl IntoStatic for Lexicon { 606 type Output = Lexicon; 607 fn into_static(self) -> Self::Output { 608 self 609 } 610} 611 612impl IntoStatic for LexStringFormat { 613 type Output = LexStringFormat; 614 fn into_static(self) -> Self::Output { 615 self 616 } 617} 618 619impl IntoStatic for LexiconDoc<'_> { 620 type Output = LexiconDoc<'static>; 621 fn into_static(self) -> Self::Output { 622 LexiconDoc { 623 lexicon: self.lexicon, 624 id: self.id.into_static(), 625 revision: self.revision, 626 description: self.description.into_static(), 627 defs: self.defs.into_static(), 628 } 629 } 630} 631 632impl IntoStatic for LexBoolean<'_> { 633 type Output = LexBoolean<'static>; 634 fn into_static(self) -> Self::Output { 635 LexBoolean { 636 description: self.description.into_static(), 637 default: self.default, 638 r#const: self.r#const, 639 } 640 } 641} 642 643impl IntoStatic for LexInteger<'_> { 644 type Output = LexInteger<'static>; 645 fn into_static(self) -> Self::Output { 646 LexInteger { 647 description: self.description.into_static(), 648 default: self.default, 649 minimum: self.minimum, 650 maximum: self.maximum, 651 r#enum: self.r#enum, 652 r#const: self.r#const, 653 } 654 } 655} 656 657impl IntoStatic for LexString<'_> { 658 type Output = LexString<'static>; 659 fn into_static(self) -> Self::Output { 660 LexString { 661 description: self.description.into_static(), 662 format: self.format, 663 default: self.default.into_static(), 664 min_length: self.min_length, 665 max_length: self.max_length, 666 min_graphemes: self.min_graphemes, 667 max_graphemes: self.max_graphemes, 668 r#enum: self.r#enum.into_static(), 669 r#const: self.r#const.into_static(), 670 known_values: self.known_values.into_static(), 671 } 672 } 673} 674 675impl IntoStatic for LexUnknown<'_> { 676 type Output = LexUnknown<'static>; 677 fn into_static(self) -> Self::Output { 678 LexUnknown { 679 description: self.description.into_static(), 680 } 681 } 682} 683 684impl IntoStatic for LexBytes<'_> { 685 type Output = LexBytes<'static>; 686 fn into_static(self) -> Self::Output { 687 LexBytes { 688 description: self.description.into_static(), 689 max_length: self.max_length, 690 min_length: self.min_length, 691 } 692 } 693} 694 695impl IntoStatic for LexCidLink<'_> { 696 type Output = LexCidLink<'static>; 697 fn into_static(self) -> Self::Output { 698 LexCidLink { 699 description: self.description.into_static(), 700 } 701 } 702} 703 704impl IntoStatic for LexRef<'_> { 705 type Output = LexRef<'static>; 706 fn into_static(self) -> Self::Output { 707 LexRef { 708 description: self.description.into_static(), 709 r#ref: self.r#ref.into_static(), 710 } 711 } 712} 713 714impl IntoStatic for LexRefUnion<'_> { 715 type Output = LexRefUnion<'static>; 716 fn into_static(self) -> Self::Output { 717 LexRefUnion { 718 description: self.description.into_static(), 719 refs: self.refs.into_static(), 720 closed: self.closed, 721 } 722 } 723} 724 725impl IntoStatic for LexBlob<'_> { 726 type Output = LexBlob<'static>; 727 fn into_static(self) -> Self::Output { 728 LexBlob { 729 description: self.description.into_static(), 730 accept: self.accept.into_static(), 731 max_size: self.max_size, 732 } 733 } 734} 735 736impl IntoStatic for LexArrayItem<'_> { 737 type Output = LexArrayItem<'static>; 738 fn into_static(self) -> Self::Output { 739 match self { 740 Self::Boolean(x) => LexArrayItem::Boolean(x.into_static()), 741 Self::Integer(x) => LexArrayItem::Integer(x.into_static()), 742 Self::String(x) => LexArrayItem::String(x.into_static()), 743 Self::Unknown(x) => LexArrayItem::Unknown(x.into_static()), 744 Self::Bytes(x) => LexArrayItem::Bytes(x.into_static()), 745 Self::CidLink(x) => LexArrayItem::CidLink(x.into_static()), 746 Self::Blob(x) => LexArrayItem::Blob(x.into_static()), 747 Self::Object(x) => LexArrayItem::Object(x.into_static()), 748 Self::Ref(x) => LexArrayItem::Ref(x.into_static()), 749 Self::Union(x) => LexArrayItem::Union(x.into_static()), 750 } 751 } 752} 753 754impl IntoStatic for LexArray<'_> { 755 type Output = LexArray<'static>; 756 fn into_static(self) -> Self::Output { 757 LexArray { 758 description: self.description.into_static(), 759 items: self.items.into_static(), 760 min_length: self.min_length, 761 max_length: self.max_length, 762 } 763 } 764} 765 766impl IntoStatic for LexPrimitiveArrayItem<'_> { 767 type Output = LexPrimitiveArrayItem<'static>; 768 fn into_static(self) -> Self::Output { 769 match self { 770 Self::Boolean(x) => LexPrimitiveArrayItem::Boolean(x.into_static()), 771 Self::Integer(x) => LexPrimitiveArrayItem::Integer(x.into_static()), 772 Self::String(x) => LexPrimitiveArrayItem::String(x.into_static()), 773 Self::Unknown(x) => LexPrimitiveArrayItem::Unknown(x.into_static()), 774 } 775 } 776} 777 778impl IntoStatic for LexPrimitiveArray<'_> { 779 type Output = LexPrimitiveArray<'static>; 780 fn into_static(self) -> Self::Output { 781 LexPrimitiveArray { 782 description: self.description.into_static(), 783 items: self.items.into_static(), 784 min_length: self.min_length, 785 max_length: self.max_length, 786 } 787 } 788} 789 790impl IntoStatic for LexToken<'_> { 791 type Output = LexToken<'static>; 792 fn into_static(self) -> Self::Output { 793 LexToken { 794 description: self.description.into_static(), 795 } 796 } 797} 798 799impl IntoStatic for LexObjectProperty<'_> { 800 type Output = LexObjectProperty<'static>; 801 fn into_static(self) -> Self::Output { 802 match self { 803 Self::Ref(x) => LexObjectProperty::Ref(x.into_static()), 804 Self::Union(x) => LexObjectProperty::Union(x.into_static()), 805 Self::Bytes(x) => LexObjectProperty::Bytes(x.into_static()), 806 Self::CidLink(x) => LexObjectProperty::CidLink(x.into_static()), 807 Self::Array(x) => LexObjectProperty::Array(x.into_static()), 808 Self::Blob(x) => LexObjectProperty::Blob(x.into_static()), 809 Self::Object(x) => LexObjectProperty::Object(x.into_static()), 810 Self::Boolean(x) => LexObjectProperty::Boolean(x.into_static()), 811 Self::Integer(x) => LexObjectProperty::Integer(x.into_static()), 812 Self::String(x) => LexObjectProperty::String(x.into_static()), 813 Self::Unknown(x) => LexObjectProperty::Unknown(x.into_static()), 814 } 815 } 816} 817 818impl IntoStatic for LexObject<'_> { 819 type Output = LexObject<'static>; 820 fn into_static(self) -> Self::Output { 821 LexObject { 822 description: self.description.into_static(), 823 required: self.required, 824 nullable: self.nullable, 825 properties: self.properties.into_static(), 826 } 827 } 828} 829 830impl IntoStatic for LexXrpcParametersProperty<'_> { 831 type Output = LexXrpcParametersProperty<'static>; 832 fn into_static(self) -> Self::Output { 833 match self { 834 Self::Boolean(x) => LexXrpcParametersProperty::Boolean(x.into_static()), 835 Self::Integer(x) => LexXrpcParametersProperty::Integer(x.into_static()), 836 Self::String(x) => LexXrpcParametersProperty::String(x.into_static()), 837 Self::Unknown(x) => LexXrpcParametersProperty::Unknown(x.into_static()), 838 Self::Array(x) => LexXrpcParametersProperty::Array(x.into_static()), 839 } 840 } 841} 842 843impl IntoStatic for LexXrpcParameters<'_> { 844 type Output = LexXrpcParameters<'static>; 845 fn into_static(self) -> Self::Output { 846 LexXrpcParameters { 847 description: self.description.into_static(), 848 required: self.required, 849 properties: self.properties.into_static(), 850 } 851 } 852} 853 854impl IntoStatic for LexXrpcBodySchema<'_> { 855 type Output = LexXrpcBodySchema<'static>; 856 fn into_static(self) -> Self::Output { 857 match self { 858 Self::Ref(x) => LexXrpcBodySchema::Ref(x.into_static()), 859 Self::Union(x) => LexXrpcBodySchema::Union(x.into_static()), 860 Self::Object(x) => LexXrpcBodySchema::Object(x.into_static()), 861 } 862 } 863} 864 865impl IntoStatic for LexXrpcBody<'_> { 866 type Output = LexXrpcBody<'static>; 867 fn into_static(self) -> Self::Output { 868 LexXrpcBody { 869 description: self.description.into_static(), 870 encoding: self.encoding.into_static(), 871 schema: self.schema.into_static(), 872 } 873 } 874} 875 876impl IntoStatic for LexXrpcSubscriptionMessageSchema<'_> { 877 type Output = LexXrpcSubscriptionMessageSchema<'static>; 878 fn into_static(self) -> Self::Output { 879 match self { 880 Self::Ref(x) => LexXrpcSubscriptionMessageSchema::Ref(x.into_static()), 881 Self::Union(x) => LexXrpcSubscriptionMessageSchema::Union(x.into_static()), 882 Self::Object(x) => LexXrpcSubscriptionMessageSchema::Object(x.into_static()), 883 } 884 } 885} 886 887impl IntoStatic for LexXrpcSubscriptionMessage<'_> { 888 type Output = LexXrpcSubscriptionMessage<'static>; 889 fn into_static(self) -> Self::Output { 890 LexXrpcSubscriptionMessage { 891 description: self.description.into_static(), 892 schema: self.schema.into_static(), 893 } 894 } 895} 896 897impl IntoStatic for LexXrpcError<'_> { 898 type Output = LexXrpcError<'static>; 899 fn into_static(self) -> Self::Output { 900 LexXrpcError { 901 description: self.description.into_static(), 902 name: self.name.into_static(), 903 } 904 } 905} 906 907impl IntoStatic for LexXrpcQueryParameter<'_> { 908 type Output = LexXrpcQueryParameter<'static>; 909 fn into_static(self) -> Self::Output { 910 match self { 911 Self::Params(x) => LexXrpcQueryParameter::Params(x.into_static()), 912 } 913 } 914} 915 916impl IntoStatic for LexXrpcQuery<'_> { 917 type Output = LexXrpcQuery<'static>; 918 fn into_static(self) -> Self::Output { 919 LexXrpcQuery { 920 description: self.description.into_static(), 921 parameters: self.parameters.into_static(), 922 output: self.output.into_static(), 923 errors: self.errors.into_static(), 924 } 925 } 926} 927 928impl IntoStatic for LexXrpcProcedureParameter<'_> { 929 type Output = LexXrpcProcedureParameter<'static>; 930 fn into_static(self) -> Self::Output { 931 match self { 932 Self::Params(x) => LexXrpcProcedureParameter::Params(x.into_static()), 933 } 934 } 935} 936 937impl IntoStatic for LexXrpcProcedure<'_> { 938 type Output = LexXrpcProcedure<'static>; 939 fn into_static(self) -> Self::Output { 940 LexXrpcProcedure { 941 description: self.description.into_static(), 942 parameters: self.parameters.into_static(), 943 input: self.input.into_static(), 944 output: self.output.into_static(), 945 errors: self.errors.into_static(), 946 } 947 } 948} 949 950impl IntoStatic for LexXrpcSubscriptionParameter<'_> { 951 type Output = LexXrpcSubscriptionParameter<'static>; 952 fn into_static(self) -> Self::Output { 953 match self { 954 Self::Params(x) => LexXrpcSubscriptionParameter::Params(x.into_static()), 955 } 956 } 957} 958 959impl IntoStatic for LexXrpcSubscription<'_> { 960 type Output = LexXrpcSubscription<'static>; 961 fn into_static(self) -> Self::Output { 962 LexXrpcSubscription { 963 description: self.description.into_static(), 964 parameters: self.parameters.into_static(), 965 message: self.message.into_static(), 966 infos: self.infos.into_static(), 967 errors: self.errors.into_static(), 968 } 969 } 970} 971 972impl IntoStatic for LexRecordRecord<'_> { 973 type Output = LexRecordRecord<'static>; 974 fn into_static(self) -> Self::Output { 975 match self { 976 Self::Object(x) => LexRecordRecord::Object(x.into_static()), 977 } 978 } 979} 980 981impl IntoStatic for LexRecord<'_> { 982 type Output = LexRecord<'static>; 983 fn into_static(self) -> Self::Output { 984 LexRecord { 985 description: self.description.into_static(), 986 key: self.key.into_static(), 987 record: self.record.into_static(), 988 } 989 } 990} 991 992impl IntoStatic for LexPermissionResource<'_> { 993 type Output = LexPermissionResource<'static>; 994 995 fn into_static(self) -> Self::Output { 996 match self { 997 LexPermissionResource::Repo { collection, action } => LexPermissionResource::Repo { 998 collection: collection.into_iter().map(|n| n.into_static()).collect(), 999 action, 1000 }, 1001 LexPermissionResource::Rpc { 1002 lxm, 1003 aud, 1004 inherit_aud, 1005 } => LexPermissionResource::Rpc { 1006 lxm: lxm.into_iter().map(|n| n.into_static()).collect(), 1007 aud: aud.map(|d| d.into_static()), 1008 inherit_aud, 1009 }, 1010 LexPermissionResource::Blob { accept, max_size } => LexPermissionResource::Blob { 1011 accept: accept.into_iter().map(|a| a.into_static()).collect(), 1012 max_size, 1013 }, 1014 LexPermissionResource::Identity { attr } => LexPermissionResource::Identity { 1015 attr: attr.into_static(), 1016 }, 1017 LexPermissionResource::Account { attr, action } => LexPermissionResource::Account { 1018 attr: attr.into_static(), 1019 action, 1020 }, 1021 } 1022 } 1023} 1024 1025impl IntoStatic for LexPermission<'_> { 1026 type Output = LexPermission<'static>; 1027 1028 fn into_static(self) -> Self::Output { 1029 match self { 1030 LexPermission::Permission { resource } => LexPermission::Permission { 1031 resource: resource.into_static(), 1032 }, 1033 } 1034 } 1035} 1036 1037impl IntoStatic for LexPermissionSet<'_> { 1038 type Output = LexPermissionSet<'static>; 1039 1040 fn into_static(self) -> Self::Output { 1041 LexPermissionSet { 1042 title: self.title.into_static(), 1043 title_lang: self.title_lang.into_static(), 1044 detail: self.detail.into_static(), 1045 detail_lang: self.detail_lang.into_static(), 1046 permissions: self 1047 .permissions 1048 .into_iter() 1049 .map(|p| p.into_static()) 1050 .collect(), 1051 } 1052 } 1053} 1054 1055impl IntoStatic for LexUserType<'_> { 1056 type Output = LexUserType<'static>; 1057 fn into_static(self) -> Self::Output { 1058 match self { 1059 Self::Record(x) => LexUserType::Record(x.into_static()), 1060 Self::XrpcQuery(x) => LexUserType::XrpcQuery(x.into_static()), 1061 Self::XrpcProcedure(x) => LexUserType::XrpcProcedure(x.into_static()), 1062 Self::XrpcSubscription(x) => LexUserType::XrpcSubscription(x.into_static()), 1063 Self::Blob(x) => LexUserType::Blob(x.into_static()), 1064 Self::Array(x) => LexUserType::Array(x.into_static()), 1065 Self::Token(x) => LexUserType::Token(x.into_static()), 1066 Self::Object(x) => LexUserType::Object(x.into_static()), 1067 Self::Boolean(x) => LexUserType::Boolean(x.into_static()), 1068 Self::Integer(x) => LexUserType::Integer(x.into_static()), 1069 Self::String(x) => LexUserType::String(x.into_static()), 1070 Self::Bytes(x) => LexUserType::Bytes(x.into_static()), 1071 Self::CidLink(x) => LexUserType::CidLink(x.into_static()), 1072 Self::Unknown(x) => LexUserType::Unknown(x.into_static()), 1073 Self::Union(x) => LexUserType::Union(x.into_static()), 1074 Self::PermissionSet(x) => LexUserType::PermissionSet(x.into_static()), 1075 } 1076 } 1077} 1078 1079#[cfg(test)] 1080mod tests { 1081 use super::*; 1082 1083 const LEXICON_EXAMPLE_TOKEN: &str = r#" 1084{ 1085 "lexicon": 1, 1086 "id": "com.socialapp.actorUser", 1087 "defs": { 1088 "main": { 1089 "type": "token", 1090 "description": "Actor type of 'User'" 1091 } 1092 } 1093}"#; 1094 1095 #[test] 1096 fn parse() { 1097 let doc = serde_json::from_str::<LexiconDoc>(LEXICON_EXAMPLE_TOKEN) 1098 .expect("failed to deserialize"); 1099 assert_eq!(doc.lexicon, Lexicon::Lexicon1); 1100 assert_eq!(doc.id, "com.socialapp.actorUser"); 1101 assert_eq!(doc.revision, None); 1102 assert_eq!(doc.description, None); 1103 assert_eq!(doc.defs.len(), 1); 1104 } 1105 1106 // Permission set tests for oauth-scopes-rework.AC5 1107 1108 const PERMISSION_SET_SIMPLE: &str = r#" 1109{ 1110 "lexicon": 1, 1111 "id": "app.bsky.authFull", 1112 "defs": { 1113 "main": { 1114 "type": "permission-set", 1115 "title": "Full Bluesky Client Access", 1116 "detail": "Allows reading and writing to Bluesky records", 1117 "permissions": [ 1118 { 1119 "type": "permission", 1120 "resource": "repo", 1121 "collection": ["app.bsky.feed.post"], 1122 "action": ["create"] 1123 } 1124 ] 1125 } 1126 } 1127} 1128"#; 1129 1130 const PERMISSION_SET_FULL: &str = r#" 1131{ 1132 "lexicon": 1, 1133 "id": "app.bsky.authFull", 1134 "defs": { 1135 "main": { 1136 "type": "permission-set", 1137 "title": "Full Bluesky Client Access", 1138 "title:lang": { 1139 "es": "Acceso completo al cliente de Bluesky" 1140 }, 1141 "detail": "Allows reading and writing to Bluesky records and making service calls", 1142 "detail:lang": { 1143 "es": "Permite leer y escribir registros de Bluesky y realizar llamadas de servicio" 1144 }, 1145 "permissions": [ 1146 { 1147 "type": "permission", 1148 "resource": "repo", 1149 "collection": ["app.bsky.feed.post", "app.bsky.feed.like"], 1150 "action": ["create", "update", "delete"] 1151 }, 1152 { 1153 "type": "permission", 1154 "resource": "repo", 1155 "collection": ["app.bsky.actor.profile"], 1156 "action": ["update"] 1157 }, 1158 { 1159 "type": "permission", 1160 "resource": "rpc", 1161 "lxm": ["app.bsky.feed.getLikes", "app.bsky.feed.getAuthorFeed"], 1162 "inheritAud": true 1163 }, 1164 { 1165 "type": "permission", 1166 "resource": "rpc", 1167 "lxm": ["app.bsky.notification.listNotifications"], 1168 "aud": "did:web:api.bsky.app" 1169 }, 1170 { 1171 "type": "permission", 1172 "resource": "identity", 1173 "attr": "handle" 1174 }, 1175 { 1176 "type": "permission", 1177 "resource": "account", 1178 "attr": "email", 1179 "action": ["read"] 1180 } 1181 ] 1182 } 1183 } 1184} 1185"#; 1186 1187 #[test] 1188 fn test_permission_set_deserialize_simple() { 1189 let doc = serde_json::from_str::<LexiconDoc>(PERMISSION_SET_SIMPLE) 1190 .expect("failed to deserialize"); 1191 assert_eq!(doc.id, "app.bsky.authFull"); 1192 1193 let main_def = doc.defs.get("main").expect("main def exists"); 1194 match main_def { 1195 LexUserType::PermissionSet(pset) => { 1196 assert_eq!( 1197 pset.title.as_ref().map(|s| s.as_ref()), 1198 Some("Full Bluesky Client Access") 1199 ); 1200 assert_eq!(pset.permissions.len(), 1); 1201 1202 let perm = &pset.permissions[0]; 1203 match perm { 1204 LexPermission::Permission { 1205 resource: LexPermissionResource::Repo { collection, action }, 1206 } => { 1207 assert_eq!(collection.len(), 1); 1208 assert_eq!(collection[0].as_ref(), "app.bsky.feed.post"); 1209 assert_eq!(action.as_ref().map(|a| a.len()), Some(1), "has action vec"); 1210 if let Some(actions) = action { 1211 assert_eq!(actions[0], RepoAction::Create); 1212 } 1213 } 1214 _ => panic!("expected Repo permission"), 1215 } 1216 } 1217 _ => panic!("expected PermissionSet"), 1218 } 1219 } 1220 1221 #[test] 1222 fn test_permission_set_deserialize_full() { 1223 let doc = 1224 serde_json::from_str::<LexiconDoc>(PERMISSION_SET_FULL).expect("failed to deserialize"); 1225 let main_def = doc.defs.get("main").expect("main def"); 1226 1227 match main_def { 1228 LexUserType::PermissionSet(pset) => { 1229 assert_eq!(pset.title_lang.as_ref().map(|m| m.len()), Some(1)); 1230 assert_eq!(pset.detail_lang.as_ref().map(|m| m.len()), Some(1)); 1231 assert_eq!(pset.permissions.len(), 6, "has 6 permission entries"); 1232 1233 // Entry 1: Repo with 2 collections and 3 actions 1234 match &pset.permissions[0] { 1235 LexPermission::Permission { 1236 resource: LexPermissionResource::Repo { collection, action }, 1237 } => { 1238 assert_eq!(collection.len(), 2); 1239 assert_eq!(action.as_ref().map(|a| a.len()), Some(3), "has 3 actions"); 1240 } 1241 _ => panic!("entry 0 should be Repo"), 1242 } 1243 1244 // Entry 3: Rpc with inherit_aud 1245 match &pset.permissions[2] { 1246 LexPermission::Permission { 1247 resource: 1248 LexPermissionResource::Rpc { 1249 lxm, inherit_aud, .. 1250 }, 1251 } => { 1252 assert_eq!(lxm.len(), 2); 1253 assert_eq!(*inherit_aud, Some(true)); 1254 } 1255 _ => panic!("entry 2 should be Rpc with inherit_aud"), 1256 } 1257 1258 // Entry 4: Rpc with explicit aud 1259 match &pset.permissions[3] { 1260 LexPermission::Permission { 1261 resource: LexPermissionResource::Rpc { aud, .. }, 1262 } => { 1263 assert!(aud.is_some(), "has aud"); 1264 } 1265 _ => panic!("entry 3 should be Rpc with aud"), 1266 } 1267 1268 // Entry 5: Identity 1269 match &pset.permissions[4] { 1270 LexPermission::Permission { 1271 resource: LexPermissionResource::Identity { attr }, 1272 } => { 1273 assert_eq!(attr.as_ref(), "handle"); 1274 } 1275 _ => panic!("entry 4 should be Identity"), 1276 } 1277 1278 // Entry 6: Account with action 1279 match &pset.permissions[5] { 1280 LexPermission::Permission { 1281 resource: LexPermissionResource::Account { attr, action }, 1282 } => { 1283 assert_eq!(attr.as_ref(), "email"); 1284 assert_eq!(action.as_ref().map(|a| a.len()), Some(1), "has 1 action"); 1285 if let Some(actions) = action { 1286 assert_eq!(actions[0], AccountAction::Read); 1287 } 1288 } 1289 _ => panic!("entry 5 should be Account"), 1290 } 1291 } 1292 _ => panic!("expected PermissionSet"), 1293 } 1294 } 1295 1296 #[test] 1297 fn test_permission_set_into_static() { 1298 let doc = 1299 serde_json::from_str::<LexiconDoc>(PERMISSION_SET_FULL).expect("failed to deserialize"); 1300 let main_def = doc 1301 .defs 1302 .get("main") 1303 .expect("main def") 1304 .clone() 1305 .into_static(); 1306 1307 match main_def { 1308 LexUserType::PermissionSet(pset) => { 1309 assert_eq!(pset.permissions.len(), 6); 1310 // Verify all borrowed fields are converted to 'static 1311 assert!(pset.title.is_some()); 1312 assert!(pset.title_lang.is_some()); 1313 } 1314 _ => panic!("expected PermissionSet"), 1315 } 1316 } 1317 1318 #[test] 1319 fn test_permission_set_namespace_violation() { 1320 let doc = serde_json::from_str::<LexiconDoc>(PERMISSION_SET_SIMPLE) 1321 .expect("failed to deserialize"); 1322 let pset = match doc.defs.get("main").expect("main def") { 1323 LexUserType::PermissionSet(p) => p, 1324 _ => panic!("expected PermissionSet"), 1325 }; 1326 1327 // Valid: app.bsky.feed.post is in app.bsky namespace 1328 assert!(pset.validate("app.bsky.authFull").is_ok()); 1329 1330 // Invalid: com.atproto is out of namespace for app.bsky 1331 let invalid_json = r#" 1332{ 1333 "lexicon": 1, 1334 "id": "app.bsky.authFull", 1335 "defs": { 1336 "main": { 1337 "type": "permission-set", 1338 "permissions": [ 1339 { 1340 "type": "permission", 1341 "resource": "repo", 1342 "collection": ["com.atproto.repo.createRecord"], 1343 "action": ["create"] 1344 } 1345 ] 1346 } 1347 } 1348} 1349"#; 1350 let doc = serde_json::from_str::<LexiconDoc>(invalid_json).expect("deserialize"); 1351 let pset = match doc.defs.get("main").expect("main def") { 1352 LexUserType::PermissionSet(p) => p, 1353 _ => panic!("expected PermissionSet"), 1354 }; 1355 let result = pset.validate("app.bsky.authFull"); 1356 assert!(result.is_err()); 1357 match result.unwrap_err() { 1358 PermissionSetError::NamespaceViolation { nsid, resource } => { 1359 assert_eq!(nsid, "app.bsky.authFull"); 1360 assert_eq!(resource, "com.atproto.repo.createRecord"); 1361 } 1362 _ => panic!("expected NamespaceViolation"), 1363 } 1364 } 1365 1366 #[test] 1367 fn test_permission_set_empty_permissions() { 1368 let json = r#" 1369{ 1370 "lexicon": 1, 1371 "id": "app.bsky.authEmpty", 1372 "defs": { 1373 "main": { 1374 "type": "permission-set", 1375 "permissions": [] 1376 } 1377 } 1378} 1379"#; 1380 let doc = serde_json::from_str::<LexiconDoc>(json).expect("deserialize"); 1381 let pset = match doc.defs.get("main").expect("main def") { 1382 LexUserType::PermissionSet(p) => p, 1383 _ => panic!("expected PermissionSet"), 1384 }; 1385 let result = pset.validate("app.bsky.authEmpty"); 1386 assert!(result.is_err()); 1387 match result.unwrap_err() { 1388 PermissionSetError::EmptyPermissions => {} 1389 _ => panic!("expected EmptyPermissions"), 1390 } 1391 } 1392 1393 #[test] 1394 fn test_permission_set_serialize_roundtrip() { 1395 let doc = serde_json::from_str::<LexiconDoc>(PERMISSION_SET_SIMPLE) 1396 .expect("failed to deserialize"); 1397 let orig_pset = match doc.defs.get("main").expect("main") { 1398 LexUserType::PermissionSet(p) => p, 1399 _ => panic!("expected PermissionSet"), 1400 }; 1401 1402 // Serialize to JSON value and back 1403 let serialized_str = serde_json::to_string(orig_pset).expect("serialize to string"); 1404 let deserialized_pset = serde_json::from_str::<LexPermissionSet>(serialized_str.as_str()) 1405 .expect("roundtrip deserialize"); 1406 1407 assert_eq!( 1408 orig_pset.permissions.len(), 1409 deserialized_pset.permissions.len() 1410 ); 1411 } 1412 1413 #[test] 1414 fn test_permission_set_invalid_nsid() { 1415 let json = r#" 1416{ 1417 "lexicon": 1, 1418 "id": "app.bsky.authBad", 1419 "defs": { 1420 "main": { 1421 "type": "permission-set", 1422 "permissions": [ 1423 { 1424 "type": "permission", 1425 "resource": "repo", 1426 "collection": ["invalid..nsid"], 1427 "action": ["create"] 1428 } 1429 ] 1430 } 1431 } 1432} 1433"#; 1434 // Invalid NSID should fail during deserialization 1435 let result = serde_json::from_str::<LexiconDoc>(json); 1436 assert!(result.is_err(), "invalid NSID should fail deserialization"); 1437 } 1438 1439 #[test] 1440 fn test_permission_set_invalid_did() { 1441 let json = r#" 1442{ 1443 "lexicon": 1, 1444 "id": "app.bsky.authBad", 1445 "defs": { 1446 "main": { 1447 "type": "permission-set", 1448 "permissions": [ 1449 { 1450 "type": "permission", 1451 "resource": "rpc", 1452 "lxm": ["app.bsky.feed.getLikes"], 1453 "aud": "not-a-did" 1454 } 1455 ] 1456 } 1457 } 1458} 1459"#; 1460 // Invalid DID should fail during deserialization 1461 let result = serde_json::from_str::<LexiconDoc>(json); 1462 assert!(result.is_err(), "invalid DID should fail deserialization"); 1463 } 1464 1465 #[test] 1466 fn test_permission_set_title_lang() { 1467 let doc = 1468 serde_json::from_str::<LexiconDoc>(PERMISSION_SET_FULL).expect("failed to deserialize"); 1469 let pset = match doc.defs.get("main").expect("main def") { 1470 LexUserType::PermissionSet(p) => p, 1471 _ => panic!("expected PermissionSet"), 1472 }; 1473 1474 let title_lang = pset.title_lang.as_ref().expect("has title:lang"); 1475 assert_eq!(title_lang.len(), 1); 1476 let es_title = title_lang 1477 .iter() 1478 .find(|(k, _)| k.as_ref() == "es") 1479 .expect("has es translation"); 1480 assert_eq!(es_title.1.as_ref(), "Acceso completo al cliente de Bluesky"); 1481 1482 // Roundtrip and verify title:lang survives 1483 let serialized = serde_json::to_value(&pset).expect("serialize"); 1484 assert!( 1485 serialized.get("title:lang").is_some(), 1486 "title:lang field preserved in JSON" 1487 ); 1488 } 1489 1490 #[test] 1491 fn test_permission_set_rpc_namespace_violation() { 1492 let json = r#" 1493{ 1494 "lexicon": 1, 1495 "id": "app.bsky.authBad", 1496 "defs": { 1497 "main": { 1498 "type": "permission-set", 1499 "permissions": [ 1500 { 1501 "type": "permission", 1502 "resource": "rpc", 1503 "lxm": ["com.atproto.server.createAccount"], 1504 "inheritAud": true 1505 } 1506 ] 1507 } 1508 } 1509} 1510"#; 1511 let doc = serde_json::from_str::<LexiconDoc>(json).expect("deserialize"); 1512 let pset = match doc.defs.get("main").expect("main def") { 1513 LexUserType::PermissionSet(p) => p, 1514 _ => panic!("expected PermissionSet"), 1515 }; 1516 let result = pset.validate("app.bsky.authBad"); 1517 assert!(result.is_err(), "rpc lxm out of namespace should fail"); 1518 match result.unwrap_err() { 1519 PermissionSetError::NamespaceViolation { resource, .. } => { 1520 assert_eq!(resource, "com.atproto.server.createAccount"); 1521 } 1522 _ => panic!("expected NamespaceViolation"), 1523 } 1524 } 1525}