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