feat: Add cache for schemas

This commit is contained in:
Dmitry Dygalo 2021-01-27 20:43:49 +01:00 committed by Dmitry Dygalo
parent b11b409cc4
commit 0080bf89af
4 changed files with 66 additions and 19 deletions

View File

@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Added
- Cache for documents loaded via the `$ref` keyword. [#75](https://github.com/Stranger6667/jsonschema-rs/issues/75)
### Performance ### Performance
- Enum validation for input values that have a type that is not present among the enum variants. [#80](https://github.com/Stranger6667/jsonschema-rs/issues/80) - Enum validation for input values that have a type that is not present among the enum variants. [#80](https://github.com/Stranger6667/jsonschema-rs/issues/80)

View File

@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Added
- Cache for documents loaded via the `$ref` keyword. [#75](https://github.com/Stranger6667/jsonschema-rs/issues/75)
### Performance ### Performance
- Enum validation for input values that have a type that is not present among the enum variants. [#80](https://github.com/Stranger6667/jsonschema-rs/issues/80) - Enum validation for input values that have a type that is not present among the enum variants. [#80](https://github.com/Stranger6667/jsonschema-rs/issues/80)

View File

@ -22,6 +22,7 @@ pub struct CompilationOptions {
content_media_type_checks: HashMap<&'static str, Option<ContentMediaTypeCheckType>>, content_media_type_checks: HashMap<&'static str, Option<ContentMediaTypeCheckType>>,
content_encoding_checks_and_converters: content_encoding_checks_and_converters:
HashMap<&'static str, Option<(ContentEncodingCheckType, ContentEncodingConverterType)>>, HashMap<&'static str, Option<(ContentEncodingCheckType, ContentEncodingConverterType)>>,
store: HashMap<String, Value>,
} }
impl CompilationOptions { impl CompilationOptions {
@ -55,7 +56,7 @@ impl CompilationOptions {
Some(url) => url::Url::parse(url)?, Some(url) => url::Url::parse(url)?,
None => DEFAULT_SCOPE.clone(), None => DEFAULT_SCOPE.clone(),
}; };
let resolver = Resolver::new(draft, &scope, schema)?; let resolver = Resolver::new(draft, &scope, schema, self.store.clone())?;
let context = CompilationContext::new(scope, processed_config); let context = CompilationContext::new(scope, processed_config);
let mut validators = compile_validators(schema, &context)?; let mut validators = compile_validators(schema, &context)?;
@ -251,6 +252,13 @@ impl CompilationOptions {
.insert(content_encoding, None); .insert(content_encoding, None);
self self
} }
/// Add a new document to the store. It works as a cache to avoid making additional network
/// calls to remote schemas via the `$ref` keyword.
pub fn with_document(mut self, id: String, document: Value) -> Self {
self.store.insert(id, document);
self
}
} }
impl fmt::Debug for CompilationOptions { impl fmt::Debug for CompilationOptions {
@ -270,6 +278,7 @@ impl fmt::Debug for CompilationOptions {
mod tests { mod tests {
use super::CompilationOptions; use super::CompilationOptions;
use crate::schemas::Draft; use crate::schemas::Draft;
use crate::JSONSchema;
use serde_json::{json, Value}; use serde_json::{json, Value};
use test_case::test_case; use test_case::test_case;
@ -287,4 +296,18 @@ mod tests {
let compiled = options.compile(schema).unwrap(); let compiled = options.compile(schema).unwrap();
compiled.context.config.draft() compiled.context.config.draft()
} }
#[test]
fn test_with_document() {
let schema = json!({"$ref": "http://example.json/schema.json#/rule"});
let compiled = JSONSchema::options()
.with_document(
"http://example.json/schema.json".to_string(),
json!({"rule": {"minLength": 5}}),
)
.compile(&schema)
.unwrap();
assert!(!compiled.is_valid(&json!("foo")));
assert!(compiled.is_valid(&json!("foobar")));
}
} }

View File

@ -5,6 +5,7 @@ use crate::{
error::{CompilationError, ValidationError}, error::{CompilationError, ValidationError},
schemas::{id_of, Draft}, schemas::{id_of, Draft},
}; };
use parking_lot::RwLock;
use serde_json::Value; use serde_json::Value;
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use url::Url; use url::Url;
@ -15,6 +16,7 @@ pub(crate) struct Resolver<'a> {
// canonical_id is composed with the root document id // canonical_id is composed with the root document id
// (if not specified, then `DEFAULT_ROOT_URL` is used for this purpose) // (if not specified, then `DEFAULT_ROOT_URL` is used for this purpose)
schemas: HashMap<String, &'a Value>, schemas: HashMap<String, &'a Value>,
store: RwLock<HashMap<String, Value>>,
} }
impl<'a> Resolver<'a> { impl<'a> Resolver<'a> {
@ -22,6 +24,7 @@ impl<'a> Resolver<'a> {
draft: Draft, draft: Draft,
scope: &Url, scope: &Url,
schema: &'a Value, schema: &'a Value,
store: HashMap<String, Value>,
) -> Result<Resolver<'a>, CompilationError> { ) -> Result<Resolver<'a>, CompilationError> {
let mut schemas = HashMap::new(); let mut schemas = HashMap::new();
// traverse the schema and store all named ones under their canonical ids // traverse the schema and store all named ones under their canonical ids
@ -29,7 +32,10 @@ impl<'a> Resolver<'a> {
schemas.insert(id, schema); schemas.insert(id, schema);
None None
})?; })?;
Ok(Resolver { schemas }) Ok(Resolver {
schemas,
store: RwLock::new(store),
})
} }
/// Load a document for the given `url`. /// Load a document for the given `url`.
@ -40,24 +46,33 @@ impl<'a> Resolver<'a> {
fn resolve_url(&self, url: &Url, schema: &'a Value) -> Result<Cow<'a, Value>, ValidationError> { fn resolve_url(&self, url: &Url, schema: &'a Value) -> Result<Cow<'a, Value>, ValidationError> {
match url.as_str() { match url.as_str() {
DEFAULT_ROOT_URL => Ok(Cow::Borrowed(schema)), DEFAULT_ROOT_URL => Ok(Cow::Borrowed(schema)),
url_str => match self.schemas.get(url_str) { url_str => {
Some(value) => Ok(Cow::Borrowed(value)), if let Some(cached) = self.store.read().get(url_str) {
None => match url.scheme() { return Ok(Cow::Owned(cached.clone()));
"http" | "https" => { }
#[cfg(any(feature = "reqwest", test))]
{ match self.schemas.get(url_str) {
let response = reqwest::blocking::get(url.as_str())?; Some(value) => Ok(Cow::Borrowed(value)),
let document: Value = response.json()?; None => match url.scheme() {
Ok(Cow::Owned(document)) "http" | "https" => {
#[cfg(any(feature = "reqwest", test))]
{
let response = reqwest::blocking::get(url.as_str())?;
let document: Value = response.json()?;
self.store
.write()
.insert(url_str.to_string(), document.clone());
Ok(Cow::Owned(document))
}
#[cfg(not(any(feature = "reqwest", test)))]
panic!("trying to resolve an http(s), but reqwest support has not been included");
} }
#[cfg(not(any(feature = "reqwest", test)))] http_scheme => Err(ValidationError::unknown_reference_scheme(
panic!("trying to resolve an http(s), but reqwest support has not been included"); http_scheme.to_owned(),
} )),
http_scheme => Err(ValidationError::unknown_reference_scheme( },
http_scheme.to_owned(), }
)), }
},
},
} }
} }
pub(crate) fn resolve_fragment( pub(crate) fn resolve_fragment(
@ -216,6 +231,7 @@ mod tests {
Draft::Draft7, Draft::Draft7,
&Url::parse("json-schema:///").unwrap(), &Url::parse("json-schema:///").unwrap(),
schema, schema,
HashMap::new(),
) )
.unwrap() .unwrap()
} }