A better Rust ATProto crate
at main 249 lines 8.1 kB view raw
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}