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