+1
crates/jacquard-lexicon/src/codegen.rs
+1
crates/jacquard-lexicon/src/codegen.rs
+281
crates/jacquard-lexicon/src/codegen/schema_impl.rs
+281
crates/jacquard-lexicon/src/codegen/schema_impl.rs
···
1
+
//! Generate LexiconSchema trait implementations for generated types
2
+
3
+
use crate::derive_impl::doc_to_tokens;
4
+
use crate::lexicon::{
5
+
LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexRecordRecord, LexString,
6
+
LexUserType, LexiconDoc,
7
+
};
8
+
use crate::schema::from_ast::{ConstraintCheck, ValidationCheck};
9
+
use proc_macro2::TokenStream;
10
+
use quote::quote;
11
+
12
+
/// Generate LexiconSchema impl for a generated type
13
+
///
14
+
/// Takes the original lexicon doc and type metadata to generate a complete
15
+
/// impl with const literal and validation code.
16
+
pub fn generate_schema_impl(type_name: &str, doc: &LexiconDoc, has_lifetime: bool) -> TokenStream {
17
+
let nsid = doc.id.as_ref();
18
+
19
+
// Generate lifetime parameter
20
+
let lifetime = if has_lifetime {
21
+
quote! { <'_> }
22
+
} else {
23
+
quote! {}
24
+
};
25
+
26
+
// Generate the lexicon doc literal using existing doc_to_tokens
27
+
let doc_literal = doc_to_tokens::doc_to_tokens(doc);
28
+
29
+
// Extract validation checks from lexicon doc
30
+
let validation_checks = extract_validation_checks(doc);
31
+
32
+
// Generate validation code using existing validations_to_tokens
33
+
let validation_code = doc_to_tokens::validations_to_tokens(&validation_checks);
34
+
35
+
let type_ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
36
+
37
+
quote! {
38
+
impl #lifetime ::jacquard_lexicon::schema::LexiconSchema for #type_ident #lifetime {
39
+
fn nsid() -> &'static str {
40
+
#nsid
41
+
}
42
+
43
+
fn lexicon_doc(
44
+
_generator: &mut ::jacquard_lexicon::schema::LexiconGenerator
45
+
) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
46
+
#doc_literal
47
+
}
48
+
49
+
fn validate(&self) -> ::std::result::Result<(), ::jacquard_lexicon::schema::ValidationError> {
50
+
#validation_code
51
+
}
52
+
}
53
+
}
54
+
}
55
+
56
+
/// Extract validation checks from a LexiconDoc
57
+
///
58
+
/// Walks the lexicon structure and builds ValidationCheck structs for all
59
+
/// constraint fields (max_length, max_graphemes, minimum, maximum, etc.)
60
+
fn extract_validation_checks(doc: &LexiconDoc) -> Vec<ValidationCheck> {
61
+
let mut checks = Vec::new();
62
+
63
+
// Get main def
64
+
if let Some(main_def) = doc.defs.get("main") {
65
+
match main_def {
66
+
LexUserType::Record(rec) => {
67
+
match &rec.record {
68
+
LexRecordRecord::Object(obj) => {
69
+
checks.extend(extract_object_validations(obj));
70
+
}
71
+
}
72
+
}
73
+
LexUserType::Object(obj) => {
74
+
checks.extend(extract_object_validations(obj));
75
+
}
76
+
// XRPC types, tokens, etc. don't need validation
77
+
_ => {}
78
+
}
79
+
}
80
+
81
+
checks
82
+
}
83
+
84
+
/// Extract validation checks from an object's properties
85
+
fn extract_object_validations(obj: &LexObject) -> Vec<ValidationCheck> {
86
+
let mut checks = Vec::new();
87
+
88
+
for (schema_name, prop) in &obj.properties {
89
+
// Convert schema name to field name (snake_case)
90
+
let field_name = to_snake_case(schema_name);
91
+
92
+
// Check if required
93
+
let is_required = obj
94
+
.required
95
+
.as_ref()
96
+
.map(|req| req.iter().any(|r| r == schema_name))
97
+
.unwrap_or(false);
98
+
99
+
// Extract checks from property
100
+
checks.extend(extract_property_validations(
101
+
&field_name,
102
+
schema_name.as_ref(),
103
+
prop,
104
+
is_required,
105
+
));
106
+
}
107
+
108
+
checks
109
+
}
110
+
111
+
/// Extract validation checks from a single property
112
+
fn extract_property_validations(
113
+
field_name: &str,
114
+
schema_name: &str,
115
+
prop: &LexObjectProperty,
116
+
is_required: bool,
117
+
) -> Vec<ValidationCheck> {
118
+
let mut checks = Vec::new();
119
+
120
+
match prop {
121
+
LexObjectProperty::String(s) => {
122
+
checks.extend(extract_string_validations(
123
+
field_name,
124
+
schema_name,
125
+
s,
126
+
is_required,
127
+
));
128
+
}
129
+
LexObjectProperty::Integer(i) => {
130
+
checks.extend(extract_integer_validations(
131
+
field_name,
132
+
schema_name,
133
+
i,
134
+
is_required,
135
+
));
136
+
}
137
+
LexObjectProperty::Array(arr) => {
138
+
if let Some(max) = arr.max_length {
139
+
checks.push(ValidationCheck {
140
+
field_name: field_name.to_string(),
141
+
schema_name: schema_name.to_string(),
142
+
field_type: "Vec<_>".to_string(),
143
+
is_required,
144
+
check: ConstraintCheck::MaxLength { max },
145
+
});
146
+
}
147
+
if let Some(min) = arr.min_length {
148
+
checks.push(ValidationCheck {
149
+
field_name: field_name.to_string(),
150
+
schema_name: schema_name.to_string(),
151
+
field_type: "Vec<_>".to_string(),
152
+
is_required,
153
+
check: ConstraintCheck::MinLength { min },
154
+
});
155
+
}
156
+
}
157
+
_ => {
158
+
// Other types don't have runtime validations in the current impl
159
+
}
160
+
}
161
+
162
+
checks
163
+
}
164
+
165
+
/// Extract validation checks from a string property
166
+
fn extract_string_validations(
167
+
field_name: &str,
168
+
schema_name: &str,
169
+
string: &LexString,
170
+
is_required: bool,
171
+
) -> Vec<ValidationCheck> {
172
+
let mut checks = Vec::new();
173
+
174
+
if let Some(max) = string.max_length {
175
+
checks.push(ValidationCheck {
176
+
field_name: field_name.to_string(),
177
+
schema_name: schema_name.to_string(),
178
+
field_type: "String".to_string(),
179
+
is_required,
180
+
check: ConstraintCheck::MaxLength { max },
181
+
});
182
+
}
183
+
184
+
if let Some(min) = string.min_length {
185
+
checks.push(ValidationCheck {
186
+
field_name: field_name.to_string(),
187
+
schema_name: schema_name.to_string(),
188
+
field_type: "String".to_string(),
189
+
is_required,
190
+
check: ConstraintCheck::MinLength { min },
191
+
});
192
+
}
193
+
194
+
if let Some(max) = string.max_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
+
check: ConstraintCheck::MaxGraphemes { max },
201
+
});
202
+
}
203
+
204
+
if let Some(min) = string.min_graphemes {
205
+
checks.push(ValidationCheck {
206
+
field_name: field_name.to_string(),
207
+
schema_name: schema_name.to_string(),
208
+
field_type: "String".to_string(),
209
+
is_required,
210
+
check: ConstraintCheck::MinGraphemes { min },
211
+
});
212
+
}
213
+
214
+
checks
215
+
}
216
+
217
+
/// Extract validation checks from an integer property
218
+
fn extract_integer_validations(
219
+
field_name: &str,
220
+
schema_name: &str,
221
+
integer: &LexInteger,
222
+
is_required: bool,
223
+
) -> Vec<ValidationCheck> {
224
+
let mut checks = Vec::new();
225
+
226
+
if let Some(max) = integer.maximum {
227
+
checks.push(ValidationCheck {
228
+
field_name: field_name.to_string(),
229
+
schema_name: schema_name.to_string(),
230
+
field_type: "i64".to_string(),
231
+
is_required,
232
+
check: ConstraintCheck::Maximum { max },
233
+
});
234
+
}
235
+
236
+
if let Some(min) = integer.minimum {
237
+
checks.push(ValidationCheck {
238
+
field_name: field_name.to_string(),
239
+
schema_name: schema_name.to_string(),
240
+
field_type: "i64".to_string(),
241
+
is_required,
242
+
check: ConstraintCheck::Minimum { min },
243
+
});
244
+
}
245
+
246
+
checks
247
+
}
248
+
249
+
/// Convert camelCase/PascalCase to snake_case
250
+
fn to_snake_case(s: &str) -> String {
251
+
let mut result = String::new();
252
+
let mut prev_is_lower = false;
253
+
254
+
for (i, ch) in s.chars().enumerate() {
255
+
if ch.is_uppercase() {
256
+
if i > 0 && prev_is_lower {
257
+
result.push('_');
258
+
}
259
+
result.push(ch.to_ascii_lowercase());
260
+
prev_is_lower = false;
261
+
} else {
262
+
result.push(ch);
263
+
prev_is_lower = ch.is_lowercase();
264
+
}
265
+
}
266
+
267
+
result
268
+
}
269
+
270
+
#[cfg(test)]
271
+
mod tests {
272
+
use super::*;
273
+
274
+
#[test]
275
+
fn test_to_snake_case() {
276
+
assert_eq!(to_snake_case("createdAt"), "created_at");
277
+
assert_eq!(to_snake_case("maxLength"), "max_length");
278
+
assert_eq!(to_snake_case("text"), "text");
279
+
assert_eq!(to_snake_case("FooBar"), "foo_bar");
280
+
}
281
+
}
+14
-1
crates/jacquard-lexicon/src/codegen/structs.rs
+14
-1
crates/jacquard-lexicon/src/codegen/structs.rs
···
1
1
use crate::error::Result;
2
2
use crate::lexicon::{
3
-
LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString,
3
+
LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString, LexUserType,
4
+
Lexicon, LexiconDoc,
4
5
};
5
6
use heck::{ToPascalCase, ToSnakeCase};
6
7
use proc_macro2::TokenStream;
7
8
use quote::quote;
9
+
use std::collections::BTreeMap;
8
10
9
11
use super::CodeGenerator;
10
12
use super::utils::{make_ident, value_to_variant_name};
···
214
216
}
215
217
};
216
218
219
+
// Generate LexiconSchema impl from original lexicon
220
+
let lex_doc = self.corpus.get(nsid).expect("nsid exists in corpus");
221
+
let schema_impl =
222
+
super::schema_impl::generate_schema_impl(&type_name, lex_doc, true);
223
+
217
224
Ok(quote! {
218
225
#struct_def
219
226
···
228
235
#collection_impl
229
236
#record_marker
230
237
#collection_marker_impl
238
+
#schema_impl
231
239
})
232
240
}
233
241
}
···
331
339
}
332
340
}
333
341
342
+
// Generate LexiconSchema impl from original lexicon
343
+
let lex_doc = self.corpus.get(nsid).expect("nsid exists in corpus");
344
+
let schema_impl = super::schema_impl::generate_schema_impl(&type_name, lex_doc, true);
345
+
334
346
Ok(quote! {
335
347
#struct_def
336
348
#(#unions)*
349
+
#schema_impl
337
350
})
338
351
}
339
352
+1
-1
crates/jacquard-lexicon/src/derive_impl/mod.rs
+1
-1
crates/jacquard-lexicon/src/derive_impl/mod.rs