A better Rust ATProto crate
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}