Skip to content

Commit

Permalink
H-3426: Implement folding for data type constraints (#5380)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimDiekmann authored Oct 17, 2024
1 parent 958822e commit b0fa599
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ use std::collections::{HashMap, hash_map::Entry};

#[cfg(feature = "postgres")]
use bytes::BytesMut;
use error_stack::{Report, bail};
use itertools::Itertools;
#[cfg(feature = "postgres")]
use postgres_types::{FromSql, IsNull, ToSql, Type};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use thiserror::Error;

use crate::{
Expand Down Expand Up @@ -60,6 +62,22 @@ pub enum ResolveClosedDataTypeError {
AmbiguousMetadata,
#[error("No description was found for the schema.")]
MissingDescription,
#[error("The data type constraints intersected to different types.")]
IntersectedDifferentTypes,
#[error("The value {} does not satisfy the constraint: {}", .0, json!(.1))]
UnsatisfiedConstraint(JsonValue, ValueConstraints),
#[error("The value {0} does not satisfy the constraint")]
UnsatisfiedEnumConstraintVariant(JsonValue),
#[error("No value satisfy the constraint: {}", json!(.0))]
UnsatisfiedEnumConstraint(ValueConstraints),
#[error("Conflicting const values: {0} and {1}")]
ConflictingConstValues(JsonValue, JsonValue),
#[error("Conflicting enum values, no common values found: {} and {}", json!(.0), json!(.1))]
ConflictingEnumValues(Vec<JsonValue>, Vec<JsonValue>),
#[error("The const value is not in the enum values: {} and {}", .0, json!(.1))]
ConflictingConstEnumValue(JsonValue, Vec<JsonValue>),
#[error("The constraint is unsatisfiable: {}", json!(.0))]
UnsatisfiableConstraint(ValueConstraints),
}

impl ClosedDataType {
Expand All @@ -74,7 +92,7 @@ impl ClosedDataType {
pub fn from_resolve_data(
data_type: DataType,
resolve_data: &DataTypeResolveData,
) -> Result<Self, ResolveClosedDataTypeError> {
) -> Result<Self, Report<ResolveClosedDataTypeError>> {
let (description, label) = if data_type.description.is_some() || !data_type.label.is_empty()
{
(data_type.description, data_type.label)
Expand All @@ -91,10 +109,9 @@ impl ClosedDataType {
title_plural: data_type.title_plural.clone(),
description: description.ok_or(ResolveClosedDataTypeError::MissingDescription)?,
label,
all_of: iter::once(&data_type.constraints)
.chain(resolve_data.constraints())
.cloned()
.collect(),
all_of: ValueConstraints::fold_intersections(
iter::once(data_type.constraints).chain(resolve_data.constraints().cloned()),
)?,
r#abstract: data_type.r#abstract,
})
}
Expand Down Expand Up @@ -209,7 +226,9 @@ impl DataTypeResolveData {
///
/// Returns an error if the metadata is ambiguous. This is the case if two schemas at the same
/// inheritance depth specify different metadata.
pub fn find_metadata_schema(&self) -> Result<Option<&DataType>, ResolveClosedDataTypeError> {
pub fn find_metadata_schema(
&self,
) -> Result<Option<&DataType>, Report<ResolveClosedDataTypeError>> {
let mut found_schema_data = None::<(InheritanceDepth, &DataType)>;
for (depth, stored_schema) in self.ordered_schemas() {
if stored_schema.description.is_some() || !stored_schema.label.is_empty() {
Expand All @@ -222,7 +241,7 @@ impl DataTypeResolveData {
if stored_schema.description != found_schema.description
|| stored_schema.label != found_schema.label
{
return Err(ResolveClosedDataTypeError::AmbiguousMetadata);
bail!(ResolveClosedDataTypeError::AmbiguousMetadata);
}
}
cmp::Ordering::Greater => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ use error_stack::{Report, ReportSink, ResultExt};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

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

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

impl Constraint<JsonValue> for AnyOfConstraints {
impl Constraint for AnyOfConstraints {
fn intersection(
self,
other: Self,
) -> Result<(Self, Option<Self>), Report<ResolveClosedDataTypeError>> {
// TODO: Implement folding for anyOf constraints
// see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints
Ok((self, Some(other)))
}
}

impl ConstraintValidator<JsonValue> for AnyOfConstraints {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use thiserror::Error;

use crate::schema::{
ConstraintError, JsonSchemaValueType, NumberSchema, StringSchema, ValueLabel,
data_type::constraint::{Constraint, boolean::BooleanSchema},
data_type::{
closed::ResolveClosedDataTypeError,
constraint::{Constraint, ConstraintValidator, boolean::BooleanSchema},
},
};

#[derive(Debug, Error)]
Expand Down Expand Up @@ -43,7 +46,27 @@ pub enum ArrayItemConstraints {
String(StringSchema),
}

impl Constraint<JsonValue> for ArrayItemConstraints {
impl Constraint for ArrayItemConstraints {
fn intersection(
self,
other: Self,
) -> Result<(Self, Option<Self>), Report<ResolveClosedDataTypeError>> {
match (self, other) {
(Self::Boolean(lhs), Self::Boolean(rhs)) => lhs
.intersection(rhs)
.map(|(lhs, rhs)| (Self::Boolean(lhs), rhs.map(Self::Boolean))),
(Self::Number(lhs), Self::Number(rhs)) => lhs
.intersection(rhs)
.map(|(lhs, rhs)| (Self::Number(lhs), rhs.map(Self::Number))),
(Self::String(lhs), Self::String(rhs)) => lhs
.intersection(rhs)
.map(|(lhs, rhs)| (Self::String(lhs), rhs.map(Self::String))),
_ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes),
}
}
}

impl ConstraintValidator<JsonValue> for ArrayItemConstraints {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
Expand Down Expand Up @@ -104,7 +127,38 @@ pub enum ArraySchema {
Tuple(TupleConstraints),
}

impl Constraint<JsonValue> for ArraySchema {
impl Constraint for ArraySchema {
fn intersection(
self,
other: Self,
) -> Result<(Self, Option<Self>), Report<ResolveClosedDataTypeError>> {
Ok(match (self, other) {
(Self::Constrained(lhs), Self::Constrained(rhs)) => {
let (combined, remainder) = lhs.intersection(rhs)?;
(
Self::Constrained(combined),
remainder.map(Self::Constrained),
)
}
(Self::Tuple(lhs), Self::Constrained(rhs)) => {
// TODO: Implement folding for array constraints
// see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints
(Self::Tuple(lhs), Some(Self::Constrained(rhs)))
}
(Self::Constrained(lhs), Self::Tuple(rhs)) => {
// TODO: Implement folding for array constraints
// see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints
(Self::Constrained(lhs), Some(Self::Tuple(rhs)))
}
(Self::Tuple(lhs), Self::Tuple(rhs)) => {
let (combined, remainder) = lhs.intersection(rhs)?;
(Self::Tuple(combined), remainder.map(Self::Tuple))
}
})
}
}

impl ConstraintValidator<JsonValue> for ArraySchema {
type Error = ConstraintError;

fn is_valid(&self, value: &JsonValue) -> bool {
Expand All @@ -127,7 +181,7 @@ impl Constraint<JsonValue> for ArraySchema {
}
}

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

fn is_valid(&self, value: &[JsonValue]) -> bool {
Expand Down Expand Up @@ -158,7 +212,18 @@ pub struct ArrayConstraints {
pub items: Option<ArrayItemsSchema>,
}

impl Constraint<[JsonValue]> for ArrayConstraints {
impl Constraint for ArrayConstraints {
fn intersection(
self,
other: Self,
) -> Result<(Self, Option<Self>), Report<ResolveClosedDataTypeError>> {
// TODO: Implement folding for array constraints
// see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints
Ok((self, Some(other)))
}
}

impl ConstraintValidator<[JsonValue]> for ArrayConstraints {
type Error = [ArrayValidationError];

fn is_valid(&self, value: &[JsonValue]) -> bool {
Expand Down Expand Up @@ -198,7 +263,18 @@ pub struct TupleConstraints {
pub prefix_items: Vec<ArrayItemsSchema>,
}

impl Constraint<[JsonValue]> for TupleConstraints {
impl Constraint for TupleConstraints {
fn intersection(
self,
other: Self,
) -> Result<(Self, Option<Self>), Report<ResolveClosedDataTypeError>> {
// TODO: Implement folding for array constraints
// see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints
Ok((self, Some(other)))
}
}

impl ConstraintValidator<[JsonValue]> for TupleConstraints {
type Error = [ArrayValidationError];

fn is_valid(&self, value: &[JsonValue]) -> bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use error_stack::{Report, bail};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

use crate::schema::{Constraint, ConstraintError, JsonSchemaValueType};
use crate::schema::{
ConstraintError, ConstraintValidator, JsonSchemaValueType,
data_type::{closed::ResolveClosedDataTypeError, constraint::Constraint},
};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))]
Expand All @@ -16,7 +19,16 @@ pub enum BooleanTypeTag {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BooleanSchema;

impl Constraint<JsonValue> for BooleanSchema {
impl Constraint for BooleanSchema {
fn intersection(
self,
_other: Self,
) -> Result<(Self, Option<Self>), Report<ResolveClosedDataTypeError>> {
Ok((self, None))
}
}

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

fn is_valid(&self, value: &JsonValue) -> bool {
Expand Down
Loading

0 comments on commit b0fa599

Please sign in to comment.