A better Rust ATProto crate

codegen schema trait impl

Orual e8718b7f efa35bdf

Changed files
+297 -2
crates
jacquard-lexicon
src
+1
crates/jacquard-lexicon/src/codegen.rs
··· 11 11 mod structs; 12 12 mod xrpc; 13 13 mod output; 14 + mod schema_impl; 14 15 15 16 /// Code generator for lexicon types 16 17 pub struct CodeGenerator<'c> {
+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
··· 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
··· 3 3 //! These functions are used by the `jacquard-derive` proc-macro crate but are also 4 4 //! available for runtime code generation in `jacquard-lexicon`. 5 5 6 - mod doc_to_tokens; 6 + pub mod doc_to_tokens; 7 7 pub mod helpers; 8 8 pub mod into_static; 9 9 pub mod lexicon_attr;