Skip to content

Commit

Permalink
H-3425: Provide a simple is_valid function (#5379)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimDiekmann authored Oct 11, 2024
1 parent 47d7f70 commit 6f0b048
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 314 deletions.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use error_stack::{Report, ReportSink, ResultExt};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

use crate::schema::{ConstraintError, SingleValueSchema};
use crate::schema::{ConstraintError, SingleValueSchema, data_type::constraint::Constraint};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))]
Expand All @@ -15,15 +15,16 @@ pub struct AnyOfConstraints {
pub any_of: Vec<SingleValueSchema>,
}

impl AnyOfConstraints {
/// Checks if the provided value is valid against any of the schemas in the `any_of` list.
///
/// # Errors
///
/// - [`AnyOf`] if the value is not valid against any of the schemas.
///
/// [`AnyOf`]: ConstraintError::AnyOf
pub fn validate_value(&self, value: &JsonValue) -> Result<(), Report<ConstraintError>> {
impl Constraint<JsonValue> for AnyOfConstraints {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
self.any_of
.iter()
.any(|schema| schema.constraints.is_valid(value))
}

fn validate_value(&self, value: &JsonValue) -> Result<(), Report<ConstraintError>> {
let mut status = ReportSink::<ConstraintError>::new();
for schema in &self.any_of {
if status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ use thiserror::Error;

use crate::schema::{
ConstraintError, JsonSchemaValueType, NumberSchema, StringSchema, ValueLabel,
data_type::constraint::{
boolean::validate_boolean_value, number::validate_number_value,
string::validate_string_value,
},
data_type::constraint::{Constraint, boolean::BooleanSchema},
};

#[derive(Debug, Error)]
Expand Down Expand Up @@ -41,17 +38,27 @@ pub enum ArrayTypeTag {
#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ArrayItemConstraints {
Boolean,
Boolean(BooleanSchema),
Number(NumberSchema),
String(StringSchema),
}

impl ArrayItemConstraints {
pub fn validate_value(&self, value: &JsonValue) -> Result<(), Report<ConstraintError>> {
impl Constraint<JsonValue> for ArrayItemConstraints {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
match self {
Self::Boolean(schema) => schema.is_valid(value),
Self::Number(schema) => schema.is_valid(value),
Self::String(schema) => schema.is_valid(value),
}
}

fn validate_value(&self, value: &JsonValue) -> Result<(), Report<ConstraintError>> {
match self {
Self::Boolean => validate_boolean_value(value),
Self::Number(schema) => validate_number_value(value, schema),
Self::String(schema) => validate_string_value(value, schema),
Self::Boolean(schema) => schema.validate_value(value),
Self::Number(schema) => schema.validate_value(value),
Self::String(schema) => schema.validate_value(value),
}
}
}
Expand Down Expand Up @@ -97,21 +104,46 @@ pub enum ArraySchema {
Tuple(TupleConstraints),
}

impl ArraySchema {
/// Validates the provided value against the number schema.
///
/// # Errors
///
/// - [`ValueConstraint`] if the value does not match the expected constraints.
///
/// [`ValueConstraint`]: ConstraintError::ValueConstraint
pub fn validate_value(&self, array: &[JsonValue]) -> Result<(), Report<ConstraintError>> {
impl Constraint<JsonValue> for ArraySchema {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
if let JsonValue::Array(array) = value {
self.is_valid(array.as_slice())
} else {
false
}
}

fn validate_value(&self, value: &JsonValue) -> Result<(), Report<ConstraintError>> {
if let JsonValue::Array(array) = value {
self.validate_value(array.as_slice())
} else {
bail!(ConstraintError::InvalidType {
actual: JsonSchemaValueType::from(value),
expected: JsonSchemaValueType::Array,
});
}
}
}

impl Constraint<[JsonValue]> for ArraySchema {
type Error = ConstraintError;

fn is_valid(&self, value: &[JsonValue]) -> bool {
match self {
Self::Constrained(constraints) => constraints.is_valid(value),
Self::Tuple(constraints) => constraints.is_valid(value),
}
}

fn validate_value(&self, value: &[JsonValue]) -> Result<(), Report<ConstraintError>> {
match self {
Self::Constrained(constraints) => constraints
.validate_value(array)
.validate_value(value)
.change_context(ConstraintError::ValueConstraint)?,
Self::Tuple(constraints) => constraints
.validate_value(array)
.validate_value(value)
.change_context(ConstraintError::ValueConstraint)?,
}
Ok(())
Expand All @@ -126,23 +158,21 @@ pub struct ArrayConstraints {
pub items: Option<ArrayItemsSchema>,
}

impl ArrayConstraints {
/// Validates the provided value against the array constraints.
///
/// # Errors
///
/// - [`Items`] if the value does not match the expected item constraints.
///
/// [`Items`]: ArrayValidationError::Items
pub fn validate_value(
&self,
values: &[JsonValue],
) -> Result<(), Report<[ArrayValidationError]>> {
impl Constraint<[JsonValue]> for ArrayConstraints {
type Error = [ArrayValidationError];

fn is_valid(&self, value: &[JsonValue]) -> bool {
self.items.as_ref().map_or(true, |items| {
value.iter().all(|value| items.constraints.is_valid(value))
})
}

fn validate_value(&self, value: &[JsonValue]) -> Result<(), Report<[ArrayValidationError]>> {
let mut status = ReportSink::new();

if let Some(items) = &self.items {
status.attempt(
values
value
.iter()
.map(|value| items.constraints.validate_value(value))
.try_collect_reports::<Vec<()>>()
Expand All @@ -168,25 +198,26 @@ pub struct TupleConstraints {
pub prefix_items: Vec<ArrayItemsSchema>,
}

impl TupleConstraints {
/// Validates the provided value against the tuple constraints.
///
/// # Errors
///
/// - [`MinItems`] if the value has too few items.
/// - [`MaxItems`] if the value has too many items.
/// - [`PrefixItems`] if the value does not match the expected item constraints.
///
/// [`MinItems`]: ArrayValidationError::MinItems
/// [`MaxItems`]: ArrayValidationError::MaxItems
/// [`PrefixItems`]: ArrayValidationError::PrefixItems
pub fn validate_value(
&self,
values: &[JsonValue],
) -> Result<(), Report<[ArrayValidationError]>> {
impl Constraint<[JsonValue]> for TupleConstraints {
type Error = [ArrayValidationError];

fn is_valid(&self, value: &[JsonValue]) -> bool {
let num_values = value.len();
let num_prefix_items = self.prefix_items.len();
if num_values != num_prefix_items {
return false;
}

self.prefix_items
.iter()
.zip(value)
.all(|(schema, value)| schema.constraints.is_valid(value))
}

fn validate_value(&self, value: &[JsonValue]) -> Result<(), Report<[ArrayValidationError]>> {
let mut status = ReportSink::new();

let num_values = values.len();
let num_values = value.len();
let num_prefix_items = self.prefix_items.len();
if num_values != num_prefix_items {
status.capture(if num_values < num_prefix_items {
Expand All @@ -205,7 +236,7 @@ impl TupleConstraints {
status.attempt(
self.prefix_items
.iter()
.zip(values)
.zip(value)
.map(|(schema, value)| schema.constraints.validate_value(value))
.try_collect_reports::<Vec<()>>()
.change_context(ArrayValidationError::PrefixItems),
Expand All @@ -215,20 +246,6 @@ impl TupleConstraints {
}
}

pub(crate) fn validate_array_value(
value: &JsonValue,
schema: &ArraySchema,
) -> Result<(), Report<ConstraintError>> {
if let JsonValue::Array(string) = value {
schema.validate_value(string)
} else {
bail!(ConstraintError::InvalidType {
actual: JsonSchemaValueType::from(value),
expected: JsonSchemaValueType::Array,
});
}
}

#[cfg(test)]
mod tests {
use serde_json::{from_value, json};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@ use error_stack::{Report, bail};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

use crate::schema::{ConstraintError, JsonSchemaValueType};
use crate::schema::{Constraint, ConstraintError, JsonSchemaValueType};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))]
#[serde(rename_all = "camelCase")]
pub enum BooleanTypeTag {
Boolean,
}

pub(crate) fn validate_boolean_value(value: &JsonValue) -> Result<(), Report<ConstraintError>> {
if value.is_boolean() {
Ok(())
} else {
bail!(ConstraintError::InvalidType {
actual: JsonSchemaValueType::from(value),
expected: JsonSchemaValueType::Boolean,
})
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BooleanSchema;

impl Constraint<JsonValue> for BooleanSchema {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
value.is_boolean()
}

fn validate_value(&self, value: &JsonValue) -> Result<(), Report<ConstraintError>> {
if value.is_boolean() {
Ok(())
} else {
bail!(ConstraintError::InvalidType {
actual: JsonSchemaValueType::from(value),
expected: JsonSchemaValueType::Boolean,
});
}
}
}
Loading

0 comments on commit 6f0b048

Please sign in to comment.