diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b3122..e7e224e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Wrong implementation of `is_valid` for `additionalProperties: false` keyword case. [#61](https://github.com/Stranger6667/jsonschema-rs/pull/61) - Possible panic due to type conversion in some numeric validators. [#72](https://github.com/Stranger6667/jsonschema-rs/pull/72) +- Precision loss in `minimum`, `maximum`, `exclusiveMinimum` and `exclusiveMaximum` validators. [#84](https://github.com/Stranger6667/jsonschema-rs/issues/84) ## [0.2.0] - 2020-03-30 diff --git a/Cargo.toml b/Cargo.toml index a74ef83..2bdc564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ chrono = ">= 0.2" rayon = "1" reqwest = { version = ">= 0.10", features = ["blocking", "json"]} parking_lot = ">= 0.1" +num-cmp = ">= 0.1" [dev-dependencies] paste = ">= 0.1" diff --git a/src/keywords/exclusive_maximum.rs b/src/keywords/exclusive_maximum.rs index 6f16a1c..7dafdb6 100644 --- a/src/keywords/exclusive_maximum.rs +++ b/src/keywords/exclusive_maximum.rs @@ -3,55 +3,92 @@ use crate::{ compilation::{CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, }; +use num_cmp::NumCmp; use serde_json::{Map, Value}; -pub struct ExclusiveMaximumValidator { +pub struct ExclusiveMaximumU64Validator { + limit: u64, +} +pub struct ExclusiveMaximumI64Validator { + limit: i64, +} +pub struct ExclusiveMaximumF64Validator { limit: f64, } -impl ExclusiveMaximumValidator { - #[inline] - pub(crate) fn compile(schema: &Value) -> CompilationResult { - if let Value::Number(limit) = schema { - return Ok(Box::new(ExclusiveMaximumValidator { - limit: limit.as_f64().expect("Always valid"), - })); - } - Err(CompilationError::SchemaError) - } -} +macro_rules! validate { + ($validator: ty) => { + impl Validate for $validator { + fn validate<'a>( + &self, + schema: &'a JSONSchema, + instance: &'a Value, + ) -> ErrorIterator<'a> { + if self.is_valid(schema, instance) { + no_error() + } else { + error(ValidationError::exclusive_maximum( + instance, + self.limit as f64, + )) // do not cast + } + } -impl Validate for ExclusiveMaximumValidator { - fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item >= self.limit { - return error(ValidationError::exclusive_maximum(instance, self.limit)); + fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { + if let Value::Number(item) = instance { + return if let Some(item) = item.as_u64() { + NumCmp::num_lt(item, self.limit) + } else if let Some(item) = item.as_i64() { + NumCmp::num_lt(item, self.limit) + } else { + let item = item.as_f64().expect("Always valid"); + NumCmp::num_lt(item, self.limit) + }; + } + true + } + + fn name(&self) -> String { + format!("exclusiveMaximum: {}", self.limit) } } - no_error() - } - - fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item >= self.limit { - return false; - } - } - true - } - - fn name(&self) -> String { - format!("exclusiveMaximum: {}", self.limit) - } + }; } +validate!(ExclusiveMaximumU64Validator); +validate!(ExclusiveMaximumI64Validator); +validate!(ExclusiveMaximumF64Validator); + #[inline] pub fn compile( _: &Map, schema: &Value, _: &CompilationContext, ) -> Option { - Some(ExclusiveMaximumValidator::compile(schema)) + if let Value::Number(limit) = schema { + return if let Some(limit) = limit.as_u64() { + Some(Ok(Box::new(ExclusiveMaximumU64Validator { limit }))) + } else if let Some(limit) = limit.as_i64() { + Some(Ok(Box::new(ExclusiveMaximumI64Validator { limit }))) + } else { + let limit = limit.as_f64().expect("Always valid"); + Some(Ok(Box::new(ExclusiveMaximumF64Validator { limit }))) + }; + } + Some(Err(CompilationError::SchemaError)) +} + +#[cfg(test)] +mod tests { + use crate::tests_util; + use serde_json::{json, Value}; + use test_case::test_case; + + #[test_case(json!({"exclusiveMaximum": 1u64 << 54}), json!(1u64 << 54))] + #[test_case(json!({"exclusiveMaximum": 1i64 << 54}), json!(1i64 << 54))] + #[test_case(json!({"exclusiveMaximum": 1u64 << 54}), json!(1u64 << 54 + 1))] + #[test_case(json!({"exclusiveMaximum": 1i64 << 54}), json!(1i64 << 54 + 1))] + fn is_not_valid(schema: Value, instance: Value) { + tests_util::is_not_valid(schema, instance) + } } diff --git a/src/keywords/exclusive_minimum.rs b/src/keywords/exclusive_minimum.rs index 96b720b..1707fb0 100644 --- a/src/keywords/exclusive_minimum.rs +++ b/src/keywords/exclusive_minimum.rs @@ -3,53 +3,92 @@ use crate::{ compilation::{CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, }; +use num_cmp::NumCmp; use serde_json::{Map, Value}; -pub struct ExclusiveMinimumValidator { +pub struct ExclusiveMinimumU64Validator { + limit: u64, +} +pub struct ExclusiveMinimumI64Validator { + limit: i64, +} +pub struct ExclusiveMinimumF64Validator { limit: f64, } -impl ExclusiveMinimumValidator { - #[inline] - pub(crate) fn compile(schema: &Value) -> CompilationResult { - if let Value::Number(limit) = schema { - let limit = limit.as_f64().expect("Always valid"); - return Ok(Box::new(ExclusiveMinimumValidator { limit })); - } - Err(CompilationError::SchemaError) - } -} +macro_rules! validate { + ($validator: ty) => { + impl Validate for $validator { + fn validate<'a>( + &self, + schema: &'a JSONSchema, + instance: &'a Value, + ) -> ErrorIterator<'a> { + if self.is_valid(schema, instance) { + no_error() + } else { + error(ValidationError::exclusive_minimum( + instance, + self.limit as f64, + )) // do not cast + } + } -impl Validate for ExclusiveMinimumValidator { - fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item <= self.limit { - return error(ValidationError::exclusive_minimum(instance, self.limit)); + fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { + if let Value::Number(item) = instance { + return if let Some(item) = item.as_u64() { + NumCmp::num_gt(item, self.limit) + } else if let Some(item) = item.as_i64() { + NumCmp::num_gt(item, self.limit) + } else { + let item = item.as_f64().expect("Always valid"); + NumCmp::num_gt(item, self.limit) + }; + } + true + } + + fn name(&self) -> String { + format!("exclusiveMinimum: {}", self.limit) } } - no_error() - } - - fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item <= self.limit { - return false; - } - } - true - } - - fn name(&self) -> String { - format!("exclusiveMinimum: {}", self.limit) - } + }; } + +validate!(ExclusiveMinimumU64Validator); +validate!(ExclusiveMinimumI64Validator); +validate!(ExclusiveMinimumF64Validator); + #[inline] pub fn compile( _: &Map, schema: &Value, _: &CompilationContext, ) -> Option { - Some(ExclusiveMinimumValidator::compile(schema)) + if let Value::Number(limit) = schema { + return if let Some(limit) = limit.as_u64() { + Some(Ok(Box::new(ExclusiveMinimumU64Validator { limit }))) + } else if let Some(limit) = limit.as_i64() { + Some(Ok(Box::new(ExclusiveMinimumI64Validator { limit }))) + } else { + let limit = limit.as_f64().expect("Always valid"); + Some(Ok(Box::new(ExclusiveMinimumF64Validator { limit }))) + }; + } + Some(Err(CompilationError::SchemaError)) +} + +#[cfg(test)] +mod tests { + use crate::tests_util; + use serde_json::{json, Value}; + use test_case::test_case; + + #[test_case(json!({"exclusiveMinimum": 1u64 << 54}), json!(1u64 << 54))] + #[test_case(json!({"exclusiveMinimum": 1i64 << 54}), json!(1i64 << 54))] + #[test_case(json!({"exclusiveMinimum": 1u64 << 54}), json!(1u64 << 54 - 1))] + #[test_case(json!({"exclusiveMinimum": 1i64 << 54}), json!(1i64 << 54 - 1))] + fn is_not_valid(schema: Value, instance: Value) { + tests_util::is_not_valid(schema, instance) + } } diff --git a/src/keywords/maximum.rs b/src/keywords/maximum.rs index 4082707..aa16be5 100644 --- a/src/keywords/maximum.rs +++ b/src/keywords/maximum.rs @@ -3,54 +3,87 @@ use crate::{ compilation::{CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, }; +use num_cmp::NumCmp; use serde_json::{Map, Value}; -pub struct MaximumValidator { +pub struct MaximumU64Validator { + limit: u64, +} +pub struct MaximumI64Validator { + limit: i64, +} +pub struct MaximumF64Validator { limit: f64, } -impl MaximumValidator { - #[inline] - pub(crate) fn compile(schema: &Value) -> CompilationResult { - if let Value::Number(limit) = schema { - let limit = limit.as_f64().expect("Always valid"); - return Ok(Box::new(MaximumValidator { limit })); - } - Err(CompilationError::SchemaError) - } -} +macro_rules! validate { + ($validator: ty) => { + impl Validate for $validator { + fn validate<'a>( + &self, + schema: &'a JSONSchema, + instance: &'a Value, + ) -> ErrorIterator<'a> { + if self.is_valid(schema, instance) { + no_error() + } else { + error(ValidationError::maximum(instance, self.limit as f64)) // do not cast + } + } -impl Validate for MaximumValidator { - fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item > self.limit { - return error(ValidationError::maximum(instance, self.limit)); + fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { + if let Value::Number(item) = instance { + return if let Some(item) = item.as_u64() { + !NumCmp::num_gt(item, self.limit) + } else if let Some(item) = item.as_i64() { + !NumCmp::num_gt(item, self.limit) + } else { + let item = item.as_f64().expect("Always valid"); + !NumCmp::num_gt(item, self.limit) + }; + } + true + } + + fn name(&self) -> String { + format!("maximum: {}", self.limit) } } - no_error() - } - - fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item > self.limit { - return false; - } - } - true - } - - fn name(&self) -> String { - format!("maximum: {}", self.limit) - } + }; } +validate!(MaximumU64Validator); +validate!(MaximumI64Validator); +validate!(MaximumF64Validator); + #[inline] pub fn compile( _: &Map, schema: &Value, _: &CompilationContext, ) -> Option { - Some(MaximumValidator::compile(schema)) + if let Value::Number(limit) = schema { + return if let Some(limit) = limit.as_u64() { + Some(Ok(Box::new(MaximumU64Validator { limit }))) + } else if let Some(limit) = limit.as_i64() { + Some(Ok(Box::new(MaximumI64Validator { limit }))) + } else { + let limit = limit.as_f64().expect("Always valid"); + Some(Ok(Box::new(MaximumF64Validator { limit }))) + }; + } + Some(Err(CompilationError::SchemaError)) +} + +#[cfg(test)] +mod tests { + use crate::tests_util; + use serde_json::{json, Value}; + use test_case::test_case; + + #[test_case(json!({"maximum": 1u64 << 54}), json!(1u64 << 54 + 1))] + #[test_case(json!({"maximum": 1i64 << 54}), json!(1i64 << 54 + 1))] + fn is_not_valid(schema: Value, instance: Value) { + tests_util::is_not_valid(schema, instance) + } } diff --git a/src/keywords/minimum.rs b/src/keywords/minimum.rs index af53ae8..722a3b6 100644 --- a/src/keywords/minimum.rs +++ b/src/keywords/minimum.rs @@ -3,54 +3,87 @@ use crate::{ compilation::{CompilationContext, JSONSchema}, error::{error, no_error, CompilationError, ErrorIterator, ValidationError}, }; +use num_cmp::NumCmp; use serde_json::{Map, Value}; -pub struct MinimumValidator { +pub struct MinimumU64Validator { + limit: u64, +} +pub struct MinimumI64Validator { + limit: i64, +} +pub struct MinimumF64Validator { limit: f64, } -impl MinimumValidator { - #[inline] - pub(crate) fn compile(schema: &Value) -> CompilationResult { - if let Value::Number(limit) = schema { - let limit = limit.as_f64().expect("Always valid"); - return Ok(Box::new(MinimumValidator { limit })); - } - Err(CompilationError::SchemaError) - } -} +macro_rules! validate { + ($validator: ty) => { + impl Validate for $validator { + fn validate<'a>( + &self, + schema: &'a JSONSchema, + instance: &'a Value, + ) -> ErrorIterator<'a> { + if self.is_valid(schema, instance) { + no_error() + } else { + error(ValidationError::minimum(instance, self.limit as f64)) // do not cast + } + } -impl Validate for MinimumValidator { - fn validate<'a>(&self, _: &'a JSONSchema, instance: &'a Value) -> ErrorIterator<'a> { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item < self.limit { - return error(ValidationError::minimum(instance, self.limit)); + fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { + if let Value::Number(item) = instance { + return if let Some(item) = item.as_u64() { + !NumCmp::num_lt(item, self.limit) + } else if let Some(item) = item.as_i64() { + !NumCmp::num_lt(item, self.limit) + } else { + let item = item.as_f64().expect("Always valid"); + !NumCmp::num_lt(item, self.limit) + }; + } + true + } + + fn name(&self) -> String { + format!("minimum: {}", self.limit) } } - no_error() - } - - fn is_valid(&self, _: &JSONSchema, instance: &Value) -> bool { - if let Value::Number(item) = instance { - let item = item.as_f64().expect("Always valid"); - if item < self.limit { - return false; - } - } - true - } - - fn name(&self) -> String { - format!("minimum: {}", self.limit) - } + }; } +validate!(MinimumU64Validator); +validate!(MinimumI64Validator); +validate!(MinimumF64Validator); + #[inline] pub fn compile( _: &Map, schema: &Value, _: &CompilationContext, ) -> Option { - Some(MinimumValidator::compile(schema)) + if let Value::Number(limit) = schema { + return if let Some(limit) = limit.as_u64() { + Some(Ok(Box::new(MinimumU64Validator { limit }))) + } else if let Some(limit) = limit.as_i64() { + Some(Ok(Box::new(MinimumI64Validator { limit }))) + } else { + let limit = limit.as_f64().expect("Always valid"); + Some(Ok(Box::new(MinimumF64Validator { limit }))) + }; + } + Some(Err(CompilationError::SchemaError)) +} + +#[cfg(test)] +mod tests { + use crate::tests_util; + use serde_json::{json, Value}; + use test_case::test_case; + + #[test_case(json!({"minimum": 1u64 << 54}), json!(1u64 << 54 - 1))] + #[test_case(json!({"minimum": 1i64 << 54}), json!(1i64 << 54 - 1))] + fn is_not_valid(schema: Value, instance: Value) { + tests_util::is_not_valid(schema, instance) + } } diff --git a/src/lib.rs b/src/lib.rs index b56dd9c..ea9788d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,6 @@ missing_docs, missing_debug_implementations, trivial_casts, - trivial_numeric_casts, unused_extern_crates, unused_import_braces, unused_qualifications, @@ -82,6 +81,21 @@ pub fn is_valid(schema: &Value, instance: &Value) -> bool { compiled.is_valid(instance) } +#[cfg(test)] +mod tests_util { + use super::JSONSchema; + use serde_json::Value; + + pub fn is_not_valid(schema: Value, instance: Value) { + let compiled = JSONSchema::compile(&schema, None).unwrap(); + assert!(!compiled.is_valid(&instance), "{} should not be valid"); + assert!( + compiled.validate(&instance).is_err(), + "{} should not be valid" + ); + } +} + #[cfg(test)] mod tests { use super::*;