diff --git a/src/schema_type.rs b/src/schema_type.rs index f3252bb..2dcd6a6 100644 --- a/src/schema_type.rs +++ b/src/schema_type.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::fmt::{Debug, Display, Formatter}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; @@ -9,16 +10,35 @@ use crate::traits::validator::Validator; pub mod basic_type; pub mod advanced_type; -#[derive(Debug, Serialize, Deserialize)] +/// Root schema type that encompasses all the different types that can be validated. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged, rename_all = "camelCase")] -#[cfg_attr(test, derive(PartialEq))] pub enum SchemaType { Basic(BasicType), Advanced(AdvancedType), - Array(Vec), + Array((Box,)), + Tuple(Vec), Object(HashMap), } +impl Display for SchemaType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SchemaType::Basic(basic_type) => Display::fmt(basic_type, f), + SchemaType::Advanced(advanced_type) => Display::fmt(advanced_type, f), + SchemaType::Array(item) => { + write!(f, "array filled with '{}'", item.0.to_string()) + } + SchemaType::Tuple(_) => { + todo!() + } + SchemaType::Object(_) => { + todo!() + } + } + } +} + #[derive(Debug, Error, PartialEq)] pub enum SchemaTypeValidationError { #[error("{0}")] @@ -26,6 +46,12 @@ pub enum SchemaTypeValidationError { #[error("{0}")] AdvancedTypeValidationError(#[from] AdvancedTypeValidationError), + + #[error("Expected an object, but got something else")] + NotAnObject, + + #[error("Missing object key: '{0}'")] + MissingObjectKey(String), } impl Validator for SchemaType { @@ -35,12 +61,31 @@ impl Validator for SchemaType { match self { SchemaType::Basic(basic_type) => Ok(basic_type.validate(value)?), SchemaType::Advanced(advanced_type) => Ok(advanced_type.validate(value)?), - SchemaType::Array(_) => { + SchemaType::Array(items) => { todo!() } - SchemaType::Object(_) => { + SchemaType::Tuple(items) => { todo!() } + SchemaType::Object(map) => { + let Value::Object(target_map) = value else { + return Err(SchemaTypeValidationError::NotAnObject); + }; + + for (key, schema) in map { + let Some(value) = target_map.get(key) else { + if let SchemaType::Advanced(AdvancedType::Optional(_)) = schema { + return Ok(()); + }; + + return Err(SchemaTypeValidationError::MissingObjectKey(key.to_string())); + }; + + schema.validate(value)?; + } + + Ok(()) + } } } } @@ -49,8 +94,10 @@ impl Validator for SchemaType { mod tests { use std::collections::HashMap; use serde_json::json; - use crate::schema_type::{AdvancedType, BasicType, SchemaType}; + use crate::schema_type::{AdvancedType, BasicType, SchemaType, SchemaTypeValidationError}; use crate::schema_type::advanced_type::advanced_string_type::AdvancedStringType; + use crate::schema_type::basic_type::BasicTypeValidationError; + use crate::traits::validator::Validator; #[test] fn basic_schema_type_can_be_deserialized() { @@ -93,8 +140,82 @@ mod tests { ])) .unwrap(); - assert_eq!(value, SchemaType::Array(vec![ - SchemaType::Basic(BasicType::String) + assert_eq!(value, SchemaType::Array(( + Box::new(SchemaType::Basic(BasicType::String)), + ))); + } + + #[test] + fn nested_values_in_tuple_are_deserialized_correctly() { + let value: SchemaType = serde_json::from_value(json!([ + "string", + "number" + ])) + .unwrap(); + + assert_eq!(value, SchemaType::Tuple(vec![ + SchemaType::Basic(BasicType::String), + SchemaType::Basic(BasicType::Number), ])); } + + #[test] + fn objects_are_validated_correctly() { + let value: SchemaType = serde_json::from_value(json!({ + "name": "string", + "age": "number", + })) + .unwrap(); + + assert_eq!(value.validate(&json!({ + "name": "Alice", + "age": 42 + })), Ok(())); + + assert_eq!(value.validate(&json!("")), Err(SchemaTypeValidationError::NotAnObject)); + + assert_eq!(value.validate(&json!({ + "age": 42 + })), Err(SchemaTypeValidationError::MissingObjectKey("name".to_string()))); + + assert_eq!(value.validate(&json!({ + "name": 10, + "age": 42 + })), Err( + SchemaTypeValidationError::BasicTypeValidationError( + BasicTypeValidationError::IncorrectType( + BasicType::String, + json!(10) + ) + ) + )); + } + + #[test] + fn optional_type_in_object_is_resolved_correctly() { + let advanced_type: SchemaType = serde_json::from_value(json!({ + "name": { + "$": "optional", + "type": "string" + } + })) + .unwrap(); + + assert_eq!(advanced_type.validate(&json!({})), Ok(())); + } + + #[test] + fn incorrect_optional_type_in_object_returns_an_error() { + let advanced_type: SchemaType = serde_json::from_value(json!({ + "name": { + "$": "optional", + "type": "string" + } + })) + .unwrap(); + + assert!(advanced_type.validate(&json!({ + "name": 10, + })).is_err()); + } } diff --git a/src/schema_type/advanced_type.rs b/src/schema_type/advanced_type.rs index 0b2fdd9..b6ef22c 100644 --- a/src/schema_type/advanced_type.rs +++ b/src/schema_type/advanced_type.rs @@ -1,24 +1,62 @@ pub mod advanced_string_type; +pub mod any_of_type; +pub mod optional_type; +use std::fmt::{Display, Formatter, Pointer}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; use crate::schema_type::advanced_type::advanced_string_type::{AdvancedStringType, StringValidationError}; -use crate::schema_type::SchemaType; +use crate::schema_type::{SchemaType, SchemaTypeValidationError}; +use crate::schema_type::advanced_type::any_of_type::{AnyOfType, AnyOfTypeError}; +use crate::schema_type::advanced_type::optional_type::OptionalType; use crate::traits::validator::Validator; -#[derive(Debug, Serialize, Deserialize)] +/// Types that require more configuration than just checking if the type matches. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "$", rename_all = "camelCase")] -#[cfg_attr(test, derive(PartialEq))] pub enum AdvancedType { String(AdvancedStringType), - Optional(Box), + AnyOf(AnyOfType), + FixedArray, + VariableArray, + Optional(OptionalType), +} + +impl Display for AdvancedType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AdvancedType::String(advanced_string_type) => advanced_string_type.fmt(f), + AdvancedType::AnyOf(advanced_enum_type) => advanced_enum_type.fmt(f), + AdvancedType::FixedArray => { + todo!() + } + AdvancedType::VariableArray => { + todo!() + } + AdvancedType::Optional(_) => { + todo!() + } + } + } } #[derive(Debug, PartialEq, Error)] pub enum AdvancedTypeValidationError { #[error("{0}")] StringValidationError(#[from] StringValidationError), + + #[error("{0}")] + AnyOfError(#[from] AnyOfTypeError), + + #[error("{0}")] + SchemaTypeValidationError(Box), +} + +impl From for AdvancedTypeValidationError { + fn from(value: SchemaTypeValidationError) -> Self { + AdvancedTypeValidationError::SchemaTypeValidationError(Box::new(value)) + } } impl Validator for AdvancedType { @@ -27,11 +65,12 @@ impl Validator for AdvancedType { fn validate(&self, value: &Value) -> Result<(), Self::E> { match self { AdvancedType::String(advanced_string) => Ok(advanced_string.validate(value)?), - AdvancedType::Optional(nested_type) => { - if let Value::Null = value { - return Ok(()) - } - + AdvancedType::Optional(optional_type) => Ok(optional_type.validate(value)?), + AdvancedType::AnyOf(advanced_enum) => Ok(advanced_enum.validate(value)?), + AdvancedType::FixedArray => { + todo!() + } + AdvancedType::VariableArray => { todo!() } } @@ -40,12 +79,60 @@ impl Validator for AdvancedType { #[cfg(test)] mod tests { + use serde_json::json; + use crate::schema_type::advanced_type::advanced_string_type::AdvancedStringType; use crate::schema_type::advanced_type::AdvancedType; + use crate::schema_type::advanced_type::any_of_type::AnyOfType; + use crate::schema_type::advanced_type::optional_type::OptionalType; use crate::schema_type::basic_type::BasicType; use crate::schema_type::SchemaType; #[test] - fn optional_advanced_type_is_resolved_correctly() { - let advanced_type = AdvancedType::Optional(Box::new(SchemaType::Basic(BasicType::String))); + fn advanced_string_type_is_deserialized_correctly() { + let advanced_type: AdvancedType = serde_json::from_value(json!({ + "$": "string", + "requireFilled": false, + "minLength": 10, + "maxLength": 20, + })) + .unwrap(); + + assert_eq!(advanced_type, AdvancedType::String(AdvancedStringType { + require_filled: false, + min_length: Some(10), + max_length: Some(20), + })); + } + + #[test] + fn any_of_type_is_deserialized_correctly() { + let advanced_type: AdvancedType = serde_json::from_value(json!({ + "$": "anyOf", + "variants": [ + "string", + "number", + ], + })) + .unwrap(); + + assert_eq!(advanced_type, AdvancedType::AnyOf(AnyOfType { + variants: vec![ + SchemaType::Basic(BasicType::String), + SchemaType::Basic(BasicType::Number), + ], + })); + } + + #[test] + fn optional_type_is_deserialized_correctly() { + let advanced_type: AdvancedType = serde_json::from_value(json!({ + "$": "optional", + "type": "string" + })) + .unwrap(); + + assert_eq!(advanced_type, AdvancedType::Optional(OptionalType { + kind: Box::new(SchemaType::Basic(BasicType::String)) + })); } } diff --git a/src/schema_type/advanced_type/advanced_string_type.rs b/src/schema_type/advanced_type/advanced_string_type.rs index fe7f850..ee39b02 100644 --- a/src/schema_type/advanced_type/advanced_string_type.rs +++ b/src/schema_type/advanced_type/advanced_string_type.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; @@ -9,9 +10,8 @@ fn default_true() -> bool { true } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(test, derive(PartialEq))] pub struct AdvancedStringType { #[serde(default = "default_true")] pub require_filled: bool, @@ -19,6 +19,16 @@ pub struct AdvancedStringType { pub max_length: Option, } +impl Display for AdvancedStringType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.require_filled { + write!(f, "filled ")?; + } + + write!(f, "string") + } +} + impl Default for AdvancedStringType { fn default() -> Self { Self { diff --git a/src/schema_type/advanced_type/any_of_type.rs b/src/schema_type/advanced_type/any_of_type.rs new file mode 100644 index 0000000..003d4c5 --- /dev/null +++ b/src/schema_type/advanced_type/any_of_type.rs @@ -0,0 +1,108 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use crate::schema_type::SchemaType; +use crate::traits::validator::Validator; + +#[derive(Debug, PartialEq)] +pub struct AnyOfTypeError(pub Vec); + +impl Error for AnyOfTypeError {} + +impl Display for AnyOfTypeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let one_of = self.0.iter() + .map(|schema| schema.to_string()) + .collect::>() + .join(", "); + + write!(f, "No matching variant. Expected one of: {}", one_of) + } +} + +/// Passes if the provided value matches any of the provided type conditions. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AnyOfType { + pub(crate) variants: Vec, +} + +impl Validator for AnyOfType { + type E = AnyOfTypeError; + + fn validate(&self, value: &Value) -> Result<(), Self::E> { + for variant in &self.variants { + if let Ok(()) = variant.validate(value) { + return Ok(()) + } + } + + Err(AnyOfTypeError(self.variants.to_vec())) + } +} + +impl From<[SchemaType; U]> for AnyOfType { + fn from(value: [SchemaType; U]) -> Self { + AnyOfType { + variants: value.into_iter() + .collect() + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use crate::schema_type::advanced_type::any_of_type::{AnyOfType, AnyOfTypeError}; + use crate::schema_type::advanced_type::AdvancedType; + use crate::schema_type::basic_type::BasicType; + use crate::schema_type::SchemaType; + use crate::traits::validator::Validator; + + #[test] + fn single_any_of_variant_is_checked_correctly() { + let enum_type = AnyOfType::from([ + SchemaType::Basic(BasicType::String), + ]); + + assert_eq!(enum_type.validate(&json!("")), Ok(())); + assert_eq!(enum_type.validate(&json!(10)), Err(AnyOfTypeError(vec![ + SchemaType::Basic(BasicType::String), + ]))); + assert_eq!(enum_type.validate(&json!(null)), Err(AnyOfTypeError(vec![ + SchemaType::Basic(BasicType::String), + ]))); + } + + #[test] + fn multiple_any_of_variant_is_checked_correctly() { + let enum_type = AnyOfType::from([ + SchemaType::Basic(BasicType::String), + SchemaType::Basic(BasicType::Number), + ]); + + assert_eq!(enum_type.validate(&json!("")), Ok(())); + assert_eq!(enum_type.validate(&json!(10)), Ok(())); + assert_eq!(enum_type.validate(&json!(null)), Err(AnyOfTypeError(vec![ + SchemaType::Basic(BasicType::String), + SchemaType::Basic(BasicType::Number), + ]))); + } + + #[test] + fn nested_any_of_are_checked_correctly() { + let enum_type = AnyOfType::from([ + SchemaType::Advanced(AdvancedType::AnyOf(AnyOfType::from([ + SchemaType::Basic(BasicType::String), + ]))), + SchemaType::Advanced(AdvancedType::AnyOf(AnyOfType::from([ + SchemaType::Basic(BasicType::Number), + ]))), + ]); + + assert_eq!(enum_type.validate(&json!("")), Ok(())); + assert_eq!(enum_type.validate(&json!(10)), Ok(())); + assert!(enum_type.validate(&json!(null)).is_err()); + } +} diff --git a/src/schema_type/advanced_type/optional_type.rs b/src/schema_type/advanced_type/optional_type.rs new file mode 100644 index 0000000..3f6bbc6 --- /dev/null +++ b/src/schema_type/advanced_type/optional_type.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use crate::schema_type::{SchemaType, SchemaTypeValidationError}; +use crate::traits::validator::Validator; +use crate::schema_type::advanced_type::AnyOfType; + +/// Optional type that can either the the requested type or null. Parent types like objects should +/// also check for this specific for example to check whether to return an error if the required +/// key is missing. If you want to explicitly indicate either null or a type, use [AnyOfType] +/// instead. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct OptionalType { + #[serde(rename = "type")] + pub(crate) kind: Box, +} + +impl Validator for OptionalType { + type E = SchemaTypeValidationError; + + fn validate(&self, value: &Value) -> Result<(), Self::E> { + if let Value::Null = value { + return Ok(()) + } + + self.kind.validate(&value)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use crate::schema_type::advanced_type::optional_type::OptionalType; + use crate::schema_type::basic_type::{BasicType, BasicTypeValidationError}; + use crate::schema_type::{SchemaType, SchemaTypeValidationError}; + use crate::traits::validator::Validator; + + #[test] + fn optional_type_is_resolved_correctly() { + let optional_type = OptionalType { + kind: Box::new(SchemaType::Basic(BasicType::String)), + }; + + assert_eq!(optional_type.validate(&json!("")), Ok(())); + assert_eq!(optional_type.validate(&json!(null)), Ok(())); + assert_eq!(optional_type.validate(&json!(10)), Err(SchemaTypeValidationError::BasicTypeValidationError(BasicTypeValidationError::IncorrectType(BasicType::String, json!(10))))); + } +} diff --git a/src/schema_type/basic_type.rs b/src/schema_type/basic_type.rs index d733ef2..dae5c41 100644 --- a/src/schema_type/basic_type.rs +++ b/src/schema_type/basic_type.rs @@ -1,13 +1,13 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; use serde::{Deserialize, Serialize}; -use serde_email::{Email, is_valid_email}; +use serde_email::{is_valid_email}; use serde_json::Value; use thiserror::Error; -use uuid::{Error, Uuid}; +use uuid::{Uuid}; use crate::traits::validator::Validator; -#[derive(Debug, PartialEq, Error)] +#[derive(Debug, Clone, PartialEq, Error)] pub enum BasicTypeValidationError { #[error("Incorrect type provided. Expected '{0}' but got '{1}'")] IncorrectType(BasicType, Value), @@ -19,6 +19,8 @@ pub enum BasicTypeValidationError { IncorrectEmail(String), } +/// Basic types don't have any additional configuration and only check the variant of [Value] and +/// might do a bit of extra validation in the case of [BasicType::Uuid] and [BasicType::Email]. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum BasicType {