A better Rust ATProto crate
1//! Generate LexiconSchema trait implementations for generated types
2
3use crate::lexicon::{
4 LexInteger, LexObject, LexObjectProperty, LexRecordRecord, LexString, LexStringFormat,
5 LexUserType, LexiconDoc,
6};
7use crate::schema::from_ast::{ConstraintCheck, ValidationCheck};
8
9/// Extract validation checks from a LexiconDoc
10///
11/// Walks the lexicon structure and builds ValidationCheck structs for all
12/// constraint fields (max_length, max_graphemes, minimum, maximum, etc.)
13pub(crate) fn extract_validation_checks(doc: &LexiconDoc, def_name: &str) -> Vec<ValidationCheck> {
14 let mut checks = Vec::new();
15
16 // Get the specified def
17 if let Some(def) = doc.defs.get(def_name) {
18 match def {
19 LexUserType::Record(rec) => match &rec.record {
20 LexRecordRecord::Object(obj) => {
21 checks.extend(extract_object_validations(obj));
22 }
23 },
24 LexUserType::Object(obj) => {
25 checks.extend(extract_object_validations(obj));
26 }
27 // XRPC types, tokens, etc. don't need validation
28 _ => {}
29 }
30 }
31
32 checks
33}
34
35/// Extract validation checks from an object's properties
36fn extract_object_validations(obj: &LexObject) -> Vec<ValidationCheck> {
37 let mut checks = Vec::new();
38
39 for (schema_name, prop) in &obj.properties {
40 // Convert schema name to field name (snake_case, with r# prefix for keywords)
41 let field_name = field_name_from_schema(schema_name);
42
43 // Check if required
44 let is_required = obj
45 .required
46 .as_ref()
47 .map(|req| req.iter().any(|r| r == schema_name))
48 .unwrap_or(false);
49
50 // Extract checks from property
51 checks.extend(extract_property_validations(
52 &field_name,
53 schema_name.as_ref(),
54 prop,
55 is_required,
56 ));
57 }
58
59 checks
60}
61
62/// Extract validation checks from a single property
63fn extract_property_validations(
64 field_name: &str,
65 schema_name: &str,
66 prop: &LexObjectProperty,
67 is_required: bool,
68) -> Vec<ValidationCheck> {
69 let mut checks = Vec::new();
70
71 match prop {
72 LexObjectProperty::String(s) => {
73 checks.extend(extract_string_validations(
74 field_name,
75 schema_name,
76 s,
77 is_required,
78 ));
79 }
80 LexObjectProperty::Integer(i) => {
81 checks.extend(extract_integer_validations(
82 field_name,
83 schema_name,
84 i,
85 is_required,
86 ));
87 }
88 LexObjectProperty::Array(arr) => {
89 if let Some(max) = arr.max_length {
90 checks.push(ValidationCheck {
91 field_name: field_name.to_string(),
92 schema_name: schema_name.to_string(),
93 field_type: "Vec<_>".to_string(),
94 is_required,
95 is_array: true,
96 check: ConstraintCheck::MaxLength { max },
97 });
98 }
99 if let Some(min) = arr.min_length {
100 checks.push(ValidationCheck {
101 field_name: field_name.to_string(),
102 schema_name: schema_name.to_string(),
103 field_type: "Vec<_>".to_string(),
104 is_required,
105 is_array: true,
106 check: ConstraintCheck::MinLength { min },
107 });
108 }
109 }
110 LexObjectProperty::Blob(b) => {
111 if let Some(max) = b.max_size {
112 checks.push(ValidationCheck {
113 field_name: field_name.to_string(),
114 schema_name: schema_name.to_string(),
115 field_type: "BlobRef".to_string(),
116 is_required,
117 is_array: false,
118 check: ConstraintCheck::BlobMaxSize { max },
119 });
120 }
121 if let Some(accept) = &b.accept {
122 if !accept.is_empty() {
123 checks.push(ValidationCheck {
124 field_name: field_name.to_string(),
125 schema_name: schema_name.to_string(),
126 field_type: "BlobRef".to_string(),
127 is_required,
128 is_array: false,
129 check: ConstraintCheck::BlobAccept {
130 accept: accept.iter().map(|m| m.as_str().to_string()).collect(),
131 },
132 });
133 }
134 }
135 }
136 _ => {
137 // Other types don't have runtime validations in the current impl.
138 }
139 }
140
141 checks
142}
143
144/// Extract validation checks from a string property
145fn extract_string_validations(
146 field_name: &str,
147 schema_name: &str,
148 string: &LexString,
149 is_required: bool,
150) -> Vec<ValidationCheck> {
151 let mut checks = Vec::new();
152
153 // Datetime maps to `chrono::DateTime<FixedOffset>` which does not implement
154 // `AsRef<str>`, so length checks cannot be emitted for it. All other formats
155 // (did, handle, at-uri, cid, nsid, tid, record-key, language, uri, etc.) use
156 // string-backed wrapper types that do implement `AsRef<str>`.
157 if matches!(string.format, Some(LexStringFormat::Datetime)) {
158 return checks;
159 }
160
161 if let Some(max) = string.max_length {
162 checks.push(ValidationCheck {
163 field_name: field_name.to_string(),
164 schema_name: schema_name.to_string(),
165 field_type: "String".to_string(),
166 is_required,
167 is_array: false,
168 check: ConstraintCheck::MaxLength { max },
169 });
170 }
171
172 if let Some(min) = string.min_length {
173 checks.push(ValidationCheck {
174 field_name: field_name.to_string(),
175 schema_name: schema_name.to_string(),
176 field_type: "String".to_string(),
177 is_required,
178 is_array: false,
179 check: ConstraintCheck::MinLength { min },
180 });
181 }
182
183 if let Some(max) = string.max_graphemes {
184 checks.push(ValidationCheck {
185 field_name: field_name.to_string(),
186 schema_name: schema_name.to_string(),
187 field_type: "String".to_string(),
188 is_required,
189 is_array: false,
190 check: ConstraintCheck::MaxGraphemes { max },
191 });
192 }
193
194 if let Some(min) = string.min_graphemes {
195 checks.push(ValidationCheck {
196 field_name: field_name.to_string(),
197 schema_name: schema_name.to_string(),
198 field_type: "String".to_string(),
199 is_required,
200 is_array: false,
201 check: ConstraintCheck::MinGraphemes { min },
202 });
203 }
204
205 checks
206}
207
208/// Extract validation checks from an integer property
209fn extract_integer_validations(
210 field_name: &str,
211 schema_name: &str,
212 integer: &LexInteger,
213 is_required: bool,
214) -> Vec<ValidationCheck> {
215 let mut checks = Vec::new();
216
217 if let Some(max) = integer.maximum {
218 checks.push(ValidationCheck {
219 field_name: field_name.to_string(),
220 schema_name: schema_name.to_string(),
221 field_type: "i64".to_string(),
222 is_required,
223 is_array: false,
224 check: ConstraintCheck::Maximum { max },
225 });
226 }
227
228 if let Some(min) = integer.minimum {
229 checks.push(ValidationCheck {
230 field_name: field_name.to_string(),
231 schema_name: schema_name.to_string(),
232 field_type: "i64".to_string(),
233 is_required,
234 is_array: false,
235 check: ConstraintCheck::Minimum { min },
236 });
237 }
238
239 checks
240}
241
242/// Convert schema field name to the Rust field identifier
243///
244/// Returns snake_case field name without r# prefix
245/// (the r# will be added by make_ident when generating tokens)
246fn field_name_from_schema(schema_name: &str) -> String {
247 use heck::ToSnakeCase;
248 schema_name.to_snake_case()
249}