Skip to content
142 changes: 82 additions & 60 deletions README.md

Large diffs are not rendered by default.

255 changes: 255 additions & 0 deletions adr/0001-derivation-form.md

Large diffs are not rendered by default.

288 changes: 288 additions & 0 deletions adr/0002-x-gts-traits-schema.md

Large diffs are not rendered by default.

294 changes: 294 additions & 0 deletions adr/0003-x-gts-traits-completeness.md

Large diffs are not rendered by default.

525 changes: 525 additions & 0 deletions adr/0004-x-gts-traits-merge-strategy.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
{
"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~",
Expand All @@ -33,5 +29,9 @@
}
}
}
]
],
"x-gts-traits": {
"topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
"retention": "P90D"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
{
"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~",
Expand All @@ -34,5 +30,9 @@
}
}
}
]
],
"x-gts-traits": {
"topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
"retention": "P90D"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
{
"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~",
Expand All @@ -35,5 +31,9 @@
}
}
}
]
],
"x-gts-traits": {
"topicRef": "gts.x.core.events.topic.v1~x.core.idp.contacts.v1",
"retention": "P365D"
}
}
71 changes: 70 additions & 1 deletion tests/helpers/http_run_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
def register(gts_id, schema_body, label="register schema"):
"""Register a schema via POST /entities."""
body = {
**schema_body,
"$$id": gts_id,
"$$schema": "http://json-schema.org/draft-07/schema#",
**schema_body,
}
return Step(
RunRequest(label)
Expand All @@ -28,7 +28,23 @@ def register_derived(gts_id, base_ref, overlay, label="register derived", top_le

top_level: optional dict of extra keys to add at schema top level
(e.g. {"x-gts-final": True}) — these MUST NOT go inside allOf.

The document-level GTS keywords x-gts-traits / x-gts-traits-schema MUST
appear at the schema top level, not nested inside an allOf entry
(GTS spec §9.12). If a caller places them in the `overlay`, they are
transparently hoisted to the top level so existing trait tests express
the spec-correct placement without restating every call site.
"""
overlay = dict(overlay)
trait_kws = ("x-gts-traits", "x-gts-traits-schema")
hoisted = {kw: overlay.pop(kw) for kw in trait_kws if kw in overlay}
if top_level:
clobbered = [kw for kw in trait_kws if kw in top_level]
if clobbered:
raise ValueError(
"top_level must not contain trait keywords "
f"{clobbered}; pass them in `overlay` so they are hoisted"
)
body = {
"$$id": gts_id,
"$$schema": "http://json-schema.org/draft-07/schema#",
Expand All @@ -38,6 +54,7 @@ def register_derived(gts_id, base_ref, overlay, label="register derived", top_le
overlay,
],
}
body.update(hoisted)
if top_level:
body.update(top_level)
return Step(
Expand All @@ -49,6 +66,58 @@ def register_derived(gts_id, base_ref, overlay, label="register derived", top_le
)


def register_derived_redeclared(
gts_id, base_ref, body, label="register derived (no allOf)", top_level=None
):
"""Register a derived schema without allOf — caller restates parent fields directly.

Per ADR-0001 (GTS as a JSON Schema extension, dialect-agnostic; not a formal
JSON Schema Dialect), derivation is established by the chained $id alone;
the body MAY use any syntactically valid JSON Schema form.
`base_ref` is accepted for parity with register_derived() and documents intent.

`body` is the entire schema body (caller is responsible for restating any
parent fields that need to participate in OP#12 compatibility). The helper
only injects $id and $schema; no allOf wrapping is added.
top_level: optional dict merged into body at the top level.
"""
full = {
**body,
"$$id": gts_id,
"$$schema": "http://json-schema.org/draft-07/schema#",
}
if top_level:
full.update(top_level)
return Step(
RunRequest(label)
.post("/entities")
.with_json(full)
.validate()
.assert_equal("status_code", 200)
)


def register_abstract(gts_id, schema_body, label="register abstract"):
"""Register a schema marked x-gts-abstract: true.

Per ADR-0003, abstract types skip the trait-completeness check at
/validate-type-schema time.
"""
body = {
**schema_body,
"$$id": gts_id,
"$$schema": "http://json-schema.org/draft-07/schema#",
"x-gts-abstract": True,
}
return Step(
RunRequest(label)
.post("/entities")
.with_json(body)
.validate()
.assert_equal("status_code", 200)
)


def register_instance(instance_body, label="register instance"):
"""Register an instance via POST /entities."""
return Step(
Expand Down
Loading
Loading