├── .gitignore ├── src ├── common │ ├── mod.rs │ └── error.rs ├── lib.rs ├── json_schema │ ├── keywords │ │ ├── const_.rs │ │ ├── not.rs │ │ ├── property_names.rs │ │ ├── unique_items.rs │ │ ├── pattern.rs │ │ ├── enum_.rs │ │ ├── multiple_of.rs │ │ ├── required.rs │ │ ├── unevaluated.rs │ │ ├── conditional.rs │ │ ├── contains.rs │ │ ├── maxmin_items.rs │ │ ├── ref_.rs │ │ ├── content_media.rs │ │ ├── maxmin_length.rs │ │ ├── maxmin_properties.rs │ │ ├── maxmin.rs │ │ ├── mod.rs │ │ ├── of.rs │ │ ├── items.rs │ │ ├── type_.rs │ │ └── dependencies.rs │ ├── validators │ │ ├── ref_.rs │ │ ├── pattern.rs │ │ ├── const_.rs │ │ ├── required.rs │ │ ├── not.rs │ │ ├── enum_.rs │ │ ├── property_names.rs │ │ ├── unique_items.rs │ │ ├── multiple_of.rs │ │ ├── maxmin_items.rs │ │ ├── maxmin_length.rs │ │ ├── maxmin_properties.rs │ │ ├── content_media.rs │ │ ├── conditional.rs │ │ ├── dependencies.rs │ │ ├── contains.rs │ │ ├── maxmin.rs │ │ ├── type_.rs │ │ ├── unevaluated.rs │ │ ├── properties.rs │ │ ├── mod.rs │ │ ├── of.rs │ │ └── items.rs │ ├── mod.rs │ ├── helpers.rs │ └── errors.rs └── json_dsl │ ├── validators │ ├── regex.rs │ ├── allowed_values.rs │ ├── rejected_values.rs │ ├── mutually_exclusive.rs │ ├── at_least_one_of.rs │ ├── exactly_one_of.rs │ └── mod.rs │ ├── errors.rs │ ├── mod.rs │ └── param.rs ├── tests ├── tests.rs ├── dsl │ └── helpers.rs └── schema │ └── schema.json ├── .gitmodules ├── Makefile ├── .travis └── update_docs.sh ├── .travis.yml ├── examples └── example01.rs ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── ci.yml └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod error; 3 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_json; 3 | 4 | mod dsl; 5 | mod schema; 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/schema/JSON-Schema-Test-Suite"] 2 | path = tests/schema/JSON-Schema-Test-Suite 3 | url = https://github.com/json-schema/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | doc: 2 | git checkout gh-pages 3 | git reset --hard master 4 | cargo doc 5 | cp -r target/doc doc 6 | git add --all 7 | msg="doc(*): rebuilding docs `date`" 8 | git commit -m "$msg" 9 | git push -f origin gh-pages 10 | git checkout master 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::bool_assert_comparison, clippy::new_without_default)] 2 | 3 | #[macro_use] 4 | extern crate serde_json; 5 | 6 | #[macro_use] 7 | pub mod common; 8 | pub mod json_dsl; 9 | pub mod json_schema; 10 | 11 | pub use crate::common::error::ValicoErrors; 12 | -------------------------------------------------------------------------------- /.travis/update_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit -o nounset 4 | 5 | git clone --branch gh-pages "https://$GH_TOKEN@github.com/${TRAVIS_REPO_SLUG}.git" deploy_docs 6 | cd deploy_docs 7 | 8 | git config user.name "Stanislav Panferov" 9 | git config user.email "fnight.m@gmail.com" 10 | 11 | rm -rf doc 12 | mv ../target/doc . 13 | 14 | git add -A . 15 | git commit -m "rebuild pages at ${TRAVIS_COMMIT}" 16 | git push -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | script: 3 | - cargo build --verbose 4 | - cargo test --verbose -- --nocapture 5 | - cargo doc 6 | after_success: 7 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && ./.travis/update_docs.sh 8 | env: 9 | global: 10 | secure: RIMyNfCB2URVtat4FbM2LNqa+TEXRSTyZiwBdhpETUnJOWpocjA0DyAvSPQKikyOoM4S35PTGK9QTMg4NgK0uPTzkbrhw22hPrC8g1J193bm6PNiQEqI24BPVJcBOxWsVO8EtGsA6PS65hLCeB1pz+1UnMRHjkwaB9nh4l0JLUs= 11 | -------------------------------------------------------------------------------- /src/json_schema/keywords/const_.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::schema; 4 | use super::super::validators; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Const; 8 | impl super::Keyword for Const { 9 | fn compile(&self, def: &Value, _ctx: &schema::WalkContext) -> super::KeywordResult { 10 | let const_ = keyword_key_exists!(def, "const"); 11 | 12 | Ok(Some(Box::new(validators::Const { 13 | item: const_.clone(), 14 | }))) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/example01.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{from_str, to_string_pretty}; 2 | use valico::json_dsl; 3 | 4 | fn main() { 5 | let params = json_dsl::Builder::build(|params| { 6 | params.req_nested("user", json_dsl::array(), |params| { 7 | params.req_typed("name", json_dsl::string()); 8 | params.req_typed("friend_ids", json_dsl::array_of(json_dsl::u64())) 9 | }); 10 | }); 11 | 12 | let mut obj = from_str(r#"{"user": {"name": "Frodo", "friend_ids": ["1223"]}}"#).unwrap(); 13 | 14 | let state = params.process(&mut obj, None); 15 | if state.is_valid() { 16 | println!("Result object is {}", to_string_pretty(&obj).unwrap()); 17 | } else { 18 | panic!("Errors during process: {:?}", state); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/json_schema/validators/ref_.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::scope; 4 | 5 | #[allow(missing_copy_implementations)] 6 | pub struct Ref { 7 | pub url: url::Url, 8 | } 9 | 10 | impl super::Validator for Ref { 11 | fn validate( 12 | &self, 13 | val: &Value, 14 | path: &str, 15 | scope: &scope::Scope, 16 | _: &super::ValidationState, 17 | ) -> super::ValidationState { 18 | let schema = scope.resolve(&self.url); 19 | 20 | if let Some(schema) = schema { 21 | schema.validate_in(val, path) 22 | } else { 23 | let mut state = super::ValidationState::new(); 24 | state.missing.push(self.url.clone()); 25 | state 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/json_schema/validators/pattern.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Pattern { 8 | pub regex: fancy_regex::Regex, 9 | } 10 | 11 | impl super::Validator for Pattern { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | _scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let string = nonstrict_process!(val.as_str(), path); 20 | 21 | if self.regex.is_match(string).unwrap_or(false) { 22 | super::ValidationState::new() 23 | } else { 24 | val_error!(errors::Pattern { 25 | path: path.to_string() 26 | }) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/json_dsl/validators/regex.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | 5 | impl super::Validator for fancy_regex::Regex { 6 | fn validate(&self, val: &Value, path: &str) -> super::ValidatorResult { 7 | let string = strict_process!(val.as_str(), path, "The value must be a string"); 8 | 9 | match self.is_match(string) { 10 | Ok(true) => Ok(()), 11 | Ok(false) => Err(vec![Box::new(errors::WrongValue { 12 | path: path.to_string(), 13 | detail: Some("Value is not matched by required pattern".to_string()), 14 | })]), 15 | Err(e) => Err(vec![Box::new(errors::WrongValue { 16 | path: path.to_string(), 17 | detail: Some(format!("Error evaluating regex '{self}': {e}")), 18 | })]), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/json_schema/validators/const_.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::helpers::is_matching; 5 | use super::super::scope; 6 | 7 | #[allow(missing_copy_implementations)] 8 | pub struct Const { 9 | pub item: Value, 10 | } 11 | 12 | impl super::Validator for Const { 13 | fn validate( 14 | &self, 15 | val: &Value, 16 | path: &str, 17 | _scope: &scope::Scope, 18 | _: &super::ValidationState, 19 | ) -> super::ValidationState { 20 | let mut state = super::ValidationState::new(); 21 | 22 | if !is_matching(&self.item, val) { 23 | state.errors.push(Box::new(errors::Const { 24 | path: path.to_string(), 25 | })) 26 | } else { 27 | state.evaluated.insert(path.to_owned()); 28 | } 29 | 30 | state 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/json_schema/validators/required.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Required { 8 | pub items: Vec, 9 | } 10 | 11 | impl super::Validator for Required { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | _scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let object = nonstrict_process!(val.as_object(), path); 20 | let mut state = super::ValidationState::new(); 21 | 22 | for key in self.items.iter() { 23 | if !object.contains_key(key) { 24 | state.errors.push(Box::new(errors::Required { 25 | path: [path, key.as_ref()].join("/"), 26 | })) 27 | } 28 | } 29 | 30 | state 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/json_schema/validators/not.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Not { 8 | pub url: url::Url, 9 | } 10 | 11 | impl super::Validator for Not { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let schema = scope.resolve(&self.url); 20 | let mut state = super::ValidationState::new(); 21 | 22 | if let Some(schema) = schema { 23 | if schema.validate_in(val, path).is_valid() { 24 | state.errors.push(Box::new(errors::Not { 25 | path: path.to_string(), 26 | })) 27 | } 28 | } else { 29 | state.missing.push(self.url.clone()); 30 | } 31 | 32 | state 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/json_schema/keywords/not.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::helpers; 4 | use super::super::schema; 5 | use super::super::validators; 6 | 7 | #[allow(missing_copy_implementations)] 8 | pub struct Not; 9 | impl super::Keyword for Not { 10 | fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult { 11 | let not = keyword_key_exists!(def, "not"); 12 | 13 | if not.is_object() || not.is_boolean() { 14 | Ok(Some(Box::new(validators::Not { 15 | url: helpers::alter_fragment_path( 16 | ctx.url.clone(), 17 | [ctx.escaped_fragment().as_ref(), "not"].join("/"), 18 | ), 19 | }))) 20 | } else { 21 | Err(schema::SchemaError::Malformed { 22 | path: ctx.fragment.join("/"), 23 | detail: "The value of `not` MUST be an object or a boolean".to_string(), 24 | }) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valico" 3 | version = "4.0.0" 4 | authors = ["Stanislav Panferov "] 5 | description = "JSON Schema validator and JSON coercer" 6 | keywords = ["json", "validator", "json-schema"] 7 | license = "MIT" 8 | documentation = "http://rustless.org/valico/doc/valico/" 9 | homepage = "https://github.com/rustless/valico" 10 | build = "build.rs" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | fancy-regex = "0.11" 15 | url = "2" 16 | jsonway = "2" 17 | uuid = { version = "1", features = ["v4"] } 18 | phf = "0.11" 19 | serde = "1" 20 | serde_json = "1" 21 | chrono = { version = "0.4.23", default-features = false, features = ["clock", "std"] } 22 | addr = "0.15.6" 23 | percent-encoding = "2.2.0" 24 | json-pointer = "0.3.4" 25 | uritemplate-next = "0.2.0" 26 | base64 = "0.21.0" 27 | erased-serde = "0.3" 28 | downcast-rs = "1" 29 | 30 | [build-dependencies] 31 | phf_codegen= "0.11.1" 32 | 33 | [[test]] 34 | name = "tests" 35 | 36 | [features] 37 | js = ["uuid/js"] 38 | -------------------------------------------------------------------------------- /src/json_dsl/validators/allowed_values.rs: -------------------------------------------------------------------------------- 1 | use super::super::errors; 2 | use serde_json::Value; 3 | 4 | pub struct AllowedValues { 5 | allowed_values: Vec, 6 | } 7 | 8 | impl AllowedValues { 9 | pub fn new(values: Vec) -> AllowedValues { 10 | AllowedValues { 11 | allowed_values: values, 12 | } 13 | } 14 | } 15 | 16 | impl super::Validator for AllowedValues { 17 | fn validate(&self, val: &Value, path: &str) -> super::ValidatorResult { 18 | let mut matched = false; 19 | for allowed_value in self.allowed_values.iter() { 20 | if val == allowed_value { 21 | matched = true; 22 | } 23 | } 24 | 25 | if matched { 26 | Ok(()) 27 | } else { 28 | Err(vec![Box::new(errors::WrongValue { 29 | path: path.to_string(), 30 | detail: Some("Value is not among allowed list".to_string()), 31 | })]) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/json_dsl/validators/rejected_values.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | 5 | pub struct RejectedValues { 6 | rejected_values: Vec, 7 | } 8 | 9 | impl RejectedValues { 10 | pub fn new(values: Vec) -> RejectedValues { 11 | RejectedValues { 12 | rejected_values: values, 13 | } 14 | } 15 | } 16 | 17 | impl super::Validator for RejectedValues { 18 | fn validate(&self, val: &Value, path: &str) -> super::ValidatorResult { 19 | let mut matched = false; 20 | for rejected_value in self.rejected_values.iter() { 21 | if val == rejected_value { 22 | matched = true; 23 | } 24 | } 25 | 26 | if matched { 27 | Err(vec![Box::new(errors::WrongValue { 28 | path: path.to_string(), 29 | detail: Some("Value is among reject list".to_string()), 30 | })]) 31 | } else { 32 | Ok(()) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/json_schema/validators/enum_.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::helpers::is_matching; 5 | use super::super::scope; 6 | 7 | #[allow(missing_copy_implementations)] 8 | pub struct Enum { 9 | pub items: Vec, 10 | } 11 | 12 | impl super::Validator for Enum { 13 | fn validate( 14 | &self, 15 | val: &Value, 16 | path: &str, 17 | _scope: &scope::Scope, 18 | _: &super::ValidationState, 19 | ) -> super::ValidationState { 20 | let mut state = super::ValidationState::new(); 21 | 22 | let mut contains = false; 23 | for value in self.items.iter() { 24 | if is_matching(val, value) { 25 | contains = true; 26 | break; 27 | } 28 | } 29 | 30 | if !contains { 31 | state.errors.push(Box::new(errors::Enum { 32 | path: path.to_string(), 33 | })) 34 | } 35 | 36 | state 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/json_schema/validators/property_names.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::scope; 4 | 5 | #[allow(missing_copy_implementations)] 6 | pub struct PropertyNames { 7 | pub url: url::Url, 8 | } 9 | 10 | impl super::Validator for PropertyNames { 11 | fn validate( 12 | &self, 13 | val: &Value, 14 | path: &str, 15 | scope: &scope::Scope, 16 | _: &super::ValidationState, 17 | ) -> super::ValidationState { 18 | let object = nonstrict_process!(val.as_object(), path); 19 | 20 | let schema = scope.resolve(&self.url); 21 | let mut state = super::ValidationState::new(); 22 | 23 | if let Some(schema) = schema { 24 | for key in object.keys() { 25 | let item_path = [path, ["[", key.as_ref(), "]"].join("").as_ref()].join("/"); 26 | state.append(schema.validate_in(&Value::from(key.clone()), item_path.as_ref())); 27 | } 28 | } else { 29 | state.missing.push(self.url.clone()); 30 | } 31 | 32 | state 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/json_schema/keywords/property_names.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::helpers; 4 | use super::super::schema; 5 | use super::super::validators; 6 | 7 | #[allow(missing_copy_implementations)] 8 | pub struct PropertyNames; 9 | impl super::Keyword for PropertyNames { 10 | fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult { 11 | let property_names = keyword_key_exists!(def, "propertyNames"); 12 | 13 | if property_names.is_object() || property_names.is_boolean() { 14 | Ok(Some(Box::new(validators::PropertyNames { 15 | url: helpers::alter_fragment_path( 16 | ctx.url.clone(), 17 | [ctx.escaped_fragment().as_ref(), "propertyNames"].join("/"), 18 | ), 19 | }))) 20 | } else { 21 | Err(schema::SchemaError::Malformed { 22 | path: ctx.fragment.join("/"), 23 | detail: "The value of propertyNames MUST be an object or a boolean".to_string(), 24 | }) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Stanislav Panferov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/json_dsl/validators/mutually_exclusive.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | 5 | pub struct MutuallyExclusive { 6 | params: Vec, 7 | } 8 | 9 | impl MutuallyExclusive { 10 | pub fn new(params: &[&str]) -> MutuallyExclusive { 11 | MutuallyExclusive { 12 | params: params.iter().map(|s| (*s).to_string()).collect(), 13 | } 14 | } 15 | } 16 | 17 | impl super::Validator for MutuallyExclusive { 18 | fn validate(&self, val: &Value, path: &str) -> super::ValidatorResult { 19 | let object = strict_process!(val.as_object(), path, "The value must be an object"); 20 | 21 | let mut matched = vec![]; 22 | for param in self.params.iter() { 23 | if object.contains_key(param) { 24 | matched.push(param.clone()); 25 | } 26 | } 27 | 28 | if matched.len() <= 1 { 29 | Ok(()) 30 | } else { 31 | Err(vec![Box::new(errors::MutuallyExclusive { 32 | path: path.to_string(), 33 | params: matched, 34 | detail: Some("Fields are mutually exclusive".to_string()), 35 | })]) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/json_dsl/validators/at_least_one_of.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | 5 | pub struct AtLeastOneOf { 6 | params: Vec, 7 | } 8 | 9 | impl AtLeastOneOf { 10 | pub fn new(params: &[&str]) -> AtLeastOneOf { 11 | AtLeastOneOf { 12 | params: params.iter().map(|s| (*s).to_string()).collect(), 13 | } 14 | } 15 | } 16 | 17 | impl super::Validator for AtLeastOneOf { 18 | fn validate(&self, val: &Value, path: &str) -> super::ValidatorResult { 19 | let object = strict_process!(val.as_object(), path, "The value must be an object"); 20 | 21 | let mut matched = vec![]; 22 | for param in self.params.iter() { 23 | if object.contains_key(param) { 24 | matched.push(param.clone()); 25 | } 26 | } 27 | 28 | let len = matched.len(); 29 | if len >= 1 { 30 | Ok(()) 31 | } else { 32 | Err(vec![Box::new(errors::AtLeastOne { 33 | path: path.to_string(), 34 | detail: Some("At least one must be present".to_string()), 35 | params: self.params.clone(), 36 | })]) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | submodules: recursive 13 | 14 | - name: Build 15 | run: cargo build --verbose 16 | 17 | - name: Build WebAssembly 18 | run: | 19 | rustup target add wasm32-unknown-unknown 20 | cargo build --verbose --target wasm32-unknown-unknown --features js 21 | 22 | - name: Run tests 23 | run: cargo test --verbose 24 | 25 | docs: 26 | name: docs 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Check docs 32 | env: 33 | RUSTDOCFLAGS: -D warnings 34 | run: cargo doc --no-deps --document-private-items --workspace 35 | 36 | rustfmt: 37 | name: rustfmt 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - name: Check formatting 43 | run: cargo fmt --all -- --check 44 | 45 | clippy: 46 | name: clippy 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | 51 | - name: Check clippy lints 52 | run: cargo clippy --all --tests --workspace -- -D warnings 53 | -------------------------------------------------------------------------------- /src/json_schema/validators/unique_items.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct UniqueItems; 8 | impl super::Validator for UniqueItems { 9 | fn validate( 10 | &self, 11 | val: &Value, 12 | path: &str, 13 | _scope: &scope::Scope, 14 | _: &super::ValidationState, 15 | ) -> super::ValidationState { 16 | let array = nonstrict_process!(val.as_array(), path); 17 | 18 | // TODO we need some quicker algorithm for this 19 | 20 | let mut unique = true; 21 | 'main: for (idx, item_i) in array.iter().enumerate() { 22 | for item_j in array[..idx].iter() { 23 | if item_i == item_j { 24 | unique = false; 25 | break 'main; 26 | } 27 | } 28 | 29 | for item_j in array[(idx + 1)..].iter() { 30 | if item_i == item_j { 31 | unique = false; 32 | break 'main; 33 | } 34 | } 35 | } 36 | 37 | if unique { 38 | super::ValidationState::new() 39 | } else { 40 | val_error!(errors::UniqueItems { 41 | path: path.to_string() 42 | }) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/json_schema/validators/multiple_of.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | use std::cmp::Ordering; 6 | use std::f64; 7 | 8 | #[allow(missing_copy_implementations)] 9 | pub struct MultipleOf { 10 | pub number: f64, 11 | } 12 | 13 | impl super::Validator for MultipleOf { 14 | fn validate( 15 | &self, 16 | val: &Value, 17 | path: &str, 18 | _scope: &scope::Scope, 19 | _: &super::ValidationState, 20 | ) -> super::ValidationState { 21 | let number = nonstrict_process!(val.as_f64(), path); 22 | 23 | let valid = if (number.fract() == 0f64) && (self.number.fract() == 0f64) { 24 | (number % self.number) == 0f64 25 | } else { 26 | let remainder: f64 = (number / self.number) % 1f64; 27 | let remainder_less_than_epsilon = matches!( 28 | remainder.partial_cmp(&f64::EPSILON), 29 | None | Some(Ordering::Less) 30 | ); 31 | let remainder_less_than_one = remainder < (1f64 - f64::EPSILON); 32 | remainder_less_than_epsilon && remainder_less_than_one 33 | }; 34 | 35 | if valid { 36 | super::ValidationState::new() 37 | } else { 38 | val_error!(errors::MultipleOf { 39 | path: path.to_string() 40 | }) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/json_dsl/validators/exactly_one_of.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::cmp::Ordering; 3 | 4 | use super::super::errors; 5 | 6 | pub struct ExactlyOneOf { 7 | params: Vec, 8 | } 9 | 10 | impl ExactlyOneOf { 11 | pub fn new(params: &[&str]) -> ExactlyOneOf { 12 | ExactlyOneOf { 13 | params: params.iter().map(|s| (*s).to_string()).collect(), 14 | } 15 | } 16 | } 17 | 18 | impl super::Validator for ExactlyOneOf { 19 | fn validate(&self, val: &Value, path: &str) -> super::ValidatorResult { 20 | let object = strict_process!(val.as_object(), path, "The value must be an object"); 21 | 22 | let mut matched = vec![]; 23 | for param in self.params.iter() { 24 | if object.contains_key(param) { 25 | matched.push(param.clone()); 26 | } 27 | } 28 | 29 | match matched.len().cmp(&1) { 30 | Ordering::Equal => Ok(()), 31 | Ordering::Greater => Err(vec![Box::new(errors::ExactlyOne { 32 | path: path.to_string(), 33 | detail: Some("Exactly one is allowed at one time".to_string()), 34 | params: matched, 35 | })]), 36 | Ordering::Less => Err(vec![Box::new(errors::ExactlyOne { 37 | path: path.to_string(), 38 | detail: Some("Exactly one must be present".to_string()), 39 | params: self.params.clone(), 40 | })]), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/json_schema/validators/maxmin_items.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct MaxItems { 8 | pub length: u64, 9 | } 10 | 11 | impl super::Validator for MaxItems { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | _scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let array = nonstrict_process!(val.as_array(), path); 20 | 21 | if (array.len() as u64) <= self.length { 22 | super::ValidationState::new() 23 | } else { 24 | val_error!(errors::MaxItems { 25 | path: path.to_string() 26 | }) 27 | } 28 | } 29 | } 30 | 31 | #[allow(missing_copy_implementations)] 32 | pub struct MinItems { 33 | pub length: u64, 34 | } 35 | 36 | impl super::Validator for MinItems { 37 | fn validate( 38 | &self, 39 | val: &Value, 40 | path: &str, 41 | _scope: &scope::Scope, 42 | _: &super::ValidationState, 43 | ) -> super::ValidationState { 44 | let array = nonstrict_process!(val.as_array(), path); 45 | 46 | if (array.len() as u64) >= self.length { 47 | super::ValidationState::new() 48 | } else { 49 | val_error!(errors::MinItems { 50 | path: path.to_string() 51 | }) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/json_schema/validators/maxmin_length.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct MaxLength { 8 | pub length: u64, 9 | } 10 | 11 | impl super::Validator for MaxLength { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | _scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let string = nonstrict_process!(val.as_str(), path); 20 | 21 | if (string.chars().count() as u64) <= self.length { 22 | super::ValidationState::new() 23 | } else { 24 | val_error!(errors::MaxLength { 25 | path: path.to_string() 26 | }) 27 | } 28 | } 29 | } 30 | 31 | #[allow(missing_copy_implementations)] 32 | pub struct MinLength { 33 | pub length: u64, 34 | } 35 | 36 | impl super::Validator for MinLength { 37 | fn validate( 38 | &self, 39 | val: &Value, 40 | path: &str, 41 | _scope: &scope::Scope, 42 | _: &super::ValidationState, 43 | ) -> super::ValidationState { 44 | let string = nonstrict_process!(val.as_str(), path); 45 | 46 | if (string.chars().count() as u64) >= self.length { 47 | super::ValidationState::new() 48 | } else { 49 | val_error!(errors::MinLength { 50 | path: path.to_string() 51 | }) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/json_schema/validators/maxmin_properties.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct MaxProperties { 8 | pub length: u64, 9 | } 10 | 11 | impl super::Validator for MaxProperties { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | _scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let object = nonstrict_process!(val.as_object(), path); 20 | 21 | if (object.len() as u64) <= self.length { 22 | super::ValidationState::new() 23 | } else { 24 | val_error!(errors::MaxProperties { 25 | path: path.to_string() 26 | }) 27 | } 28 | } 29 | } 30 | 31 | #[allow(missing_copy_implementations)] 32 | pub struct MinProperties { 33 | pub length: u64, 34 | } 35 | 36 | impl super::Validator for MinProperties { 37 | fn validate( 38 | &self, 39 | val: &Value, 40 | path: &str, 41 | _scope: &scope::Scope, 42 | _: &super::ValidationState, 43 | ) -> super::ValidationState { 44 | let object = nonstrict_process!(val.as_object(), path); 45 | 46 | if (object.len() as u64) >= self.length { 47 | super::ValidationState::new() 48 | } else { 49 | val_error!(errors::MinProperties { 50 | path: path.to_string() 51 | }) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/json_schema/keywords/unique_items.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::schema; 4 | use super::super::validators; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct UniqueItems; 8 | impl super::Keyword for UniqueItems { 9 | fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult { 10 | let uniq = keyword_key_exists!(def, "uniqueItems"); 11 | 12 | if uniq.is_boolean() { 13 | if uniq.as_bool().unwrap() { 14 | Ok(Some(Box::new(validators::UniqueItems))) 15 | } else { 16 | Ok(None) 17 | } 18 | } else { 19 | Err(schema::SchemaError::Malformed { 20 | path: ctx.fragment.join("/"), 21 | detail: "The value of pattern MUST be boolean".to_string(), 22 | }) 23 | } 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | use super::super::builder; 29 | #[cfg(test)] 30 | use super::super::scope; 31 | #[cfg(test)] 32 | use serde_json::to_value; 33 | 34 | #[test] 35 | fn validate_unique_items() { 36 | let mut scope = scope::Scope::new(); 37 | let schema = scope 38 | .compile_and_return(builder::schema(|s| s.unique_items(true)).into_json(), true) 39 | .ok() 40 | .unwrap(); 41 | 42 | assert_eq!( 43 | schema.validate(&to_value([1, 2, 3, 4]).unwrap()).is_valid(), 44 | true 45 | ); 46 | assert_eq!( 47 | schema.validate(&to_value([1, 1, 3, 4]).unwrap()).is_valid(), 48 | false 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/json_dsl/validators/mod.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::fmt; 3 | 4 | use crate::common::error; 5 | 6 | pub use self::allowed_values::AllowedValues; 7 | pub use self::at_least_one_of::AtLeastOneOf; 8 | pub use self::exactly_one_of::ExactlyOneOf; 9 | pub use self::mutually_exclusive::MutuallyExclusive; 10 | pub use self::rejected_values::RejectedValues; 11 | 12 | macro_rules! strict_process { 13 | ($val:expr, $path:ident, $err:expr) => {{ 14 | let maybe_val = $val; 15 | if maybe_val.is_none() { 16 | return Err(vec![Box::new($crate::json_dsl::errors::WrongType { 17 | path: $path.to_string(), 18 | detail: $err.to_string(), 19 | })]); 20 | } 21 | 22 | maybe_val.unwrap() 23 | }}; 24 | } 25 | 26 | mod allowed_values; 27 | mod at_least_one_of; 28 | mod exactly_one_of; 29 | mod mutually_exclusive; 30 | mod regex; 31 | mod rejected_values; 32 | 33 | pub type ValidatorResult = Result<(), error::ValicoErrors>; 34 | 35 | pub trait Validator { 36 | fn validate(&self, item: &Value, _: &str) -> ValidatorResult; 37 | } 38 | 39 | impl fmt::Debug for dyn Validator + 'static { 40 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | fmt.write_str("[validator]") 42 | } 43 | } 44 | 45 | pub type BoxedValidator = Box; 46 | pub type Validators = Vec; 47 | 48 | impl Validator for T 49 | where 50 | T: Fn(&Value, &str) -> ValidatorResult, 51 | { 52 | fn validate(&self, val: &Value, path: &str) -> ValidatorResult { 53 | self(val, path) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/json_schema/validators/content_media.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | use super::super::keywords::content_media::{ContentEncoding, ContentMediaType}; 7 | 8 | #[allow(missing_copy_implementations)] 9 | pub struct ContentMedia { 10 | pub type_: Option, 11 | pub encoding: Option, 12 | } 13 | 14 | impl super::Validator for ContentMedia { 15 | fn validate( 16 | &self, 17 | val: &Value, 18 | path: &str, 19 | _scope: &scope::Scope, 20 | _: &super::ValidationState, 21 | ) -> super::ValidationState { 22 | let decoded_val = if self.encoding.is_some() && val.is_string() { 23 | let v = self 24 | .encoding 25 | .as_ref() 26 | .unwrap() 27 | .decode_val(val.as_str().unwrap()); 28 | if v.is_err() { 29 | return val_error!(errors::Format { 30 | path: path.to_string(), 31 | detail: v.err().unwrap(), 32 | }); 33 | } 34 | Some(Value::String(v.ok().unwrap())) 35 | } else { 36 | None 37 | }; 38 | 39 | let val_ = if decoded_val.is_some() { 40 | decoded_val.as_ref().unwrap() 41 | } else { 42 | val 43 | }; 44 | 45 | if self.type_.is_some() 46 | && val_.is_string() 47 | && !self 48 | .type_ 49 | .as_ref() 50 | .unwrap() 51 | .validate(val_.as_str().unwrap()) 52 | { 53 | return val_error!(errors::Format { 54 | path: path.to_string(), 55 | detail: "".to_string(), 56 | }); 57 | } 58 | 59 | super::ValidationState::new() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/json_schema/validators/conditional.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use url; 3 | 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Conditional { 8 | pub if_: url::Url, 9 | pub then_: Option, 10 | pub else_: Option, 11 | } 12 | 13 | impl super::Validator for Conditional { 14 | fn validate( 15 | &self, 16 | val: &Value, 17 | path: &str, 18 | scope: &scope::Scope, 19 | _: &super::ValidationState, 20 | ) -> super::ValidationState { 21 | let mut state = super::ValidationState::new(); 22 | 23 | let schema_if_ = scope.resolve(&self.if_); 24 | if let Some(schema_if) = schema_if_ { 25 | // TODO should the validation be strict? 26 | let if_state = schema_if.validate_in(val, path); 27 | if if_state.is_valid() { 28 | state.evaluated.extend(if_state.evaluated); 29 | if self.then_.is_some() { 30 | let schema_then_ = scope.resolve(self.then_.as_ref().unwrap()); 31 | 32 | if let Some(schema_then) = schema_then_ { 33 | state.append(schema_then.validate_in(val, path)); 34 | } else { 35 | state.missing.push(self.then_.as_ref().unwrap().clone()); 36 | } 37 | } 38 | } else if self.else_.is_some() { 39 | let schema_else_ = scope.resolve(self.else_.as_ref().unwrap()); 40 | 41 | if let Some(schema_else) = schema_else_ { 42 | state.append(schema_else.validate_in(val, path)); 43 | } else { 44 | state.missing.push(self.else_.as_ref().unwrap().clone()); 45 | } 46 | } 47 | } else { 48 | state.missing.push(self.if_.clone()); 49 | } 50 | state 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/json_schema/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str; 3 | 4 | #[macro_use] 5 | pub mod helpers; 6 | #[macro_use] 7 | pub mod keywords; 8 | pub mod builder; 9 | pub mod errors; 10 | pub mod schema; 11 | pub mod scope; 12 | pub mod validators; 13 | 14 | pub use self::builder::{schema, Builder}; 15 | pub use self::schema::{Schema, SchemaError}; 16 | pub use self::scope::Scope; 17 | pub use self::validators::ValidationState; 18 | 19 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Ord, PartialOrd)] 20 | /// Represents the schema version to use. 21 | pub enum SchemaVersion { 22 | /// Use draft 7. 23 | Draft7, 24 | /// Use draft 2019-09. 25 | Draft2019_09, 26 | } 27 | 28 | #[derive(Copy, Debug, Clone)] 29 | pub enum PrimitiveType { 30 | Array, 31 | Boolean, 32 | Integer, 33 | Number, 34 | Null, 35 | Object, 36 | String, 37 | } 38 | 39 | impl str::FromStr for PrimitiveType { 40 | type Err = (); 41 | fn from_str(s: &str) -> Result { 42 | match s { 43 | "array" => Ok(PrimitiveType::Array), 44 | "boolean" => Ok(PrimitiveType::Boolean), 45 | "integer" => Ok(PrimitiveType::Integer), 46 | "number" => Ok(PrimitiveType::Number), 47 | "null" => Ok(PrimitiveType::Null), 48 | "object" => Ok(PrimitiveType::Object), 49 | "string" => Ok(PrimitiveType::String), 50 | _ => Err(()), 51 | } 52 | } 53 | } 54 | 55 | impl fmt::Display for PrimitiveType { 56 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | fmt.write_str(match self { 58 | PrimitiveType::Array => "array", 59 | PrimitiveType::Boolean => "boolean", 60 | PrimitiveType::Integer => "integer", 61 | PrimitiveType::Number => "number", 62 | PrimitiveType::Null => "null", 63 | PrimitiveType::Object => "object", 64 | PrimitiveType::String => "string", 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/json_schema/validators/dependencies.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::borrow::Cow; 3 | 4 | use super::super::errors; 5 | use super::super::scope; 6 | 7 | #[derive(Debug)] 8 | pub enum DepKind { 9 | Schema(url::Url), 10 | Property(Vec), 11 | } 12 | 13 | #[allow(missing_copy_implementations)] 14 | pub struct Dependencies { 15 | pub items: Vec<(String, DepKind)>, 16 | } 17 | 18 | impl super::Validator for Dependencies { 19 | fn validate( 20 | &self, 21 | val: &Value, 22 | path: &str, 23 | scope: &scope::Scope, 24 | _: &super::ValidationState, 25 | ) -> super::ValidationState { 26 | let mut state = super::ValidationState::new(); 27 | if !val.is_object() { 28 | return state; 29 | } 30 | let mut object = Cow::Borrowed(val); 31 | 32 | for (key, dep) in self.items.iter() { 33 | if object.get(key).is_some() { 34 | match dep { 35 | DepKind::Schema(ref url) => { 36 | let schema = scope.resolve(url); 37 | if let Some(schema) = schema { 38 | let mut result = schema.validate_in(&object, path); 39 | if result.is_valid() && result.replacement.is_some() { 40 | *object.to_mut() = result.replacement.take().unwrap(); 41 | } 42 | state.append(result); 43 | } else { 44 | state.missing.push(url.clone()) 45 | } 46 | } 47 | DepKind::Property(ref keys) => { 48 | for key in keys.iter() { 49 | if object.get(key).is_none() { 50 | state.errors.push(Box::new(errors::Required { 51 | path: [path, key.as_ref()].join("/"), 52 | })) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | state.set_replacement(object); 61 | state 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/json_dsl/errors.rs: -------------------------------------------------------------------------------- 1 | use super::super::common::error::ValicoError; 2 | use serde::{Serialize, Serializer}; 3 | use serde_json::{to_value, Value}; 4 | 5 | #[derive(Debug)] 6 | #[allow(missing_copy_implementations)] 7 | pub struct Required { 8 | pub path: String, 9 | } 10 | impl_err!(Required, "required", "This field is required"); 11 | impl_serialize!(Required); 12 | 13 | #[derive(Debug)] 14 | #[allow(missing_copy_implementations)] 15 | pub struct WrongType { 16 | pub path: String, 17 | pub detail: String, 18 | } 19 | impl_err!(WrongType, "wrong_type", "Type of the value is wrong", +detail); 20 | impl_serialize!(WrongType); 21 | 22 | #[derive(Debug)] 23 | #[allow(missing_copy_implementations)] 24 | pub struct WrongValue { 25 | pub path: String, 26 | pub detail: Option, 27 | } 28 | impl_err!(WrongValue, "wrong_value", "The value is wrong or mailformed", +opt_detail); 29 | impl_serialize!(WrongValue); 30 | 31 | #[derive(Debug)] 32 | #[allow(missing_copy_implementations)] 33 | pub struct MutuallyExclusive { 34 | pub path: String, 35 | pub detail: Option, 36 | pub params: Vec, 37 | } 38 | impl_err!(MutuallyExclusive, "mutually_exclusive", "The values are mutually exclusive", +opt_detail); 39 | impl_serialize!(MutuallyExclusive, |err: &MutuallyExclusive, 40 | map: &mut ::serde_json::Map< 41 | String, 42 | Value, 43 | >| { 44 | map.insert("params".to_string(), to_value(&err.params).unwrap()); 45 | }); 46 | 47 | #[derive(Debug)] 48 | #[allow(missing_copy_implementations)] 49 | pub struct ExactlyOne { 50 | pub path: String, 51 | pub detail: Option, 52 | pub params: Vec, 53 | } 54 | impl_err!(ExactlyOne, "exactly_one", "Exacly one of the values must be present", +opt_detail); 55 | impl_serialize!( 56 | ExactlyOne, 57 | |err: &ExactlyOne, map: &mut ::serde_json::Map| map 58 | .insert("params".to_string(), to_value(&err.params).unwrap()) 59 | ); 60 | 61 | #[derive(Debug)] 62 | #[allow(missing_copy_implementations)] 63 | pub struct AtLeastOne { 64 | pub path: String, 65 | pub detail: Option, 66 | pub params: Vec, 67 | } 68 | impl_err!(AtLeastOne, "at_least_one", "At least one of the values must be present", +opt_detail); 69 | impl_serialize!( 70 | AtLeastOne, 71 | |err: &AtLeastOne, map: &mut ::serde_json::Map| map 72 | .insert("params".to_string(), to_value(&err.params).unwrap()) 73 | ); 74 | -------------------------------------------------------------------------------- /src/json_schema/keywords/pattern.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::schema; 4 | use super::super::validators; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Pattern; 8 | impl super::Keyword for Pattern { 9 | fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult { 10 | let pattern = keyword_key_exists!(def, "pattern"); 11 | 12 | if pattern.is_string() { 13 | let pattern_val = pattern.as_str().unwrap(); 14 | match fancy_regex::Regex::new(pattern_val) { 15 | Ok(re) => Ok(Some(Box::new(validators::Pattern { regex: re }))), 16 | Err(err) => Err(schema::SchemaError::Malformed { 17 | path: ctx.fragment.join("/"), 18 | detail: format!("The value of pattern MUST be a valid RegExp, but {err:?}"), 19 | }), 20 | } 21 | } else { 22 | Err(schema::SchemaError::Malformed { 23 | path: ctx.fragment.join("/"), 24 | detail: "The value of pattern MUST be a string".to_string(), 25 | }) 26 | } 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | use super::super::builder; 32 | #[cfg(test)] 33 | use super::super::scope; 34 | #[cfg(test)] 35 | use serde_json::to_value; 36 | 37 | #[test] 38 | fn validate() { 39 | let mut scope = scope::Scope::new(); 40 | let schema = scope 41 | .compile_and_return( 42 | builder::schema(|s| { 43 | s.pattern(r"abb.*"); 44 | }) 45 | .into_json(), 46 | true, 47 | ) 48 | .ok() 49 | .unwrap(); 50 | 51 | assert_eq!(schema.validate(&to_value("abb").unwrap()).is_valid(), true); 52 | assert_eq!(schema.validate(&to_value("abbd").unwrap()).is_valid(), true); 53 | assert_eq!(schema.validate(&to_value("abd").unwrap()).is_valid(), false); 54 | } 55 | 56 | #[test] 57 | fn mailformed() { 58 | let mut scope = scope::Scope::new(); 59 | 60 | assert!(scope 61 | .compile_and_return( 62 | jsonway::object(|schema| { 63 | schema.set("pattern", "([]".to_string()); 64 | }) 65 | .unwrap(), 66 | true 67 | ) 68 | .is_err()); 69 | 70 | assert!(scope 71 | .compile_and_return( 72 | jsonway::object(|schema| { 73 | schema.set("pattern", 2); 74 | }) 75 | .unwrap(), 76 | true 77 | ) 78 | .is_err()); 79 | } 80 | -------------------------------------------------------------------------------- /src/json_schema/validators/contains.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::borrow::Cow; 3 | 4 | use super::super::errors; 5 | use super::super::scope; 6 | 7 | #[allow(missing_copy_implementations)] 8 | pub struct Contains { 9 | pub url: url::Url, 10 | pub max_contains: Option, 11 | pub min_contains: Option, 12 | } 13 | 14 | impl super::Validator for Contains { 15 | fn validate( 16 | &self, 17 | val: &Value, 18 | path: &str, 19 | scope: &scope::Scope, 20 | _: &super::ValidationState, 21 | ) -> super::ValidationState { 22 | let mut array = Cow::Borrowed(nonstrict_process!(val.as_array(), path)); 23 | 24 | let schema = scope.resolve(&self.url); 25 | let mut state = super::ValidationState::new(); 26 | 27 | if let Some(schema) = schema { 28 | let mut matched_count = 0; 29 | for idx in 0..array.len() { 30 | let item_path = [path, idx.to_string().as_ref()].join("/"); 31 | let item = &array[idx]; 32 | let mut result = schema.validate_in(item, item_path.as_ref()); 33 | if result.is_valid() { 34 | matched_count += 1; 35 | if let Some(result) = result.replacement.take() { 36 | array.to_mut()[idx] = result; 37 | } 38 | if self.max_contains.is_none() && self.min_contains.is_none() { 39 | break; 40 | } 41 | } 42 | } 43 | 44 | if matched_count == 0 && self.min_contains != Some(0) { 45 | state.errors.push(Box::new(errors::Contains { 46 | path: path.to_string(), 47 | })) 48 | } 49 | 50 | if self 51 | .max_contains 52 | .map(|max| matched_count > max) 53 | .unwrap_or(false) 54 | { 55 | state.errors.push(Box::new(errors::ContainsMinMax { 56 | path: path.to_string(), 57 | })); 58 | } 59 | 60 | if self 61 | .min_contains 62 | .map(|min| matched_count < min) 63 | .unwrap_or(false) 64 | { 65 | state.errors.push(Box::new(errors::ContainsMinMax { 66 | path: path.to_string(), 67 | })); 68 | } 69 | } else { 70 | state.missing.push(self.url.clone()); 71 | } 72 | 73 | state.set_replacement(array); 74 | state 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/dsl/helpers.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::match_wild_err_arm)] 2 | 3 | use serde_json::{from_str, to_string, Value}; 4 | use valico::common::error; 5 | use valico::json_dsl; 6 | use valico::json_schema; 7 | 8 | pub fn test_result( 9 | params: &json_dsl::Builder, 10 | scope: Option<&json_schema::Scope>, 11 | body: &str, 12 | ) -> Value { 13 | let obj = from_str(body); 14 | match obj { 15 | Ok(mut json) => { 16 | let state = params.process(&mut json, scope); 17 | if state.is_strictly_valid() { 18 | json 19 | } else { 20 | panic!("Errors during process: {:?}", state); 21 | } 22 | } 23 | Err(_) => { 24 | panic!("Invalid JSON"); 25 | } 26 | } 27 | } 28 | 29 | pub fn get_errors( 30 | params: &json_dsl::Builder, 31 | scope: Option<&json_schema::Scope>, 32 | body: &str, 33 | ) -> Vec> { 34 | let obj = from_str(body); 35 | match obj { 36 | Ok(mut json) => { 37 | let state = params.process(&mut json, scope); 38 | if state.is_strictly_valid() { 39 | panic!("Success response when we await some errors"); 40 | } else { 41 | state.errors 42 | } 43 | } 44 | Err(_) => { 45 | panic!("Invalid JSON"); 46 | } 47 | } 48 | } 49 | 50 | pub fn assert_str_eq_with_scope( 51 | params: &json_dsl::Builder, 52 | scope: Option<&json_schema::Scope>, 53 | body: &str, 54 | res: &str, 55 | ) { 56 | assert_eq!( 57 | to_string(&test_result(params, scope, body)).unwrap(), 58 | res.to_string() 59 | ); 60 | } 61 | 62 | pub fn assert_error_with_scope( 63 | params: &json_dsl::Builder, 64 | scope: Option<&json_schema::Scope>, 65 | body: &str, 66 | path: &str, 67 | ) { 68 | let errors = get_errors(params, scope, body); 69 | let error = errors.iter().find(|error| { 70 | let err = error.downcast_ref::(); 71 | err.is_some() && err.unwrap().get_path() == path 72 | }); 73 | 74 | assert!( 75 | error.is_some(), 76 | "{}", 77 | "Can't find error in {path}. Errors: {errors:?}" 78 | ) 79 | } 80 | 81 | pub fn assert_str_eq(params: &json_dsl::Builder, body: &str, res: &str) { 82 | assert_str_eq_with_scope(params, None, body, res); 83 | } 84 | 85 | pub fn assert_error( 86 | params: &json_dsl::Builder, 87 | body: &str, 88 | path: &str, 89 | ) { 90 | assert_error_with_scope::(params, None, body, path); 91 | } 92 | -------------------------------------------------------------------------------- /src/json_schema/keywords/enum_.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::schema; 4 | use super::super::validators; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Enum; 8 | impl super::Keyword for Enum { 9 | fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult { 10 | let enum_ = keyword_key_exists!(def, "enum"); 11 | 12 | if enum_.is_array() { 13 | let enum_ = enum_.as_array().unwrap(); 14 | 15 | if enum_.is_empty() { 16 | return Err(schema::SchemaError::Malformed { 17 | path: ctx.fragment.join("/"), 18 | detail: "This array MUST have at least one element.".to_string(), 19 | }); 20 | } 21 | 22 | Ok(Some(Box::new(validators::Enum { 23 | items: enum_.clone(), 24 | }))) 25 | } else { 26 | Err(schema::SchemaError::Malformed { 27 | path: ctx.fragment.join("/"), 28 | detail: "The value of this keyword MUST be an array.".to_string(), 29 | }) 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | use super::super::builder; 36 | #[cfg(test)] 37 | use super::super::scope; 38 | 39 | #[cfg(test)] 40 | use serde_json::to_value; 41 | 42 | #[test] 43 | fn validate() { 44 | let mut scope = scope::Scope::new(); 45 | let schema = scope 46 | .compile_and_return( 47 | builder::schema(|s| { 48 | s.enum_(|items| { 49 | items.push("prop1".to_string()); 50 | items.push("prop2".to_string()); 51 | }) 52 | }) 53 | .into_json(), 54 | true, 55 | ) 56 | .ok() 57 | .unwrap(); 58 | 59 | assert_eq!( 60 | schema.validate(&to_value("prop1").unwrap()).is_valid(), 61 | true 62 | ); 63 | assert_eq!( 64 | schema.validate(&to_value("prop2").unwrap()).is_valid(), 65 | true 66 | ); 67 | assert_eq!( 68 | schema.validate(&to_value("prop3").unwrap()).is_valid(), 69 | false 70 | ); 71 | assert_eq!(schema.validate(&to_value(1).unwrap()).is_valid(), false); 72 | } 73 | 74 | #[test] 75 | fn malformed() { 76 | let mut scope = scope::Scope::new(); 77 | 78 | assert!(scope 79 | .compile_and_return( 80 | jsonway::object(|schema| { 81 | schema.array("enum", |_| {}); 82 | }) 83 | .unwrap(), 84 | true 85 | ) 86 | .is_err()); 87 | 88 | assert!(scope 89 | .compile_and_return( 90 | jsonway::object(|schema| { 91 | schema.object("enum", |_| {}); 92 | }) 93 | .unwrap(), 94 | true 95 | ) 96 | .is_err()); 97 | } 98 | -------------------------------------------------------------------------------- /src/json_schema/keywords/multiple_of.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::schema; 4 | use super::super::validators; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct MultipleOf; 8 | impl super::Keyword for MultipleOf { 9 | fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult { 10 | let multiple_of = keyword_key_exists!(def, "multipleOf"); 11 | 12 | if multiple_of.is_number() { 13 | let multiple_of = multiple_of.as_f64().unwrap(); 14 | if multiple_of > 0f64 { 15 | Ok(Some(Box::new(validators::MultipleOf { 16 | number: multiple_of, 17 | }))) 18 | } else { 19 | Err(schema::SchemaError::Malformed { 20 | path: ctx.fragment.join("/"), 21 | detail: "The value of multipleOf MUST be strictly greater than 0".to_string(), 22 | }) 23 | } 24 | } else { 25 | Err(schema::SchemaError::Malformed { 26 | path: ctx.fragment.join("/"), 27 | detail: "The value of multipleOf MUST be a JSON number".to_string(), 28 | }) 29 | } 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | use super::super::builder; 35 | #[cfg(test)] 36 | use super::super::scope; 37 | #[cfg(test)] 38 | use serde_json::to_value; 39 | 40 | #[test] 41 | fn validate() { 42 | let mut scope = scope::Scope::new(); 43 | let schema = scope 44 | .compile_and_return( 45 | builder::schema(|s| { 46 | s.multiple_of(3.5f64); 47 | }) 48 | .into_json(), 49 | true, 50 | ) 51 | .ok() 52 | .unwrap(); 53 | 54 | assert_eq!(schema.validate(&to_value("").unwrap()).is_valid(), true); 55 | assert_eq!(schema.validate(&to_value(7).unwrap()).is_valid(), true); 56 | assert_eq!(schema.validate(&to_value(6).unwrap()).is_valid(), false); 57 | } 58 | 59 | #[test] 60 | fn malformed() { 61 | let mut scope = scope::Scope::new(); 62 | 63 | assert!(scope 64 | .compile_and_return( 65 | jsonway::object(|schema| { 66 | schema.set("multipleOf", "".to_string()); 67 | }) 68 | .unwrap(), 69 | true 70 | ) 71 | .is_err()); 72 | 73 | assert!(scope 74 | .compile_and_return( 75 | jsonway::object(|schema| { 76 | schema.set("multipleOf", to_value(0).unwrap()); 77 | }) 78 | .unwrap(), 79 | true 80 | ) 81 | .is_err()); 82 | 83 | assert!(scope 84 | .compile_and_return( 85 | jsonway::object(|schema| { 86 | schema.set("multipleOf", to_value(-1).unwrap()); 87 | }) 88 | .unwrap(), 89 | true 90 | ) 91 | .is_err()); 92 | } 93 | -------------------------------------------------------------------------------- /src/json_schema/validators/maxmin.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | #[allow(missing_copy_implementations)] 7 | pub struct Maximum { 8 | pub number: f64, 9 | } 10 | 11 | impl super::Validator for Maximum { 12 | fn validate( 13 | &self, 14 | val: &Value, 15 | path: &str, 16 | _scope: &scope::Scope, 17 | _: &super::ValidationState, 18 | ) -> super::ValidationState { 19 | let number = nonstrict_process!(val.as_f64(), path); 20 | 21 | if number <= self.number { 22 | super::ValidationState::new() 23 | } else { 24 | val_error!(errors::Maximum { 25 | path: path.to_string() 26 | }) 27 | } 28 | } 29 | } 30 | 31 | #[allow(missing_copy_implementations)] 32 | pub struct ExclusiveMaximum { 33 | pub number: f64, 34 | } 35 | 36 | impl super::Validator for ExclusiveMaximum { 37 | fn validate( 38 | &self, 39 | val: &Value, 40 | path: &str, 41 | _scope: &scope::Scope, 42 | _: &super::ValidationState, 43 | ) -> super::ValidationState { 44 | let number = nonstrict_process!(val.as_f64(), path); 45 | 46 | if number < self.number { 47 | super::ValidationState::new() 48 | } else { 49 | val_error!(errors::Maximum { 50 | path: path.to_string() 51 | }) 52 | } 53 | } 54 | } 55 | 56 | #[allow(missing_copy_implementations)] 57 | pub struct Minimum { 58 | pub number: f64, 59 | } 60 | 61 | impl super::Validator for Minimum { 62 | fn validate( 63 | &self, 64 | val: &Value, 65 | path: &str, 66 | _scope: &scope::Scope, 67 | _: &super::ValidationState, 68 | ) -> super::ValidationState { 69 | let number = nonstrict_process!(val.as_f64(), path); 70 | 71 | if number >= self.number { 72 | super::ValidationState::new() 73 | } else { 74 | val_error!(errors::Minimum { 75 | path: path.to_string() 76 | }) 77 | } 78 | } 79 | } 80 | 81 | #[allow(missing_copy_implementations)] 82 | pub struct ExclusiveMinimum { 83 | pub number: f64, 84 | } 85 | 86 | impl super::Validator for ExclusiveMinimum { 87 | fn validate( 88 | &self, 89 | val: &Value, 90 | path: &str, 91 | _scope: &scope::Scope, 92 | _: &super::ValidationState, 93 | ) -> super::ValidationState { 94 | let number = nonstrict_process!(val.as_f64(), path); 95 | 96 | if number > self.number { 97 | super::ValidationState::new() 98 | } else { 99 | val_error!(errors::Minimum { 100 | path: path.to_string() 101 | }) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/json_dsl/mod.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod coercers; 3 | pub mod errors; 4 | mod param; 5 | #[macro_use] 6 | pub mod validators; 7 | 8 | use super::json_schema; 9 | 10 | pub use self::builder::Builder; 11 | pub use self::coercers::{ 12 | ArrayCoercer, BooleanCoercer, Coercer, F64Coercer, I64Coercer, NullCoercer, ObjectCoercer, 13 | PrimitiveType, StringCoercer, U64Coercer, 14 | }; 15 | pub use self::param::Param; 16 | 17 | pub fn i64() -> Box { 18 | Box::new(coercers::I64Coercer) 19 | } 20 | pub fn u64() -> Box { 21 | Box::new(coercers::U64Coercer) 22 | } 23 | pub fn f64() -> Box { 24 | Box::new(coercers::F64Coercer) 25 | } 26 | pub fn string() -> Box { 27 | Box::new(coercers::StringCoercer) 28 | } 29 | pub fn boolean() -> Box { 30 | Box::new(coercers::BooleanCoercer) 31 | } 32 | pub fn null() -> Box { 33 | Box::new(coercers::NullCoercer) 34 | } 35 | pub fn array() -> Box { 36 | Box::new(coercers::ArrayCoercer::new()) 37 | } 38 | pub fn array_of( 39 | coercer: Box, 40 | ) -> Box { 41 | Box::new(coercers::ArrayCoercer::of_type(coercer)) 42 | } 43 | 44 | pub fn encoded_array(separator: &str) -> Box { 45 | Box::new(coercers::ArrayCoercer::encoded(separator.to_string())) 46 | } 47 | 48 | pub fn encoded_array_of( 49 | separator: &str, 50 | coercer: Box, 51 | ) -> Box { 52 | Box::new(coercers::ArrayCoercer::encoded_of( 53 | separator.to_string(), 54 | coercer, 55 | )) 56 | } 57 | 58 | pub fn object() -> Box { 59 | Box::new(coercers::ObjectCoercer) 60 | } 61 | 62 | pub struct ExtendedResult { 63 | value: T, 64 | state: json_schema::ValidationState, 65 | } 66 | 67 | impl ExtendedResult { 68 | pub fn new(value: T) -> ExtendedResult { 69 | ExtendedResult { 70 | value, 71 | state: json_schema::ValidationState::new(), 72 | } 73 | } 74 | 75 | pub fn with_errors(value: T, errors: super::ValicoErrors) -> ExtendedResult { 76 | ExtendedResult { 77 | value, 78 | state: json_schema::ValidationState { 79 | errors, 80 | missing: vec![], 81 | replacement: None, 82 | evaluated: Default::default(), 83 | }, 84 | } 85 | } 86 | 87 | pub fn is_valid(&self) -> bool { 88 | self.state.is_valid() 89 | } 90 | 91 | pub fn append(&mut self, second: json_schema::ValidationState) { 92 | self.state.append(second); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/json_schema/validators/type_.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use super::super::errors; 4 | use super::super::scope; 5 | 6 | use crate::json_schema; 7 | 8 | #[derive(Debug)] 9 | pub enum TypeKind { 10 | Single(json_schema::PrimitiveType), 11 | Set(Vec), 12 | } 13 | 14 | #[allow(missing_copy_implementations)] 15 | pub struct Type { 16 | pub item: TypeKind, 17 | } 18 | 19 | fn check_type(val: &Value, ty: json_schema::PrimitiveType) -> bool { 20 | match ty { 21 | json_schema::PrimitiveType::Array => val.is_array(), 22 | json_schema::PrimitiveType::Boolean => val.is_boolean(), 23 | json_schema::PrimitiveType::Integer => { 24 | let is_true_integer = val.is_u64() || val.is_i64(); 25 | let is_integer_float = val.is_f64() && val.as_f64().unwrap().fract() == 0.0; 26 | is_true_integer || is_integer_float 27 | } 28 | json_schema::PrimitiveType::Number => val.is_number(), 29 | json_schema::PrimitiveType::Null => val.is_null(), 30 | json_schema::PrimitiveType::Object => val.is_object(), 31 | json_schema::PrimitiveType::String => val.is_string(), 32 | } 33 | } 34 | 35 | impl super::Validator for Type { 36 | fn validate( 37 | &self, 38 | val: &Value, 39 | path: &str, 40 | _scope: &scope::Scope, 41 | _: &super::ValidationState, 42 | ) -> super::ValidationState { 43 | let mut state = super::ValidationState::new(); 44 | 45 | match self.item { 46 | TypeKind::Single(t) => { 47 | if !check_type(val, t) { 48 | state.errors.push(Box::new(errors::WrongType { 49 | path: path.to_string(), 50 | detail: format!("The value must be {t}"), 51 | })) 52 | } else { 53 | state.evaluated.insert(path.to_owned()); 54 | } 55 | } 56 | TypeKind::Set(ref set) => { 57 | let mut is_type_match = false; 58 | for ty in set.iter() { 59 | if check_type(val, *ty) { 60 | is_type_match = true; 61 | break; 62 | } 63 | } 64 | 65 | if !is_type_match { 66 | state.errors.push(Box::new(errors::WrongType { 67 | path: path.to_string(), 68 | detail: format!( 69 | "The value must be any of: {}", 70 | set.iter() 71 | .map(|ty| ty.to_string()) 72 | .collect::>() 73 | .join(", ") 74 | ), 75 | })) 76 | } else { 77 | state.evaluated.insert(path.to_owned()); 78 | } 79 | } 80 | } 81 | 82 | state 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Valico 2 | 3 | You want to contribute? You're awesome! When submitting a Pull Request, please have your commits follow these guidelines: 4 | 5 | ## Getting source 6 | 7 | When cloning, ensure to clone recursively to get the submodules. 8 | 9 | git clone --recurse-submodules git@github.com:rustless/valico.git 10 | 11 | If you've already cloned normally, use this to get the submodules: 12 | 13 | git submodule update --init --recursive 14 | 15 | ## Git Commit Guidelines 16 | 17 | These guidelines have been copied from the [AngularJS](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#-git-commit-guidelines) 18 | project. 19 | 20 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 21 | readable messages** that are easy to follow when looking through the **project history**. But also, 22 | we use the git commit messages to **generate the change log**. 23 | 24 | ### Commit Message Format 25 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 26 | format that includes a **type**, a **scope** and a **subject**: 27 | 28 | `` 29 | (): 30 | 31 | 32 | 33 |