fix: Precision loss in `minimum`, `maximum`, `exclusiveMinimum` and `exclusiveMaximum` validators
This commit is contained in:
parent
093ea199ea
commit
f169c8e527
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<String, Value>,
|
||||
schema: &Value,
|
||||
_: &CompilationContext,
|
||||
) -> Option<CompilationResult> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Value>,
|
||||
schema: &Value,
|
||||
_: &CompilationContext,
|
||||
) -> Option<CompilationResult> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Value>,
|
||||
schema: &Value,
|
||||
_: &CompilationContext,
|
||||
) -> Option<CompilationResult> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Value>,
|
||||
schema: &Value,
|
||||
_: &CompilationContext,
|
||||
) -> Option<CompilationResult> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
16
src/lib.rs
16
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::*;
|
||||
|
|
Loading…
Reference in New Issue