From a5d268ad0a2e1a9cb6129442f72a824d9d012f86 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Fri, 11 Oct 2024 18:23:29 +0200 Subject: [PATCH 1/7] Implement data type constraint folding infrastructure --- .../rust/src/schema/data_type/closed.rs | 33 ++++++-- .../src/schema/data_type/constraint/any_of.rs | 18 +++- .../src/schema/data_type/constraint/array.rs | 63 ++++++++++++-- .../schema/data_type/constraint/boolean.rs | 16 +++- .../src/schema/data_type/constraint/mod.rs | 82 +++++++++++++++++-- .../src/schema/data_type/constraint/null.rs | 16 +++- .../src/schema/data_type/constraint/number.rs | 50 ++++++++--- .../src/schema/data_type/constraint/object.rs | 30 ++++++- .../src/schema/data_type/constraint/string.rs | 37 ++++++++- .../rust/src/schema/data_type/mod.rs | 10 +-- .../type-system/rust/src/schema/mod.rs | 2 +- .../@local/hash-validation/src/entity_type.rs | 6 +- 12 files changed, 310 insertions(+), 53 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/closed.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/closed.rs index 24a34eb835a..6a9065dee27 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/closed.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/closed.rs @@ -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::{ @@ -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, Vec), + #[error("The const value is not in the enum values: {} and {}", .0, json!(.1))] + ConflictingConstEnumValue(JsonValue, Vec), + #[error("The constraint is unsatisfiable: {}", json!(.0))] + UnsatisfiableConstraint(ValueConstraints), } impl ClosedDataType { @@ -74,7 +92,7 @@ impl ClosedDataType { pub fn from_resolve_data( data_type: DataType, resolve_data: &DataTypeResolveData, - ) -> Result { + ) -> Result> { let (description, label) = if data_type.description.is_some() || !data_type.label.is_empty() { (data_type.description, data_type.label) @@ -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, }) } @@ -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, ResolveClosedDataTypeError> { + pub fn find_metadata_schema( + &self, + ) -> Result, Report> { 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() { @@ -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 => { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs index 56607f0027a..98408024e01 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs @@ -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))] @@ -15,7 +21,15 @@ pub struct AnyOfConstraints { pub any_of: Vec, } -impl Constraint for AnyOfConstraints { +impl Constraint for AnyOfConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + // TODO: Implement folding for anyOf constraints + // see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints + Ok(Some(other)) + } +} + +impl ConstraintValidator for AnyOfConstraints { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs index 99790cf5c79..58b01e9b4e5 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs @@ -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)] @@ -43,7 +46,18 @@ pub enum ArrayItemConstraints { String(StringSchema), } -impl Constraint for ArrayItemConstraints { +impl Constraint for ArrayItemConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + Ok(match (self, other) { + (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs.combine(rhs)?.map(Self::Boolean), + (Self::Number(lhs), Self::Number(rhs)) => lhs.combine(rhs)?.map(Self::Number), + (Self::String(lhs), Self::String(rhs)) => lhs.combine(rhs)?.map(Self::String), + _ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes), + }) + } +} + +impl ConstraintValidator for ArrayItemConstraints { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -104,7 +118,28 @@ pub enum ArraySchema { Tuple(TupleConstraints), } -impl Constraint for ArraySchema { +impl Constraint for ArraySchema { + fn combine(&mut self, other: Self) -> Result, Report> { + Ok(match (self, other) { + (Self::Constrained(lhs), Self::Constrained(rhs)) => { + lhs.combine(rhs)?.map(Self::Constrained) + } + (Self::Tuple(_), Self::Constrained(typed)) => { + // TODO: Implement folding for array constraints + // see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints + Some(Self::Constrained(typed)) + } + (Self::Constrained(_), Self::Tuple(any_of)) => { + // TODO: Implement folding for array constraints + // see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints + Some(Self::Tuple(any_of)) + } + (Self::Tuple(lhs), Self::Tuple(rhs)) => lhs.combine(rhs)?.map(Self::Tuple), + }) + } +} + +impl ConstraintValidator for ArraySchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -127,7 +162,7 @@ impl Constraint for ArraySchema { } } -impl Constraint<[JsonValue]> for ArraySchema { +impl ConstraintValidator<[JsonValue]> for ArraySchema { type Error = ConstraintError; fn is_valid(&self, value: &[JsonValue]) -> bool { @@ -158,7 +193,15 @@ pub struct ArrayConstraints { pub items: Option, } -impl Constraint<[JsonValue]> for ArrayConstraints { +impl Constraint for ArrayConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + // TODO: Implement folding for array constraints + // see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints + Ok(Some(other)) + } +} + +impl ConstraintValidator<[JsonValue]> for ArrayConstraints { type Error = [ArrayValidationError]; fn is_valid(&self, value: &[JsonValue]) -> bool { @@ -198,7 +241,15 @@ pub struct TupleConstraints { pub prefix_items: Vec, } -impl Constraint<[JsonValue]> for TupleConstraints { +impl Constraint for TupleConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + // TODO: Implement folding for array constraints + // see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints + Ok(Some(other)) + } +} + +impl ConstraintValidator<[JsonValue]> for TupleConstraints { type Error = [ArrayValidationError]; fn is_valid(&self, value: &[JsonValue]) -> bool { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs index b1fab79abfb..1e01eec44d7 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs @@ -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))] @@ -16,7 +19,16 @@ pub enum BooleanTypeTag { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct BooleanSchema; -impl Constraint for BooleanSchema { +impl Constraint for BooleanSchema { + fn combine( + &mut self, + _other: Self, + ) -> Result, Report> { + Ok(None) + } +} + +impl ConstraintValidator for BooleanSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index 50bab635ae7..a115d508362 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -7,7 +7,7 @@ mod number; mod object; mod string; -use error_stack::Report; +use error_stack::{Report, bail}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; @@ -24,9 +24,21 @@ pub use self::{ StringValidationError, }, }; -use crate::schema::ValueLabel; +use crate::schema::{ValueLabel, data_type::closed::ResolveClosedDataTypeError}; -pub trait Constraint: Sized { +pub trait Constraint: Sized { + /// Combines the current constraints with the provided one. + /// + /// If the constraints cannot be combined completely, the method will return the remaining + /// constraints that could not be combined, otherwise `None`. + /// + /// # Errors + /// + /// If the constraints exclude each other, an error is returned. + fn combine(&mut self, other: Self) -> Result, Report>; +} + +pub trait ConstraintValidator: Constraint { type Error: ?Sized; /// Checks if the provided value is valid against this constraint. @@ -61,7 +73,51 @@ pub enum ValueConstraints { AnyOf(AnyOfConstraints), } -impl Constraint for ValueConstraints { +impl ValueConstraints { + pub fn fold_intersections( + schemas: impl IntoIterator, + ) -> Result, Report> { + schemas + .into_iter() + .try_fold(vec![], |mut acc, constraints| { + if acc.is_empty() { + acc.push(constraints); + return Ok(acc); + } + + let mut remainder = Some(constraints); + for current in &mut acc { + let Some(new) = remainder.take() else { break }; + remainder = current.combine(new)?; + } + if let Some(remainder) = remainder { + acc.push(remainder); + } + Ok::<_, Report>(acc) + }) + } +} + +impl Constraint for ValueConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + Ok(match (self, other) { + (Self::Typed(lhs), Self::Typed(rhs)) => lhs.combine(rhs)?.map(Self::Typed), + (Self::AnyOf(_), Self::Typed(typed)) => { + // TODO: Implement folding for anyOf constraints + // see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints + Some(Self::Typed(typed)) + } + (Self::Typed(_), Self::AnyOf(any_of)) => { + // TODO: Implement folding for anyOf constraints + // see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints + Some(Self::AnyOf(any_of)) + } + (Self::AnyOf(lhs), Self::AnyOf(rhs)) => lhs.combine(rhs)?.map(Self::AnyOf), + }) + } +} + +impl ConstraintValidator for ValueConstraints { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -91,7 +147,21 @@ pub enum SingleValueConstraints { Object(ObjectSchema), } -impl Constraint for SingleValueConstraints { +impl Constraint for SingleValueConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + Ok(match (self, other) { + (Self::Null(lhs), Self::Null(rhs)) => lhs.combine(rhs)?.map(Self::Null), + (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs.combine(rhs)?.map(Self::Boolean), + (Self::Number(lhs), Self::Number(rhs)) => lhs.combine(rhs)?.map(Self::Number), + (Self::String(lhs), Self::String(rhs)) => lhs.combine(rhs)?.map(Self::String), + (Self::Array(lhs), Self::Array(rhs)) => lhs.combine(rhs)?.map(Self::Array), + (Self::Object(lhs), Self::Object(rhs)) => lhs.combine(rhs)?.map(Self::Object), + _ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes), + }) + } +} + +impl ConstraintValidator for SingleValueConstraints { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -158,7 +228,7 @@ mod tests { use error_stack::Frame; use serde_json::Value as JsonValue; - use crate::schema::data_type::constraint::{Constraint, ValueConstraints}; + use crate::schema::data_type::constraint::{ConstraintValidator, ValueConstraints}; pub(crate) fn read_schema(schema: &JsonValue) -> ValueConstraints { let parsed = serde_json::from_value(schema.clone()).expect("Failed to parse schema"); diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs index 61f3e665009..5f1f88a78f0 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs @@ -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))] @@ -16,7 +19,16 @@ pub enum NullTypeTag { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct NullSchema; -impl Constraint for NullSchema { +impl Constraint for NullSchema { + fn combine( + &mut self, + _other: Self, + ) -> Result, Report> { + Ok(None) + } +} + +impl ConstraintValidator for NullSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { 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 b51fd7d799b..c0c5d90f604 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 @@ -1,9 +1,15 @@ -use error_stack::{Report, ReportSink, ResultExt, bail}; +use error_stack::{Report, ReportSink, ResultExt, bail, ensure}; use serde::{Deserialize, Serialize}; use serde_json::{Number as JsonNumber, Value as JsonValue, json}; use thiserror::Error; -use crate::schema::{ConstraintError, JsonSchemaValueType, data_type::constraint::Constraint}; +use crate::schema::{ + ConstraintError, JsonSchemaValueType, + data_type::{ + closed::ResolveClosedDataTypeError, + constraint::{Constraint, ConstraintValidator}, + }, +}; #[expect( clippy::trivially_copy_pass_by_ref, @@ -108,7 +114,20 @@ fn float_multiple_of(lhs: f64, rhs: f64) -> bool { (quotient - quotient.round()).abs() < f64::EPSILON } -impl Constraint for NumberSchema { +impl Constraint for NumberSchema { + fn combine(&mut self, other: Self) -> Result, Report> { + match (&mut *self, other) { + (Self::Constrained(lhs), Self::Constrained(rhs)) => { + Ok(lhs.combine(rhs)?.map(Self::Constrained)) + } + // TODO: Implement folding for number constraints + // see https://linear.app/hash/issue/H-3427/implement-folding-for-number-constraints + (_, rhs) => Ok(Some(rhs)), + } + } +} + +impl ConstraintValidator for NumberSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -131,7 +150,7 @@ impl Constraint for NumberSchema { } } -impl Constraint for NumberSchema { +impl ConstraintValidator for NumberSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonNumber) -> bool { @@ -153,7 +172,7 @@ impl Constraint for NumberSchema { } } -impl Constraint for NumberSchema { +impl ConstraintValidator for NumberSchema { type Error = ConstraintError; fn is_valid(&self, value: &f64) -> bool { @@ -178,19 +197,20 @@ impl Constraint for NumberSchema { } } Self::Enum { r#enum } => { - if !r#enum.iter().any(|expected| float_eq(*value, *expected)) { - bail!(ConstraintError::InvalidEnumValue { + ensure!( + r#enum.iter().any(|expected| float_eq(*value, *expected)), + ConstraintError::InvalidEnumValue { actual: json!(*value), expected: r#enum.iter().map(|value| json!(*value)).collect(), - }); - } + } + ); } } Ok(()) } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct NumberConstraints { @@ -206,7 +226,15 @@ pub struct NumberConstraints { pub multiple_of: Option, } -impl Constraint for NumberConstraints { +impl Constraint for NumberConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + // TODO: Implement folding for number constraints + // see https://linear.app/hash/issue/H-3427/implement-folding-for-number-constraints + Ok(Some(other)) + } +} + +impl ConstraintValidator for NumberConstraints { type Error = [NumberValidationError]; fn is_valid(&self, value: &f64) -> bool { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs index c81a9d8a760..189ca47603d 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs @@ -5,7 +5,10 @@ use thiserror::Error; type JsonObject = serde_json::Map; -use crate::schema::{Constraint, ConstraintError, JsonSchemaValueType}; +use crate::schema::{ + ConstraintError, ConstraintValidator, JsonSchemaValueType, + data_type::{closed::ResolveClosedDataTypeError, constraint::Constraint}, +}; #[derive(Debug, Error)] pub enum ObjectValidationError {} @@ -24,7 +27,17 @@ pub enum ObjectSchema { Constrained(ObjectConstraints), } -impl Constraint for ObjectSchema { +impl Constraint for ObjectSchema { + fn combine(&mut self, other: Self) -> Result, Report> { + match (&mut *self, other) { + (Self::Constrained(lhs), Self::Constrained(rhs)) => { + Ok(lhs.combine(rhs)?.map(Self::Constrained)) + } + } + } +} + +impl ConstraintValidator for ObjectSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -47,7 +60,7 @@ impl Constraint for ObjectSchema { } } -impl Constraint for ObjectSchema { +impl ConstraintValidator for ObjectSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonObject) -> bool { @@ -75,7 +88,16 @@ impl Constraint for ObjectSchema { )] pub struct ObjectConstraints {} -impl Constraint for ObjectConstraints { +impl Constraint for ObjectConstraints { + fn combine( + &mut self, + _other: Self, + ) -> Result, Report> { + Ok(None) + } +} + +impl ConstraintValidator for ObjectConstraints { type Error = [ObjectValidationError]; fn is_valid(&self, _value: &JsonObject) -> bool { 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 27cea9de133..71679caa948 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 @@ -14,7 +14,13 @@ use thiserror::Error; use url::{Host, Url}; use uuid::Uuid; -use crate::schema::{ConstraintError, JsonSchemaValueType, data_type::constraint::Constraint}; +use crate::schema::{ + ConstraintError, JsonSchemaValueType, + data_type::{ + closed::ResolveClosedDataTypeError, + constraint::{Constraint, ConstraintValidator}, + }, +}; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] @@ -221,7 +227,20 @@ pub enum StringSchema { }, } -impl Constraint for StringSchema { +impl Constraint for StringSchema { + fn combine(&mut self, other: Self) -> Result, Report> { + match (&mut *self, other) { + (Self::Constrained(lhs), Self::Constrained(rhs)) => { + Ok(lhs.combine(rhs)?.map(Self::Constrained)) + } + // TODO: Implement folding for string constraints + // see https://linear.app/hash/issue/H-3428/implement-folding-for-string-constraints + (_, rhs) => Ok(Some(rhs)), + } + } +} + +impl ConstraintValidator for StringSchema { type Error = ConstraintError; fn is_valid(&self, value: &JsonValue) -> bool { @@ -244,7 +263,7 @@ impl Constraint for StringSchema { } } -impl Constraint for StringSchema { +impl ConstraintValidator for StringSchema { type Error = ConstraintError; fn is_valid(&self, value: &str) -> bool { @@ -300,7 +319,17 @@ pub struct StringConstraints { pub format: Option, } -impl Constraint for StringConstraints { +// TODO: Implement folding for string constraints +// see https://linear.app/hash/issue/H-3428/implement-folding-for-string-constraints +impl Constraint for StringConstraints { + fn combine(&mut self, other: Self) -> Result, Report> { + // TODO: Implement folding for string constraints + // see https://linear.app/hash/issue/H-3428/implement-folding-for-string-constraints + Ok(Some(other)) + } +} + +impl ConstraintValidator for StringConstraints { type Error = [StringValidationError]; fn is_valid(&self, value: &str) -> bool { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/mod.rs index 2c6789a8fd5..068db47d982 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/mod.rs @@ -5,11 +5,11 @@ pub use self::{ closed::{ClosedDataType, DataTypeResolveData, InheritanceDepth, ResolvedDataType}, constraint::{ AnyOfConstraints, ArrayConstraints, ArraySchema, ArrayTypeTag, ArrayValidationError, - BooleanSchema, BooleanTypeTag, Constraint, ConstraintError, NullSchema, NullTypeTag, - NumberConstraints, NumberSchema, NumberTypeTag, NumberValidationError, ObjectConstraints, - ObjectSchema, ObjectTypeTag, ObjectValidationError, SingleValueConstraints, - SingleValueSchema, StringConstraints, StringFormat, StringFormatError, StringSchema, - StringTypeTag, StringValidationError, TupleConstraints, + BooleanSchema, BooleanTypeTag, ConstraintError, ConstraintValidator, NullSchema, + NullTypeTag, NumberConstraints, NumberSchema, NumberTypeTag, NumberValidationError, + ObjectConstraints, ObjectSchema, ObjectTypeTag, ObjectValidationError, + SingleValueConstraints, SingleValueSchema, StringConstraints, StringFormat, + StringFormatError, StringSchema, StringTypeTag, StringValidationError, TupleConstraints, }, conversion::{ ConversionDefinition, ConversionExpression, ConversionValue, Conversions, Operator, diff --git a/libs/@blockprotocol/type-system/rust/src/schema/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/mod.rs index 711bb0d177b..efc475fada2 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/mod.rs @@ -19,7 +19,7 @@ pub use self::{ array::{PropertyArraySchema, PropertyValueArray, ValueOrArray}, data_type::{ AnyOfConstraints, ArrayConstraints, ArraySchema, ArrayTypeTag, ArrayValidationError, - BooleanSchema, BooleanTypeTag, ClosedDataType, Constraint, ConstraintError, + BooleanSchema, BooleanTypeTag, ClosedDataType, ConstraintError, ConstraintValidator, ConversionDefinition, ConversionExpression, ConversionValue, Conversions, DataType, DataTypeEdge, DataTypeReference, DataTypeResolveData, DataTypeValidator, InheritanceDepth, JsonSchemaValueType, NullSchema, NullTypeTag, NumberConstraints, NumberSchema, diff --git a/libs/@local/hash-validation/src/entity_type.rs b/libs/@local/hash-validation/src/entity_type.rs index 782ee70b1a0..0816f351042 100644 --- a/libs/@local/hash-validation/src/entity_type.rs +++ b/libs/@local/hash-validation/src/entity_type.rs @@ -25,9 +25,9 @@ use serde_json::Value as JsonValue; use thiserror::Error; use type_system::{ schema::{ - ClosedEntityType, Constraint, DataTypeReference, JsonSchemaValueType, PropertyObjectSchema, - PropertyType, PropertyTypeReference, PropertyValueArray, PropertyValueSchema, - PropertyValues, ValueOrArray, + ClosedEntityType, ConstraintValidator, DataTypeReference, JsonSchemaValueType, + PropertyObjectSchema, PropertyType, PropertyTypeReference, PropertyValueArray, + PropertyValueSchema, PropertyValues, ValueOrArray, }, url::{BaseUrl, OntologyTypeVersion, VersionedUrl}, }; From 7d5b02e2404ab3bae072511f24c269eaff4cc0f1 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Mon, 14 Oct 2024 15:51:05 +0200 Subject: [PATCH 2/7] Change signature for `combine` --- .../src/schema/data_type/constraint/any_of.rs | 7 +- .../src/schema/data_type/constraint/array.rs | 59 ++++++++---- .../schema/data_type/constraint/boolean.rs | 6 +- .../src/schema/data_type/constraint/mod.rs | 95 +++++++++++++------ .../src/schema/data_type/constraint/null.rs | 6 +- .../src/schema/data_type/constraint/number.rs | 24 +++-- .../src/schema/data_type/constraint/object.rs | 21 ++-- .../src/schema/data_type/constraint/string.rs | 26 +++-- 8 files changed, 168 insertions(+), 76 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs index 98408024e01..d5d489f7c59 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs @@ -22,10 +22,13 @@ pub struct AnyOfConstraints { } impl Constraint for AnyOfConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { // TODO: Implement folding for anyOf constraints // see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints - Ok(Some(other)) + Ok((self, Some(other))) } } diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs index 58b01e9b4e5..501d6553757 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs @@ -47,13 +47,22 @@ pub enum ArrayItemConstraints { } impl Constraint for ArrayItemConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { - Ok(match (self, other) { - (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs.combine(rhs)?.map(Self::Boolean), - (Self::Number(lhs), Self::Number(rhs)) => lhs.combine(rhs)?.map(Self::Number), - (Self::String(lhs), Self::String(rhs)) => lhs.combine(rhs)?.map(Self::String), + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { + match (self, other) { + (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Boolean(lhs), rhs.map(Self::Boolean))), + (Self::Number(lhs), Self::Number(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Number(lhs), rhs.map(Self::Number))), + (Self::String(lhs), Self::String(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::String(lhs), rhs.map(Self::String))), _ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes), - }) + } } } @@ -119,22 +128,32 @@ pub enum ArraySchema { } impl Constraint for ArraySchema { - fn combine(&mut self, other: Self) -> Result, Report> { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - lhs.combine(rhs)?.map(Self::Constrained) + let (combined, remainder) = lhs.combine(rhs)?; + ( + Self::Constrained(combined), + remainder.map(Self::Constrained), + ) } - (Self::Tuple(_), Self::Constrained(typed)) => { + (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 - Some(Self::Constrained(typed)) + (Self::Tuple(lhs), Some(Self::Constrained(rhs))) } - (Self::Constrained(_), Self::Tuple(any_of)) => { + (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 - Some(Self::Tuple(any_of)) + (Self::Constrained(lhs), Some(Self::Tuple(rhs))) + } + (Self::Tuple(lhs), Self::Tuple(rhs)) => { + let (combined, remainder) = lhs.combine(rhs)?; + (Self::Tuple(combined), remainder.map(Self::Tuple)) } - (Self::Tuple(lhs), Self::Tuple(rhs)) => lhs.combine(rhs)?.map(Self::Tuple), }) } } @@ -194,10 +213,13 @@ pub struct ArrayConstraints { } impl Constraint for ArrayConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { // TODO: Implement folding for array constraints // see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints - Ok(Some(other)) + Ok((self, Some(other))) } } @@ -242,10 +264,13 @@ pub struct TupleConstraints { } impl Constraint for TupleConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { // TODO: Implement folding for array constraints // see https://linear.app/hash/issue/H-3429/implement-folding-for-array-constraints - Ok(Some(other)) + Ok((self, Some(other))) } } diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs index 1e01eec44d7..a6a0944017f 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs @@ -21,10 +21,10 @@ pub struct BooleanSchema; impl Constraint for BooleanSchema { fn combine( - &mut self, + self, _other: Self, - ) -> Result, Report> { - Ok(None) + ) -> Result<(Self, Option), Report> { + Ok((self, None)) } } diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index a115d508362..cedfa78d521 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -29,13 +29,16 @@ use crate::schema::{ValueLabel, data_type::closed::ResolveClosedDataTypeError}; pub trait Constraint: Sized { /// Combines the current constraints with the provided one. /// - /// If the constraints cannot be combined completely, the method will return the remaining - /// constraints that could not be combined, otherwise `None`. + /// It returns the combination of the two constraints. If they can fully be merged, the second + /// value is returned as `None`. If the constraints exclude each other, an error is returned. /// /// # Errors /// /// If the constraints exclude each other, an error is returned. - fn combine(&mut self, other: Self) -> Result, Report>; + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report>; } pub trait ConstraintValidator: Constraint { @@ -85,35 +88,56 @@ impl ValueConstraints { return Ok(acc); } - let mut remainder = Some(constraints); - for current in &mut acc { - let Some(new) = remainder.take() else { break }; - remainder = current.combine(new)?; + let mut new_constraints = Vec::new(); + let mut next = Some(constraints); + + // We try to combine the constraints as much to the left as possible + let mut acc = acc.into_iter(); + while let Some(current) = acc.next() { + let Some(to_combine) = next.take() else { + break; + }; + let (combined, remainder) = current.combine(to_combine)?; + new_constraints.push(combined); + + if let Some(remainder) = remainder { + next = Some(remainder); + } else { + new_constraints.extend(acc); + break; + } } - if let Some(remainder) = remainder { - acc.push(remainder); + if let Some(remainder) = next { + new_constraints.push(remainder); } - Ok::<_, Report>(acc) + Ok::<_, Report>(new_constraints) }) } } impl Constraint for ValueConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { - Ok(match (self, other) { - (Self::Typed(lhs), Self::Typed(rhs)) => lhs.combine(rhs)?.map(Self::Typed), - (Self::AnyOf(_), Self::Typed(typed)) => { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { + match (self, other) { + (Self::Typed(lhs), Self::Typed(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Typed(lhs), rhs.map(Self::Typed))), + (Self::AnyOf(lhs), Self::Typed(rhs)) => { // TODO: Implement folding for anyOf constraints // see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints - Some(Self::Typed(typed)) + Ok((Self::AnyOf(lhs), Some(Self::Typed(rhs)))) } - (Self::Typed(_), Self::AnyOf(any_of)) => { + (Self::Typed(lhs), Self::AnyOf(rhs)) => { // TODO: Implement folding for anyOf constraints // see https://linear.app/hash/issue/H-3430/implement-folding-for-anyof-constraints - Some(Self::AnyOf(any_of)) + Ok((Self::Typed(lhs), Some(Self::AnyOf(rhs)))) } - (Self::AnyOf(lhs), Self::AnyOf(rhs)) => lhs.combine(rhs)?.map(Self::AnyOf), - }) + (Self::AnyOf(lhs), Self::AnyOf(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::AnyOf(lhs), rhs.map(Self::AnyOf))), + } } } @@ -148,16 +172,31 @@ pub enum SingleValueConstraints { } impl Constraint for SingleValueConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { - Ok(match (self, other) { - (Self::Null(lhs), Self::Null(rhs)) => lhs.combine(rhs)?.map(Self::Null), - (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs.combine(rhs)?.map(Self::Boolean), - (Self::Number(lhs), Self::Number(rhs)) => lhs.combine(rhs)?.map(Self::Number), - (Self::String(lhs), Self::String(rhs)) => lhs.combine(rhs)?.map(Self::String), - (Self::Array(lhs), Self::Array(rhs)) => lhs.combine(rhs)?.map(Self::Array), - (Self::Object(lhs), Self::Object(rhs)) => lhs.combine(rhs)?.map(Self::Object), + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { + match (self, other) { + (Self::Null(lhs), Self::Null(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Null(lhs), rhs.map(Self::Null))), + (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Boolean(lhs), rhs.map(Self::Boolean))), + (Self::Number(lhs), Self::Number(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Number(lhs), rhs.map(Self::Number))), + (Self::String(lhs), Self::String(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::String(lhs), rhs.map(Self::String))), + (Self::Array(lhs), Self::Array(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Array(lhs), rhs.map(Self::Array))), + (Self::Object(lhs), Self::Object(rhs)) => lhs + .combine(rhs) + .map(|(lhs, rhs)| (Self::Object(lhs), rhs.map(Self::Object))), _ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes), - }) + } } } diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs index 5f1f88a78f0..fa712cfb70a 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs @@ -21,10 +21,10 @@ pub struct NullSchema; impl Constraint for NullSchema { fn combine( - &mut self, + self, _other: Self, - ) -> Result, Report> { - Ok(None) + ) -> Result<(Self, Option), Report> { + Ok((self, None)) } } 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 c0c5d90f604..2a887e32995 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 @@ -115,15 +115,22 @@ fn float_multiple_of(lhs: f64, rhs: f64) -> bool { } impl Constraint for NumberSchema { - fn combine(&mut self, other: Self) -> Result, Report> { - match (&mut *self, other) { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { + Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - Ok(lhs.combine(rhs)?.map(Self::Constrained)) + let (combined, remainder) = lhs.combine(rhs)?; + ( + Self::Constrained(combined), + remainder.map(Self::Constrained), + ) } // TODO: Implement folding for number constraints // see https://linear.app/hash/issue/H-3427/implement-folding-for-number-constraints - (_, rhs) => Ok(Some(rhs)), - } + (lhs, rhs) => (lhs, Some(rhs)), + }) } } @@ -227,10 +234,13 @@ pub struct NumberConstraints { } impl Constraint for NumberConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { // TODO: Implement folding for number constraints // see https://linear.app/hash/issue/H-3427/implement-folding-for-number-constraints - Ok(Some(other)) + Ok((self, Some(other))) } } diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs index 189ca47603d..394438b956f 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs @@ -28,12 +28,19 @@ pub enum ObjectSchema { } impl Constraint for ObjectSchema { - fn combine(&mut self, other: Self) -> Result, Report> { - match (&mut *self, other) { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { + Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - Ok(lhs.combine(rhs)?.map(Self::Constrained)) + let (combined, remainder) = lhs.combine(rhs)?; + ( + Self::Constrained(combined), + remainder.map(Self::Constrained), + ) } - } + }) } } @@ -90,10 +97,10 @@ pub struct ObjectConstraints {} impl Constraint for ObjectConstraints { fn combine( - &mut self, + self, _other: Self, - ) -> Result, Report> { - Ok(None) + ) -> Result<(Self, Option), Report> { + Ok((self, None)) } } 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 71679caa948..714b5ecbe69 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 @@ -228,15 +228,22 @@ pub enum StringSchema { } impl Constraint for StringSchema { - fn combine(&mut self, other: Self) -> Result, Report> { - match (&mut *self, other) { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { + Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - Ok(lhs.combine(rhs)?.map(Self::Constrained)) + let (combined, remainder) = lhs.combine(rhs)?; + ( + Self::Constrained(combined), + remainder.map(Self::Constrained), + ) } // TODO: Implement folding for string constraints // see https://linear.app/hash/issue/H-3428/implement-folding-for-string-constraints - (_, rhs) => Ok(Some(rhs)), - } + (lhs, rhs) => (lhs, Some(rhs)), + }) } } @@ -319,13 +326,14 @@ pub struct StringConstraints { pub format: Option, } -// TODO: Implement folding for string constraints -// see https://linear.app/hash/issue/H-3428/implement-folding-for-string-constraints impl Constraint for StringConstraints { - fn combine(&mut self, other: Self) -> Result, Report> { + fn combine( + self, + other: Self, + ) -> Result<(Self, Option), Report> { // TODO: Implement folding for string constraints // see https://linear.app/hash/issue/H-3428/implement-folding-for-string-constraints - Ok(Some(other)) + Ok((self, Some(other))) } } From 23550890ff65d082fc2ea20c4f386586043cf129 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Mon, 14 Oct 2024 16:20:16 +0200 Subject: [PATCH 3/7] Simplify folding logic --- .../src/schema/data_type/constraint/mod.rs | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index cedfa78d521..a8c5f204312 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -82,35 +82,30 @@ impl ValueConstraints { ) -> Result, Report> { schemas .into_iter() - .try_fold(vec![], |mut acc, constraints| { - if acc.is_empty() { - acc.push(constraints); - return Ok(acc); - } - - let mut new_constraints = Vec::new(); + .try_fold(Vec::::new(), |folded, constraints| { + // let mut new_constraints = Vec::new(); let mut next = Some(constraints); - // We try to combine the constraints as much to the left as possible - let mut acc = acc.into_iter(); - while let Some(current) = acc.next() { - let Some(to_combine) = next.take() else { - break; - }; - let (combined, remainder) = current.combine(to_combine)?; - new_constraints.push(combined); + let mut new_constraints = folded + .into_iter() + .map(|existing| { + if let Some(to_combine) = next.take() { + let (combined, remainder) = existing.combine(to_combine)?; + if let Some(remainder) = remainder { + next = Some(remainder); + } + Ok::<_, Report>(combined) + } else { + Ok(existing) + } + }) + .collect::, _>>()?; - if let Some(remainder) = remainder { - next = Some(remainder); - } else { - new_constraints.extend(acc); - break; - } - } if let Some(remainder) = next { new_constraints.push(remainder); } - Ok::<_, Report>(new_constraints) + + Ok(new_constraints) }) } } From 9b10e09aa1a529c73540150c76ef6ef575f2424c Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Mon, 14 Oct 2024 16:27:32 +0200 Subject: [PATCH 4/7] Simplify folding logic further --- .../type-system/rust/src/schema/data_type/constraint/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index a8c5f204312..6e9390bd8e7 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -83,7 +83,6 @@ impl ValueConstraints { schemas .into_iter() .try_fold(Vec::::new(), |folded, constraints| { - // let mut new_constraints = Vec::new(); let mut next = Some(constraints); let mut new_constraints = folded @@ -94,7 +93,7 @@ impl ValueConstraints { if let Some(remainder) = remainder { next = Some(remainder); } - Ok::<_, Report>(combined) + Ok::<_, Report<_>>(combined) } else { Ok(existing) } From 31088c5160b6e5ab47d9ec5b80f046aed012b1b6 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Mon, 14 Oct 2024 16:30:28 +0200 Subject: [PATCH 5/7] Simplify folding logic further --- .../rust/src/schema/data_type/constraint/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index 6e9390bd8e7..3e3ea05523c 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -82,10 +82,10 @@ impl ValueConstraints { ) -> Result, Report> { schemas .into_iter() - .try_fold(Vec::::new(), |folded, constraints| { + .try_fold(Vec::::new(), |mut folded, constraints| { let mut next = Some(constraints); - let mut new_constraints = folded + folded = folded .into_iter() .map(|existing| { if let Some(to_combine) = next.take() { @@ -101,10 +101,10 @@ impl ValueConstraints { .collect::, _>>()?; if let Some(remainder) = next { - new_constraints.push(remainder); + folded.push(remainder); } - Ok(new_constraints) + Ok(folded) }) } } From d43ce6b27be287321a9cf935fbff16deee257ed0 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Mon, 14 Oct 2024 18:17:07 +0200 Subject: [PATCH 6/7] Add documentation --- .../src/schema/data_type/constraint/mod.rs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index 3e3ea05523c..9188fe56ab7 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -77,22 +77,35 @@ pub enum ValueConstraints { } impl ValueConstraints { + /// Folds multiple constraints into fewer constraints. + /// + /// This function attempts to combine as many constraints as possible. If two constraints + /// cannot be fully merged, they are kept separate. + /// + /// The algorithm works as follows: + /// - It iterattes over all constraints + /// - for each constraint, it tries to merge them with the constraints that have already been + /// merged from the previous iterations from left to right + /// - if a constraint cannot be fully merged, it is either combined with the next constraint or + /// added to the list of constraints that have already been merged. + /// + /// # Errors + /// + /// If two constraints exclude each other, an error is returned. pub fn fold_intersections( schemas: impl IntoIterator, ) -> Result, Report> { schemas .into_iter() - .try_fold(Vec::::new(), |mut folded, constraints| { - let mut next = Some(constraints); - + .map(Some) + .try_fold(Vec::::new(), |mut folded, mut constraints| { folded = folded .into_iter() .map(|existing| { - if let Some(to_combine) = next.take() { + if let Some(to_combine) = constraints.take() { let (combined, remainder) = existing.combine(to_combine)?; - if let Some(remainder) = remainder { - next = Some(remainder); - } + // The remainder is used for the next iteration + constraints = remainder; Ok::<_, Report<_>>(combined) } else { Ok(existing) @@ -100,7 +113,7 @@ impl ValueConstraints { }) .collect::, _>>()?; - if let Some(remainder) = next { + if let Some(remainder) = constraints { folded.push(remainder); } From d49eb1eb1a85a2ee4b167e638b652a844a0ce5b1 Mon Sep 17 00:00:00 2001 From: Tim Diekmann Date: Thu, 17 Oct 2024 11:00:42 +0200 Subject: [PATCH 7/7] Rename `combine` to `intersection` --- .../src/schema/data_type/constraint/any_of.rs | 2 +- .../src/schema/data_type/constraint/array.rs | 18 ++++++------- .../schema/data_type/constraint/boolean.rs | 2 +- .../src/schema/data_type/constraint/mod.rs | 26 +++++++++---------- .../src/schema/data_type/constraint/null.rs | 2 +- .../src/schema/data_type/constraint/number.rs | 6 ++--- .../src/schema/data_type/constraint/object.rs | 6 ++--- .../src/schema/data_type/constraint/string.rs | 6 ++--- 8 files changed, 34 insertions(+), 34 deletions(-) diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs index d5d489f7c59..57c1c4d2938 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/any_of.rs @@ -22,7 +22,7 @@ pub struct AnyOfConstraints { } impl Constraint for AnyOfConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs index 501d6553757..da75c2cc835 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/array.rs @@ -47,19 +47,19 @@ pub enum ArrayItemConstraints { } impl Constraint for ArrayItemConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { match (self, other) { (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Boolean(lhs), rhs.map(Self::Boolean))), (Self::Number(lhs), Self::Number(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Number(lhs), rhs.map(Self::Number))), (Self::String(lhs), Self::String(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::String(lhs), rhs.map(Self::String))), _ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes), } @@ -128,13 +128,13 @@ pub enum ArraySchema { } impl Constraint for ArraySchema { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - let (combined, remainder) = lhs.combine(rhs)?; + let (combined, remainder) = lhs.intersection(rhs)?; ( Self::Constrained(combined), remainder.map(Self::Constrained), @@ -151,7 +151,7 @@ impl Constraint for ArraySchema { (Self::Constrained(lhs), Some(Self::Tuple(rhs))) } (Self::Tuple(lhs), Self::Tuple(rhs)) => { - let (combined, remainder) = lhs.combine(rhs)?; + let (combined, remainder) = lhs.intersection(rhs)?; (Self::Tuple(combined), remainder.map(Self::Tuple)) } }) @@ -213,7 +213,7 @@ pub struct ArrayConstraints { } impl Constraint for ArrayConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { @@ -264,7 +264,7 @@ pub struct TupleConstraints { } impl Constraint for TupleConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs index a6a0944017f..f2cc0c4c823 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/boolean.rs @@ -20,7 +20,7 @@ pub enum BooleanTypeTag { pub struct BooleanSchema; impl Constraint for BooleanSchema { - fn combine( + fn intersection( self, _other: Self, ) -> Result<(Self, Option), Report> { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs index 9188fe56ab7..2763669b7ae 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/mod.rs @@ -35,7 +35,7 @@ pub trait Constraint: Sized { /// # Errors /// /// If the constraints exclude each other, an error is returned. - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report>; @@ -83,7 +83,7 @@ impl ValueConstraints { /// cannot be fully merged, they are kept separate. /// /// The algorithm works as follows: - /// - It iterattes over all constraints + /// - It iterates over all constraints /// - for each constraint, it tries to merge them with the constraints that have already been /// merged from the previous iterations from left to right /// - if a constraint cannot be fully merged, it is either combined with the next constraint or @@ -103,7 +103,7 @@ impl ValueConstraints { .into_iter() .map(|existing| { if let Some(to_combine) = constraints.take() { - let (combined, remainder) = existing.combine(to_combine)?; + let (combined, remainder) = existing.intersection(to_combine)?; // The remainder is used for the next iteration constraints = remainder; Ok::<_, Report<_>>(combined) @@ -123,13 +123,13 @@ impl ValueConstraints { } impl Constraint for ValueConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { match (self, other) { (Self::Typed(lhs), Self::Typed(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Typed(lhs), rhs.map(Self::Typed))), (Self::AnyOf(lhs), Self::Typed(rhs)) => { // TODO: Implement folding for anyOf constraints @@ -142,7 +142,7 @@ impl Constraint for ValueConstraints { Ok((Self::Typed(lhs), Some(Self::AnyOf(rhs)))) } (Self::AnyOf(lhs), Self::AnyOf(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::AnyOf(lhs), rhs.map(Self::AnyOf))), } } @@ -179,28 +179,28 @@ pub enum SingleValueConstraints { } impl Constraint for SingleValueConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { match (self, other) { (Self::Null(lhs), Self::Null(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Null(lhs), rhs.map(Self::Null))), (Self::Boolean(lhs), Self::Boolean(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Boolean(lhs), rhs.map(Self::Boolean))), (Self::Number(lhs), Self::Number(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Number(lhs), rhs.map(Self::Number))), (Self::String(lhs), Self::String(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::String(lhs), rhs.map(Self::String))), (Self::Array(lhs), Self::Array(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Array(lhs), rhs.map(Self::Array))), (Self::Object(lhs), Self::Object(rhs)) => lhs - .combine(rhs) + .intersection(rhs) .map(|(lhs, rhs)| (Self::Object(lhs), rhs.map(Self::Object))), _ => bail!(ResolveClosedDataTypeError::IntersectedDifferentTypes), } diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs index fa712cfb70a..fdcb625b23b 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/null.rs @@ -20,7 +20,7 @@ pub enum NullTypeTag { pub struct NullSchema; impl Constraint for NullSchema { - fn combine( + fn intersection( self, _other: Self, ) -> Result<(Self, Option), Report> { 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 2a887e32995..f998d97dc63 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 @@ -115,13 +115,13 @@ fn float_multiple_of(lhs: f64, rhs: f64) -> bool { } impl Constraint for NumberSchema { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - let (combined, remainder) = lhs.combine(rhs)?; + let (combined, remainder) = lhs.intersection(rhs)?; ( Self::Constrained(combined), remainder.map(Self::Constrained), @@ -234,7 +234,7 @@ pub struct NumberConstraints { } impl Constraint for NumberConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs index 394438b956f..8caf53b9192 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/constraint/object.rs @@ -28,13 +28,13 @@ pub enum ObjectSchema { } impl Constraint for ObjectSchema { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - let (combined, remainder) = lhs.combine(rhs)?; + let (combined, remainder) = lhs.intersection(rhs)?; ( Self::Constrained(combined), remainder.map(Self::Constrained), @@ -96,7 +96,7 @@ impl ConstraintValidator for ObjectSchema { pub struct ObjectConstraints {} impl Constraint for ObjectConstraints { - fn combine( + fn intersection( self, _other: Self, ) -> Result<(Self, Option), Report> { 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 714b5ecbe69..fd8fd23129a 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 @@ -228,13 +228,13 @@ pub enum StringSchema { } impl Constraint for StringSchema { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> { Ok(match (self, other) { (Self::Constrained(lhs), Self::Constrained(rhs)) => { - let (combined, remainder) = lhs.combine(rhs)?; + let (combined, remainder) = lhs.intersection(rhs)?; ( Self::Constrained(combined), remainder.map(Self::Constrained), @@ -327,7 +327,7 @@ pub struct StringConstraints { } impl Constraint for StringConstraints { - fn combine( + fn intersection( self, other: Self, ) -> Result<(Self, Option), Report> {