From 120de699ea0b7213a6133441fa9808555627fd6e Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Thu, 26 Sep 2024 18:50:33 +0200 Subject: [PATCH] Add more tests --- .../src/schema/data_type/constraint/number.rs | 91 ++++++++++++- .../src/schema/data_type/constraint/string.rs | 127 +++++++++++++++++- 2 files changed, 210 insertions(+), 8 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/number.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/number.rs index 3ff20f7dc6b..63fed6910ef 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/number.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/number.rs @@ -96,6 +96,18 @@ fn float_less(lhs: f64, rhs: f64) -> bool { float_less_eq(lhs, rhs) && !float_eq(lhs, rhs) } +#[expect( + clippy::float_arithmetic, + reason = "Validation requires floating point arithmetic" +)] +fn float_multiple_of(lhs: f64, rhs: f64) -> bool { + if float_eq(rhs, 0.0) { + return false; + } + let quotient = lhs / rhs; + (quotient - quotient.floor()).abs() < f64::EPSILON +} + impl NumberSchema { /// Validates the provided value against the number schema. /// @@ -211,12 +223,7 @@ impl NumberConstraints { } if let Some(expected) = self.multiple_of { - #[expect( - clippy::float_arithmetic, - clippy::modulo_arithmetic, - reason = "Validation requires floating point arithmetic" - )] - if !float_eq(number % expected, 0.0) { + if !float_multiple_of(number, expected) { status.capture(NumberValidationError::MultipleOf { actual: number, expected, @@ -272,6 +279,20 @@ mod tests { assert!(!float_less_eq(1.0, 1.0 - f64::EPSILON)); } + #[test] + fn compare_modulo() { + assert!(float_multiple_of(10.0, 5.0)); + assert!(!float_multiple_of(10.0, 3.0)); + assert!(float_multiple_of(10.0, 2.5)); + assert!(float_multiple_of(1e9, 1e6)); + assert!(float_multiple_of(0.0001, 0.00001)); + assert!(float_multiple_of(-10.0, -5.0)); + assert!(float_multiple_of(-10.0, 5.0)); + assert!(!float_multiple_of(10.0, 0.0)); + assert!(float_multiple_of(0.0, 5.0)); + assert!(!float_multiple_of(0.1, 0.03)); + } + #[test] fn unconstrained() { let number_schema = read_schema(&json!({ @@ -295,7 +316,8 @@ mod tests { "maximum": 10.0, })); - check_constraints(&number_schema, &json!(5)); + check_constraints(&number_schema, &json!(0)); + check_constraints(&number_schema, &json!(10)); check_constraints_error(&number_schema, &json!("2"), [ ConstraintError::InvalidType { actual: JsonSchemaValueType::String, @@ -316,6 +338,61 @@ mod tests { ]); } + #[test] + fn simple_number_exclusive() { + let number_schema = read_schema(&json!({ + "type": "number", + "minimum": 0.0, + "exclusiveMinimum": true, + "maximum": 10.0, + "exclusiveMaximum": true, + })); + + check_constraints(&number_schema, &json!(0.1)); + check_constraints(&number_schema, &json!(0.9)); + check_constraints_error(&number_schema, &json!("2"), [ + ConstraintError::InvalidType { + actual: JsonSchemaValueType::String, + expected: JsonSchemaValueType::Number, + }, + ]); + check_constraints_error(&number_schema, &json!(0), [ + NumberValidationError::ExclusiveMinimum { + actual: 0.0, + expected: 0.0, + }, + ]); + check_constraints_error(&number_schema, &json!(10), [ + NumberValidationError::ExclusiveMaximum { + actual: 10.0, + expected: 10.0, + }, + ]); + } + + #[test] + fn multiple_of() { + let number_schema = read_schema(&json!({ + "type": "number", + "multipleOf": 0.1, + })); + + check_constraints(&number_schema, &json!(0.1)); + check_constraints(&number_schema, &json!(0.9)); + check_constraints_error(&number_schema, &json!("2"), [ + ConstraintError::InvalidType { + actual: JsonSchemaValueType::String, + expected: JsonSchemaValueType::Number, + }, + ]); + check_constraints_error(&number_schema, &json!(0.11), [ + NumberValidationError::MultipleOf { + actual: 0.11, + expected: 0.1, + }, + ]); + } + #[test] fn constant() { let number_schema = read_schema(&json!({ diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/string.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/string.rs index f92caeb7c63..424a5c3c65f 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/string.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/string.rs @@ -210,7 +210,7 @@ pub enum StringTypeTag { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] -#[serde(untagged, rename_all = "camelCase")] +#[serde(untagged, rename_all = "camelCase", deny_unknown_fields)] pub enum StringSchema { Constrained(StringConstraints), Const { @@ -332,3 +332,128 @@ impl StringConstraints { status.finish() } } + +#[cfg(test)] +mod tests { + use serde_json::{from_value, json}; + + use super::*; + use crate::schema::{ + JsonSchemaValueType, + data_type::constraint::{ + ValueConstraints, + tests::{check_constraints, check_constraints_error, read_schema}, + }, + }; + #[test] + fn unconstrained() { + let string_schema = read_schema(&json!({ + "type": "string", + })); + + check_constraints(&string_schema, &json!("NaN")); + check_constraints_error(&string_schema, &json!(10), [ConstraintError::InvalidType { + actual: JsonSchemaValueType::Number, + expected: JsonSchemaValueType::String, + }]); + } + + #[test] + fn simple_string() { + let string_schema = read_schema(&json!({ + "type": "string", + "minLength": 5, + "maxLength": 10, + })); + + check_constraints(&string_schema, &json!("12345")); + check_constraints(&string_schema, &json!("1234567890")); + check_constraints_error(&string_schema, &json!(2), [ConstraintError::InvalidType { + actual: JsonSchemaValueType::Number, + expected: JsonSchemaValueType::String, + }]); + check_constraints_error(&string_schema, &json!("1234"), [ + StringValidationError::MinLength { + actual: "1234".to_owned(), + expected: 5, + }, + ]); + check_constraints_error(&string_schema, &json!("12345678901"), [ + StringValidationError::MaxLength { + actual: "12345678901".to_owned(), + expected: 10, + }, + ]); + } + + #[test] + fn constant() { + let string_schema = read_schema(&json!({ + "type": "string", + "const": "foo", + })); + + check_constraints(&string_schema, &json!("foo")); + check_constraints_error(&string_schema, &json!("bar"), [ + ConstraintError::InvalidConstValue { + actual: json!("bar"), + expected: json!("foo"), + }, + ]); + } + + #[test] + fn enumeration() { + let string_schema = read_schema(&json!({ + "type": "string", + "enum": ["foo"], + })); + + check_constraints(&string_schema, &json!("foo")); + check_constraints_error(&string_schema, &json!("bar"), [ + ConstraintError::InvalidEnumValue { + actual: json!("bar"), + expected: vec![json!("foo")], + }, + ]); + } + + #[test] + fn missing_type() { + from_value::(json!({ + "minLength": 0.0, + })) + .expect_err("Deserialized string schema without type"); + } + + #[test] + fn additional_string_properties() { + from_value::(json!({ + "type": "string", + "additional": false, + })) + .expect_err("Deserialized string schema with additional properties"); + } + + #[test] + fn mixed() { + from_value::(json!({ + "type": "string", + "const": "foo", + "minLength": 5, + })) + .expect_err("Deserialized string schema with mixed properties"); + from_value::(json!({ + "type": "string", + "enum": ["foo", "bar"], + "minLength": 5, + })) + .expect_err("Deserialized string schema with mixed properties"); + from_value::(json!({ + "type": "string", + "const": "bar", + "enum": ["foo", "bar"], + })) + .expect_err("Deserialized string schema with mixed properties"); + } +}