Skip to content

Commit 4ce41ea

Browse files
committed
fix(validation): defer relative refs in bulk registration
Allow bulk registration to store schemas whose relative x-gts-ref pointers require explicit validation, while keeping single registration strict. Recognize the validation query alias so callers can request eager semantic checks consistently. Signed-off-by: Artifizer <artifizer@gmail.com>
1 parent 0d51d0f commit 4ce41ea

7 files changed

Lines changed: 80 additions & 27 deletions

File tree

‎gts/src/gts/_server.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,11 @@ async def add_entity(
345345
self,
346346
body: dict[str, Any] = Body(...), # noqa: B008 - FastAPI dependency pattern
347347
validate: bool = Query(False),
348+
validation: bool = Query(False),
348349
) -> JSONResponse:
349-
result = self.ops.add_entity(body, validate=validate)
350+
result = self.ops.add_entity(
351+
body, validate=validate is True or validation is True
352+
)
350353
status_code = 200 if result.ok else 409 if result.conflict else 422
351354
return JSONResponse(result.to_dict(), status_code=status_code)
352355

‎gts/src/gts/ops.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,10 @@ def reload_from_path(self, path: str | builtins.list[str]) -> None:
384384
self.store = GtsStore(self._reader)
385385

386386
def add_entity(
387-
self, content: dict[str, Any], validate: bool = False
387+
self,
388+
content: dict[str, Any],
389+
validate: bool = False,
390+
resolve_relative: bool = True,
388391
) -> GtsAddEntityResult:
389392
entity = GtsEntity(content=content, cfg=self.cfg)
390393

@@ -427,7 +430,9 @@ def add_entity(
427430

428431
try:
429432
if entity.is_schema:
430-
self.store.validate_schema_basic(entity.gts_id.id)
433+
self.store.validate_schema_basic(
434+
entity.gts_id.id, resolve_relative=resolve_relative
435+
)
431436
if validate:
432437
self.store.validate_schema(entity.gts_id.id)
433438
elif validate:
@@ -456,7 +461,7 @@ def add_entities(
456461
) -> GtsAddEntitiesResult:
457462
results: list[GtsAddEntityResult] = []
458463
for it in items:
459-
results.append(self.add_entity(it))
464+
results.append(self.add_entity(it, resolve_relative=False))
460465
ok = all(r.ok for r in results)
461466
return GtsAddEntitiesResult(ok=ok, results=results)
462467

‎gts/src/gts/store.py‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,9 @@ def _validate_schema_ref_targets(
327327
for index, item in enumerate(schema):
328328
self._validate_schema_ref_targets(item, f"{path}[{index}]", visited)
329329

330-
def _validate_schema_x_gts_refs(self, gts_id: str) -> None:
330+
def _validate_schema_x_gts_refs(
331+
self, gts_id: str, resolve_relative: bool = True
332+
) -> None:
331333
"""
332334
Validate a schema's x-gts-ref fields.
333335
@@ -342,16 +344,23 @@ def _validate_schema_x_gts_refs(self, gts_id: str) -> None:
342344
if not schema_entity.is_schema:
343345
raise ValueError(f"Entity '{schema_id.id}' is not a schema")
344346

345-
self._validate_schema_x_gts_refs_content(schema_id.id, schema_entity.content)
347+
self._validate_schema_x_gts_refs_content(
348+
schema_id.id, schema_entity.content, resolve_relative=resolve_relative
349+
)
346350

347351
def _validate_schema_x_gts_refs_content(
348-
self, gts_id: str, schema_content: dict[str, Any]
352+
self,
353+
gts_id: str,
354+
schema_content: dict[str, Any],
355+
resolve_relative: bool = True,
349356
) -> None:
350357
logger.info(f"Validating schema x-gts-ref fields for {gts_id}")
351358

352359
# Validate x-gts-ref constraints in the schema
353360
x_gts_ref_validator = XGtsRefValidator(store=self)
354-
x_gts_ref_errors = x_gts_ref_validator.validate_schema(schema_content)
361+
x_gts_ref_errors = x_gts_ref_validator.validate_schema(
362+
schema_content, resolve_relative=resolve_relative
363+
)
355364
if x_gts_ref_errors:
356365
error_messages = [
357366
f"{err.field_path}: {err.reason}" for err in x_gts_ref_errors
@@ -624,7 +633,7 @@ def _validate_traits(
624633
f"Schema '{gts_id}' trait validation failed: " + "; ".join(errors)
625634
)
626635

627-
def validate_schema_basic(self, gts_id: str) -> None:
636+
def validate_schema_basic(self, gts_id: str, resolve_relative: bool = True) -> None:
628637
"""Basic schema validation during registration (no chain validation).
629638
630639
Checks:
@@ -661,7 +670,7 @@ def validate_schema_basic(self, gts_id: str) -> None:
661670
self._validate_schema_refs(schema_content, "")
662671

663672
# 2. Validate x-gts-ref fields
664-
self._validate_schema_x_gts_refs(gts_id)
673+
self._validate_schema_x_gts_refs(gts_id, resolve_relative=resolve_relative)
665674

666675
# 3. Validate GTS keywords (x-gts-final, x-gts-abstract, placement)
667676
self._validate_gts_keywords(schema_content)

‎gts/src/gts/x_gts_ref.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def validate_schema(
222222
schema: dict[str, Any],
223223
schema_path: str = "",
224224
root_schema: dict[str, Any] | None = None,
225+
resolve_relative: bool = True,
225226
) -> list[XGtsRefValidationError]:
226227
"""
227228
Validate x-gts-ref fields in a schema definition.
@@ -230,6 +231,7 @@ def validate_schema(
230231
schema: The JSON schema to validate
231232
schema_path: Current path in schema (for error reporting)
232233
root_schema: The root schema (for resolving relative refs)
234+
resolve_relative: Whether relative refs must resolve during this check
233235
234236
Returns:
235237
List of validation errors (empty if valid)
@@ -242,6 +244,12 @@ def validate_schema(
242244
if "x-gts-ref" not in subschema:
243245
continue
244246
ref_value = subschema["x-gts-ref"]
247+
if (
248+
not resolve_relative
249+
and isinstance(ref_value, str)
250+
and ref_value.startswith("/")
251+
):
252+
continue
245253
ref_path = f"{path}/x-gts-ref" if path else "x-gts-ref"
246254
error = self._validate_ref_pattern(ref_value, ref_path, root_schema)
247255
if error:

‎tests/test_ops.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ def test_add_entities_batch(self, ops):
144144
assert result.ok is True
145145
assert len(result.results) == 2
146146

147+
def test_add_entities_defers_relative_pointer_resolution(self, ops):
148+
schema = {
149+
"$schema": "http://json-schema.org/draft-07/schema#",
150+
"$id": "gts://gts.x.test._.relative.v1~",
151+
"properties": {"ref": {"x-gts-ref": "/missing"}},
152+
}
153+
result = ops.add_entities([schema])
154+
assert result.ok is True
155+
assert ops.validate_schema("gts.x.test._.relative.v1~").ok is False
156+
147157

148158
class TestAddSchemaLegacy:
149159
def test_add_schema_legacy_success(self, ops):

‎tests/test_server.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ def test_add_entity_failure(self, server):
7575
resp = run(server.add_entity(body={"no": "id"}, validate=False))
7676
assert resp.status_code == 422
7777

78+
def test_add_entity_validation_alias(self, server):
79+
schema = {
80+
**SCHEMA,
81+
"$id": "gts://gts.x.test._.relative.v1~",
82+
"properties": {"ref": {"x-gts-ref": "/missing"}},
83+
}
84+
resp = run(server.add_entity(body=schema, validate=False, validation=True))
85+
assert resp.status_code == 422
86+
7887
def test_add_changed_entity_conflict(self, server):
7988
assert run(server.add_entity(body=SCHEMA, validate=False)).status_code == 200
8089
changed_schema = {**SCHEMA, "properties": {"name": {"type": "integer"}}}

‎tests/test_store_extra.py‎

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -147,35 +147,27 @@ def test_missing_derived_gts_ref_target_raises(self):
147147
store = GtsStore(MockGtsReader([target]))
148148
with pytest.raises(ValueError, match="Unresolvable \\$ref"):
149149
store._validate_schema_ref_targets(
150-
{
151-
"$ref": (
152-
"gts://gts.x.test._.target.v1~"
153-
"x.test._.missing.v1~"
154-
)
155-
}
150+
{"$ref": ("gts://gts.x.test._.target.v1~x.test._.missing.v1~")}
156151
)
157152

158153

159154
class TestSchemaDependencies:
160155
def test_ignores_x_gts_ref_in_annotation_data(self):
161156
store = GtsStore(reader=None)
162-
assert list(
163-
store._schema_dependencies(
164-
{"const": {"x-gts-ref": "gts.x.test._.missing.v1~"}}
157+
assert (
158+
list(
159+
store._schema_dependencies(
160+
{"const": {"x-gts-ref": "gts.x.test._.missing.v1~"}}
161+
)
165162
)
166-
) == []
163+
== []
164+
)
167165

168166
def test_finds_constraint_under_property_named_x_gts_ref(self):
169167
store = GtsStore(reader=None)
170168
assert list(
171169
store._schema_dependencies(
172-
{
173-
"properties": {
174-
"x-gts-ref": {
175-
"x-gts-ref": "gts.x.test._.missing.v1~"
176-
}
177-
}
178-
}
170+
{"properties": {"x-gts-ref": {"x-gts-ref": "gts.x.test._.missing.v1~"}}}
179171
)
180172
) == [("gts.x.test._.missing.v1~", True)]
181173

@@ -256,6 +248,23 @@ def test_invalid_x_gts_ref_raises(self):
256248
with pytest.raises(Exception, match="x-gts-ref validation failed"):
257249
store._validate_schema_x_gts_refs("gts.x.test._.foo.v1~")
258250

251+
def test_basic_validation_can_defer_relative_pointer_resolution(self):
252+
schema = _schema_entity(
253+
"gts.x.test._.foo.v1~",
254+
{
255+
"x-gts-traits-schema": {
256+
"properties": {
257+
"ref": {"x-gts-ref": "/x-gts-traits-schema/missingTarget"}
258+
}
259+
}
260+
},
261+
)
262+
store = GtsStore(reader=None)
263+
store.register(schema)
264+
store.validate_schema_basic("gts.x.test._.foo.v1~", resolve_relative=False)
265+
with pytest.raises(Exception, match="x-gts-ref validation failed"):
266+
store.validate_schema_basic("gts.x.test._.foo.v1~")
267+
259268

260269
class TestValidateSchemaChain:
261270
def test_single_segment_no_parent_ok(self):

0 commit comments

Comments
 (0)