From 47d7f704845de082f5070a55109cc4093f28621d Mon Sep 17 00:00:00 2001 From: Tim Diekmann <21277928+TimDiekmann@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:35:45 +0200 Subject: [PATCH] H-3421: Resolve closed data types on creation/updating (#5361) --- Cargo.lock | 1 + Cargo.toml | 1 + .../libs/graph/src/store/ontology.rs | 4 +- .../libs/graph/src/store/postgres/mod.rs | 10 +- .../src/store/postgres/ontology/data_type.rs | 123 ++-- .../type-system/rust/Cargo.toml | 1 + .../rust/src/schema/data_type/closed.rs | 527 +++++++++++++++++- .../rust/src/schema/data_type/mod.rs | 339 +++++------ .../rust/src/schema/data_type/reference.rs | 2 +- .../rust/src/schema/data_type/validation.rs | 21 +- .../type-system/rust/src/schema/mod.rs | 8 +- 11 files changed, 789 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f46886b3341..3fd14c3f34f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7668,6 +7668,7 @@ dependencies = [ "futures", "graph-test-data", "iso8601-duration", + "itertools 0.13.0", "postgres-types", "pretty_assertions", "regex", diff --git a/Cargo.toml b/Cargo.toml index 88416820d72..6da88d49fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,6 +159,7 @@ humansize = { version = "=2.1.3", default-features = false } hyper = { version = "=1.4.1", default-features = false } include_dir = { version = "=0.7.4", default-features = false } insta = { version = "=1.40.0", default-features = false } +itertools = { version = "0.13.0", default-features = false } jsonschema = { version = "=0.22.3", default-features = false } justjson = { version = "=0.3.0", default-features = false } lexical = { version = "=7.0.2", default-features = false } diff --git a/apps/hash-graph/libs/graph/src/store/ontology.rs b/apps/hash-graph/libs/graph/src/store/ontology.rs index ccee296e0a0..3ce277bc16e 100644 --- a/apps/hash-graph/libs/graph/src/store/ontology.rs +++ b/apps/hash-graph/libs/graph/src/store/ontology.rs @@ -204,13 +204,11 @@ pub trait DataTypeStore { params: CountDataTypesParams<'_>, ) -> impl Future> + Send; - /// Get the [`DataTypes`] specified by the [`GetDataTypesParams`]. + /// Get the [`DataType`]s specified by the [`GetDataTypesParams`]. /// /// # Errors /// /// - if the requested [`DataType`] doesn't exist. - /// - /// [`DataTypes`]: DataType fn get_data_types( &self, actor_id: AccountId, diff --git a/apps/hash-graph/libs/graph/src/store/postgres/mod.rs b/apps/hash-graph/libs/graph/src/store/postgres/mod.rs index 39c741dd0e7..5e34bc24b2c 100644 --- a/apps/hash-graph/libs/graph/src/store/postgres/mod.rs +++ b/apps/hash-graph/libs/graph/src/store/postgres/mod.rs @@ -44,8 +44,8 @@ use tokio_postgres::{GenericClient, error::SqlState}; use type_system::{ Valid, schema::{ - ClosedEntityType, Conversions, DataType, DataTypeId, DataTypeInheritanceData, - DataTypeReference, EntityType, EntityTypeReference, PropertyType, PropertyTypeReference, + ClosedEntityType, Conversions, DataType, DataTypeId, DataTypeReference, + DataTypeResolveData, EntityType, EntityTypeReference, PropertyType, PropertyTypeReference, }, url::{BaseUrl, OntologyTypeVersion, VersionedUrl}, }; @@ -459,9 +459,9 @@ where pub async fn insert_data_type_references( &self, ontology_id: DataTypeId, - metadata: &DataTypeInheritanceData, + metadata: &DataTypeResolveData, ) -> Result<(), InsertionError> { - for (target, depth) in &metadata.inheritance_depths { + for (target, depth) in metadata.inheritance_depths() { self.as_client() .query( " @@ -475,7 +475,7 @@ where $3 ); ", - &[&ontology_id, target, depth], + &[&ontology_id, &target, &depth], ) .await .change_context(InsertionError)?; diff --git a/apps/hash-graph/libs/graph/src/store/postgres/ontology/data_type.rs b/apps/hash-graph/libs/graph/src/store/postgres/ontology/data_type.rs index c66501c4f0e..41e3e2f278a 100644 --- a/apps/hash-graph/libs/graph/src/store/postgres/ontology/data_type.rs +++ b/apps/hash-graph/libs/graph/src/store/postgres/ontology/data_type.rs @@ -37,10 +37,10 @@ use temporal_versioning::{RightBoundedTemporalInterval, Timestamp, TransactionTi use tokio_postgres::{GenericClient, Row}; use tracing::instrument; use type_system::{ - Validator, + Valid, Validator, schema::{ - ConversionDefinition, Conversions, DataTypeId, DataTypeInheritanceData, DataTypeValidator, - InheritanceDepth, OntologyTypeResolver, + ClosedDataType, ConversionDefinition, Conversions, DataType, DataTypeEdge, DataTypeId, + DataTypeResolveData, DataTypeValidator, InheritanceDepth, OntologyTypeResolver, }, url::{BaseUrl, OntologyTypeVersion, VersionedUrl}, }; @@ -116,14 +116,24 @@ where async fn get_data_type_inheritance_metadata( &self, data_types: &[DataTypeId], - ) -> Result, QueryError> { + ) -> Result, QueryError> { Ok(self .as_client() .query( " - SELECT source_data_type_ontology_id, target_data_type_ontology_ids, depths - FROM data_type_inherits_from_aggregation - WHERE source_data_type_ontology_id = ANY($1) + SELECT + source_data_type_ontology_id, + array_agg(target_data_type_ontology_id), + array_agg(depth), + array_agg(schema) + FROM ( + SELECT * + FROM data_type_inherits_from + JOIN data_types ON target_data_type_ontology_id = ontology_id + WHERE source_data_type_ontology_id = ANY($1) + ORDER BY source_data_type_ontology_id, depth, schema->>'$id' + ) AS subquery + GROUP BY source_data_type_ontology_id; ", &[&data_types], ) @@ -131,12 +141,22 @@ where .change_context(QueryError)? .into_iter() .map(|row| { - let source: DataTypeId = row.get(0); + let source_id: DataTypeId = row.get(0); let targets: Vec = row.get(1); let depths: Vec = row.get(2); - (source, DataTypeInheritanceData { - inheritance_depths: targets.into_iter().zip(depths).collect(), - }) + let schemas: Vec> = row.get(3); + + let mut resolve_data = DataTypeResolveData::default(); + for ((target_id, schema), depth) in targets.into_iter().zip(schemas).zip(depths) { + resolve_data.add_edge( + DataTypeEdge::Inheritance, + Arc::new(schema.into_inner()), + target_id, + depth.inner(), + ); + } + + (source_id, resolve_data) })) } @@ -456,7 +476,12 @@ where let mut ontology_type_resolver = OntologyTypeResolver::default(); + for (data_type_id, inserted_data_type) in &inserted_data_types { + ontology_type_resolver.add_unresolved(*data_type_id, Arc::clone(inserted_data_type)); + } + let required_parent_ids = data_type_reference_ids.into_iter().collect::>(); + let mut parent_inheritance_data = transaction .get_data_type_inheritance_metadata(&required_parent_ids) .await @@ -498,35 +523,26 @@ where if !parent.schema.all_of.is_empty() { tracing::warn!("No inheritance data found for `{}`", parent.schema.id); } - ontology_type_resolver.add_open(parent_id, Arc::new(parent.schema)); + ontology_type_resolver.add_unresolved(parent_id, Arc::new(parent.schema)); } }); - for (data_type_id, inserted_data_type) in &inserted_data_types { - ontology_type_resolver.add_open(*data_type_id, Arc::clone(inserted_data_type)); - } - let schema_metadata = inserted_data_types + let closed_schemas = inserted_data_types .iter() - .map(|(data_type_id, _)| { - ontology_type_resolver + .map(|(data_type_id, data_type)| { + let closed_metadata = ontology_type_resolver .resolve_data_type_metadata(*data_type_id) - .change_context(InsertionError) + .change_context(InsertionError)?; + let closed_schema = + ClosedDataType::from_resolve_data((**data_type).clone(), &closed_metadata) + .change_context(InsertionError)?; + + Ok((closed_schema, closed_metadata)) }) .collect::, _>>()?; let data_type_validator = DataTypeValidator; for (data_type_id, data_type) in &inserted_data_types { - // TODO: Validate ontology types on creation - // see https://linear.app/hash/issue/H-2976/validate-ontology-types-on-creation - // let closed_schema = data_type_validator - // .validate( - // ontology_type_resolver - // .get_closed_data_type(*data_type_id) - // .change_context(InsertionError)?, - // ) - // .await - // .attach(StatusCode::InvalidArgument) - // .change_context(InsertionError)?; let schema = data_type_validator .validate_ref(&**data_type) .await @@ -536,10 +552,17 @@ where .insert_data_type_with_id(*data_type_id, schema) .await?; } - for (schema_metadata, (data_type_id, _)) in schema_metadata.iter().zip(&inserted_data_types) + for ((closed_schema, closed_metadata), (data_type_id, _)) in + closed_schemas.iter().zip(&inserted_data_types) { + data_type_validator + .validate_ref(closed_schema) + .await + .attach(StatusCode::InvalidArgument) + .change_context(InsertionError)?; + transaction - .insert_data_type_references(*data_type_id, schema_metadata) + .insert_data_type_references(*data_type_id, closed_metadata) .await?; } @@ -809,7 +832,7 @@ where }; let schema = data_type_validator - .validate_ref(¶ms.schema) + .validate(params.schema) .await .change_context(UpdateError)?; @@ -861,25 +884,23 @@ where if !parent.schema.all_of.is_empty() { tracing::warn!("No inheritance data found for `{}`", parent.schema.id); } - ontology_type_resolver.add_open(parent_id, Arc::new(parent.schema)); + ontology_type_resolver.add_unresolved(parent_id, Arc::new(parent.schema)); } }); - ontology_type_resolver.add_open(new_ontology_id, Arc::new(schema.clone().into_inner())); - let metadata = ontology_type_resolver + ontology_type_resolver + .add_unresolved(new_ontology_id, Arc::new(schema.clone().into_inner())); + let resolve_data = ontology_type_resolver .resolve_data_type_metadata(new_ontology_id) .change_context(UpdateError)?; - // TODO: Validate ontology types on creation - // see https://linear.app/hash/issue/H-2976/validate-ontology-types-on-creation - // let closed_schema = data_type_validator - // .validate( - // ontology_type_resolver - // .get_closed_data_type(new_ontology_id) - // .change_context(UpdateError)?, - // ) - // .await - // .change_context(UpdateError)?; + let closed_schema = data_type_validator + .validate( + ClosedDataType::from_resolve_data(schema.clone().into_inner(), &resolve_data) + .change_context(UpdateError)?, + ) + .await + .change_context(UpdateError)?; let (ontology_id, owned_by_id, temporal_versioning) = transaction .update_owned_ontology_id(&schema.id, &provenance.edition) .await?; @@ -887,11 +908,11 @@ where let data_type_id = DataTypeId::from(ontology_id); transaction - .insert_data_type_with_id(data_type_id, schema) + .insert_data_type_with_id(data_type_id, &schema) .await .change_context(UpdateError)?; transaction - .insert_data_type_references(data_type_id, &metadata) + .insert_data_type_references(data_type_id, &resolve_data) .await .change_context(UpdateError)?; @@ -960,7 +981,7 @@ where Err(error.change_context(UpdateError)) } else { let metadata = DataTypeMetadata { - record_id: OntologyTypeRecordId::from(params.schema.id.clone()), + record_id: OntologyTypeRecordId::from(closed_schema.id.clone()), classification: OntologyTypeClassificationMetadata::Owned { owned_by_id }, temporal_versioning, provenance, @@ -970,7 +991,7 @@ where if let Some(temporal_client) = &self.temporal_client { temporal_client .start_update_data_type_embeddings_workflow(actor_id, &[DataTypeWithMetadata { - schema: params.schema, + schema: schema.into_inner(), metadata: metadata.clone(), }]) .await @@ -1107,7 +1128,7 @@ where .map(|data_type| { let schema = Arc::new(data_type.schema); let data_type_id = DataTypeId::from_url(&schema.id); - ontology_type_resolver.add_open(data_type_id, Arc::clone(&schema)); + ontology_type_resolver.add_unresolved(data_type_id, Arc::clone(&schema)); data_type_id }) .collect::>(); diff --git a/libs/@blockprotocol/type-system/rust/Cargo.toml b/libs/@blockprotocol/type-system/rust/Cargo.toml index 8c898704e0b..307d8e05efe 100644 --- a/libs/@blockprotocol/type-system/rust/Cargo.toml +++ b/libs/@blockprotocol/type-system/rust/Cargo.toml @@ -31,6 +31,7 @@ uuid = { workspace = true, public = true, features = ["v5", "serde", "std"] } # Private workspace dependencies futures = { workspace = true } +itertools = { workspace = true, features = ["use_alloc"] } # Private third-party dependencies regex = { workspace = true, features = ["std"] } 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 49869001947..ad62983a314 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 @@ -1,31 +1,106 @@ use alloc::sync::Arc; -use core::cmp; #[cfg(feature = "postgres")] use core::error::Error; +use core::{cmp, iter}; use std::collections::{HashMap, hash_map::Entry}; #[cfg(feature = "postgres")] use bytes::BytesMut; +use itertools::Itertools; #[cfg(feature = "postgres")] use postgres_types::{FromSql, IsNull, ToSql, Type}; use serde::{Deserialize, Serialize}; +use thiserror::Error; use crate::{ Valid, - schema::{DataType, DataTypeId, data_type::DataTypeEdge}, + schema::{ + DataType, DataTypeId, ValueLabel, + data_type::{DataTypeEdge, constraint::ValueConstraints}, + }, url::VersionedUrl, }; #[derive(Debug, Clone, Serialize, Deserialize)] // #[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] -pub struct ClosedDataType { +pub struct ResolvedDataType { #[serde(flatten)] pub schema: Arc, #[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "$defs")] pub definitions: HashMap>, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ClosedDataType { + #[serde(rename = "$id")] + pub id: VersionedUrl, + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title_plural: Option, + pub description: String, + #[serde(default, skip_serializing_if = "ValueLabel::is_empty")] + pub label: ValueLabel, + + #[cfg_attr( + target_arch = "wasm32", + tsify(type = "[ValueConstraints, ...ValueConstraints[]]") + )] + pub all_of: Vec, + pub r#abstract: bool, +} + +#[derive(Debug, Error)] +pub enum ResolveClosedDataTypeError { + #[error( + "The metadata (such as description or label) is ambiguous. This happens if the schema \ + itself does not specify metadata but two parents at the same inheritance depth do." + )] + AmbiguousMetadata, + #[error("No description was found for the schema.")] + MissingDescription, +} + impl ClosedDataType { + /// Creates a closed data type from a resolved data type. + /// + /// # Errors + /// + /// Returns an error if + /// - The metadata (such as description or label) is ambiguous, e.g. when two parents at the + /// same inheritance depth specify different metadata. + /// - No description was found for the schema or any parent schema. + pub fn from_resolve_data( + data_type: DataType, + resolve_data: &DataTypeResolveData, + ) -> Result { + let (description, label) = if data_type.description.is_some() || !data_type.label.is_empty() + { + (data_type.description, data_type.label) + } else { + resolve_data + .find_metadata_schema()? + .map(|schema| (schema.description.clone(), schema.label.clone())) + .unwrap_or_default() + }; + + Ok(Self { + id: data_type.id.clone(), + title: data_type.title.clone(), + 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(), + r#abstract: data_type.r#abstract, + }) + } +} + +impl ResolvedDataType { #[must_use] pub fn data_type(&self) -> &Valid { // Valid closed schemas imply that the schema is valid @@ -73,22 +148,452 @@ impl<'a> FromSql<'a> for InheritanceDepth { } } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct DataTypeInheritanceData { - pub inheritance_depths: HashMap, +#[derive(Debug, Default, Clone)] +pub struct DataTypeResolveData { + inheritance_depths: HashMap)>, } -impl DataTypeInheritanceData { - pub fn add_edge(&mut self, edge: DataTypeEdge, target: DataTypeId, depth: u16) { +impl DataTypeResolveData { + pub fn add_edge( + &mut self, + edge: DataTypeEdge, + target: Arc, + target_id: DataTypeId, + depth: u16, + ) { + let depth = InheritanceDepth::new(depth); match edge { - DataTypeEdge::Inheritance => match self.inheritance_depths.entry(target) { + DataTypeEdge::Inheritance => match self.inheritance_depths.entry(target_id) { Entry::Occupied(mut entry) => { - *entry.get_mut() = InheritanceDepth::new(cmp::min(depth, entry.get().inner())); + entry.get_mut().0 = cmp::min(depth, entry.get().0); } Entry::Vacant(entry) => { - entry.insert(InheritanceDepth::new(depth)); + entry.insert((depth, target)); } }, } } + + pub fn extend_edges(&mut self, depth_offset: u16, other: &Self) { + for (target_id, (relative_depth, schema)) in &other.inheritance_depths { + let absolut_depth = InheritanceDepth::new(relative_depth.inner() + depth_offset); + match self.inheritance_depths.entry(*target_id) { + Entry::Occupied(mut entry) => { + entry.get_mut().0 = cmp::min(absolut_depth, entry.get().0); + } + Entry::Vacant(entry) => { + entry.insert((absolut_depth, Arc::clone(schema))); + } + } + } + } + + pub fn inheritance_depths(&self) -> impl Iterator { + self.inheritance_depths + .iter() + .map(|(id, (depth, _))| (*id, *depth)) + } + + /// Returns an iterator over the schemas ordered by inheritance depth and data type id. + fn ordered_schemas(&self) -> impl Iterator { + // TODO: Construct the sorted list on the fly when constructing this struct + self.inheritance_depths + .iter() + .sorted_by_key(|(data_type_id, (depth, _))| (*depth, data_type_id.into_uuid())) + .map(|(_, (depth, schema))| (*depth, &**schema)) + } + + /// Resolves the metadata schema for the data type. + /// + /// # Errors + /// + /// 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> { + 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() { + if let Some((found_depth, found_schema)) = found_schema_data { + match depth.cmp(&found_depth) { + cmp::Ordering::Less => { + found_schema_data = Some((depth, found_schema)); + } + cmp::Ordering::Equal => { + if stored_schema.description != found_schema.description + || stored_schema.label != found_schema.label + { + return Err(ResolveClosedDataTypeError::AmbiguousMetadata); + } + } + cmp::Ordering::Greater => { + // We have covered all schemas with an inheritance depth less than the + // current schema, so we can break early + break; + } + } + } else { + found_schema_data = Some((depth, stored_schema)); + } + } + } + + Ok(found_schema_data.map(|(_, schema)| schema)) + } + + pub fn constraints(&self) -> impl Iterator { + self.ordered_schemas() + .map(|(_, schema)| &schema.constraints) + } +} + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + + use itertools::Itertools; + use serde_json::json; + + use crate::{ + schema::{ClosedDataType, DataType, DataTypeId, DataTypeValidator, OntologyTypeResolver}, + utils::tests::{JsonEqualityCheck, ensure_validation, ensure_validation_from_str}, + }; + + struct DataTypeDefinitions { + value: DataType, + number: DataType, + integer: DataType, + unsigned: DataType, + unsigned_int: DataType, + small: DataType, + unsigned_small_int: DataType, + } + + #[expect(clippy::too_many_lines, reason = "Test seeding")] + async fn seed() -> DataTypeDefinitions { + let value = ensure_validation_from_str::( + graph_test_data::data_type::VALUE_V1, + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + let number = ensure_validation_from_str::( + graph_test_data::data_type::NUMBER_V1, + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + let integer = ensure_validation::( + json!({ + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "$id": "https://example.com/data-type/integer/v/1", + "title": "Integer", + "allOf": [{ "$ref": number.id }], + "type": "number", + "abstract": false, + "multipleOf": 1.0, + }), + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + let unsigned = ensure_validation::( + json!({ + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "$id": "https://example.com/data-type/unsigned/v/1", + "title": "Unsigned", + "allOf": [{ "$ref": number.id }], + "type": "number", + "abstract": false, + "minimum": 0.0, + }), + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + let unsigned_int = ensure_validation::( + json!({ + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "$id": "https://example.com/data-type/unsigned-int/v/1", + "title": "Unsigned Integer", + "allOf": [{ "$ref": integer.id }, { "$ref": unsigned.id }], + "type": "number", + "maximum": 4_294_967_295.0, + "abstract": false, + }), + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + let small = ensure_validation::( + json!({ + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "$id": "https://example.com/data-type/very-small/v/1", + "title": "Small number", + "description": "A small number", + "allOf": [{ "$ref": number.id }], + "type": "number", + "maximum": 255.0, + "abstract": false, + }), + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + let unsigned_small_int = ensure_validation::( + json!({ + "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", + "kind": "dataType", + "$id": "https://example.com/data-type/unsigned-small-int/v/1", + "title": "Unsigned Integer", + "allOf": [{ "$ref": unsigned_int.id }, { "$ref": small.id }], + "type": "number", + "maximum": 100.0, + "abstract": false, + }), + DataTypeValidator, + JsonEqualityCheck::Yes, + ) + .await + .into_inner(); + + DataTypeDefinitions { + value, + number, + integer, + unsigned, + unsigned_int, + small, + unsigned_small_int, + } + } + + fn check_closed_value(value: &ClosedDataType, defs: &DataTypeDefinitions) { + assert_eq!(value.id, defs.value.id); + assert_eq!(value.title, defs.value.title); + assert_eq!(value.title_plural, defs.value.title_plural); + assert_eq!( + value.description, + defs.value + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(value.label, defs.value.label); + assert_eq!(value.r#abstract, defs.value.r#abstract); + assert_eq!(json!(value.all_of), json!([defs.value.constraints])); + } + + fn check_closed_number(number: &ClosedDataType, defs: &DataTypeDefinitions) { + assert_eq!(number.id, defs.number.id); + assert_eq!(number.title, defs.number.title); + assert_eq!(number.title_plural, defs.number.title_plural); + assert_eq!( + number.description, + defs.number + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(number.label, defs.number.label); + assert_eq!(number.r#abstract, defs.number.r#abstract); + assert_eq!( + json!(number.all_of), + json!([defs.number.constraints, defs.value.constraints]) + ); + } + + fn check_closed_integer(integer: &ClosedDataType, defs: &DataTypeDefinitions) { + assert_eq!(integer.id, defs.integer.id); + assert_eq!(integer.title, defs.integer.title); + assert_eq!(integer.title_plural, defs.integer.title_plural); + assert_eq!( + integer.description, + defs.number + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(integer.label, defs.number.label); + assert_eq!(integer.r#abstract, defs.integer.r#abstract); + assert_eq!( + json!(integer.all_of), + json!([ + defs.integer.constraints, + defs.number.constraints, + defs.value.constraints + ]) + ); + } + + fn check_closed_unsigned(unsigned: &ClosedDataType, defs: &DataTypeDefinitions) { + assert_eq!(unsigned.id, defs.unsigned.id); + assert_eq!(unsigned.title, defs.unsigned.title); + assert_eq!(unsigned.title_plural, defs.unsigned.title_plural); + assert_eq!( + unsigned.description, + defs.number + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(unsigned.label, defs.number.label); + assert_eq!(unsigned.r#abstract, defs.unsigned.r#abstract); + assert_eq!( + json!(unsigned.all_of), + json!([ + defs.unsigned.constraints, + defs.number.constraints, + defs.value.constraints + ]) + ); + } + + fn check_closed_unsigned_int(unsigned_int: &ClosedDataType, defs: &DataTypeDefinitions) { + assert_eq!(unsigned_int.id, defs.unsigned_int.id); + assert_eq!(unsigned_int.title, defs.unsigned_int.title); + assert_eq!(unsigned_int.title_plural, defs.unsigned_int.title_plural); + assert_eq!( + unsigned_int.description, + defs.number + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(unsigned_int.label, defs.number.label); + assert_eq!(unsigned_int.r#abstract, defs.unsigned_int.r#abstract); + assert_eq!( + json!(unsigned_int.all_of), + json!([ + defs.unsigned_int.constraints, + defs.unsigned.constraints, + defs.integer.constraints, + defs.number.constraints, + defs.value.constraints + ]) + ); + } + + fn check_closed_small(small: &ClosedDataType, defs: &DataTypeDefinitions) { + assert_eq!(small.id, defs.small.id); + assert_eq!(small.title, defs.small.title); + assert_eq!(small.title_plural, defs.small.title_plural); + assert_eq!( + small.description, + defs.small + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(small.label, defs.small.label); + assert_eq!(small.r#abstract, defs.small.r#abstract); + assert_eq!( + json!(small.all_of), + json!([ + defs.small.constraints, + defs.number.constraints, + defs.value.constraints + ]) + ); + } + + fn check_closed_unsigned_small_int( + unsigned_small_int: &ClosedDataType, + defs: &DataTypeDefinitions, + ) { + assert_eq!(unsigned_small_int.id, defs.unsigned_small_int.id); + assert_eq!(unsigned_small_int.title, defs.unsigned_small_int.title); + assert_eq!( + unsigned_small_int.title_plural, + defs.unsigned_small_int.title_plural + ); + assert_eq!( + unsigned_small_int.description, + defs.small + .description + .as_deref() + .expect("Missing description") + ); + assert_eq!(unsigned_small_int.label, defs.small.label); + assert_eq!( + unsigned_small_int.r#abstract, + defs.unsigned_small_int.r#abstract + ); + assert_eq!( + json!(unsigned_small_int.all_of), + json!([ + defs.unsigned_small_int.constraints, + defs.small.constraints, + defs.unsigned_int.constraints, + defs.unsigned.constraints, + defs.integer.constraints, + defs.number.constraints, + defs.value.constraints + ]) + ); + } + + #[tokio::test] + async fn resolve() { + let defs = seed().await; + + let permutations = [ + ( + defs.value.clone(), + check_closed_value as fn(&ClosedDataType, &DataTypeDefinitions), + ), + (defs.number.clone(), check_closed_number), + (defs.integer.clone(), check_closed_integer), + (defs.unsigned.clone(), check_closed_unsigned), + (defs.unsigned_int.clone(), check_closed_unsigned_int), + (defs.small.clone(), check_closed_small), + ( + defs.unsigned_small_int.clone(), + check_closed_unsigned_small_int, + ), + ]; + + for definitions in permutations.iter().permutations(permutations.len()) { + let mut resolver = OntologyTypeResolver::default(); + for definition in &definitions { + resolver.add_unresolved( + DataTypeId::from_url(&definition.0.id), + Arc::new(definition.0.clone()), + ); + } + + for (data_type, check) in definitions { + let closed = ClosedDataType::from_resolve_data( + data_type.clone(), + &resolver + .resolve_data_type_metadata(DataTypeId::from_url(&data_type.id)) + .unwrap_or_else(|error| { + panic!("Failed to resolve {}: {error:?}", data_type.id) + }), + ) + .unwrap_or_else(|error| { + panic!( + "Failed to create closed data type for {}: {error:?}", + data_type.id + ) + }); + check(&closed, &defs); + } + } + } } 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 1d50d648b86..34e26dbc357 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 @@ -2,7 +2,7 @@ mod constraint; mod conversion; pub use self::{ - closed::{ClosedDataType, DataTypeInheritanceData, InheritanceDepth}, + closed::{ClosedDataType, DataTypeResolveData, InheritanceDepth, ResolvedDataType}, constraint::{ AnyOfConstraints, ArrayConstraints, ArraySchema, ArrayTypeTag, ArrayValidationError, BooleanTypeTag, ConstraintError, NullTypeTag, NumberConstraints, NumberSchema, @@ -23,7 +23,7 @@ mod closed; mod reference; mod validation; -use alloc::sync::Arc; +use alloc::{collections::BTreeSet, sync::Arc}; use core::{fmt, mem}; use std::collections::{HashMap, HashSet}; @@ -139,16 +139,34 @@ pub enum DataTypeSchemaTag { V3, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ValueSchemaMetadata { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "ValueLabel::is_empty")] + pub label: ValueLabel, +} + +impl ValueSchemaMetadata { + #[must_use] + pub const fn is_empty(&self) -> bool { + self.description.is_none() && self.label.is_empty() + } +} + mod raw { + use alloc::collections::BTreeSet; use std::collections::HashSet; use serde::{Deserialize, Serialize}; - use super::{DataTypeSchemaTag, DataTypeTag}; + use super::{DataTypeSchemaTag, DataTypeTag, ValueSchemaMetadata}; use crate::{ schema::{ ArrayTypeTag, BooleanTypeTag, DataTypeReference, NullTypeTag, NumberTypeTag, - ObjectTypeTag, StringTypeTag, ValueLabel, + ObjectTypeTag, StringTypeTag, data_type::constraint::{ AnyOfConstraints, ArrayConstraints, ArraySchema, NumberConstraints, NumberSchema, SingleValueConstraints, StringConstraints, StringSchema, TupleConstraints, @@ -161,7 +179,7 @@ mod raw { #[derive(Serialize, Deserialize)] #[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] #[serde(rename_all = "camelCase", deny_unknown_fields)] - pub struct ValueSchemaMetadata { + pub struct DataTypeBase { #[serde(rename = "$schema")] schema: DataTypeSchemaTag, kind: DataTypeTag, @@ -170,12 +188,8 @@ mod raw { title: String, #[serde(default, skip_serializing_if = "Option::is_none")] title_plural: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, - #[serde(default, skip_serializing_if = "ValueLabel::is_empty")] - label: ValueLabel, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - all_of: Vec, + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + all_of: BTreeSet, #[serde(default)] r#abstract: bool, @@ -187,73 +201,97 @@ mod raw { Null { r#type: NullTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, }, Boolean { r#type: BooleanTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, }, Number { r#type: NumberTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, #[serde(flatten)] constraints: NumberConstraints, }, NumberConst { r#type: NumberTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, r#const: f64, }, NumberEnum { r#type: NumberTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, r#enum: Vec, }, String { r#type: StringTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, #[serde(flatten)] constraints: StringConstraints, }, StringConst { r#type: StringTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, r#const: String, }, StringEnum { r#type: StringTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, r#enum: HashSet, }, Object { r#type: ObjectTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, }, Array { r#type: ArrayTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, #[serde(flatten)] constraints: ArrayConstraints, }, Tuple { r#type: ArrayTypeTag, #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, #[serde(flatten)] constraints: TupleConstraints, }, AnyOf { #[serde(flatten)] - common: ValueSchemaMetadata, + base: DataTypeBase, + #[serde(flatten)] + metadata: ValueSchemaMetadata, #[serde(flatten)] constraints: AnyOfConstraints, }, @@ -272,9 +310,11 @@ mod raw { enum DataType { Schema { #[serde(flatten)] - common: ValueSchemaMetadata, + common: DataTypeBase, #[serde(flatten)] constraints: ValueConstraints, + #[serde(flatten)] + metadata: ValueSchemaMetadata, }, } } @@ -288,115 +328,147 @@ mod raw { little benefit." )] fn from(value: DataType) -> Self { - let (common, constraints) = match value { - DataType::Null { r#type: _, common } => ( - common, + let (base, metadata, constraints) = match value { + DataType::Null { + r#type: _, + base, + metadata, + } => ( + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Null), ), - DataType::Boolean { r#type: _, common } => ( - common, + DataType::Boolean { + r#type: _, + base, + metadata, + } => ( + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Boolean), ), DataType::Number { r#type: _, - common, + base, + metadata, constraints, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Number( NumberSchema::Constrained(constraints), )), ), DataType::NumberConst { r#type: _, - common, + base, + metadata, r#const, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Number(NumberSchema::Const { r#const, })), ), DataType::NumberEnum { r#type: _, - common, + base, + metadata, r#enum, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Number(NumberSchema::Enum { r#enum, })), ), DataType::String { r#type: _, - common, + base, + metadata, constraints, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::String( StringSchema::Constrained(constraints), )), ), DataType::StringConst { r#type: _, - common, + base, + metadata, r#const, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::String(StringSchema::Const { r#const, })), ), DataType::StringEnum { r#type: _, - common, + base, + metadata, r#enum, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::String(StringSchema::Enum { r#enum, })), ), - DataType::Object { r#type: _, common } => ( - common, + DataType::Object { + r#type: _, + base, + metadata, + } => ( + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Object), ), DataType::Array { r#type: _, - common, + base, + metadata, constraints, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Array( ArraySchema::Constrained(constraints), )), ), DataType::Tuple { r#type: _, - common, + base, + metadata, constraints, } => ( - common, + base, + metadata, ValueConstraints::Typed(SingleValueConstraints::Array(ArraySchema::Tuple( constraints, ))), ), DataType::AnyOf { - common, + base, + metadata, constraints: any_of, - } => (common, ValueConstraints::AnyOf(any_of)), + } => (base, metadata, ValueConstraints::AnyOf(any_of)), }; Self { - schema: common.schema, - kind: common.kind, - id: common.id, - title: common.title, - title_plural: common.title_plural, - description: common.description, - label: common.label, - all_of: common.all_of, - r#abstract: common.r#abstract, + schema: base.schema, + kind: base.kind, + id: base.id, + title: base.title, + title_plural: base.title_plural, + description: metadata.description, + label: metadata.label, + all_of: base.all_of, + r#abstract: base.r#abstract, constraints, } } @@ -418,8 +490,10 @@ pub struct DataType { pub description: Option, #[serde(default, skip_serializing_if = "ValueLabel::is_empty")] pub label: ValueLabel, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub all_of: Vec, + // Lexicographically ordered so we have a deterministic order for inheriting parent + // schemas. + #[serde(skip_serializing_if = "BTreeSet::is_empty")] + pub all_of: BTreeSet, pub r#abstract: bool, #[serde(flatten)] @@ -451,10 +525,10 @@ pub enum DataTypeResolveError { MissingDataTypes, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug)] struct DataTypeCacheEntry { - pub data_type: Arc, - pub metadata: Option>, + data_type: Arc, + resolve_data: Option>, } #[derive(Debug, Default)] @@ -463,7 +537,7 @@ pub struct OntologyTypeResolver { } impl OntologyTypeResolver { - pub fn add_open(&mut self, data_type_id: DataTypeId, data_type: Arc) { + pub fn add_unresolved(&mut self, data_type_id: DataTypeId, data_type: Arc) { debug_assert_eq!( data_type_id, DataTypeId::from_url(&data_type.id), @@ -471,9 +545,9 @@ impl OntologyTypeResolver { ); self.data_types .entry(data_type_id) - .or_insert_with(|| DataTypeCacheEntry { + .or_insert(DataTypeCacheEntry { data_type, - metadata: None, + resolve_data: None, }); } @@ -481,32 +555,25 @@ impl OntologyTypeResolver { &mut self, data_type_id: DataTypeId, data_type: Arc, - metadata: Arc, + metadata: Arc, ) { - debug_assert_eq!( - data_type_id, - DataTypeId::from_url(&data_type.id), - "The data type ID must match the URL" - ); - self.data_types - .insert(DataTypeId::from_url(&data_type.id), DataTypeCacheEntry { - data_type, - metadata: Some(metadata), - }); + self.data_types.insert(data_type_id, DataTypeCacheEntry { + data_type, + resolve_data: Some(metadata), + }); } - pub fn update_metadata( + fn close( &mut self, data_type_id: DataTypeId, - metadata: Arc, - ) -> Option> { - self.data_types + metadata: Arc, + ) -> Result<(), DataTypeResolveError> { + let data_type_entry = self + .data_types .get_mut(&data_type_id) - .map(|entry| Arc::clone(entry.metadata.insert(metadata))) - } - - fn get(&self, id: DataTypeId) -> Option<&DataTypeCacheEntry> { - self.data_types.get(&id) + .ok_or(DataTypeResolveError::UnknownDataTypeId)?; + data_type_entry.resolve_data = Some(metadata); + Ok(()) } /// Resolves the metadata for the given data types. @@ -520,37 +587,27 @@ impl OntologyTypeResolver { pub fn resolve_data_type_metadata( &mut self, data_type_id: DataTypeId, - ) -> Result, Report> { - let Some(data_type_entry) = self.get(data_type_id) else { + ) -> Result, Report> { + let Some(data_type_entry) = self.data_types.get(&data_type_id) else { bail!(DataTypeResolveError::UnknownDataTypeId); }; - if let Some(metadata) = &data_type_entry.metadata { - // If the metadata is already resolved, we can return it immediately. - return Ok(Arc::clone(metadata)); - } + let data_type = Arc::clone(&data_type_entry.data_type); // We add all requested types to the cache to ensure that we can resolve all types. The // cache will be updated with the resolved metadata. We extract the IDs so that we can // resolve the metadata in the correct order. // Double buffering is used to avoid unnecessary allocations. let mut data_types_to_resolve = Vec::new(); - let mut next_data_types_to_resolve = vec![Arc::clone(&data_type_entry.data_type)]; + let mut next_data_types_to_resolve = vec![data_type]; // We keep a list of all schemas that are missing from the cache. If we encounter a schema // that is not in the cache, we add it to this list. If we are unable to resolve all // schemas, we return an error with this list. let mut missing_schemas = HashSet::new(); - // We also keep a list of all schemas that we already processed. This is used to prevent - // infinite loops in the inheritance chain. New values are added to this list as we add new - // schemas to resolve. - let mut processed_schemas = HashSet::from([data_type_id]); - // The currently closed schema being resolved. This can be used later to resolve - let mut in_progress_schema = DataTypeInheritanceData { - inheritance_depths: HashMap::new(), - }; + let mut in_progress_schema = DataTypeResolveData::default(); let mut current_depth = 0; while !next_data_types_to_resolve.is_empty() { @@ -561,16 +618,8 @@ impl OntologyTypeResolver { See https://github.com/rust-lang/rust-clippy/issues/8539" )] for data_type in data_types_to_resolve.drain(..) { - let data_type_id = DataTypeId::from_url(&data_type.id); for (data_type_reference, edge) in data_type.data_type_references() { let data_type_reference_id = DataTypeId::from_url(&data_type_reference.url); - if processed_schemas.contains(&data_type_reference_id) { - // We ignore the already processed schemas to prevent infinite loops. - continue; - } - - in_progress_schema.add_edge(edge, data_type_reference_id, current_depth); - processed_schemas.insert(data_type_reference_id); let Some(data_type_entry) = self.data_types.get(&data_type_reference_id) else { // If the data type is not in the cache, we add it to the list of missing @@ -579,21 +628,16 @@ impl OntologyTypeResolver { continue; }; - if let Some(metadata) = &data_type_entry.metadata { - // If the metadata is already resolved, we can reuse it. - for (data_type_ref, depth) in &metadata.inheritance_depths { - if data_type_id != *data_type_ref { - in_progress_schema.add_edge( - edge, - *data_type_ref, - depth.inner() + current_depth + 1, - ); - } - } + in_progress_schema.add_edge( + edge, + Arc::clone(&data_type_entry.data_type), + data_type_reference_id, + current_depth, + ); + + if let Some(resolve_data) = &data_type_entry.resolve_data { + in_progress_schema.extend_edges(current_depth + 1, resolve_data); } else { - // We encountered a schema that we haven't resolved yet. We add it to the - // list of schemas to find and update the inheritance depth of the current - // type. next_data_types_to_resolve.push(Arc::clone(&data_type_entry.data_type)); } } @@ -605,58 +649,15 @@ impl OntologyTypeResolver { if missing_schemas.is_empty() { // We create the resolved metadata for the current data type and update the cache so // that we don't need to resolve it again. - Ok(self - .update_metadata(data_type_id, Arc::new(in_progress_schema)) - .unwrap_or_else(|| { - unreachable!( - "The data type was removed from the cache while resolving the metadata" - ) - })) + let in_progress_schema = Arc::new(in_progress_schema); + self.close(data_type_id, Arc::clone(&in_progress_schema))?; + Ok(in_progress_schema) } else { Err(Report::from(DataTypeResolveError::MissingSchemas { schemas: missing_schemas, })) } } - - /// Returns the closed data type for the given data type. - /// - /// This method returns the closed data type for the given data type. The closed data type - /// includes the schema of the data type and all its parents. - /// - /// # Errors - /// - /// Returns an error if the closed data type could not be resolved. - pub fn get_closed_data_type( - &self, - data_type_id: DataTypeId, - ) -> Result> { - let Some(data_type_entry) = self.get(data_type_id) else { - bail!(DataTypeResolveError::UnknownDataTypeId); - }; - - let metadata = data_type_entry - .metadata - .as_ref() - .ok_or_else(|| DataTypeResolveError::MissingClosedDataType)?; - - Ok(ClosedDataType { - schema: Arc::clone(&data_type_entry.data_type), - definitions: metadata - .inheritance_depths - .keys() - .map(|&id| { - let definition_entry = self - .get(id) - .ok_or(DataTypeResolveError::MissingClosedDataType)?; - Ok(( - definition_entry.data_type.id.clone(), - Arc::clone(&definition_entry.data_type), - )) - }) - .collect::>()?, - }) - } } impl DataType { diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/reference.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/reference.rs index 210b23ab5d0..9c6319b812c 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/reference.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/reference.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::url::VersionedUrl; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] #[serde(deny_unknown_fields)] #[repr(transparent)] diff --git a/libs/@blockprotocol/type-system/rust/src/schema/data_type/validation.rs b/libs/@blockprotocol/type-system/rust/src/schema/data_type/validation.rs index 42e2464f852..89dc629f263 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/data_type/validation.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/data_type/validation.rs @@ -6,7 +6,7 @@ use thiserror::Error; use crate::{ Valid, Validator, - schema::{ClosedDataType, DataType, DataTypeReference}, + schema::{ClosedDataType, DataType, DataTypeReference, ResolvedDataType}, url::VersionedUrl, }; @@ -80,13 +80,13 @@ impl Validator for DataTypeValidator { } } -impl Validator for DataTypeValidator { +impl Validator for DataTypeValidator { type Error = ValidateDataTypeError; async fn validate_ref<'v>( &self, - value: &'v ClosedDataType, - ) -> Result<&'v Valid, Self::Error> { + value: &'v ResolvedDataType, + ) -> Result<&'v Valid, Self::Error> { let mut checked_types = HashSet::new(); let mut types_to_check = value .schema @@ -115,3 +115,16 @@ impl Validator for DataTypeValidator { Ok(Valid::new_ref_unchecked(value)) } } + +impl Validator for DataTypeValidator { + type Error = ValidateDataTypeError; + + async fn validate_ref<'v>( + &self, + value: &'v ClosedDataType, + ) -> Result<&'v Valid, Self::Error> { + // TODO: Validate ontology types on creation + // see https://linear.app/hash/issue/H-2976/validate-ontology-types-on-creation + Ok(Valid::new_ref_unchecked(value)) + } +} diff --git a/libs/@blockprotocol/type-system/rust/src/schema/mod.rs b/libs/@blockprotocol/type-system/rust/src/schema/mod.rs index bfa3d56bf9f..d282fd3d4e6 100644 --- a/libs/@blockprotocol/type-system/rust/src/schema/mod.rs +++ b/libs/@blockprotocol/type-system/rust/src/schema/mod.rs @@ -18,13 +18,13 @@ pub use self::{ data_type::{ AnyOfConstraints, ArrayConstraints, ArraySchema, ArrayTypeTag, ArrayValidationError, BooleanTypeTag, ClosedDataType, ConstraintError, ConversionDefinition, - ConversionExpression, ConversionValue, Conversions, DataType, DataTypeId, - DataTypeInheritanceData, DataTypeReference, DataTypeValidator, InheritanceDepth, + ConversionExpression, ConversionValue, Conversions, DataType, DataTypeEdge, DataTypeId, + DataTypeReference, DataTypeResolveData, DataTypeValidator, InheritanceDepth, JsonSchemaValueType, NullTypeTag, NumberConstraints, NumberSchema, NumberTypeTag, - NumberValidationError, ObjectTypeTag, OntologyTypeResolver, Operator, + NumberValidationError, ObjectTypeTag, OntologyTypeResolver, Operator, ResolvedDataType, SingleValueConstraints, SingleValueSchema, StringConstraints, StringFormat, StringFormatError, StringSchema, StringTypeTag, StringValidationError, TupleConstraints, - ValidateDataTypeError, ValueLabel, Variable, + ValidateDataTypeError, ValueLabel, ValueSchemaMetadata, Variable, }, entity_type::{ ClosedEntityType, ClosedEntityTypeSchemaData, EntityType, EntityTypeReference,