Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions gts/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value> = Vec::new();
Expand Down
64 changes: 64 additions & 0 deletions gts/src/store_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading