1use crate::json::Value;
4use alloc::{collections::BTreeSet, format, string::String, vec::Vec};
5
6#[derive(Clone, Copy, Debug)]
8#[non_exhaustive]
9pub enum Schema {
10 Any,
12 Null,
14 Boolean,
16 Number,
18 Integer {
20 min: i64,
22 max: i64,
24 },
25 Unsigned {
27 min: u64,
29 max: u64,
31 },
32 String {
34 min: usize,
36 max: usize,
38 choices: &'static [&'static str],
40 },
41 Array {
43 items: &'static Schema,
45 min: usize,
47 max: usize,
49 },
50 Object {
52 properties: &'static [Property],
54 additional: bool,
56 },
57 OneOf(&'static [Schema]),
59}
60
61#[derive(Clone, Copy, Debug)]
63pub struct Property {
64 pub name: &'static str,
66 pub schema: &'static Schema,
68 pub required: bool,
70}
71
72pub static EMPTY_OBJECT: Schema = Schema::Object {
74 properties: &[],
75 additional: false,
76};
77pub static OBJECT: Schema = Schema::Object {
79 properties: &[],
80 additional: true,
81};
82pub static ANY: Schema = Schema::Any;
84pub static BOOLEAN: Schema = Schema::Boolean;
86pub static U64: Schema = Schema::Unsigned {
88 min: 0,
89 max: u64::MAX,
90};
91pub static STRING: Schema = Schema::String {
93 min: 0,
94 max: 4096,
95 choices: &[],
96};
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct Violation {
101 pub path: String,
103 pub reason: &'static str,
105}
106
107impl Schema {
108 pub fn validate(&self, value: &Value) -> Result<(), Violation> {
110 self.validate_at(value, String::new(), 0)
111 }
112
113 fn validate_at(&self, value: &Value, path: String, depth: usize) -> Result<(), Violation> {
114 let bad = |reason| {
115 Err(Violation {
116 path: path.clone(),
117 reason,
118 })
119 };
120 if depth > 32 {
121 return bad("schema_depth_limit");
122 }
123 match self {
124 Self::Any => Ok(()),
125 Self::Null if matches!(value, Value::Null) => Ok(()),
126 Self::Boolean if matches!(value, Value::Bool(_)) => Ok(()),
127 Self::Number if matches!(value, Value::Number(_)) => Ok(()),
128 Self::Integer { min, max } => match value.as_i64() {
129 Some(v) if v >= *min && v <= *max => Ok(()),
130 Some(_) => bad("out_of_range"),
131 _ => bad("expected_integer"),
132 },
133 Self::Unsigned { min, max } => match value.as_u64() {
134 Some(v) if v >= *min && v <= *max => Ok(()),
135 Some(_) => bad("out_of_range"),
136 _ => bad("expected_unsigned_integer"),
137 },
138 Self::String { min, max, choices } => {
139 let Some(text) = value.as_str() else {
140 return bad("expected_string");
141 };
142 let count = text.chars().count();
143 if count < *min || count > *max {
144 return bad("string_length");
145 }
146 if !choices.is_empty() && !choices.contains(&text) {
147 return bad("unknown_choice");
148 }
149 Ok(())
150 }
151 Self::Array { items, min, max } => {
152 let Some(values) = value.as_array() else {
153 return bad("expected_array");
154 };
155 if values.len() < *min || values.len() > *max {
156 return bad("array_length");
157 }
158 for (index, item) in values.iter().enumerate() {
159 items.validate_at(item, format!("{path}/{index}"), depth + 1)?;
160 }
161 Ok(())
162 }
163 Self::Object {
164 properties,
165 additional,
166 } => {
167 let Some(fields) = value.as_object() else {
168 return bad("expected_object");
169 };
170 for property in *properties {
171 match value.get(property.name) {
172 Some(v) => property.schema.validate_at(
173 v,
174 pointer(&path, property.name),
175 depth + 1,
176 )?,
177 None if property.required => {
178 return Err(Violation {
179 path: pointer(&path, property.name),
180 reason: "required",
181 })
182 }
183 None => (),
184 }
185 }
186 let mut seen = BTreeSet::new();
187 for (name, _) in fields.iter() {
188 if !seen.insert(name.as_str()) {
189 return Err(Violation {
190 path: pointer(&path, name),
191 reason: "duplicate_key",
192 });
193 }
194 if !*additional && !properties.iter().any(|p| p.name == name) {
195 return Err(Violation {
196 path: pointer(&path, name),
197 reason: "unknown_property",
198 });
199 }
200 }
201 Ok(())
202 }
203 Self::OneOf(options) => {
204 let mut matches = 0;
205 for option in *options {
206 if option.validate_at(value, path.clone(), depth + 1).is_ok() {
207 matches += 1;
208 }
209 }
210 if matches == 1 {
211 Ok(())
212 } else {
213 bad("one_of")
214 }
215 }
216 _ => bad("type_mismatch"),
217 }
218 }
219
220 pub fn to_json(&self) -> Value {
222 self.to_json_at(0)
223 }
224
225 fn to_json_at(&self, depth: usize) -> Value {
226 if depth > 32 {
227 return Value::Bool(false);
228 }
229 let typed = |name: &str| Value::object([("type", Value::string(name))]);
230 match self {
231 Self::Any => Value::object([]),
232 Self::Null => typed("null"),
233 Self::Boolean => typed("boolean"),
234 Self::Number => typed("number"),
235 Self::Integer { min, max } => Value::object([
236 ("type", Value::string("integer")),
237 ("minimum", Value::i64(*min)),
238 ("maximum", Value::i64(*max)),
239 ]),
240 Self::Unsigned { min, max } => Value::object([
241 ("type", Value::string("integer")),
242 ("minimum", Value::u64(*min)),
243 ("maximum", Value::u64(*max)),
244 ]),
245 Self::String { min, max, choices } => {
246 let mut value = Value::object([
247 ("type", Value::string("string")),
248 ("minLength", Value::u64(*min as u64)),
249 ("maxLength", Value::u64(*max as u64)),
250 ]);
251 if !choices.is_empty() {
252 value.insert(
253 "enum",
254 Value::Array(choices.iter().map(|s| Value::string(*s)).collect()),
255 );
256 }
257 value
258 }
259 Self::Array { items, min, max } => Value::object([
260 ("type", Value::string("array")),
261 ("items", items.to_json_at(depth + 1)),
262 ("minItems", Value::u64(*min as u64)),
263 ("maxItems", Value::u64(*max as u64)),
264 ]),
265 Self::Object {
266 properties,
267 additional,
268 } => {
269 let members = properties
270 .iter()
271 .map(|p| (String::from(p.name), p.schema.to_json_at(depth + 1)))
272 .collect();
273 let required: Vec<Value> = properties
274 .iter()
275 .filter(|p| p.required)
276 .map(|p| Value::string(p.name))
277 .collect();
278 Value::object([
279 ("type", Value::string("object")),
280 ("properties", Value::Object(members)),
281 ("required", Value::Array(required)),
282 ("additionalProperties", Value::Bool(*additional)),
283 ])
284 }
285 Self::OneOf(options) => Value::object([(
286 "oneOf",
287 Value::Array(options.iter().map(|s| s.to_json_at(depth + 1)).collect()),
288 )]),
289 }
290 }
291}
292
293fn pointer(prefix: &str, name: &str) -> String {
294 format!("{prefix}/{}", name.replace('~', "~0").replace('/', "~1"))
295}