diff --git a/gts/src/store.rs b/gts/src/store.rs index a2263f7..72bd3b7 100644 --- a/gts/src/store.rs +++ b/gts/src/store.rs @@ -943,6 +943,23 @@ impl GtsStore { let gid = GtsID::new(gts_id) .map_err(|e| StoreError::ValidationError(format!("Invalid GTS ID: {e}")))?; + // If the type named by `gts_id` is abstract, it is exempt from the OP#13 + // entity-level checks (completeness + closed trait schema). An abstract + // type is a template, not a deployable standalone entity: it may carry an + // `x-gts-traits-schema` without resolving its required traits, and its + // descendants are expected to close them. The exemption is keyed on + // `x-gts-abstract` specifically — `x-gts-final` types are non-abstract and + // MUST satisfy completeness themselves (gts-spec §9.7.5 / §9.11.4, + // ADR-0003). Mirrors the abstract skip in `validate_schema_traits`. + if let Some(leaf_entity) = self.get(gts_id) + && leaf_entity + .content + .get(crate::schema_modifiers::X_GTS_ABSTRACT) + == Some(&Value::Bool(true)) + { + return Ok(()); + } + let segments = &gid.gts_id_segments; let mut trait_schemas: Vec = Vec::new(); diff --git a/gts/src/store_test.rs b/gts/src/store_test.rs index 6820522..52f29a8 100644 --- a/gts/src/store_test.rs +++ b/gts/src/store_test.rs @@ -3851,6 +3851,70 @@ fn test_op13_traits_missing_required_fails() { assert!(result.is_err(), "Missing topicRef should fail"); } +#[test] +fn test_op13_entity_traits_abstract_base_skips_completeness() { + // gts-spec §9.7.5 / §9.11.4 (ADR-0003): a type marked `x-gts-abstract: true` + // is exempt from the OP#13 entity-level completeness check. It may declare an + // `x-gts-traits-schema` without resolving any `x-gts-traits` values — concrete + // descendants are expected to close the required traits. + let mut store = GtsStore::new(None); + + let base = json!({ + "$id": "gts://gts.x.test13.abs.base.v1~", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "x-gts-abstract": true, + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "topicRef": {"type": "string"} + } + }, + "properties": {"id": {"type": "string"}} + }); + store + .register_schema("gts.x.test13.abs.base.v1~", &base) + .expect("register abstract base"); + + let result = store.validate_entity_traits("gts.x.test13.abs.base.v1~"); + assert!( + result.is_ok(), + "Abstract base must be exempt from the OP#13 completeness check: {result:?}" + ); +} + +#[test] +fn test_op13_entity_traits_non_abstract_base_without_values_fails() { + // The flip side of the abstract exemption: a non-abstract type that declares + // a trait schema but supplies no `x-gts-traits` values anywhere in its chain + // is incomplete and must still fail OP#13. + let mut store = GtsStore::new(None); + + let base = json!({ + "$id": "gts://gts.x.test13.conc.base.v1~", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "topicRef": {"type": "string"} + } + }, + "properties": {"id": {"type": "string"}} + }); + store + .register_schema("gts.x.test13.conc.base.v1~", &base) + .expect("register concrete base"); + + let result = store.validate_entity_traits("gts.x.test13.conc.base.v1~"); + assert!( + result.is_err(), + "Non-abstract base with no trait values must fail the OP#13 completeness check" + ); +} + #[test] fn test_op13_traits_wrong_type_fails() { let mut store = GtsStore::new(None);