1use serde_json::Value;
2use std::borrow::Cow;
3use std::cell::{Ref, RefCell};
4use std::collections;
5use std::ops;
6use url::Url;
7
8use super::keywords;
9use super::scope;
10use super::validators;
11use super::{helpers, SchemaVersion};
12use std::error::Error;
13use std::fmt;
14use std::fmt::{Display, Formatter};
15
16#[derive(Debug)]
17pub struct WalkContext<'a> {
18 pub url: &'a Url,
19 pub fragment: Vec<String>,
20 pub scopes: &'a mut collections::HashMap<String, Vec<String>>,
21 pub version: SchemaVersion,
22}
23
24impl<'a> WalkContext<'a> {
25 pub fn escaped_fragment(&self) -> String {
26 helpers::connect(
27 self.fragment
28 .iter()
29 .map(|s| s.as_ref())
30 .collect::<Vec<&str>>()
31 .as_ref(),
32 )
33 }
34}
35
36#[derive(Debug)]
37#[allow(missing_copy_implementations)]
38pub enum SchemaError {
39 WrongId,
40 IdConflicts,
41 NotAnObject,
42 UrlParseError(url::ParseError),
43 UnknownKey(String),
44 Malformed { path: String, detail: String },
45}
46
47impl Display for SchemaError {
48 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
49 match *self {
50 SchemaError::WrongId => write!(f, "wrong id"),
51 SchemaError::IdConflicts => write!(f, "id conflicts"),
52 SchemaError::NotAnObject => write!(f, "not an object"),
53 SchemaError::UrlParseError(ref e) => write!(f, "url parse error: {e}"),
54 SchemaError::UnknownKey(ref k) => write!(f, "unknown key: {k}"),
55 SchemaError::Malformed {
56 ref path,
57 ref detail,
58 } => write!(f, "malformed path: `{path}`, details: {detail}"),
59 }
60 }
61}
62
63impl Error for SchemaError {}
64
65#[derive(Debug)]
66pub struct ScopedSchema<'a> {
67 scope: &'a scope::Scope,
68 schema: &'a Schema,
69}
70
71impl<'a> ops::Deref for ScopedSchema<'a> {
72 type Target = Schema;
73
74 fn deref(&self) -> &Schema {
75 self.schema
76 }
77}
78
79impl<'a> ScopedSchema<'a> {
80 pub fn new(scope: &'a scope::Scope, schema: &'a Schema) -> ScopedSchema<'a> {
81 ScopedSchema { scope, schema }
82 }
83
84 pub fn validate(&self, data: &Value) -> validators::ValidationState {
85 self.schema.validate_in_scope(data, "", self.scope)
86 }
87
88 pub fn validate_in(&self, data: &Value, path: &str) -> validators::ValidationState {
89 self.schema.validate_in_scope(data, path, self.scope)
90 }
91}
92
93#[derive(Debug)]
94#[allow(dead_code)]
95pub struct Schema {
96 pub id: Option<Url>,
97 schema: Option<Url>,
98 original: Value,
99 tree: collections::BTreeMap<String, Schema>,
100 validators: validators::Validators,
101 scopes: collections::HashMap<String, Vec<String>>,
102 default: RefCell<Option<Value>>,
103}
104
105include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
106
107pub struct CompilationSettings<'a> {
108 pub keywords: &'a keywords::KeywordMap,
109 pub ban_unknown_keywords: bool,
110 pub schema_version: SchemaVersion,
111}
112
113impl<'a> CompilationSettings<'a> {
114 pub fn new(
115 keywords: &'a keywords::KeywordMap,
116 ban_unknown_keywords: bool,
117 schema_version: SchemaVersion,
118 ) -> CompilationSettings<'a> {
119 CompilationSettings {
120 keywords,
121 ban_unknown_keywords,
122 schema_version,
123 }
124 }
125}
126
127impl Schema {
128 fn compile(
129 def: Value,
130 external_id: Option<Url>,
131 settings: CompilationSettings,
132 ) -> Result<Schema, SchemaError> {
133 let def = helpers::convert_boolean_schema(def);
134
135 if !def.is_object() {
136 return Err(SchemaError::NotAnObject);
137 }
138
139 let mut id = if let Some(id) = external_id {
140 id
141 } else {
142 helpers::parse_url_key("$id", &def)?.unwrap_or_else(helpers::generate_id)
143 };
144
145 if settings.schema_version >= SchemaVersion::Draft2019_09 {
146 if let Some(anchor) = def.get("$anchor") {
147 let anchor = anchor.as_str().ok_or_else(|| SchemaError::Malformed {
148 path: "".to_string(),
149 detail: "$anchor must be a string".to_string(),
150 })?;
151 id.set_fragment(Some(anchor));
152 };
153 }
154
155 let schema = helpers::parse_url_key("$schema", &def)?;
156
157 let (tree, mut scopes) = {
158 let mut tree = collections::BTreeMap::new();
159 let obj = def.as_object().unwrap();
160
161 let mut scopes = collections::HashMap::new();
162
163 for (key, value) in obj.iter() {
164 if !value.is_object() && !value.is_array() && !value.is_boolean() {
165 continue;
166 }
167 if FINAL_KEYS.contains(&key[..]) {
168 continue;
169 }
170
171 let mut context = WalkContext {
172 url: &id,
173 fragment: vec![key.clone()],
174 scopes: &mut scopes,
175 version: settings.schema_version,
176 };
177
178 let scheme = Schema::compile_sub(
179 value.clone(),
180 &mut context,
181 &settings,
182 !NON_SCHEMA_KEYS.contains(&key[..]),
183 )?;
184
185 tree.insert(helpers::encode(key), scheme);
186 }
187
188 (tree, scopes)
189 };
190
191 let validators = Schema::compile_keywords(
192 &def,
193 &WalkContext {
194 url: &id,
195 fragment: vec![],
196 scopes: &mut scopes,
197 version: settings.schema_version,
198 },
199 &settings,
200 )?;
201
202 let schema = Schema {
203 id: Some(id),
204 schema,
205 original: def,
206 tree,
207 validators,
208 scopes,
209 default: RefCell::new(None),
210 };
211
212 Ok(schema)
213 }
214
215 fn unsafe_set_default(&self, default: Option<Value>) {
224 self.default.replace(default);
225 }
226
227 fn unsafe_get_default(&self) -> Ref<Option<Value>> {
234 self.default.borrow()
235 }
236
237 pub fn get_default(&self) -> Option<Value> {
238 self.unsafe_get_default().clone()
239 }
240
241 pub fn has_default(&self) -> bool {
242 self.unsafe_get_default().is_some()
243 }
244
245 pub fn add_defaults(&mut self, id: &Url, scope: &scope::Scope) {
246 self.add_defaults_recursive(self, id, scope);
247 }
248
249 fn add_defaults_recursive(&self, top: &Schema, id: &Url, scope: &scope::Scope) {
250 if self.has_default() {
252 return;
253 }
254
255 for (_, schema) in self.tree.iter() {
257 schema.add_defaults_recursive(top, id, scope);
258 }
259
260 if let Some(default) = self.original.get("default") {
262 self.unsafe_set_default(Some(default.clone()));
263 return;
264 }
265
266 if let Some(ref_) = self.original.get("$ref").and_then(|r| r.as_str()) {
269 if let Ok(url) = Url::options().base_url(Some(id)).parse(ref_) {
270 if let Some(schema) = top.resolve_internal(&url) {
273 schema.add_defaults_recursive(top, id, scope);
274 self.unsafe_set_default(schema.get_default());
275 } else if let Some(schema) = scope.resolve(&url) {
276 self.unsafe_set_default(schema.get_default());
277 }
278 }
279 return;
281 }
282 if let Some(properties) = self.tree.get("properties") {
284 let mut default = serde_json::Map::default();
285 for (key, schema) in properties.tree.iter() {
286 if let Some(value) = schema.get_default() {
287 default.insert(key.clone(), value);
288 }
289 }
290 if !default.is_empty() {
291 self.unsafe_set_default(Some(default.into()));
292 return;
293 }
294 }
295 if self
298 .original
299 .get("items")
300 .map(|i| i.is_array())
301 .unwrap_or(false)
302 {
303 let items = self.tree.get("items").unwrap();
304 let mut default = vec![];
305 for idx in 0.. {
306 if let Some(schema) = items.tree.get(&idx.to_string()) {
307 if let Some(def) = schema.get_default() {
308 default.push(def);
309 } else {
310 break;
311 }
312 } else {
313 break;
314 }
315 }
316 if default.len() == items.tree.len() {
317 self.unsafe_set_default(Some(default.into()));
318 }
319 }
320 }
321
322 fn compile_keywords(
323 def: &Value,
324 context: &WalkContext,
325 settings: &CompilationSettings,
326 ) -> Result<validators::Validators, SchemaError> {
327 let mut validators = vec![];
328 let mut end_validators = vec![];
329 let mut keys: collections::HashSet<&str> = def
330 .as_object()
331 .unwrap()
332 .keys()
333 .map(|key| key.as_ref())
334 .collect();
335 let mut not_consumed = collections::HashSet::new();
336
337 loop {
338 let key = keys.iter().next().cloned();
339 if let Some(key) = key {
340 match settings.keywords.get(&key) {
341 Some(keyword) => {
342 keyword.consume(&mut keys);
343
344 let is_exclusive_keyword =
345 keyword.keyword.is_exclusive(settings.schema_version);
346
347 if let Some(validator) = keyword.keyword.compile(def, context)? {
348 if is_exclusive_keyword {
349 validators = vec![validator];
350 end_validators = vec![];
351 } else if keyword.keyword.place_first() {
352 validators.splice(0..0, std::iter::once(validator));
353 } else if keyword.keyword.place_last() {
354 end_validators.push(validator);
355 } else {
356 validators.push(validator);
357 }
358 }
359
360 if is_exclusive_keyword {
361 break;
362 }
363 }
364 None => {
365 keys.remove(&key);
366 if settings.ban_unknown_keywords {
367 not_consumed.insert(key);
368 }
369 }
370 }
371 } else {
372 break;
373 }
374 }
375
376 if settings.ban_unknown_keywords && !not_consumed.is_empty() {
377 for key in not_consumed.iter() {
378 if !ALLOW_NON_CONSUMED_KEYS.contains(&key[..]) {
379 return Err(SchemaError::UnknownKey((*key).to_string()));
380 }
381 }
382 }
383
384 validators.extend(end_validators);
385 Ok(validators)
386 }
387
388 fn compile_sub(
389 def: Value,
390 context: &mut WalkContext,
391 keywords: &CompilationSettings,
392 is_schema: bool,
393 ) -> Result<Schema, SchemaError> {
394 let def = helpers::convert_boolean_schema(def);
395
396 let id = if is_schema {
397 let mut id_url = helpers::parse_url_key_with_base("$id", &def, context.url)?;
398 if keywords.schema_version >= SchemaVersion::Draft2019_09 {
399 if let Some(anchor) = def.get("$anchor") {
400 let anchor = anchor.as_str().ok_or_else(|| SchemaError::Malformed {
401 path: "".to_string(),
402 detail: "$anchor must be a string".to_string(),
403 })?;
404
405 if id_url.is_none() {
407 id_url = Some(context.url.clone());
408 }
409
410 id_url.as_mut().unwrap().set_fragment(Some(anchor));
411 }
412 }
413 id_url
414 } else {
415 None
416 };
417
418 let schema = if is_schema {
419 helpers::parse_url_key("$schema", &def)?
420 } else {
421 None
422 };
423
424 let tree = {
425 let mut tree = collections::BTreeMap::new();
426
427 if def.is_object() {
428 let obj = def.as_object().unwrap();
429 let parent_key = &context.fragment[context.fragment.len() - 1];
430
431 for (key, value) in obj.iter() {
432 if !value.is_object() && !value.is_array() && !value.is_boolean() {
433 continue;
434 }
435 if !PROPERTY_KEYS.contains(&parent_key[..]) && FINAL_KEYS.contains(&key[..]) {
436 continue;
437 }
438
439 let mut current_fragment = context.fragment.clone();
440 current_fragment.push(key.clone());
441
442 let is_schema = PROPERTY_KEYS.contains(&parent_key[..])
443 || !NON_SCHEMA_KEYS.contains(&key[..]);
444
445 let mut context = WalkContext {
446 url: id.as_ref().unwrap_or(context.url),
447 fragment: current_fragment,
448 scopes: context.scopes,
449 version: keywords.schema_version,
450 };
451
452 let scheme =
453 Schema::compile_sub(value.clone(), &mut context, keywords, is_schema)?;
454
455 tree.insert(helpers::encode(key), scheme);
456 }
457 } else if def.is_array() {
458 let array = def.as_array().unwrap();
459 let parent_key = &context.fragment[context.fragment.len() - 1];
460
461 for (idx, value) in array.iter().enumerate() {
462 let mut value = value.clone();
463
464 if BOOLEAN_SCHEMA_ARRAY_KEYS.contains(&parent_key[..]) {
465 value = helpers::convert_boolean_schema(value);
466 }
467
468 if !value.is_object() && !value.is_array() {
469 continue;
470 }
471
472 let mut current_fragment = context.fragment.clone();
473 current_fragment.push(idx.to_string().clone());
474
475 let mut context = WalkContext {
476 url: id.as_ref().unwrap_or(context.url),
477 fragment: current_fragment,
478 scopes: context.scopes,
479 version: keywords.schema_version,
480 };
481
482 let scheme = Schema::compile_sub(value.clone(), &mut context, keywords, true)?;
483
484 tree.insert(idx.to_string().clone(), scheme);
485 }
486 }
487
488 tree
489 };
490
491 if id.is_some() {
492 context
493 .scopes
494 .insert(id.clone().unwrap().into(), context.fragment.clone());
495 }
496
497 let validators = if is_schema && def.is_object() {
498 Schema::compile_keywords(&def, context, keywords)?
499 } else {
500 vec![]
501 };
502
503 let schema = Schema {
504 id,
505 schema,
506 original: def,
507 tree,
508 validators,
509 scopes: collections::HashMap::new(),
510 default: RefCell::new(None),
511 };
512
513 Ok(schema)
514 }
515
516 pub fn resolve(&self, id: &str) -> Option<&Schema> {
517 let path = self.scopes.get(id);
518 path.map(|path| {
519 let mut schema = self;
520 for item in path.iter() {
521 schema = &schema.tree[item]
522 }
523 schema
524 })
525 }
526
527 fn resolve_internal(&self, url: &Url) -> Option<&Schema> {
528 let (schema_path, fragment) = helpers::serialize_schema_path(url);
529 if self.id.is_some() && schema_path.as_str() == self.id.as_ref().unwrap().as_str() {
530 if let Some(fragment) = fragment {
531 self.resolve_fragment(fragment.as_str())
532 } else {
533 Some(self)
534 }
535 } else if let Some(id_path) = self.scopes.get(&schema_path) {
536 let mut schema = self;
537 for item in id_path.iter() {
538 schema = &schema.tree[item]
539 }
540 if let Some(fragment) = fragment {
541 schema.resolve_fragment(fragment.as_str())
542 } else {
543 Some(schema)
544 }
545 } else {
546 None
547 }
548 }
549
550 pub fn resolve_fragment(&self, fragment: &str) -> Option<&Schema> {
551 assert!(fragment.starts_with('/'), "Can't resolve id fragments");
552
553 let parts = fragment[1..].split('/');
554 let mut schema = self;
555 for part in parts {
556 match schema.tree.get(part) {
557 Some(sch) => schema = sch,
558 None => return None,
559 }
560 }
561
562 Some(schema)
563 }
564}
565
566impl Schema {
567 fn validate_in_scope(
568 &self,
569 data: &Value,
570 path: &str,
571 scope: &scope::Scope,
572 ) -> validators::ValidationState {
573 let mut state = validators::ValidationState::new();
574 let mut data = Cow::Borrowed(data);
575
576 for validator in self.validators.iter() {
577 let mut result = validator.validate(&data, path, scope, &state);
578 if result.is_valid() && result.replacement.is_some() {
579 *data.to_mut() = result.replacement.take().unwrap();
580 }
581 state.append(result);
582 }
583
584 state.set_replacement(data);
585 state
586 }
587}
588
589pub fn compile(
590 def: Value,
591 external_id: Option<Url>,
592 settings: CompilationSettings<'_>,
593) -> Result<Schema, SchemaError> {
594 Schema::compile(def, external_id, settings)
595}
596
597#[test]
598fn schema_doesnt_compile_not_object() {
599 assert!(Schema::compile(
600 json!(0),
601 None,
602 CompilationSettings::new(&keywords::default(), true, SchemaVersion::Draft7)
603 )
604 .is_err());
605}
606
607#[test]
608fn schema_compiles_boolean_schema() {
609 assert!(Schema::compile(
610 json!(true),
611 None,
612 CompilationSettings::new(&keywords::default(), true, SchemaVersion::Draft7)
613 )
614 .is_ok());
615}