A better Rust ATProto crate
1use jacquard_common::CowStr;
2use jacquard_common::types::string::Datetime;
3use jacquard_lexicon::lexicon::{
4 LexObject, LexObjectProperty, LexRecord, LexRecordRecord, LexString, LexStringFormat,
5 LexUserType, Lexicon, LexiconDoc,
6};
7use jacquard_lexicon::schema::LexiconSchema;
8use jacquard_lexicon::validation::{ConstraintError, ValidationPath};
9use std::collections::BTreeMap;
10
11// Simple test type
12#[derive(Debug, Clone)]
13struct SimpleRecord<'a> {
14 text: CowStr<'a>,
15 #[allow(dead_code)]
16 timestamp: Datetime,
17}
18
19impl LexiconSchema for SimpleRecord<'_> {
20 fn nsid() -> &'static str {
21 "com.example.simple"
22 }
23
24 fn lexicon_doc() -> LexiconDoc<'static> {
25 let mut properties = BTreeMap::new();
26
27 properties.insert(
28 "text".into(),
29 LexObjectProperty::String(LexString {
30 description: None,
31 format: None,
32 default: None,
33 min_length: None,
34 max_length: Some(1000),
35 min_graphemes: None,
36 max_graphemes: None,
37 r#enum: None,
38 r#const: None,
39 known_values: None,
40 }),
41 );
42
43 properties.insert(
44 "timestamp".into(),
45 LexObjectProperty::String(LexString {
46 description: None,
47 format: Some(LexStringFormat::Datetime),
48 default: None,
49 min_length: None,
50 max_length: None,
51 min_graphemes: None,
52 max_graphemes: None,
53 r#enum: None,
54 r#const: None,
55 known_values: None,
56 }),
57 );
58
59 let record_obj = LexObject {
60 description: None,
61 required: Some(vec!["text".into(), "timestamp".into()]),
62 nullable: None,
63 properties,
64 };
65
66 let record = LexRecord {
67 description: Some("Simple record type".into()),
68 key: Some("tid".into()),
69 record: LexRecordRecord::Object(record_obj),
70 };
71
72 let mut defs = BTreeMap::new();
73 defs.insert("main".into(), LexUserType::Record(record));
74
75 LexiconDoc {
76 lexicon: Lexicon::Lexicon1,
77 id: Self::nsid().into(),
78 revision: None,
79 description: Some("Test schema".into()),
80 defs,
81 }
82 }
83
84 fn validate(&self) -> Result<(), ConstraintError> {
85 // Check text length
86 if self.text.len() > 1000 {
87 return Err(ConstraintError::MaxLength {
88 path: ValidationPath::from_field("text"),
89 max: 1000,
90 actual: self.text.len(),
91 });
92 }
93
94 Ok(())
95 }
96}
97
98#[test]
99fn test_manual_impl_generates_valid_schema() {
100 let doc = SimpleRecord::lexicon_doc();
101
102 // Verify structure
103 assert_eq!(doc.id.as_ref(), "com.example.simple");
104 assert!(doc.defs.contains_key("main"));
105
106 // Serialize to JSON
107 let json = serde_json::to_string_pretty(&doc).expect("serialize");
108 println!("{}", json);
109
110 // Should be valid lexicon JSON
111 assert!(json.contains("\"lexicon\": 1"));
112 assert!(json.contains("\"id\": \"com.example.simple\""));
113}
114
115#[test]
116fn test_validation_works() {
117 let record = SimpleRecord {
118 text: "a".repeat(5000).into(), // Too long
119 timestamp: Datetime::now(),
120 };
121
122 let result = record.validate();
123 assert!(result.is_err());
124
125 let err = result.unwrap_err();
126 assert!(matches!(err, ConstraintError::MaxLength { .. }));
127}
128
129#[test]
130fn test_validation_passes() {
131 let record = SimpleRecord {
132 text: "Hello, world!".into(),
133 timestamp: Datetime::now(),
134 };
135
136 let result = record.validate();
137 assert!(result.is_ok());
138}