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]
### Added
- Cache for documents loaded via the `$ref` keyword. [#75](https://github.com/Stranger6667/jsonschema-rs/issues/75)
### 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)

View File

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Cache for documents loaded via the `$ref` keyword. [#75](https://github.com/Stranger6667/jsonschema-rs/issues/75)
### 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)

View File

@ -22,6 +22,7 @@ pub struct CompilationOptions {
content_media_type_checks: HashMap<&'static str, Option<ContentMediaTypeCheckType>>,
content_encoding_checks_and_converters:
HashMap<&'static str, Option<(ContentEncodingCheckType, ContentEncodingConverterType)>>,
store: HashMap<String, Value>,
}
impl CompilationOptions {
@ -55,7 +56,7 @@ impl CompilationOptions {
Some(url) => url::Url::parse(url)?,
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 mut validators = compile_validators(schema, &context)?;
@ -251,6 +252,13 @@ impl CompilationOptions {
.insert(content_encoding, None);
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 {
@ -270,6 +278,7 @@ impl fmt::Debug for CompilationOptions {
mod tests {
use super::CompilationOptions;
use crate::schemas::Draft;
use crate::JSONSchema;
use serde_json::{json, Value};
use test_case::test_case;
@ -287,4 +296,18 @@ mod tests {
let compiled = options.compile(schema).unwrap();
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},
schemas::{id_of, Draft},
};
use parking_lot::RwLock;
use serde_json::Value;
use std::{borrow::Cow, collections::HashMap};
use url::Url;
@ -15,6 +16,7 @@ pub(crate) struct Resolver<'a> {
// canonical_id is composed with the root document id
// (if not specified, then `DEFAULT_ROOT_URL` is used for this purpose)
schemas: HashMap<String, &'a Value>,
store: RwLock<HashMap<String, Value>>,
}
impl<'a> Resolver<'a> {
@ -22,6 +24,7 @@ impl<'a> Resolver<'a> {
draft: Draft,
scope: &Url,
schema: &'a Value,
store: HashMap<String, Value>,
) -> Result<Resolver<'a>, CompilationError> {
let mut schemas = HashMap::new();
// traverse the schema and store all named ones under their canonical ids
@ -29,7 +32,10 @@ impl<'a> Resolver<'a> {
schemas.insert(id, schema);
None
})?;
Ok(Resolver { schemas })
Ok(Resolver {
schemas,
store: RwLock::new(store),
})
}
/// 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> {
match url.as_str() {
DEFAULT_ROOT_URL => Ok(Cow::Borrowed(schema)),
url_str => match self.schemas.get(url_str) {
Some(value) => Ok(Cow::Borrowed(value)),
None => match url.scheme() {
"http" | "https" => {
#[cfg(any(feature = "reqwest", test))]
{
let response = reqwest::blocking::get(url.as_str())?;
let document: Value = response.json()?;
Ok(Cow::Owned(document))
url_str => {
if let Some(cached) = self.store.read().get(url_str) {
return Ok(Cow::Owned(cached.clone()));
}
match self.schemas.get(url_str) {
Some(value) => Ok(Cow::Borrowed(value)),
None => match url.scheme() {
"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)))]
panic!("trying to resolve an http(s), but reqwest support has not been included");
}
http_scheme => Err(ValidationError::unknown_reference_scheme(
http_scheme.to_owned(),
)),
},
},
http_scheme => Err(ValidationError::unknown_reference_scheme(
http_scheme.to_owned(),
)),
},
}
}
}
}
pub(crate) fn resolve_fragment(
@ -216,6 +231,7 @@ mod tests {
Draft::Draft7,
&Url::parse("json-schema:///").unwrap(),
schema,
HashMap::new(),
)
.unwrap()
}