From 1f6e82089047e5d74866ced2aa53d8269ffabd15 Mon Sep 17 00:00:00 2001 From: Artifizer Date: Tue, 17 Feb 2026 12:24:03 +0200 Subject: [PATCH 1/6] feat(spec): 0.8Beta2 implement specification for traits and introduce OP#13 for traits validation This commit introduces traits schema and values definition rules that can be used to configure semantic behaviour of the systems that need to interpret objects/instances differently depending on it's type Signed-off-by: Artifizer --- README.md | 180 +- .../gts.x.core.events.type.v1~.schema.json | 19 +- ...erce.orders.order_placed.v1.0~.schema.json | 4 + ...erce.orders.order_placed.v1.1~.schema.json | 4 + ...x.core.idp.contact_created.v1~.schema.json | 7 +- tests/test_op13_schema_traits_validation.py | 1611 +++++++++++++++++ 6 files changed, 1812 insertions(+), 13 deletions(-) create mode 100644 tests/test_op13_schema_traits_validation.py diff --git a/README.md b/README.md index b6f857f..0424bdf 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ See the [Practical Benefits for Service and Platform Vendors](#51-practical-bene - [8.1 Single-segment regex (type or instance)](#81-single-segment-regex-type-or-instance) - [8.2 Chained identifier regex](#82-chained-identifier-regex) - [9. Reference Implementation Recommendations](#9-reference-implementation-recommendations) + - [9.7 Schema Traits (`x-gts-traits-schema` / `x-gts-traits`)](#97---schema-traits-x-gts-traits-schema--x-gts-traits) - [10. Collecting Identifiers with Wildcards](#10-collecting-identifiers-with-wildcards) - [11. JSON and JSON Schema Conventions](#11-json-and-json-schema-conventions) - [12. Notes and Best Practices](#12-notes-and-best-practices) @@ -92,6 +93,7 @@ See the [Practical Benefits for Service and Platform Vendors](#51-practical-bene | 0.6 | Introduced well-known/anonymous instance term; defined field naming implementation recommendations | | 0.7 | BREAKING: require $ref value to start with 'gts://'; strict rules for schema/instance distinction; prohibiting well-known instances without left-hand type segments | | 0.8beta1 | Add OP#12 (schema vs schema validation), unified validation endpoint (/validate-entity), and clarify instance -> schema and schema -> schema validation semantics for chained GTS IDs | +| 0.8beta2 | Introduce schema traits (`x-gts-traits-schema`, `x-gts-traits`) and OP#13 (schema traits validation) | ## 1. Motivation @@ -1211,9 +1213,9 @@ See working examples under `./examples/events`: - Well-known topics: `./examples/events/instances/gts.x.core.events.topic.v1~x.commerce.orders.orders.v1.0.json` - Anonymous events: `./examples/events/instances/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1~.examples.json` -### 9.2 - GTS operations (OP#1 - OP#12): +### 9.2 - GTS operations (OP#1 - OP#13): -Implement and expose all operations OP#1–OP#12 listed above and add appropriate unit tests. +Implement and expose all operations OP#1–OP#13 listed above and add appropriate unit tests. - **OP#1 - ID Validation**: Verify identifier syntax - **OP#2 - ID Extraction**: Extract identifiers from JSON objects or JSON Schema documents @@ -1227,6 +1229,7 @@ Implement and expose all operations OP#1–OP#12 listed above and add appropriat - **OP#10 - Query Execution**: Filter identifier collections using the GTS query language - **OP#11 - Attribute Access**: Retrieve property values and metadata using the attribute selector (`@`) - **OP#12 - Schema vs Schema Validation**: Validate derived schemas against their base schemas. Derived schemas using `allOf` must conform to all constraints defined in their parent schemas throughout the inheritance hierarchy. This ensures type safety in schema extension and prevents constraint violations in multi-level schema hierarchies. +- **OP#13 - Schema Traits Validation**: Validate that `x-gts-traits` values in derived schemas conform to the `x-gts-traits-schema` defined in their base schemas. Verify that all trait properties are resolved (via `const`/direct value or `default`) and that trait values satisfy the trait schema constraints. Uses the same validation endpoints (`/validate-schema`, `/validate-entity`). ### 9.3 - GTS entities registration @@ -1264,17 +1267,184 @@ Implementation notes: - For nested paths (e.g., `./properties/id`), resolve the pointer accordinly to the field path in the JSON Schema document. -### 9.7 - YAML support +### 9.7 - Schema Traits (`x-gts-traits-schema` / `x-gts-traits`) + +A **schema trait** is a semantic annotation attached to a GTS schema that describes **system behaviour** for processing instances of that type. Traits are not part of the object data model — they do not define instance properties. Instead, they configure cross-cutting concerns such as: + +- **Retention rules** — how long instances of this type are kept (e.g., object TTL) +- **Processing directives** — how attributes should be handled (e.g., PII masking, indexing hints) +- **Association links** — linking schemas to related entities (e.g., associating an event type with its topic/stream) + +#### 9.7.1 Keywords + +Two JSON Schema annotation keywords are used together: + +| Keyword | JSON type | Purpose | Typical location | +|---------|-----------|---------|------------------| +| **`x-gts-traits-schema`** | JSON Schema (object) | Defines the **shape** of the trait — property names, types, constraints, and `default` values | Base / ancestor schemas | +| **`x-gts-traits`** | Plain JSON object | Provides concrete **values** for the trait properties | Derived (leaf) schemas; may also appear alongside `x-gts-traits-schema` in the same schema | + +A single schema MAY contain both keywords. This is explicitly allowed and useful when a mid-level schema defines new trait properties (`x-gts-traits-schema`) while also resolving traits inherited from its parent (`x-gts-traits`). + +**`x-gts-traits-schema`** MUST be a valid JSON Schema. It MAY be: + +- An **inline** schema object +- A **`$ref`** to a standalone, reusable trait schema +- A **composition** using `allOf`, `oneOf`, `anyOf`, etc. + +Standard JSON Schema `$ref` resolution rules apply — implementations MUST NOT invent a custom reference mechanism. + +#### 9.7.2 Trait schema definition (`x-gts-traits-schema`) + +A base schema declares the trait schema — the shape and defaults of all trait fields. This tells the system which traits exist and what values are acceptable. + +**Inline definition:** + +```json +{ + "$id": "gts://gts.x.core.events.type.v1~", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "topicRef": { + "description": "GTS ID of the topic/stream where events of this type are published.", + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + "default": "gts.x.core.events.topic.v1~x.core._.default.v1" + }, + "retention": { + "description": "ISO 8601 duration for event retention.", + "type": "string", + "default": "P30D" + } + } + }, + "properties": { "..." : {} } +} +``` + +**`$ref` to reusable trait schemas:** + +A platform MAY publish standalone, reusable trait schemas (e.g., `RetentionTrait`, `TopicTrait`, `PIITrait`). Base schemas reference them via standard `$ref`: + +```json +{ + "$id": "gts://gts.x.core.events.type.v1~", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "allOf": [ + { "$ref": "gts://gts.x.core.traits.retention.v1~" }, + { "$ref": "gts://gts.x.core.traits.topic.v1~" } + ] + }, + "properties": { "..." : {} } +} +``` + +Where each referenced trait schema is a standalone JSON Schema registered as a GTS entity: + +```json +{ + "$id": "gts://gts.x.core.traits.retention.v1~", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "retention": { + "description": "ISO 8601 duration for data retention.", + "type": "string", + "default": "P30D" + } + } +} +``` + +#### 9.7.3 Trait values in derived schemas (`x-gts-traits`) + +Derived schemas **resolve** (configure) trait values by providing a plain JSON object via `x-gts-traits`. Trait values MUST be valid against the effective trait schema derived from the inheritance chain as defined below. + +```json +{ + "$id": "gts://gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~", + "allOf": [ + { "$ref": "gts://gts.x.core.events.type.v1~" }, + { + "x-gts-traits": { + "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1", + "retention": "P90D" + } + } + ] +} +``` + +#### 9.7.4 Both keywords in the same schema + +A mid-level schema MAY extend the trait schema while also providing values for inherited traits: + +```json +{ + "$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~", + "allOf": [ + { "$ref": "gts://gts.x.core.events.type.v1~" }, + { + "x-gts-traits-schema": { + "type": "object", + "properties": { + "auditRetention": { + "description": "Retention override for audit compliance.", + "type": "string", + "default": "P365D" + } + } + }, + "x-gts-traits": { + "topicRef": "gts.x.core.events.topic.v1~x.core._.audit.v1" + } + } + ] +} +``` + +#### 9.7.5 Trait merge and validation semantics (normative) + +Traits MUST follow standard JSON Schema practices. The key rule is that **the registry MUST treat trait schemas as normal JSON Schemas** and MUST rely on standard JSON Schema composition and `$ref` semantics (especially `allOf`) rather than inventing a bespoke merge algorithm. + +Given an inheritance chain `S₀ → S₁ → … → Sₙ`: + +- **Trait schema merge** + - The registry MUST build an *effective trait schema* by composing all encountered `x-gts-traits-schema` values using JSON Schema `allOf`. + - Any `$ref` appearing inside `x-gts-traits-schema` MUST be resolved using standard JSON Schema `$ref` resolution rules (base URI resolution + JSON Pointer fragments). + - Derived schemas MAY further constrain (narrow) traits by adding additional schema constraints in their `x-gts-traits-schema` (this is naturally enforced by `allOf`). + +- **Trait value merge** + - The registry MUST build an *effective traits object* by shallow-merging all encountered `x-gts-traits` objects in chain order (left-to-right), where the **rightmost** value for a given trait key wins. + - Defaults declared in the effective trait schema SHOULD be used as normal JSON Schema defaults to produce a complete effective traits object. + +- **Validation** + - The registry MUST validate the effective traits object against the effective trait schema using standard JSON Schema validation. + - If the effective trait schema cannot be satisfied (e.g., contradictory constraints introduced across the chain), schema validation MUST fail. + - If a trait is required by the effective trait schema (i.e., not covered by a default) but is not provided by any `x-gts-traits` in the chain, schema validation MUST fail for concrete (leaf) schemas. + +These rules are intentionally aligned with existing JSON Schema composition semantics and GTS schema chaining practices. + +See `./examples/events/schemas/` for complete examples demonstrating trait definition and resolution. + +### 9.8 - YAML support Accept and emit both JSON and YAML (`.json`, `.yaml`, `.yml`) for schemas and instances. Ensure conversions are lossless; preserve `$id`, `gtsId`, and custom extensions like `x-gts-ref`. -### 9.8 - TypeSpec support +### 9.9 - TypeSpec support Support generating JSON Schema and OpenAPI from TypeSpec while preserving GTS semantics. Ensure generated schemas use GTS identifiers as `$id` for types and keep any `x-gts-*` extensions intact. -### 9.9 - UUID as object IDs +### 9.10 - UUID as object IDs Support UUIDs (format: `uuid`) for instance `id` fields. diff --git a/examples/events/schemas/gts.x.core.events.type.v1~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~.schema.json index 7e39d00..8a1c33c 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~.schema.json @@ -10,12 +10,21 @@ "tenantId", "occurredAt" ], - "x-event-type-settings": { + "x-gts-traits-schema": { + "type": "object", "additionalProperties": false, - "topicRef": { - "description": "ID of the topic where events of this type are stored.", - "type": "string", - "x-gts-ref":"gts.x.core.events.topic.v1~" + "properties": { + "topicRef": { + "description": "GTS ID of the topic/stream where events of this type are published.", + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + "default": "gts.x.core.events.topic.v1~x.core._.default.v1" + }, + "retention": { + "description": "ISO 8601 duration for event retention.", + "type": "string", + "default": "P30D" + } } }, "properties": { diff --git a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json index 012eb69..0a32dde 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json @@ -8,6 +8,10 @@ { "type": "object", "required": ["type", "payload", "subjectType"], + "x-gts-traits": { + "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1", + "retention": "P90D" + }, "properties": { "type": { "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~", diff --git a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json index 33cbf71..aa5047a 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json @@ -8,6 +8,10 @@ { "type": "object", "required": ["type", "payload", "subjectType"], + "x-gts-traits": { + "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1", + "retention": "P90D" + }, "properties": { "type": { "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~", diff --git a/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json b/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json index d6db51e..f18f9d6 100644 --- a/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json +++ b/examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json @@ -9,6 +9,10 @@ { "type": "object", "required": ["type", "payload", "subjectType"], + "x-gts-traits": { + "topicRef": "gts.x.core.events.topic.v1~x.core.idp.contacts.v1", + "retention": "P365D" + }, "properties": { "type": { "const": "gts.x.core.events.type.v1~x.core.idp.contact_created.v1.0~", @@ -29,9 +33,6 @@ "type": "string", "x-gts-ref": "gts.x.core.idp.contact.v1.0~" } - }, - "x-event-type-settings": { - "topicRef": { "const": "gts.x.core.events.topic.v1~x.core.idp.contacts.v1" } } } ] diff --git a/tests/test_op13_schema_traits_validation.py b/tests/test_op13_schema_traits_validation.py new file mode 100644 index 0000000..c8153ce --- /dev/null +++ b/tests/test_op13_schema_traits_validation.py @@ -0,0 +1,1611 @@ +from .conftest import get_gts_base_url +from httprunner import HttpRunner, Config, Step, RunRequest + + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +def _register(gts_id, schema_body, label="register schema"): + """Register a schema via POST /entities.""" + body = { + "$$id": gts_id, + "$$schema": "http://json-schema.org/draft-07/schema#", + **schema_body, + } + return Step( + RunRequest(label) + .post("/entities") + .with_json(body) + .validate() + .assert_equal("status_code", 200) + ) + + +def _register_derived(gts_id, base_ref, overlay, label="register derived"): + """Register a derived schema that uses allOf with a $$ref.""" + body = { + "$$id": gts_id, + "$$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "allOf": [ + {"$$ref": base_ref}, + overlay, + ], + } + return Step( + RunRequest(label) + .post("/entities") + .with_json(body) + .validate() + .assert_equal("status_code", 200) + ) + + +def _validate_schema(schema_id, expect_ok, label="validate schema"): + """Validate a derived schema via POST /validate-schema.""" + step = ( + RunRequest(label) + .post("/validate-schema") + .with_json({"schema_id": schema_id}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.ok", expect_ok) + ) + return Step(step) + + +def _validate_entity(entity_id, expect_ok, label="validate entity"): + """Validate an entity via POST /validate-entity.""" + step = ( + RunRequest(label) + .post("/validate-entity") + .with_json({"entity_id": entity_id}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.ok", expect_ok) + ) + return Step(step) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestCaseOp13_TraitsValid_AllResolved(HttpRunner): + """OP#13 - Traits: derived schema provides all trait values. + + Validation passes. + """ + config = Config("OP#13 - All Traits Resolved").base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.traits.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "topicRef": { + "type": "string", + "description": "Topic reference", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + "retention": { + "type": "string", + "description": "Retention period", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with traits-schema (no defaults)", + ), + _register_derived( + "gts://gts.x.test13.traits.event.v1~x.test13._.order_event.v1~", + "gts://gts.x.test13.traits.event.v1~", + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + "retention": "P90D", + }, + }, + "register derived with all traits resolved", + ), + _validate_schema( + "gts.x.test13.traits.event.v1~x.test13._.order_event.v1~", + True, + "validate derived - all traits resolved", + ), + ] + + +class TestCaseOp13_TraitsValid_DefaultsUsed(HttpRunner): + """OP#13 - Traits: base provides defaults, derived omits them - passes""" + config = Config("OP#13 - Traits Defaults Used").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.dfl.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + "default": ( + "gts.x.core.events.topic.v1~" + "x.core._.default.v1" + ), + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with traits-schema (all defaults)", + ), + _register_derived( + "gts://gts.x.test13.dfl.event.v1~x.test13._.simple_event.v1~", + "gts://gts.x.test13.dfl.event.v1~", + { + "type": "object", + }, + "register derived with no x-gts-traits (rely on defaults)", + ), + _validate_schema( + "gts.x.test13.dfl.event.v1~x.test13._.simple_event.v1~", + True, + "validate derived - defaults fill all traits", + ), + ] + + +class TestCaseOp13_TraitsInvalid_MissingRequired(HttpRunner): + """OP#13 - Traits: trait property has no default. + + Derived omits it - fails. + """ + config = Config("OP#13 - Missing Required Trait").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.miss.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "topicRef": { + "type": "string", + "description": "Required - no default", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with one trait without default", + ), + _register_derived( + "gts://gts.x.test13.miss.event.v1~x.test13._.incomplete.v1~", + "gts://gts.x.test13.miss.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P90D", + }, + }, + "register derived missing topicRef trait", + ), + _validate_schema( + "gts.x.test13.miss.event.v1~x.test13._.incomplete.v1~", + False, + "validate should fail - topicRef not resolved", + ), + ] + + +class TestCaseOp13_TraitsInvalid_WrongType(HttpRunner): + """OP#13 - Traits: trait value violates trait schema type - fails""" + config = Config("OP#13 - Trait Value Wrong Type").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.wtype.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "maxRetries": { + "type": "integer", + "minimum": 0, + "default": 3, + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with integer trait", + ), + _register_derived( + "gts://gts.x.test13.wtype.event.v1~x.test13._.bad_type.v1~", + "gts://gts.x.test13.wtype.event.v1~", + { + "type": "object", + "x-gts-traits": { + "maxRetries": "not_a_number", + "retention": "P90D", + }, + }, + "register derived with wrong type for maxRetries", + ), + _validate_schema( + "gts.x.test13.wtype.event.v1~x.test13._.bad_type.v1~", + False, + "validate should fail - maxRetries is not integer", + ), + ] + + +class TestCaseOp13_TraitsInvalid_UnknownProperty(HttpRunner): + """OP#13 - Traits: trait value includes unknown property. + + additionalProperties false - fails. + """ + config = Config("OP#13 - Unknown Trait Property").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.unk.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with closed traits-schema", + ), + _register_derived( + "gts://gts.x.test13.unk.event.v1~x.test13._.extra_trait.v1~", + "gts://gts.x.test13.unk.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P90D", + "unknownTrait": "some_value", + }, + }, + "register derived with unknown trait property", + ), + _validate_schema( + "gts.x.test13.unk.event.v1~x.test13._.extra_trait.v1~", + False, + "validate should fail - unknownTrait not in schema", + ), + ] + + +class TestCaseOp13_TraitsValid_PartialOverride(HttpRunner): + """OP#13 - Traits: derived overrides one trait. + + Other uses default - passes. + """ + config = Config("OP#13 - Partial Override With Defaults").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.part.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + "default": ( + "gts.x.core.events.topic.v1~" + "x.core._.default.v1" + ), + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with all defaults", + ), + _register_derived( + "gts://gts.x.test13.part.event.v1~x.test13._.partial.v1~", + "gts://gts.x.test13.part.event.v1~", + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.custom.v1" + ), + }, + }, + "register derived overriding only topicRef", + ), + _validate_schema( + "gts.x.test13.part.event.v1~x.test13._.partial.v1~", + True, + "validate - topicRef overridden, retention uses default", + ), + ] + + +class TestCaseOp13_TraitsValid_BothKeywordsInSameSchema(HttpRunner): + """OP#13 - Traits: mid-level schema has both x-gts-traits-schema. + + And x-gts-traits. + """ + config = Config("OP#13 - Both Keywords Same Schema").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.both.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with traits-schema", + ), + _register_derived( + "gts://gts.x.test13.both.event.v1~x.test13._.audit.v1~", + "gts://gts.x.test13.both.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "auditRetention": { + "type": "string", + "default": "P365D", + }, + }, + }, + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.audit.v1" + ), + }, + }, + "register mid-level with both keywords", + ), + _validate_schema( + "gts.x.test13.both.event.v1~x.test13._.audit.v1~", + True, + "validate mid-level - topicRef resolved, retention has default", + ), + _register_derived( + ( + "gts://gts.x.test13.both.event.v1~" + "x.test13._.audit.v1~" + "x.test13._.login_audit.v1~" + ), + "gts://gts.x.test13.both.event.v1~x.test13._.audit.v1~", + { + "type": "object", + "x-gts-traits": { + "auditRetention": "P730D", + }, + }, + "register leaf resolving auditRetention", + ), + _validate_schema( + ( + "gts.x.test13.both.event.v1~" + "x.test13._.audit.v1~" + "x.test13._.login_audit.v1~" + ), + True, + "validate leaf - all traits resolved across chain", + ), + ] + + +class TestCaseOp13_TraitsInvalid_3Level_MissingInLeaf(HttpRunner): + """OP#13 - Traits: 3-level chain. + + Leaf missing trait from mid-level schema - fails. + """ + config = Config("OP#13 - 3-Level Missing Trait In Leaf").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.l3miss.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base", + ), + _register_derived( + "gts://gts.x.test13.l3miss.event.v1~x.test13._.mid.v1~", + "gts://gts.x.test13.l3miss.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "priority": { + "type": "string", + "description": "No default - must be resolved", + }, + }, + }, + }, + "register mid-level adding priority trait (no default)", + ), + _register_derived( + ( + "gts://gts.x.test13.l3miss.event.v1~" + "x.test13._.mid.v1~" + "x.test13._.leaf_missing.v1~" + ), + "gts://gts.x.test13.l3miss.event.v1~x.test13._.mid.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P90D", + }, + }, + "register leaf missing priority trait", + ), + _validate_schema( + ( + "gts.x.test13.l3miss.event.v1~" + "x.test13._.mid.v1~" + "x.test13._.leaf_missing.v1~" + ), + False, + "validate should fail - priority not resolved", + ), + ] + + +class TestCaseOp13_TraitsValid_OverrideInChain(HttpRunner): + """OP#13 - Traits: rightmost x-gts-traits value overrides earlier one""" + config = Config("OP#13 - Override In Chain").base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.ovr.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "retention": { + "type": "string", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base", + ), + _register_derived( + "gts://gts.x.test13.ovr.event.v1~x.test13._.mid_ovr.v1~", + "gts://gts.x.test13.ovr.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P30D", + }, + }, + "register mid-level setting retention=P30D", + ), + _validate_schema( + "gts.x.test13.ovr.event.v1~x.test13._.mid_ovr.v1~", + True, + "validate mid-level", + ), + _register_derived( + ( + "gts://gts.x.test13.ovr.event.v1~" + "x.test13._.mid_ovr.v1~" + "x.test13._.leaf_ovr.v1~" + ), + "gts://gts.x.test13.ovr.event.v1~x.test13._.mid_ovr.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P365D", + }, + }, + "register leaf overriding retention=P365D", + ), + _validate_schema( + ( + "gts.x.test13.ovr.event.v1~" + "x.test13._.mid_ovr.v1~" + "x.test13._.leaf_ovr.v1~" + ), + True, + "validate leaf - override is valid", + ), + ] + + +class TestCaseOp13_TraitsInvalid_ConstraintViolation(HttpRunner): + """OP#13 - Traits: trait value violates enum constraint. + + In trait schema - fails. + """ + config = Config("OP#13 - Trait Constraint Violation").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.enum.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "critical"], + "default": "medium", + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with enum-constrained trait", + ), + _register_derived( + "gts://gts.x.test13.enum.event.v1~x.test13._.bad_enum.v1~", + "gts://gts.x.test13.enum.event.v1~", + { + "type": "object", + "x-gts-traits": { + "priority": "ultra_high", + "retention": "P90D", + }, + }, + "register derived with invalid enum value", + ), + _validate_schema( + "gts.x.test13.enum.event.v1~x.test13._.bad_enum.v1~", + False, + "validate should fail - priority not in enum", + ), + ] + + +class TestCaseOp13_TraitsValid_ValidateEntity(HttpRunner): + """OP#13 - Traits: validate-entity endpoint also checks traits""" + config = Config("OP#13 - Validate Entity With Traits").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.ent.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base", + ), + _register_derived( + "gts://gts.x.test13.ent.event.v1~x.test13._.good_ent.v1~", + "gts://gts.x.test13.ent.event.v1~", + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + "retention": "P90D", + }, + }, + "register derived with traits", + ), + _validate_entity( + "gts.x.test13.ent.event.v1~x.test13._.good_ent.v1~", + True, + "validate-entity should pass", + ), + ] + + +class TestCaseOp13_TraitsInvalid_ValidateEntity_MissingTrait(HttpRunner): + """OP#13 - Traits: validate-entity catches missing trait""" + config = Config("OP#13 - Validate Entity Missing Trait").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.entm.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + "retention": { + "type": "string", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base (no defaults)", + ), + _register_derived( + "gts://gts.x.test13.entm.event.v1~x.test13._.bad_ent.v1~", + "gts://gts.x.test13.entm.event.v1~", + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + }, + }, + "register derived missing retention", + ), + _validate_entity( + "gts.x.test13.entm.event.v1~x.test13._.bad_ent.v1~", + False, + "validate-entity should fail - retention not resolved", + ), + ] + + +class TestCaseOp13_TraitsValid_BaseSchemaNoTraits(HttpRunner): + """OP#13 - Traits: base has no traits-schema. + + Derived has no traits - passes. + """ + config = Config("OP#13 - No Traits At All").base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.notr.event.v1~", + { + "type": "object", + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base without traits-schema", + ), + _register_derived( + "gts://gts.x.test13.notr.event.v1~x.test13._.plain.v1~", + "gts://gts.x.test13.notr.event.v1~", + { + "type": "object", + "properties": { + "extra": {"type": "string"}, + }, + }, + "register derived without traits", + ), + _validate_schema( + "gts.x.test13.notr.event.v1~x.test13._.plain.v1~", + True, + "validate - no traits to check, should pass", + ), + ] + + +class TestCaseOp13_TraitsInvalid_MinimumViolation(HttpRunner): + """OP#13 - Traits: integer trait violates minimum constraint - fails""" + config = Config("OP#13 - Trait Minimum Violation").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.minv.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "maxRetries": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "default": 3, + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with integer trait with min/max", + ), + _register_derived( + "gts://gts.x.test13.minv.event.v1~x.test13._.neg_retry.v1~", + "gts://gts.x.test13.minv.event.v1~", + { + "type": "object", + "x-gts-traits": { + "maxRetries": -1, + }, + }, + "register derived with negative maxRetries", + ), + _validate_schema( + "gts.x.test13.minv.event.v1~x.test13._.neg_retry.v1~", + False, + "validate should fail - maxRetries below minimum", + ), + ] + + +class TestCaseOp13_TraitsValid_RefBasedTraitSchema(HttpRunner): + """OP#13 - Traits: base uses $ref to standalone reusable trait schemas""" + config = Config( + "OP#13 - Ref-Based Trait Schema" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + # Register standalone reusable trait schema: RetentionTrait + _register( + "gts://gts.x.test13.traits.retention.v1~", + { + "type": "object", + "properties": { + "retention": { + "description": "ISO 8601 retention duration.", + "type": "string", + "default": "P30D", + }, + }, + }, + "register standalone RetentionTrait schema", + ), + # Register standalone reusable trait schema: TopicTrait + _register( + "gts://gts.x.test13.traits.topic.v1~", + { + "type": "object", + "properties": { + "topicRef": { + "description": "Topic reference.", + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + }, + }, + "register standalone TopicTrait schema", + ), + # Register base that composes traits via $ref + allOf + _register( + "gts://gts.x.test13.ref.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".traits.retention.v1~" + ), + }, + { + "$$ref": ( + "gts://gts.x.test13" + ".traits.topic.v1~" + ), + }, + ], + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with $ref trait schemas", + ), + # Derived provides all trait values + _register_derived( + ( + "gts://gts.x.test13.ref.event.v1~" + "x.test13._.ref_leaf.v1~" + ), + "gts://gts.x.test13.ref.event.v1~", + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + "retention": "P90D", + }, + }, + "register derived resolving $ref traits", + ), + _validate_schema( + ( + "gts.x.test13.ref.event.v1~" + "x.test13._.ref_leaf.v1~" + ), + True, + "validate - $ref traits resolved", + ), + ] + + +class TestCaseOp13_TraitsInvalid_RefBasedMissingTrait(HttpRunner): + """OP#13 - Traits: $ref trait schema, derived missing required trait""" + config = Config( + "OP#13 - Ref-Based Missing Trait" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + # Reuse the standalone trait schemas registered above + # (tests run in sequence within a session) + # Register a new base using $ref traits + _register( + "gts://gts.x.test13.refm.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".traits.retention.v1~" + ), + }, + { + "$$ref": ( + "gts://gts.x.test13" + ".traits.topic.v1~" + ), + }, + ], + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with $ref trait schemas", + ), + # Derived only provides retention, missing topicRef + _register_derived( + ( + "gts://gts.x.test13.refm.event.v1~" + "x.test13._.ref_incomplete.v1~" + ), + "gts://gts.x.test13.refm.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P90D", + }, + }, + "register derived missing topicRef from $ref trait", + ), + _validate_schema( + ( + "gts.x.test13.refm.event.v1~" + "x.test13._.ref_incomplete.v1~" + ), + False, + "validate should fail - topicRef not resolved", + ), + ] + + +class TestCaseOp13_TraitsValid_NarrowingInDerived(HttpRunner): + """OP#13 - Traits: derived narrows trait schema (adds constraints)""" + config = Config( + "OP#13 - Trait Schema Narrowing" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.narrow.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "priority": { + "type": "string", + "description": "Processing priority.", + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with open priority trait", + ), + # Mid-level narrows priority to enum + _register_derived( + ( + "gts://gts.x.test13.narrow.event.v1~" + "x.test13._.mid_narrow.v1~" + ), + "gts://gts.x.test13.narrow.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "priority": { + "type": "string", + "enum": [ + "low", "medium", + "high", "critical", + ], + }, + }, + }, + "x-gts-traits": { + "priority": "high", + }, + }, + "register mid-level narrowing priority to enum", + ), + _validate_schema( + ( + "gts.x.test13.narrow.event.v1~" + "x.test13._.mid_narrow.v1~" + ), + True, + "validate - narrowed trait with valid value", + ), + # Leaf provides value within narrowed enum + _register_derived( + ( + "gts://gts.x.test13.narrow.event.v1~" + "x.test13._.mid_narrow.v1~" + "x.test13._.leaf_narrow.v1~" + ), + ( + "gts://gts.x.test13.narrow.event.v1~" + "x.test13._.mid_narrow.v1~" + ), + { + "type": "object", + "x-gts-traits": { + "priority": "critical", + }, + }, + "register leaf with valid narrowed priority", + ), + _validate_schema( + ( + "gts.x.test13.narrow.event.v1~" + "x.test13._.mid_narrow.v1~" + "x.test13._.leaf_narrow.v1~" + ), + True, + "validate leaf - priority within narrowed enum", + ), + ] + + +class TestCaseOp13_TraitsInvalid_NarrowingViolation(HttpRunner): + """OP#13 - Traits: leaf value violates narrowed enum from mid-level""" + config = Config( + "OP#13 - Narrowing Violation" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.nv.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "priority": { + "type": "string", + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base", + ), + _register_derived( + ( + "gts://gts.x.test13.nv.event.v1~" + "x.test13._.mid_nv.v1~" + ), + "gts://gts.x.test13.nv.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "priority": { + "type": "string", + "enum": [ + "low", "medium", + "high", "critical", + ], + }, + }, + }, + "x-gts-traits": { + "priority": "high", + }, + }, + "register mid-level narrowing priority", + ), + _register_derived( + ( + "gts://gts.x.test13.nv.event.v1~" + "x.test13._.mid_nv.v1~" + "x.test13._.leaf_bad_nv.v1~" + ), + ( + "gts://gts.x.test13.nv.event.v1~" + "x.test13._.mid_nv.v1~" + ), + { + "type": "object", + "x-gts-traits": { + "priority": "ultra_high", + }, + }, + "register leaf with value outside narrowed enum", + ), + _validate_schema( + ( + "gts.x.test13.nv.event.v1~" + "x.test13._.mid_nv.v1~" + "x.test13._.leaf_bad_nv.v1~" + ), + False, + "validate should fail - priority not in enum", + ), + ] + + +class TestCaseOp13_TraitsValid_DefaultsFromRefSchema(HttpRunner): + """OP#13 - Traits: defaults from $ref trait schema fill values""" + config = Config( + "OP#13 - Defaults From Ref Schema" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + # Use the standalone RetentionTrait (default P30D) + # and TopicTrait (no default) registered earlier + _register( + "gts://gts.x.test13.refd.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".traits.retention.v1~" + ), + }, + ], + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with $ref retention trait only", + ), + # Derived provides no traits - retention default fills + _register_derived( + ( + "gts://gts.x.test13.refd.event.v1~" + "x.test13._.default_ref.v1~" + ), + "gts://gts.x.test13.refd.event.v1~", + { + "type": "object", + }, + "register derived with no traits (rely on $ref default)", + ), + _validate_schema( + ( + "gts.x.test13.refd.event.v1~" + "x.test13._.default_ref.v1~" + ), + True, + "validate - retention default from $ref schema fills", + ), + ] + + +class TestCaseOp13_TraitsInvalid_APBlocksExtension(HttpRunner): + """OP#13 - Traits: base additionalProperties=false blocks extension.""" + config = Config( + "OP#13 - Traits additionalProperties Blocks Extension" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.ap.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": { + "retention": {"type": "string"}, + }, + }, + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register base with traits-schema additionalProperties=false", + ), + _register_derived( + "gts://gts.x.test13.ap.event.v1~x.test13._.ap_mid.v1~", + "gts://gts.x.test13.ap.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": "gts.x.core.events.topic.v1~", + }, + }, + }, + "x-gts-traits": { + "retention": "P30D", + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + }, + }, + "register mid-level that extends trait schema with topicRef", + ), + _validate_schema( + "gts.x.test13.ap.event.v1~x.test13._.ap_mid.v1~", + False, + ( + "validate should fail - base additionalProperties=false " + "blocks topicRef" + ), + ), + ] + + +class TestCaseOp13_TraitsInvalid_DerivedHasTraitsButNoTraitSchema(HttpRunner): + """OP#13 - Traits: derived provides x-gts-traits. + + No x-gts-traits-schema exists. + """ + config = Config( + "OP#13 - Derived Traits Without Trait Schema" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.nt0.event.v1~", + { + "type": "object", + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register base without traits-schema", + ), + _register_derived( + ( + "gts://gts.x.test13.nt0.event.v1~" + "x.test13._.derived_has_traits.v1~" + ), + "gts://gts.x.test13.nt0.event.v1~", + { + "type": "object", + "x-gts-traits": {"retention": "P30D"}, + }, + "register derived with x-gts-traits but no traits-schema", + ), + _validate_schema( + "gts.x.test13.nt0.event.v1~x.test13._.derived_has_traits.v1~", + False, + "validate should fail - trait values have no trait schema", + ), + ] + + +class TestCaseOp13_TraitsInvalid_BaseHasTraitsButNoTraitSchema(HttpRunner): + """OP#13 - Traits: base provides x-gts-traits. + + No x-gts-traits-schema exists. + """ + config = Config( + "OP#13 - Base Traits Without Trait Schema" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.nt1.event.v1~", + { + "type": "object", + "x-gts-traits": {"retention": "P30D"}, + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register base with x-gts-traits but no traits-schema", + ), + _validate_schema( + "gts.x.test13.nt1.event.v1~", + False, + "validate should fail - x-gts-traits without x-gts-traits-schema", + ), + ] + + +class TestCaseOp13_TraitsInvalid_ConstNarrowingViolationInLeaf(HttpRunner): + """OP#13 - Traits: mid-level narrows retention to const. + + Leaf tries different value. + """ + config = Config( + "OP#13 - Const Narrowing Violation" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.const.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": {"retention": {"type": "string"}}, + }, + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register base with retention trait", + ), + _register_derived( + "gts://gts.x.test13.const.event.v1~x.test13._.mid_const.v1~", + "gts://gts.x.test13.const.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": {"retention": {"const": "P30D"}}, + }, + "x-gts-traits": {"retention": "P30D"}, + }, + "register mid-level narrowing retention to const P30D", + ), + _validate_schema( + "gts.x.test13.const.event.v1~x.test13._.mid_const.v1~", + True, + "validate mid-level - const narrowing", + ), + _register_derived( + ( + "gts://gts.x.test13.const.event.v1~" + "x.test13._.mid_const.v1~" + "x.test13._.leaf_bad_const.v1~" + ), + "gts://gts.x.test13.const.event.v1~x.test13._.mid_const.v1~", + { + "type": "object", + "x-gts-traits": {"retention": "P90D"}, + }, + "register leaf overriding retention to P90D", + ), + _validate_schema( + ( + "gts.x.test13.const.event.v1~" + "x.test13._.mid_const.v1~" + "x.test13._.leaf_bad_const.v1~" + ), + False, + "validate should fail - leaf violates const retention=P30D", + ), + ] + + +class TestCaseOp13_TraitsValid_ConstNarrowingLeafMatches(HttpRunner): + """OP#13 - Traits: mid-level narrows retention to const. + + Leaf provides same value. + """ + config = Config( + "OP#13 - Const Narrowing Leaf Match" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.constm.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "additionalProperties": False, + "properties": {"retention": {"type": "string"}}, + }, + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register base with retention trait", + ), + _register_derived( + "gts://gts.x.test13.constm.event.v1~x.test13._.mid_constm.v1~", + "gts://gts.x.test13.constm.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": {"retention": {"const": "P30D"}}, + }, + "x-gts-traits": {"retention": "P30D"}, + }, + "register mid-level narrowing retention to const P30D", + ), + _validate_schema( + "gts.x.test13.constm.event.v1~x.test13._.mid_constm.v1~", + True, + "validate mid-level - const narrowing", + ), + _register_derived( + ( + "gts://gts.x.test13.constm.event.v1~" + "x.test13._.mid_constm.v1~" + "x.test13._.leaf_ok_constm.v1~" + ), + "gts://gts.x.test13.constm.event.v1~x.test13._.mid_constm.v1~", + { + "type": "object", + "x-gts-traits": {"retention": "P30D"}, + }, + "register leaf with retention matching const P30D", + ), + _validate_schema( + ( + "gts.x.test13.constm.event.v1~" + "x.test13._.mid_constm.v1~" + "x.test13._.leaf_ok_constm.v1~" + ), + True, + "validate leaf - retention matches const", + ), + ] From 82441c84cee183288a69533f723de370ac665386 Mon Sep 17 00:00:00 2001 From: Artifizer Date: Tue, 17 Feb 2026 13:04:40 +0200 Subject: [PATCH 2/6] tests: add cycling ref coverage for OP#12 and OP#13 - OP#12: add schema-vs-schema validation cases for self-ref and multi-node ref cycles - OP#13: add traits validation cases for cycling refs inside x-gts-traits-schema - Avoid HttpRunner variable interpolation by removing "$ref" from test names/labels Signed-off-by: Artifizer --- .../test_op12_schema_vs_schema_validation.py | 228 ++++++++++++++++++ tests/test_op13_schema_traits_validation.py | 177 ++++++++++++++ 2 files changed, 405 insertions(+) diff --git a/tests/test_op12_schema_vs_schema_validation.py b/tests/test_op12_schema_vs_schema_validation.py index 7bee938..565e746 100644 --- a/tests/test_op12_schema_vs_schema_validation.py +++ b/tests/test_op12_schema_vs_schema_validation.py @@ -3342,5 +3342,233 @@ def test_start(self): ] +class TestCaseOp12_CyclingRef_SelfReference(HttpRunner): + """OP#12 - Cycling ref: schema references itself""" + config = Config("OP#12 - Self-Referencing Ref").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test12.cycle.self.v1~", + { + "type": "object", + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register base schema", + ), + _register_derived( + ( + "gts://gts.x.test12.cycle.self.v1~" + "x.test12._.self_ref.v1~" + ), + "gts://gts.x.test12.cycle.self.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test12.cycle.self.v1~" + "x.test12._.self_ref.v1~" + ), + }, + ], + }, + "register derived that refs itself", + ), + _validate_schema( + ( + "gts.x.test12.cycle.self.v1~" + "x.test12._.self_ref.v1~" + ), + False, + "validate should fail - self-referencing cycle", + ), + ] + + +class TestCaseOp12_CyclingRef_TwoNodeCycle(HttpRunner): + """OP#12 - Cycling ref: A refs B, B refs A""" + config = Config("OP#12 - Two-Node Ref Cycle").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test12.cycle2.base.v1~", + { + "type": "object", + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register root base schema", + ), + _register_derived( + ( + "gts://gts.x.test12.cycle2.base.v1~" + "x.test12._.node_a.v1~" + ), + "gts://gts.x.test12.cycle2.base.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test12.cycle2.base.v1~" + "x.test12._.node_b.v1~" + ), + }, + ], + }, + "register node A referencing node B", + ), + _register_derived( + ( + "gts://gts.x.test12.cycle2.base.v1~" + "x.test12._.node_b.v1~" + ), + "gts://gts.x.test12.cycle2.base.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test12.cycle2.base.v1~" + "x.test12._.node_a.v1~" + ), + }, + ], + }, + "register node B referencing node A", + ), + _validate_schema( + ( + "gts.x.test12.cycle2.base.v1~" + "x.test12._.node_a.v1~" + ), + False, + "validate node A should fail - two-node cycle", + ), + _validate_schema( + ( + "gts.x.test12.cycle2.base.v1~" + "x.test12._.node_b.v1~" + ), + False, + "validate node B should fail - two-node cycle", + ), + ] + + +class TestCaseOp12_CyclingRef_ThreeNodeCycle(HttpRunner): + """OP#12 - Cycling ref: A -> B -> C -> A""" + config = Config("OP#12 - Three-Node Ref Cycle").base_url( + get_gts_base_url() + ) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test12.cycle3.base.v1~", + { + "type": "object", + "required": ["id"], + "properties": {"id": {"type": "string"}}, + }, + "register root base schema", + ), + _register_derived( + ( + "gts://gts.x.test12.cycle3.base.v1~" + "x.test12._.node_a.v1~" + ), + "gts://gts.x.test12.cycle3.base.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test12.cycle3.base.v1~" + "x.test12._.node_b.v1~" + ), + }, + ], + }, + "register node A referencing node B", + ), + _register_derived( + ( + "gts://gts.x.test12.cycle3.base.v1~" + "x.test12._.node_b.v1~" + ), + "gts://gts.x.test12.cycle3.base.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test12.cycle3.base.v1~" + "x.test12._.node_c.v1~" + ), + }, + ], + }, + "register node B referencing node C", + ), + _register_derived( + ( + "gts://gts.x.test12.cycle3.base.v1~" + "x.test12._.node_c.v1~" + ), + "gts://gts.x.test12.cycle3.base.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test12.cycle3.base.v1~" + "x.test12._.node_a.v1~" + ), + }, + ], + }, + "register node C referencing node A", + ), + _validate_schema( + ( + "gts.x.test12.cycle3.base.v1~" + "x.test12._.node_a.v1~" + ), + False, + "validate node A should fail - three-node cycle", + ), + _validate_schema( + ( + "gts.x.test12.cycle3.base.v1~" + "x.test12._.node_b.v1~" + ), + False, + "validate node B should fail - three-node cycle", + ), + _validate_schema( + ( + "gts.x.test12.cycle3.base.v1~" + "x.test12._.node_c.v1~" + ), + False, + "validate node C should fail - three-node cycle", + ), + ] + + if __name__ == "__main__": TestCaseTestOp12SchemaValidation_DerivedSchemaFullyMatches().test_start() diff --git a/tests/test_op13_schema_traits_validation.py b/tests/test_op13_schema_traits_validation.py index c8153ce..41e4a24 100644 --- a/tests/test_op13_schema_traits_validation.py +++ b/tests/test_op13_schema_traits_validation.py @@ -1609,3 +1609,180 @@ def test_start(self): "validate leaf - retention matches const", ), ] + + +class TestCaseOp13_TraitsInvalid_CyclingRef_SelfRef(HttpRunner): + """OP#13 - Traits: x-gts-traits-schema refs itself.""" + config = Config( + "OP#13 - Traits Self-Referencing Ref" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.cyc.selfref.v1~", + { + "type": "object", + "properties": { + "retention": { + "type": "string", + }, + }, + }, + "register standalone trait schema", + ), + _register( + "gts://gts.x.test13.cyc.self.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".cyc.selfref.v1~" + ), + }, + { + "$$ref": ( + "gts://gts.x.test13" + ".cyc.selfref.v1~" + ), + }, + ], + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with self-cycling trait ref", + ), + _register_derived( + ( + "gts://gts.x.test13.cyc.self.event.v1~" + "x.test13._.cyc_self_leaf.v1~" + ), + "gts://gts.x.test13.cyc.self.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P30D", + }, + }, + "register derived with traits", + ), + _validate_schema( + ( + "gts.x.test13.cyc.self.event.v1~" + "x.test13._.cyc_self_leaf.v1~" + ), + False, + "validate should fail - cycling ref in traits-schema", + ), + ] + + +class TestCaseOp13_TraitsInvalid_CyclingRef_TwoNode(HttpRunner): + """OP#13 - Traits: trait schema A refs B, B refs A.""" + config = Config( + "OP#13 - Traits Two-Node Ref Cycle" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.cyc2.trait_a.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".cyc2.trait_b.v1~" + ), + }, + ], + "properties": { + "retention": {"type": "string"}, + }, + }, + "register trait schema A referencing B", + ), + _register( + "gts://gts.x.test13.cyc2.trait_b.v1~", + { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".cyc2.trait_a.v1~" + ), + }, + ], + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": ( + "gts.x.core.events.topic.v1~" + ), + }, + }, + }, + "register trait schema B referencing A", + ), + _register( + "gts://gts.x.test13.cyc2.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "allOf": [ + { + "$$ref": ( + "gts://gts.x.test13" + ".cyc2.trait_a.v1~" + ), + }, + ], + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with cycling trait refs", + ), + _register_derived( + ( + "gts://gts.x.test13.cyc2.event.v1~" + "x.test13._.cyc2_leaf.v1~" + ), + "gts://gts.x.test13.cyc2.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P30D", + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + }, + }, + "register derived with traits", + ), + _validate_schema( + ( + "gts.x.test13.cyc2.event.v1~" + "x.test13._.cyc2_leaf.v1~" + ), + False, + "validate should fail - two-node cycle in trait refs", + ), + ] From ac1d816f133f6217cf3f87cb5c142af98fa5e1af Mon Sep 17 00:00:00 2001 From: Artifizer Date: Tue, 17 Feb 2026 14:13:05 +0200 Subject: [PATCH 3/6] test: fix the GTS ids in tests/test_op13_schema_traits_validation.py Signed-off-by: Artifizer --- tests/test_op13_schema_traits_validation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_op13_schema_traits_validation.py b/tests/test_op13_schema_traits_validation.py index 41e4a24..18562de 100644 --- a/tests/test_op13_schema_traits_validation.py +++ b/tests/test_op13_schema_traits_validation.py @@ -1634,7 +1634,7 @@ def test_start(self): "register standalone trait schema", ), _register( - "gts://gts.x.test13.cyc.self.event.v1~", + "gts://gts.x.test13.cyc.selfevt.v1~", { "type": "object", "x-gts-traits-schema": { @@ -1663,10 +1663,10 @@ def test_start(self): ), _register_derived( ( - "gts://gts.x.test13.cyc.self.event.v1~" + "gts://gts.x.test13.cyc.selfevt.v1~" "x.test13._.cyc_self_leaf.v1~" ), - "gts://gts.x.test13.cyc.self.event.v1~", + "gts://gts.x.test13.cyc.selfevt.v1~", { "type": "object", "x-gts-traits": { @@ -1677,7 +1677,7 @@ def test_start(self): ), _validate_schema( ( - "gts.x.test13.cyc.self.event.v1~" + "gts.x.test13.cyc.selfevt.v1~" "x.test13._.cyc_self_leaf.v1~" ), False, From 567211ddce93e5210f4f892137a969cf1ab989b2 Mon Sep 17 00:00:00 2001 From: Artifizer Date: Tue, 17 Feb 2026 14:13:29 +0200 Subject: [PATCH 4/6] doc: set correct spec verion (0.8Beta2) in the README.md header Signed-off-by: Artifizer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0424bdf..9bd8064 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -> **VERSION**: GTS specification draft, version 0.7 +> **VERSION**: GTS specification draft, version 0.8Beta2 # Global Type System (GTS) Specification From fa060653078ae5b4579609f7cd995345237bde2a Mon Sep 17 00:00:00 2001 From: Artifizer Date: Tue, 17 Feb 2026 14:32:27 +0200 Subject: [PATCH 5/6] tests(op13): make RefBasedMissingTrait self-contained Register referenced standalone trait schemas inside the test so it no longer depends on execution order of other OP#13 tests. Signed-off-by: Artifizer --- tests/test_op13_schema_traits_validation.py | 35 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/test_op13_schema_traits_validation.py b/tests/test_op13_schema_traits_validation.py index 18562de..533f101 100644 --- a/tests/test_op13_schema_traits_validation.py +++ b/tests/test_op13_schema_traits_validation.py @@ -1034,9 +1034,38 @@ def test_start(self): super().test_start() teststeps = [ - # Reuse the standalone trait schemas registered above - # (tests run in sequence within a session) - # Register a new base using $ref traits + _register( + "gts://gts.x.test13.traits.retention.v1~", + { + "type": "object", + "properties": { + "retention": { + "description": ( + "ISO 8601 retention duration." + ), + "type": "string", + "default": "P30D", + }, + }, + }, + "register standalone RetentionTrait schema", + ), + _register( + "gts://gts.x.test13.traits.topic.v1~", + { + "type": "object", + "properties": { + "topicRef": { + "description": "Topic reference.", + "type": "string", + "x-gts-ref": ( + "gts.x.core.events.topic.v1~" + ), + }, + }, + }, + "register standalone TopicTrait schema", + ), _register( "gts://gts.x.test13.refm.event.v1~", { From 58988253dc5577d88cc8a816f81e0c0b36f2c79b Mon Sep 17 00:00:00 2001 From: Artifizer Date: Tue, 17 Feb 2026 19:03:58 +0200 Subject: [PATCH 6/6] docs+tests: clarify trait trait semantics and cover immutable defaults Signed-off-by: Artifizer --- README.md | 24 +- tests/test_op13_schema_traits_validation.py | 336 +++++++++++++++++++- 2 files changed, 354 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9bd8064..6c655a7 100644 --- a/README.md +++ b/README.md @@ -1229,7 +1229,7 @@ Implement and expose all operations OP#1–OP#13 listed above and add appropriat - **OP#10 - Query Execution**: Filter identifier collections using the GTS query language - **OP#11 - Attribute Access**: Retrieve property values and metadata using the attribute selector (`@`) - **OP#12 - Schema vs Schema Validation**: Validate derived schemas against their base schemas. Derived schemas using `allOf` must conform to all constraints defined in their parent schemas throughout the inheritance hierarchy. This ensures type safety in schema extension and prevents constraint violations in multi-level schema hierarchies. -- **OP#13 - Schema Traits Validation**: Validate that `x-gts-traits` values in derived schemas conform to the `x-gts-traits-schema` defined in their base schemas. Verify that all trait properties are resolved (via `const`/direct value or `default`) and that trait values satisfy the trait schema constraints. Uses the same validation endpoints (`/validate-schema`, `/validate-entity`). +- **OP#13 - Schema Traits Validation**: Validate schema traits (`x-gts-traits-schema` / `x-gts-traits`). See section 9.7 for full semantics and validation rules. ### 9.3 - GTS entities registration @@ -1269,6 +1269,8 @@ Implementation notes: ### 9.7 - Schema Traits (`x-gts-traits-schema` / `x-gts-traits`) +**OP#13 - Schema Traits Validation**: Validate that `x-gts-traits` values in derived schemas conform to the `x-gts-traits-schema` defined in their base schemas. Verify that all trait properties are resolved (via direct value or `default`) and that trait values satisfy the trait schema constraints. Trait values set by an ancestor are immutable — descendants MUST NOT override them with a different value. Both `x-gts-traits-schema` and `x-gts-traits` are schema-only keywords and MUST NOT appear in instances. `x-gts-traits-schema` MUST have `"type": "object"`. Uses the same validation endpoints (`/validate-schema`, `/validate-entity`). + A **schema trait** is a semantic annotation attached to a GTS schema that describes **system behaviour** for processing instances of that type. Traits are not part of the object data model — they do not define instance properties. Instead, they configure cross-cutting concerns such as: - **Retention rules** — how long instances of this type are kept (e.g., object TTL) @@ -1284,9 +1286,11 @@ Two JSON Schema annotation keywords are used together: | **`x-gts-traits-schema`** | JSON Schema (object) | Defines the **shape** of the trait — property names, types, constraints, and `default` values | Base / ancestor schemas | | **`x-gts-traits`** | Plain JSON object | Provides concrete **values** for the trait properties | Derived (leaf) schemas; may also appear alongside `x-gts-traits-schema` in the same schema | +**Schema-only keywords:** Both `x-gts-traits-schema` and `x-gts-traits` are **schema annotation keywords** and MUST only appear in JSON Schema documents (documents with `$schema`). They MUST NOT appear in instance documents. Implementations MUST reject instances that contain these keywords. + A single schema MAY contain both keywords. This is explicitly allowed and useful when a mid-level schema defines new trait properties (`x-gts-traits-schema`) while also resolving traits inherited from its parent (`x-gts-traits`). -**`x-gts-traits-schema`** MUST be a valid JSON Schema. It MAY be: +**`x-gts-traits-schema`** MUST be a valid JSON Schema with `"type": "object"` at its top level. Implementations MUST reject trait schemas that declare a different type (e.g., `"type": "integer"`). It MAY be: - An **inline** schema object - A **`$ref`** to a standalone, reusable trait schema @@ -1294,6 +1298,8 @@ A single schema MAY contain both keywords. This is explicitly allowed and useful Standard JSON Schema `$ref` resolution rules apply — implementations MUST NOT invent a custom reference mechanism. +**`x-gts-traits`** is a plain JSON object of concrete values. Constraint keywords like `const` belong in `x-gts-traits-schema` (the trait schema), not in `x-gts-traits` (the trait values). + #### 9.7.2 Trait schema definition (`x-gts-traits-schema`) A base schema declares the trait schema — the shape and defaults of all trait fields. This tells the system which traits exist and what values are acceptable. @@ -1420,15 +1426,27 @@ Given an inheritance chain `S₀ → S₁ → … → Sₙ`: - The registry MUST build an *effective trait schema* by composing all encountered `x-gts-traits-schema` values using JSON Schema `allOf`. - Any `$ref` appearing inside `x-gts-traits-schema` MUST be resolved using standard JSON Schema `$ref` resolution rules (base URI resolution + JSON Pointer fragments). - Derived schemas MAY further constrain (narrow) traits by adding additional schema constraints in their `x-gts-traits-schema` (this is naturally enforced by `allOf`). + - **Immutable defaults:** `default` values declared in an ancestor's `x-gts-traits-schema` MUST NOT be changed by a descendant's `x-gts-traits-schema`. If a descendant redeclares a trait property with a different `default`, schema validation MUST fail. - **Trait value merge** - - The registry MUST build an *effective traits object* by shallow-merging all encountered `x-gts-traits` objects in chain order (left-to-right), where the **rightmost** value for a given trait key wins. + - The registry MUST build an *effective traits object* by collecting all `x-gts-traits` objects encountered in the chain (left-to-right). + - **Immutable-once-set:** Once a trait key is assigned a concrete value by a schema in the chain, **no descendant may override it**. If a descendant's `x-gts-traits` provides a different value for a key already set by an ancestor, schema validation MUST fail. Providing the **same** value is permitted (idempotent). - Defaults declared in the effective trait schema SHOULD be used as normal JSON Schema defaults to produce a complete effective traits object. - **Validation** - The registry MUST validate the effective traits object against the effective trait schema using standard JSON Schema validation. - If the effective trait schema cannot be satisfied (e.g., contradictory constraints introduced across the chain), schema validation MUST fail. - If a trait is required by the effective trait schema (i.e., not covered by a default) but is not provided by any `x-gts-traits` in the chain, schema validation MUST fail for concrete (leaf) schemas. + - If a descendant attempts to override a trait value already set by an ancestor with a different value, schema validation MUST fail. + +**Example — immutable trait override (failure):** + +Consider a 3-level chain: `base → audit_event → most_derived_event`. + +- `audit_event` sets `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.audit.v1` +- `most_derived_event` attempts to set `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.notification.v1` + +Validation of `most_derived_event` MUST fail because `topicRef` was already set by `audit_event` and the new value differs. These rules are intentionally aligned with existing JSON Schema composition semantics and GTS schema chaining practices. diff --git a/tests/test_op13_schema_traits_validation.py b/tests/test_op13_schema_traits_validation.py index 533f101..534a3ed 100644 --- a/tests/test_op13_schema_traits_validation.py +++ b/tests/test_op13_schema_traits_validation.py @@ -589,8 +589,8 @@ def test_start(self): ] -class TestCaseOp13_TraitsValid_OverrideInChain(HttpRunner): - """OP#13 - Traits: rightmost x-gts-traits value overrides earlier one""" +class TestCaseOp13_TraitsInvalid_OverrideInChain(HttpRunner): + """OP#13 - Traits: descendant cannot override ancestor trait value.""" config = Config("OP#13 - Override In Chain").base_url(get_gts_base_url()) def test_start(self): @@ -653,8 +653,181 @@ def test_start(self): "x.test13._.mid_ovr.v1~" "x.test13._.leaf_ovr.v1~" ), + False, + "validate should fail - trait override not allowed", + ), + ] + + +class TestCaseOp13_TraitsInvalid_OverrideTopicRef3Level(HttpRunner): + """OP#13 - Traits: 3-level chain, leaf overrides topicRef. + + Mid-level sets topicRef to audit topic, leaf tries notification + topic. Validation must fail (immutable-once-set). + """ + config = Config( + "OP#13 - Override TopicRef 3-Level" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.ovt.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "topicRef": { + "type": "string", + "x-gts-ref": ( + "gts.x.core.events.topic.v1~" + ), + }, + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with topicRef + retention traits", + ), + _register_derived( + ( + "gts://gts.x.test13.ovt.event.v1~" + "x.test13._.audit_evt.v1~" + ), + "gts://gts.x.test13.ovt.event.v1~", + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.core._.audit.v1" + ), + }, + }, + "register mid-level setting topicRef=audit", + ), + _validate_schema( + ( + "gts.x.test13.ovt.event.v1~" + "x.test13._.audit_evt.v1~" + ), True, - "validate leaf - override is valid", + "validate mid-level - topicRef set", + ), + _register_derived( + ( + "gts://gts.x.test13.ovt.event.v1~" + "x.test13._.audit_evt.v1~" + "x.test13._.most_derived.v1~" + ), + ( + "gts://gts.x.test13.ovt.event.v1~" + "x.test13._.audit_evt.v1~" + ), + { + "type": "object", + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.core._.notification.v1" + ), + }, + }, + "register leaf overriding topicRef=notification", + ), + _validate_schema( + ( + "gts.x.test13.ovt.event.v1~" + "x.test13._.audit_evt.v1~" + "x.test13._.most_derived.v1~" + ), + False, + "validate should fail - topicRef override not allowed", + ), + ] + + +class TestCaseOp13_TraitsInvalid_ChangeDefaultInMid(HttpRunner): + """OP#13 - Traits: mid-level changes default set by base. + + Base sets retention default=P30D. Mid-level redeclares + retention default=P90D. Validation must fail - defaults + set by ancestor are immutable. + """ + config = Config( + "OP#13 - Change Default In Mid-Level" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.chdfl.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "retention": { + "type": "string", + "default": "P30D", + }, + "topicRef": { + "type": "string", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with retention default=P30D", + ), + _register_derived( + ( + "gts://gts.x.test13.chdfl.event.v1~" + "x.test13._.chdfl_mid.v1~" + ), + "gts://gts.x.test13.chdfl.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "retention": { + "type": "string", + "default": "P90D", + }, + }, + }, + "x-gts-traits": { + "topicRef": ( + "gts.x.core.events.topic.v1~" + "x.test13._.orders.v1" + ), + }, + }, + "register mid changing retention default to P90D", + ), + _validate_schema( + ( + "gts.x.test13.chdfl.event.v1~" + "x.test13._.chdfl_mid.v1~" + ), + False, + "validate should fail - default override not allowed", ), ] @@ -1815,3 +1988,160 @@ def test_start(self): "validate should fail - two-node cycle in trait refs", ), ] + +class TestCaseOp13_TraitsInvalid_TraitsSchemaNotObject(HttpRunner): + """OP#13 - Traits: x-gts-traits-schema with type=integer. + + Must fail - trait schema must have type=object. + """ + config = Config( + "OP#13 - Traits Schema Not Object" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.tsnobj.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "integer", + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base with type=integer trait schema", + ), + _register_derived( + ( + "gts://gts.x.test13.tsnobj.event.v1~" + "x.test13._.tsnobj_leaf.v1~" + ), + "gts://gts.x.test13.tsnobj.event.v1~", + { + "type": "object", + }, + "register derived", + ), + _validate_schema( + ( + "gts.x.test13.tsnobj.event.v1~" + "x.test13._.tsnobj_leaf.v1~" + ), + False, + "validate should fail - trait schema type is integer", + ), + ] + + +class TestCaseOp13_TraitsInvalid_TraitsInInstance(HttpRunner): + """OP#13 - Traits: x-gts-traits in an instance document. + + Trait keywords are schema-only. Instance with x-gts-traits + must fail entity validation. + """ + config = Config( + "OP#13 - Traits In Instance" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.tinst.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base schema with traits", + ), + _register_derived( + ( + "gts://gts.x.test13.tinst.event.v1~" + "x.test13._.tinst_leaf.v1~" + ), + "gts://gts.x.test13.tinst.event.v1~", + { + "type": "object", + "x-gts-traits": { + "retention": "P90D", + }, + }, + "register derived with traits", + ), + _validate_schema( + ( + "gts.x.test13.tinst.event.v1~" + "x.test13._.tinst_leaf.v1~" + ), + True, + "validate derived schema - ok", + ), + _validate_entity( + ( + "gts.x.test13.tinst.event.v1~" + "x.test13._.tinst_leaf.v1~" + ), + False, + "validate entity should fail - traits in instance", + ), + ] + + +class TestCaseOp13_TraitsInvalid_TraitsSchemaInInstance(HttpRunner): + """OP#13 - Traits: x-gts-traits-schema in an instance document. + + Trait keywords are schema-only. Instance with + x-gts-traits-schema must fail entity validation. + """ + config = Config( + "OP#13 - Traits Schema In Instance" + ).base_url(get_gts_base_url()) + + def test_start(self): + super().test_start() + + teststeps = [ + _register( + "gts://gts.x.test13.tsinst.event.v1~", + { + "type": "object", + "x-gts-traits-schema": { + "type": "object", + "properties": { + "retention": { + "type": "string", + "default": "P30D", + }, + }, + }, + "required": ["id"], + "properties": { + "id": {"type": "string"}, + }, + }, + "register base schema with traits-schema", + ), + _validate_entity( + "gts.x.test13.tsinst.event.v1~", + False, + "validate entity should fail - traits-schema in instance", + ), + ]