A better Rust ATProto crate
1use jacquard_common::CowStr;
2use jacquard_common::types::string::Datetime;
3use jacquard_derive::{LexiconSchema, open_union};
4use jacquard_lexicon::schema::LexiconSchema as LexiconSchemaTrait;
5use serde::{Deserialize, Serialize};
6extern crate alloc;
7
8#[test]
9fn test_simple_struct() {
10 #[derive(LexiconSchema)]
11 #[lexicon(nsid = "com.example.simple", record, key = "tid")]
12 struct SimpleRecord<'a> {
13 #[allow(dead_code)]
14 pub text: CowStr<'a>,
15 #[allow(dead_code)]
16 pub created_at: Datetime,
17 }
18
19 assert_eq!(SimpleRecord::nsid(), "com.example.simple");
20 assert_eq!(SimpleRecord::schema_id().as_ref(), "com.example.simple");
21
22 let doc = SimpleRecord::lexicon_doc();
23
24 assert_eq!(doc.id.as_ref(), "com.example.simple");
25 assert!(doc.defs.contains_key("main"));
26
27 // Serialize to JSON to verify structure
28 let json = serde_json::to_string_pretty(&doc).unwrap();
29 println!("{}", json);
30
31 // Should contain record type
32 assert!(json.contains("\"type\": \"record\""));
33 // Should have camelCase field names (default)
34 assert!(json.contains("\"createdAt\""));
35}
36
37#[test]
38fn test_struct_with_constraints() {
39 #[derive(LexiconSchema)]
40 #[lexicon(nsid = "com.example.constrained", record)]
41 struct ConstrainedRecord<'a> {
42 #[lexicon(max_graphemes = 300, max_length = 3000)]
43 pub text: CowStr<'a>,
44
45 #[lexicon(minimum = 0, maximum = 100)]
46 pub score: i64,
47 }
48
49 let doc = ConstrainedRecord::lexicon_doc();
50
51 let json = serde_json::to_string_pretty(&doc).unwrap();
52 println!("{}", json);
53
54 // Verify constraints are in schema
55 assert!(json.contains("\"maxGraphemes\": 300"));
56 assert!(json.contains("\"maxLength\": 3000"));
57 assert!(json.contains("\"minimum\": 0"));
58 assert!(json.contains("\"maximum\": 100"));
59}
60
61#[test]
62fn test_validation() {
63 #[derive(LexiconSchema)]
64 #[lexicon(nsid = "com.example.validated", record)]
65 struct ValidatedRecord<'a> {
66 #[lexicon(max_length = 100)]
67 pub text: CowStr<'a>,
68
69 #[lexicon(minimum = 0, maximum = 10)]
70 pub count: i64,
71 }
72
73 // Valid
74 let valid = ValidatedRecord {
75 text: "hello".into(),
76 count: 5,
77 };
78 assert!(valid.validate().is_ok());
79
80 // Text too long
81 let invalid_text = ValidatedRecord {
82 text: "a".repeat(150).into(),
83 count: 5,
84 };
85 assert!(invalid_text.validate().is_err());
86
87 // Count too high
88 let invalid_count = ValidatedRecord {
89 text: "hello".into(),
90 count: 15,
91 };
92 assert!(invalid_count.validate().is_err());
93
94 // Count too low
95 let invalid_low = ValidatedRecord {
96 text: "hello".into(),
97 count: -5,
98 };
99 assert!(invalid_low.validate().is_err());
100}
101
102#[test]
103fn test_serde_rename() {
104 #[derive(Serialize, Deserialize, LexiconSchema)]
105 #[lexicon(nsid = "com.example.renamed", record)]
106 #[serde(rename_all = "snake_case")]
107 struct RenamedRecord {
108 pub some_field: i64,
109 pub another_field: i64,
110 }
111 let doc = RenamedRecord::lexicon_doc();
112
113 let json = serde_json::to_string_pretty(&doc).unwrap();
114 println!("{}", json);
115
116 // Should use snake_case not camelCase
117 assert!(json.contains("\"some_field\""));
118 assert!(json.contains("\"another_field\""));
119}
120
121#[test]
122fn test_default_camel_case() {
123 #[derive(LexiconSchema)]
124 #[lexicon(nsid = "com.example.camel", record)]
125 struct CamelCaseRecord {
126 #[allow(dead_code)]
127 pub field_one: i64,
128 #[allow(dead_code)]
129 pub field_two: i64,
130 }
131
132 let doc = CamelCaseRecord::lexicon_doc();
133
134 let json = serde_json::to_string_pretty(&doc).unwrap();
135 println!("{}", json);
136
137 // Should default to camelCase
138 assert!(json.contains("\"fieldOne\""));
139 assert!(json.contains("\"fieldTwo\""));
140}
141
142#[test]
143fn test_basic_enum() {
144 #[derive(LexiconSchema)]
145 #[lexicon(nsid = "com.example.union")]
146 enum BasicUnion {
147 #[nsid = "com.example.variant.one"]
148 #[allow(dead_code)]
149 VariantOne,
150
151 #[nsid = "com.example.variant.two"]
152 #[allow(dead_code)]
153 VariantTwo,
154 }
155
156 let doc = BasicUnion::lexicon_doc();
157
158 let json = serde_json::to_string_pretty(&doc).unwrap();
159 println!("{}", json);
160
161 // Should be a union type
162 assert!(json.contains("\"type\": \"union\""));
163 // Should have refs
164 assert!(json.contains("com.example.variant.one"));
165 assert!(json.contains("com.example.variant.two"));
166 // Should be closed by default
167 assert!(json.contains("\"closed\": true"));
168}
169
170#[test]
171fn test_open_union_attribute_adds_unknown_variant() {
172 #[open_union]
173 #[derive(Serialize, Deserialize, LexiconSchema)]
174 #[lexicon(nsid = "com.example.open")]
175 enum OpenUnion<S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
176 #[nsid = "com.example.variant"]
177 #[allow(dead_code)]
178 Variant,
179 }
180
181 let doc = OpenUnion::<jacquard_common::DefaultStr>::lexicon_doc();
182
183 let json = serde_json::to_string_pretty(&doc).unwrap();
184 println!("{}", json);
185
186 // Should be a union type with known refs, while remaining open by omitting closed.
187 assert!(json.contains("\"type\": \"union\""));
188 assert!(json.contains("com.example.variant"));
189 assert!(!json.contains("\"closed\""));
190}
191
192#[test]
193fn test_open_union_detects_existing_unknown_variant() {
194 #[derive(LexiconSchema)]
195 #[lexicon(nsid = "com.example.open_existing")]
196 enum OpenUnion<S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
197 #[nsid = "com.example.variant"]
198 #[allow(dead_code)]
199 Variant,
200
201 #[allow(dead_code)]
202 Unknown(jacquard_common::types::value::Data<S>),
203 }
204
205 let doc = OpenUnion::<jacquard_common::DefaultStr>::lexicon_doc();
206
207 let json = serde_json::to_string_pretty(&doc).unwrap();
208 println!("{}", json);
209
210 assert!(json.contains("\"type\": \"union\""));
211 assert!(json.contains("com.example.variant"));
212 assert!(!json.contains("\"closed\""));
213}
214
215#[test]
216fn test_generic_record() {
217 #[derive(LexiconSchema)]
218 #[lexicon(nsid = "com.example.generic", record)]
219 struct GenericRecord<S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
220 #[lexicon(max_length = 100)]
221 #[allow(dead_code)]
222 text: S,
223 }
224
225 let doc = GenericRecord::<jacquard_common::DefaultStr>::lexicon_doc();
226 let json = serde_json::to_string_pretty(&doc).unwrap();
227 println!("{}", json);
228
229 assert!(json.contains("\"type\": \"string\""));
230 assert!(json.contains("\"maxLength\": 100"));
231
232 let record = GenericRecord {
233 text: jacquard_common::DefaultStr::from("hello"),
234 };
235 assert!(record.validate().is_ok());
236}
237
238#[test]
239fn test_optional_generic_record() {
240 #[derive(LexiconSchema)]
241 #[lexicon(nsid = "com.example.optional_generic", record)]
242 struct OptionalGeneric<S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
243 #[lexicon(max_length = 100)]
244 #[allow(dead_code)]
245 text: Option<S>,
246 }
247
248 let doc = OptionalGeneric::<jacquard_common::DefaultStr>::lexicon_doc();
249 let json = serde_json::to_string_pretty(&doc).unwrap();
250 println!("{}", json);
251
252 assert!(json.contains("\"type\": \"string\""));
253 assert!(json.contains("\"maxLength\": 100"));
254 assert!(!json.contains("\"required\""));
255}
256
257#[test]
258fn test_generic_array_record() {
259 #[derive(LexiconSchema)]
260 #[lexicon(nsid = "com.example.generic_array", record)]
261 struct GenericArray<S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
262 #[lexicon(max_items = 5)]
263 #[allow(dead_code)]
264 tags: Vec<S>,
265 }
266
267 let doc = GenericArray::<jacquard_common::DefaultStr>::lexicon_doc();
268 let json = serde_json::to_string_pretty(&doc).unwrap();
269 println!("{}", json);
270
271 assert!(json.contains("\"type\": \"array\""));
272 assert!(json.contains("\"items\""));
273 assert!(json.contains("\"type\": \"string\""));
274 assert!(json.contains("\"maxLength\": 5"));
275}
276
277#[test]
278fn test_non_s_bos_type_parameter_maps_to_string() {
279 #[derive(LexiconSchema)]
280 #[lexicon(nsid = "com.example.non_s_generic", record)]
281 struct GenericRecord<Text: jacquard_common::BosStr = jacquard_common::DefaultStr> {
282 #[allow(dead_code)]
283 text: Text,
284 }
285
286 let doc = GenericRecord::<jacquard_common::DefaultStr>::lexicon_doc();
287 let json = serde_json::to_string_pretty(&doc).unwrap();
288 println!("{}", json);
289
290 assert!(json.contains("\"type\": \"string\""));
291}
292
293#[test]
294fn test_old_style_bos_bound_maps_to_string() {
295 #[derive(LexiconSchema)]
296 #[lexicon(nsid = "com.example.old_style_bos", record)]
297 struct GenericRecord<S: jacquard_common::Bos<str> + AsRef<str> = jacquard_common::DefaultStr> {
298 #[allow(dead_code)]
299 text: S,
300 }
301
302 let doc = GenericRecord::<jacquard_common::DefaultStr>::lexicon_doc();
303 let json = serde_json::to_string_pretty(&doc).unwrap();
304 println!("{}", json);
305
306 assert!(json.contains("\"type\": \"string\""));
307}
308
309#[test]
310fn test_lifetime_and_bos_type_parameter_record() {
311 #[derive(LexiconSchema)]
312 #[lexicon(nsid = "com.example.mixed_generics", record)]
313 struct Mixed<'a, S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
314 #[allow(dead_code)]
315 borrowed: CowStr<'a>,
316 #[allow(dead_code)]
317 text: S,
318 }
319
320 let doc = Mixed::<'static, jacquard_common::DefaultStr>::lexicon_doc();
321 let json = serde_json::to_string_pretty(&doc).unwrap();
322 println!("{}", json);
323
324 assert_eq!(json.matches("\"type\": \"string\"").count(), 2);
325}
326
327#[test]
328fn test_generic_open_union_schema_and_unknown_roundtrip() {
329 use jacquard_common::types::value::{Data, Object};
330 use std::collections::BTreeMap;
331
332 #[open_union]
333 #[derive(Serialize, Deserialize, Debug, PartialEq, LexiconSchema)]
334 #[serde(tag = "$type")]
335 #[lexicon(nsid = "com.example.generic_open_roundtrip")]
336 enum GenericOpenUnion<S: jacquard_common::BosStr = jacquard_common::DefaultStr> {
337 #[serde(rename = "com.example.known")]
338 #[nsid = "com.example.known"]
339 Known { value: S },
340 }
341
342 let doc = GenericOpenUnion::<jacquard_common::DefaultStr>::lexicon_doc();
343 let json = serde_json::to_string_pretty(&doc).unwrap();
344 println!("{}", json);
345
346 assert!(json.contains("\"type\": \"union\""));
347 assert!(json.contains("com.example.known"));
348 assert!(!json.contains("\"closed\""));
349
350 let unknown_json = r#"{"$type":"com.example.unknown","value":"hello"}"#;
351 let parsed: GenericOpenUnion = serde_json::from_str(unknown_json).unwrap();
352 match parsed {
353 GenericOpenUnion::Unknown(Data::Object(obj)) => {
354 assert!(obj.0.contains_key("$type"));
355 assert!(obj.0.contains_key("value"));
356 }
357 _ => panic!("expected Unknown variant"),
358 }
359
360 let mut map = BTreeMap::new();
361 map.insert(
362 "$type".into(),
363 Data::String(jacquard_common::types::string::AtprotoStr::String(
364 jacquard_common::DefaultStr::from("com.example.other"),
365 )),
366 );
367 map.insert("count".into(), Data::Integer(7));
368
369 let union = GenericOpenUnion::Unknown(Data::Object(Object(map)));
370 let serialized = serde_json::to_string(&union).unwrap();
371 let roundtripped: GenericOpenUnion = serde_json::from_str(&serialized).unwrap();
372 assert!(matches!(
373 roundtripped,
374 GenericOpenUnion::Unknown(Data::Object(_))
375 ));
376}
377
378#[test]
379fn test_enum_with_serde_rename() {
380 #[derive(Serialize, Deserialize, LexiconSchema)]
381 #[lexicon(nsid = "com.example.renamed_union")]
382 enum RenamedUnion {
383 #[serde(rename = "app.bsky.embed.images")]
384 Images,
385
386 #[serde(rename = "app.bsky.embed.video")]
387 Video,
388 }
389
390 let doc = RenamedUnion::lexicon_doc();
391
392 let json = serde_json::to_string_pretty(&doc).unwrap();
393 println!("{}", json);
394
395 // Should use serde rename values
396 assert!(json.contains("app.bsky.embed.images"));
397 assert!(json.contains("app.bsky.embed.video"));
398}
399
400#[test]
401fn test_enum_fragment_inference() {
402 #[derive(LexiconSchema)]
403 #[lexicon(nsid = "com.example.fragments")]
404 enum FragmentUnion {
405 // Should generate com.example.fragments#variantOne
406 #[allow(dead_code)]
407 VariantOne,
408 // Should generate com.example.fragments#variantTwo
409 #[allow(dead_code)]
410 VariantTwo,
411 }
412 let doc = FragmentUnion::lexicon_doc();
413
414 let json = serde_json::to_string_pretty(&doc).unwrap();
415 println!("{}", json);
416
417 // Should have fragment refs
418 assert!(json.contains("com.example.fragments#variantOne"));
419 assert!(json.contains("com.example.fragments#variantTwo"));
420}