Parakeet is a Rust-based Bluesky AppView aiming to implement most of the functionality required to support the Bluesky client
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}