Parakeet is a Rust-based Bluesky AppServer aiming to implement most of the functionality required to support the Bluesky client
appview atproto bluesky rust appserver
66
fork

Configure Feed

Select the types of activity you want to include in your feed.

lexgen lexicon validation

Mia 4ec9513d a16243cb

+233
+4
parakeet-lexgen/src/main.rs
··· 2 2 use walkdir::WalkDir; 3 3 4 4 mod types; 5 + mod validate; 5 6 6 7 #[derive(Debug, Parser)] 7 8 struct Cli { 8 9 sources: Vec<String>, 9 10 #[clap(short, long)] 10 11 out: String, 12 + /// Turn off lexicon validation - this may cause Rust compilation to fail on generated types. 13 + #[clap(short, long)] 14 + no_validate: bool, 11 15 } 12 16 13 17 fn main() -> eyre::Result<()> {
+229
parakeet-lexgen/src/validate.rs
··· 1 + use crate::types::{ 2 + Lexicon, LexiconDef, LexiconObject, LexiconProcedure, LexiconQuery, LexiconSchema, 3 + LexiconString, LexiconSubscription, LexiconUnion, RecordKey, 4 + }; 5 + use std::collections::BTreeMap; 6 + 7 + #[derive(Debug)] 8 + pub 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 17 + pub 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 + 56 + fn 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 + 77 + fn 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 + 94 + fn 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 + 118 + fn 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 + 135 + fn 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 + 160 + fn 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 + 182 + fn 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 + 193 + fn 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 + 220 + fn validate_schema_btree<'a, T>( 221 + mut schema: T, 222 + lexica: &BTreeMap<String, Lexicon>, 223 + this_lex: &str, 224 + ) -> Option<ValidationError> 225 + where 226 + T: Iterator<Item = &'a LexiconSchema>, 227 + { 228 + schema.find_map(|schema| validate_schema(schema, lexica, this_lex)) 229 + }