diff --git a/apps/hash-graph/libs/graph/src/store/validation.rs b/apps/hash-graph/libs/graph/src/store/validation.rs index 20035a47ea9..47bf6e68c9e 100644 --- a/apps/hash-graph/libs/graph/src/store/validation.rs +++ b/apps/hash-graph/libs/graph/src/store/validation.rs @@ -160,6 +160,8 @@ where C: AsClient, A: AuthorizationApi, { + type Value = Arc; + #[expect(refining_impl_trait)] async fn provide_type( &self, @@ -345,6 +347,8 @@ where C: AsClient, A: AuthorizationApi, { + type Value = Arc; + #[expect(refining_impl_trait)] async fn provide_type( &self, @@ -464,6 +468,8 @@ where C: AsClient, A: AuthorizationApi, { + type Value = Arc; + #[expect(refining_impl_trait)] async fn provide_type( &self, diff --git a/apps/hash-graph/libs/store/src/filter/mod.rs b/apps/hash-graph/libs/store/src/filter/mod.rs index 8cb7e458204..e8082170a70 100644 --- a/apps/hash-graph/libs/store/src/filter/mod.rs +++ b/apps/hash-graph/libs/store/src/filter/mod.rs @@ -519,6 +519,8 @@ mod tests { #[expect(refining_impl_trait)] impl OntologyTypeProvider for TestDataTypeProvider { + type Value = DataTypeWithMetadata; + async fn provide_type(&self, _: &VersionedUrl) -> Result> { unimplemented!() } 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 c78d1e5e1de..fa61374ccb8 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 @@ -132,6 +132,9 @@ mod raw { )] #[serde(default, skip_serializing_if = "Vec::is_empty")] all_of: Vec, + + #[serde(default)] + r#abstract: bool, } #[derive(Deserialize)] @@ -194,6 +197,7 @@ mod raw { title: common.title, title_plural: common.title_plural, all_of: common.all_of, + r#abstract: common.r#abstract, constraints, } } @@ -214,6 +218,7 @@ pub struct DataType { #[serde(skip_serializing_if = "Vec::is_empty")] pub all_of: Vec, + pub r#abstract: bool, #[serde(flatten)] pub constraints: ValueConstraints, } diff --git a/libs/@local/hash-graph-types/rust/src/knowledge/property/visitor.rs b/libs/@local/hash-graph-types/rust/src/knowledge/property/visitor.rs index 9c5d937c8bf..5d6d9e16670 100644 --- a/libs/@local/hash-graph-types/rust/src/knowledge/property/visitor.rs +++ b/libs/@local/hash-graph-types/rust/src/knowledge/property/visitor.rs @@ -60,6 +60,8 @@ pub enum TraversalError { actual: VersionedUrl, expected: VersionedUrl, }, + #[error("Values cannot be assigned to an abstract data type. `{id}` is abstract.")] + AbstractDataType { id: VersionedUrl }, #[error("the value provided does not match the constraints of the data type")] ConstraintUnfulfilled, #[error("the property `{key}` was required, but not specified")] diff --git a/libs/@local/hash-graph-types/rust/src/lib.rs b/libs/@local/hash-graph-types/rust/src/lib.rs index 0c4f5f2cf6d..af7ece07d02 100644 --- a/libs/@local/hash-graph-types/rust/src/lib.rs +++ b/libs/@local/hash-graph-types/rust/src/lib.rs @@ -2,7 +2,6 @@ #![feature(hash_raw_entry)] extern crate alloc; -extern crate core; pub mod knowledge; pub mod ontology; diff --git a/libs/@local/hash-graph-types/rust/src/ontology/mod.rs b/libs/@local/hash-graph-types/rust/src/ontology/mod.rs index d2009a13f9b..0afcc580e8a 100644 --- a/libs/@local/hash-graph-types/rust/src/ontology/mod.rs +++ b/libs/@local/hash-graph-types/rust/src/ontology/mod.rs @@ -169,10 +169,12 @@ pub struct OntologyTypeWithMetadata { } pub trait OntologyTypeProvider { + type Value: Borrow + Send; + fn provide_type( &self, type_id: &VersionedUrl, - ) -> impl Future + Send, Report>> + Send; + ) -> impl Future>> + Send; } pub trait DataTypeProvider: OntologyTypeProvider { diff --git a/libs/@local/hash-validation/src/entity_type.rs b/libs/@local/hash-validation/src/entity_type.rs index d109e0367aa..90f8af6352c 100644 --- a/libs/@local/hash-validation/src/entity_type.rs +++ b/libs/@local/hash-validation/src/entity_type.rs @@ -267,17 +267,30 @@ impl EntityVisitor for ValueValidator { &mut self, data_type: &DataTypeWithMetadata, value: &mut JsonValue, - _: &mut ValueMetadata, + metadata: &mut ValueMetadata, _: &P, ) -> Result<(), Report<[TraversalError]>> where P: DataTypeProvider + Sync, { - data_type - .schema - .validate_constraints(value) - .change_context(TraversalError::ConstraintUnfulfilled) - .map_err(Report::expand) + let mut status = ReportSink::new(); + + status.attempt( + data_type + .schema + .validate_constraints(value) + .change_context(TraversalError::ConstraintUnfulfilled), + ); + + if metadata.data_type_id.as_ref() == Some(&data_type.schema.id) + && data_type.schema.r#abstract + { + status.capture(TraversalError::AbstractDataType { + id: data_type.schema.id.clone(), + }); + } + + status.finish() } } @@ -312,20 +325,20 @@ impl EntityVisitor for EntityPreprocessor { }); } - if let Err(error) = type_provider + let desired_data_type = type_provider .provide_type(data_type_url) .await .change_context_lazy(|| TraversalError::DataTypeRetrieval { id: DataTypeReference { url: data_type.schema.id.clone(), }, - })? - .borrow() - .schema - .validate_constraints(value) - .change_context(TraversalError::ConstraintUnfulfilled) + })?; + + if let Err(error) = ValueValidator + .visit_value(desired_data_type.borrow(), value, metadata, type_provider) + .await { - status.capture(error); + status.append(error); } } } else { diff --git a/libs/@local/hash-validation/src/lib.rs b/libs/@local/hash-validation/src/lib.rs index 28b436f4ed2..4816bf807b3 100644 --- a/libs/@local/hash-validation/src/lib.rs +++ b/libs/@local/hash-validation/src/lib.rs @@ -1,6 +1,8 @@ #![feature(extend_one)] #![feature(hash_raw_entry)] +extern crate alloc; + pub mod error; pub use self::entity_type::{EntityPreprocessor, EntityValidationError}; @@ -92,6 +94,7 @@ pub trait EntityProvider { #[cfg(test)] mod tests { + use alloc::sync::Arc; use std::collections::HashMap; use graph_types::{ @@ -150,9 +153,9 @@ mod tests { struct Provider { entities: HashMap, - entity_types: HashMap, - property_types: HashMap, - data_types: HashMap, + entity_types: HashMap>, + property_types: HashMap>, + data_types: HashMap>, } impl Provider { fn new( @@ -166,14 +169,22 @@ mod tests { .into_iter() .map(|entity| (entity.metadata.record_id.entity_id, entity)) .collect(), - entity_types: entity_types.into_iter().collect(), + entity_types: entity_types + .into_iter() + .map(|(url, schema)| (url, Arc::new(schema))) + .collect(), property_types: property_types .into_iter() - .map(|schema| (schema.id.clone(), schema)) + .map(|schema| (schema.id.clone(), Arc::new(schema))) .collect(), data_types: data_types .into_iter() - .map(|schema| (schema.id.clone(), generate_data_type_metadata(schema))) + .map(|schema| { + ( + schema.id.clone(), + Arc::new(generate_data_type_metadata(schema)), + ) + }) .collect(), } } @@ -232,42 +243,54 @@ mod tests { } impl OntologyTypeProvider for Provider { + type Value = Arc; + #[expect(refining_impl_trait)] async fn provide_type( &self, type_id: &VersionedUrl, - ) -> Result<&ClosedEntityType, Report> { - self.entity_types.get(type_id).ok_or_else(|| { - Report::new(InvalidEntityType { - id: type_id.clone(), + ) -> Result, Report> { + self.entity_types + .get(type_id) + .map(Arc::clone) + .ok_or_else(|| { + Report::new(InvalidEntityType { + id: type_id.clone(), + }) }) - }) } } impl OntologyTypeProvider for Provider { + type Value = Arc; + #[expect(refining_impl_trait)] async fn provide_type( &self, type_id: &VersionedUrl, - ) -> Result<&PropertyType, Report> { - self.property_types.get(type_id).ok_or_else(|| { - Report::new(InvalidPropertyType { - id: type_id.clone(), + ) -> Result, Report> { + self.property_types + .get(type_id) + .map(Arc::clone) + .ok_or_else(|| { + Report::new(InvalidPropertyType { + id: type_id.clone(), + }) }) - }) } } impl PropertyTypeProvider for Provider {} impl OntologyTypeProvider for Provider { + type Value = Arc; + #[expect(refining_impl_trait)] async fn provide_type( &self, type_id: &VersionedUrl, - ) -> Result<&DataTypeWithMetadata, Report> { - self.data_types.get(type_id).ok_or_else(|| { + ) -> Result, Report> { + self.data_types.get(type_id).map(Arc::clone).ok_or_else(|| { Report::new(InvalidDataType { id: type_id.clone(), }) diff --git a/tests/hash-graph-http/tests/ambiguous.http b/tests/hash-graph-http/tests/ambiguous.http index 726fd6a44ac..4a34d01647b 100644 --- a/tests/hash-graph-http/tests/ambiguous.http +++ b/tests/hash-graph-http/tests/ambiguous.http @@ -49,7 +49,8 @@ X-Authenticated-User-Actor-Id: {{account_id}} "title": "Length", "type": "number", "description": "A unit of length", - "minimum": 0 + "minimum": 0, + "abstract": true }, { "$schema": "https://blockprotocol.org/types/modules/graph/0.3/schema/data-type", diff --git a/tests/hash-graph-test-data/rust/src/data_type/boolean.json b/tests/hash-graph-test-data/rust/src/data_type/boolean.json index 0b0dc879588..d7ae2d2c226 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/boolean.json +++ b/tests/hash-graph-test-data/rust/src/data_type/boolean.json @@ -4,5 +4,6 @@ "$id": "https://blockprotocol.org/@blockprotocol/types/data-type/boolean/v/1", "title": "Boolean", "description": "A True or False value", - "type": "boolean" + "type": "boolean", + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/centimeter_v1.json b/tests/hash-graph-test-data/rust/src/data_type/centimeter_v1.json index 7d1e5e08974..43c9332d228 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/centimeter_v1.json +++ b/tests/hash-graph-test-data/rust/src/data_type/centimeter_v1.json @@ -9,5 +9,6 @@ { "$ref": "https://hash.ai/@hash/types/data-type/length/v/1" } - ] + ], + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/centimeter_v2.json b/tests/hash-graph-test-data/rust/src/data_type/centimeter_v2.json index a10c846d433..3ebe56806a5 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/centimeter_v2.json +++ b/tests/hash-graph-test-data/rust/src/data_type/centimeter_v2.json @@ -9,5 +9,6 @@ { "$ref": "https://hash.ai/@hash/types/data-type/length/v/1" } - ] + ], + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/empty_list.json b/tests/hash-graph-test-data/rust/src/data_type/empty_list.json index c2612344f1b..d6b1d26bf29 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/empty_list.json +++ b/tests/hash-graph-test-data/rust/src/data_type/empty_list.json @@ -5,5 +5,6 @@ "title": "Empty List", "description": "An Empty List", "type": "array", - "const": [] + "const": [], + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/length.json b/tests/hash-graph-test-data/rust/src/data_type/length.json index 46edf85010c..01b35f7fbe9 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/length.json +++ b/tests/hash-graph-test-data/rust/src/data_type/length.json @@ -9,5 +9,6 @@ { "$ref": "https://blockprotocol.org/@blockprotocol/types/data-type/number/v/1" } - ] + ], + "abstract": true } diff --git a/tests/hash-graph-test-data/rust/src/data_type/meter.json b/tests/hash-graph-test-data/rust/src/data_type/meter.json index e1c6f1aa2eb..5eb30a74d86 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/meter.json +++ b/tests/hash-graph-test-data/rust/src/data_type/meter.json @@ -9,5 +9,6 @@ { "$ref": "https://hash.ai/@hash/types/data-type/length/v/1" } - ] + ], + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/null.json b/tests/hash-graph-test-data/rust/src/data_type/null.json index 4784ffd0d38..a958b53362a 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/null.json +++ b/tests/hash-graph-test-data/rust/src/data_type/null.json @@ -4,5 +4,6 @@ "$id": "https://blockprotocol.org/@blockprotocol/types/data-type/null/v/1", "title": "Null", "description": "A placeholder value representing 'nothing'", - "type": "null" + "type": "null", + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/number.json b/tests/hash-graph-test-data/rust/src/data_type/number.json index 3f03e57eb7c..255475d44a8 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/number.json +++ b/tests/hash-graph-test-data/rust/src/data_type/number.json @@ -4,5 +4,6 @@ "$id": "https://blockprotocol.org/@blockprotocol/types/data-type/number/v/1", "title": "Number", "description": "An arithmetical value (in the Real number system)", - "type": "number" + "type": "number", + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/object_v1.json b/tests/hash-graph-test-data/rust/src/data_type/object_v1.json index e0b192d412b..f16381b5773 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/object_v1.json +++ b/tests/hash-graph-test-data/rust/src/data_type/object_v1.json @@ -4,5 +4,6 @@ "$id": "https://blockprotocol.org/@blockprotocol/types/data-type/object/v/1", "title": "Object", "description": "A plain JSON object with no pre-defined structure", - "type": "object" + "type": "object", + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/object_v2.json b/tests/hash-graph-test-data/rust/src/data_type/object_v2.json index af03911bd2d..c90f27d0c95 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/object_v2.json +++ b/tests/hash-graph-test-data/rust/src/data_type/object_v2.json @@ -4,5 +4,6 @@ "$id": "https://blockprotocol.org/@blockprotocol/types/data-type/object/v/2", "title": "Object", "description": "An updated plain JSON object with no pre-defined structure", - "type": "object" + "type": "object", + "abstract": false } diff --git a/tests/hash-graph-test-data/rust/src/data_type/text.json b/tests/hash-graph-test-data/rust/src/data_type/text.json index 29ca3c970d2..6fda52ece87 100644 --- a/tests/hash-graph-test-data/rust/src/data_type/text.json +++ b/tests/hash-graph-test-data/rust/src/data_type/text.json @@ -4,5 +4,6 @@ "$id": "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", "title": "Text", "description": "An ordered sequence of characters", - "type": "string" + "type": "string", + "abstract": false }