Parakeet is a Rust-based Bluesky AppView aiming to implement most of the functionality required to support the Bluesky client
at main 7.3 kB view raw
1use crate::types::{ 2 Lexicon, LexiconDef, LexiconObject, LexiconProcedure, LexiconQuery, LexiconSchema, 3 LexiconString, LexiconSubscription, LexiconUnion, RecordKey, 4}; 5use std::collections::BTreeMap; 6 7#[derive(Debug)] 8pub enum ValidationError { 9 InvalidReference(String), 10 MissingProperty, 11 MissingReference(String), 12 RecordKey, 13 StringConstAndDefault, 14} 15 16/// validates lexicon definition files - returns keys for any that don't 17pub fn validate(lexica: &BTreeMap<String, Lexicon>) -> Vec<(String, ValidationError)> { 18 let mut errors = Vec::new(); 19 20 for (lexicon_id, lexicon) in lexica { 21 for (name, def) in &lexicon.defs { 22 let pass = match def { 23 LexiconDef::Query(query) => validate_query(query, lexica, lexicon_id), 24 LexiconDef::Procedure(proc) => validate_procedure(proc, lexica, lexicon_id), 25 LexiconDef::Subscription(sub) => validate_subscription(sub, lexica, lexicon_id), 26 LexiconDef::Record(rec) => { 27 // validate the rkey 28 let rkey_okay = match &rec.key { 29 // typed is always okay - serde validates 30 RecordKey::Typed(_) => None, 31 // we may want to validate literals according to record key syntax but idk? 32 RecordKey::Other(text) if text.starts_with("literal:") => None, 33 RecordKey::Other(_) => Some(ValidationError::RecordKey), 34 }; 35 36 // and the schema def 37 let obj_okay = validate_object(&rec.record, lexica, lexicon_id); 38 39 rkey_okay.or(obj_okay) 40 } 41 LexiconDef::String(lex_str) => validate_string(lex_str, lexica, lexicon_id), 42 LexiconDef::Array(array) => validate_schema(&array.items, lexica, lexicon_id), 43 LexiconDef::Object(obj) => validate_object(obj, lexica, lexicon_id), 44 LexiconDef::Token { .. } => continue, 45 }; 46 47 if let Some(err) = pass { 48 errors.push((format!("{lexicon_id}#{name}"), err)); 49 } 50 } 51 } 52 53 errors 54} 55 56fn validate_schema( 57 schema: &LexiconSchema, 58 lexica: &BTreeMap<String, Lexicon>, 59 this_lex: &str, 60) -> Option<ValidationError> { 61 match schema { 62 LexiconSchema::String(lex_str) => validate_string(lex_str, lexica, this_lex), 63 LexiconSchema::Array(array) => validate_schema(&array.items, lexica, this_lex), 64 LexiconSchema::Object(obj) => validate_object(obj, lexica, this_lex), 65 LexiconSchema::Ref { ref_to, .. } => lookup(ref_to, this_lex, lexica), 66 LexiconSchema::Union(union) => validate_union(union, lexica, this_lex), 67 LexiconSchema::Null { .. } 68 | LexiconSchema::Boolean { .. } 69 | LexiconSchema::Integer(_) 70 | LexiconSchema::Bytes { .. } 71 | LexiconSchema::CidLink { .. } 72 | LexiconSchema::Blob { .. } 73 | LexiconSchema::Unknown { .. } => None, 74 } 75} 76 77fn validate_query( 78 query: &LexiconQuery, 79 lexica: &BTreeMap<String, Lexicon>, 80 this_lex: &str, 81) -> Option<ValidationError> { 82 if let Some(params) = &query.parameters { 83 validate_schema_btree(params.properties.values(), lexica, this_lex)?; 84 } 85 if let Some(output) = &query.output { 86 if let Some(schema) = &output.schema { 87 validate_schema(schema, lexica, this_lex)?; 88 } 89 } 90 91 None 92} 93 94fn validate_procedure( 95 proc: &LexiconProcedure, 96 lexica: &BTreeMap<String, Lexicon>, 97 this_lex: &str, 98) -> Option<ValidationError> { 99 if let Some(params) = &proc.parameters { 100 validate_schema_btree(params.properties.values(), lexica, this_lex)?; 101 } 102 103 if let Some(input) = &proc.input { 104 if let Some(schema) = &input.schema { 105 validate_schema(schema, lexica, this_lex)?; 106 } 107 } 108 109 if let Some(output) = &proc.output { 110 if let Some(schema) = &output.schema { 111 validate_schema(schema, lexica, this_lex)?; 112 } 113 } 114 115 None 116} 117 118fn validate_subscription( 119 sub: &LexiconSubscription, 120 lexica: &BTreeMap<String, Lexicon>, 121 this_lex: &str, 122) -> Option<ValidationError> { 123 if let Some(params) = &sub.parameters { 124 validate_schema_btree(params.properties.values(), lexica, this_lex)?; 125 } 126 127 sub.message.values().find_map(|item| { 128 item.schema 129 .as_ref() 130 .map(|union| validate_union(&union, lexica, this_lex)) 131 .unwrap_or_default() 132 }) 133} 134 135fn validate_object( 136 object: &LexiconObject, 137 lexica: &BTreeMap<String, Lexicon>, 138 this_lex: &str, 139) -> Option<ValidationError> { 140 // check that everything in required and nullable exists in properties 141 if let Some(required) = &object.required { 142 for key in required { 143 if !object.properties.contains_key(key) { 144 return Some(ValidationError::MissingProperty); 145 } 146 } 147 } 148 if let Some(nullable) = &object.nullable { 149 for key in nullable { 150 if !object.properties.contains_key(key) { 151 return Some(ValidationError::MissingProperty); 152 } 153 } 154 } 155 156 // and now validate properties 157 validate_schema_btree(object.properties.values(), lexica, this_lex) 158} 159 160fn validate_string( 161 lex_string: &LexiconString, 162 lexica: &BTreeMap<String, Lexicon>, 163 this_lex: &str, 164) -> Option<ValidationError> { 165 if lex_string.constant.is_some() && lex_string.default.is_some() { 166 return Some(ValidationError::StringConstAndDefault); 167 } 168 169 if let Some(known_values) = &lex_string.known_values { 170 known_values.iter().find_map(|value| { 171 if value.contains("#") { 172 lookup(value, this_lex, lexica) 173 } else { 174 None 175 } 176 }) 177 } else { 178 None 179 } 180} 181 182fn validate_union( 183 union: &LexiconUnion, 184 lexica: &BTreeMap<String, Lexicon>, 185 this_lex: &str, 186) -> Option<ValidationError> { 187 union 188 .refs 189 .iter() 190 .find_map(|value| lookup(value, this_lex, lexica)) 191} 192 193fn lookup( 194 reference: &str, 195 this_lex: &str, 196 lexica: &BTreeMap<String, Lexicon>, 197) -> Option<ValidationError> { 198 let reference = if reference.contains("#") { 199 match reference.strip_prefix("#") { 200 Some(local_ref) => format!("{this_lex}#{local_ref}"), 201 None => reference.to_string(), 202 } 203 } else { 204 format!("{}#main", reference) 205 }; 206 207 if let Some((nsid, name)) = reference.split_once("#") { 208 match lexica.get(nsid) { 209 Some(def) => match def.defs.contains_key(name) { 210 true => None, 211 false => Some(ValidationError::MissingReference(reference)), 212 }, 213 None => Some(ValidationError::MissingReference(reference)), 214 } 215 } else { 216 Some(ValidationError::InvalidReference(reference)) 217 } 218} 219 220fn validate_schema_btree<'a, T>( 221 mut schema: T, 222 lexica: &BTreeMap<String, Lexicon>, 223 this_lex: &str, 224) -> Option<ValidationError> 225where 226 T: Iterator<Item = &'a LexiconSchema>, 227{ 228 schema.find_map(|schema| validate_schema(schema, lexica, this_lex)) 229}