Skip to content

Commit b98fe5f

Browse files
authored
Merge pull request #29 from GlobalTypeSystem/validate-json
gts-spec v0.14 compliance
2 parents c13297b + d7600f4 commit b98fe5f

19 files changed

Lines changed: 1053 additions & 276 deletions

‎Makefile‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ $(error PYTHON must be set for local package targets (examples: venv: PYTHON=.ve
2626
endif
2727
endif
2828

29-
.PHONY: help py-env install build install-local uninstall-local clean dev-fmt all check fmt lint clippy mypy test security update-spec e2e coverage
29+
.PHONY: help py-env install build install-local uninstall-local clean dev-fmt all check fmt lint clippy mypy test security update-spec e2e coverage gts-server
3030

3131
# Default target - show help
3232
.DEFAULT_GOAL := help
@@ -100,6 +100,11 @@ lint: py-env
100100
clippy: py-env
101101
$(PYTHON) -m ruff check --fix gts/src
102102

103+
# Format code and apply auto-fixable lint corrections
104+
fix: py-env
105+
$(PYTHON) -m ruff format gts/src
106+
$(PYTHON) -m ruff check --fix gts/src
107+
103108
# Run type checker
104109
mypy: py-env
105110
$(PYTHON) -m mypy gts/src/gts --ignore-missing-imports
@@ -115,6 +120,11 @@ coverage: install
115120
$(PYTHON) -m pip install 'pytest-cov>=5,<7'
116121
$(PYTHON) -m pytest tests/ --cov=gts --cov-report=xml --cov-report=term
117122

123+
PORT ?= 8000
124+
125+
gts-server: install
126+
$(PYTHON) -m gts server --host 127.0.0.1 --port $(PORT)
127+
118128
# Run end-to-end tests against gts-spec
119129
e2e: install
120130
@echo "Starting server in background..."

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
A minimal, idiomatic Python library for working with **GTS** ([Global Type System](https://github.com/gts-spec/gts-spec)) identifiers and JSON/JSON Schema artifacts.
66

7-
Current supported GTS spec version: `0.13.4`
7+
Current supported GTS spec version: `0.14.0`
88

99
## Roadmap
1010

‎gts/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Python helpers and a reference HTTP service for the [Global Type System (GTS)](https://github.com/globaltypesystem/gts-spec). The package supports GTS identifier parsing, JSON Schema-backed validation, schema compatibility and derivation checks, traits, casting, queries, file loading, a CLI, and a FastAPI application.
44

5-
The package targets GTS specification v0.13.4 and requires Python 3.9 or later.
5+
The package targets GTS specification v0.14.0 and requires Python 3.9 or later.
66

77
## Installation
88

‎gts/openapi.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.1.0",
33
"info": {
44
"title": "GTS Server",
5-
"version": "0.13.4"
5+
"version": "0.14.0"
66
},
77
"paths": {
88
"/entities": {

‎gts/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "gts"
7-
version = "0.13.4"
7+
version = "0.14.0"
88
description = "Global Type System (GTS) helpers: identifiers, parsing, validation, and operations"
99
readme = "README.md"
1010
authors = [{ name = "GTS Community" }]

‎gts/src/gts/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
GtsIdSegment,
1515
GtsWildcard,
1616
)
17+
from .gts_ref_validation import GtsRefValidationMode
1718
from .path_resolver import GtsPathResolver
1819
from .store import (
1920
GtsReader,
@@ -30,6 +31,7 @@
3031
"GtsIdSegment",
3132
"GtsPathResolver",
3233
"GtsReader",
34+
"GtsRefValidationMode",
3335
"GtsStore",
3436
"GtsWildcard",
3537
"JsonEntity",

‎gts/src/gts/_server.py‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
# ruff: noqa: B008
34
import logging
45
import sys
56
import time
@@ -10,9 +11,13 @@
1011
from pydantic import BaseModel, model_validator
1112
from starlette.middleware.base import BaseHTTPMiddleware
1213

14+
from .gts_ref_validation import GtsRefValidationMode
1315
from .ops import GtsOps
1416

1517
logger = logging.getLogger(__name__)
18+
GTS_REF_VALIDATION_QUERY = Query(
19+
GtsRefValidationMode.ANY_VALID, alias="gts-ref-validation"
20+
)
1621

1722

1823
# ANSI color codes
@@ -188,7 +193,7 @@ def __init__(
188193
self.host = host
189194
self.port = port
190195
self.base_url = f"http://{self.host}:{self.port}"
191-
self.app = FastAPI(title="GTS Server", version="0.13.4")
196+
self.app = FastAPI(title="GTS Server", version="0.14.0")
192197
self.app.add_middleware(
193198
_RequestLoggingMiddleware,
194199
verbose=self.ops.verbose,
@@ -343,16 +348,22 @@ def _register_routes(self) -> None:
343348
# Handlers as methods (no free functions)
344349
async def add_entity(
345350
self,
346-
body: dict[str, Any] = Body(...), # noqa: B008 - FastAPI dependency pattern
351+
body: dict[str, Any] = Body(...),
347352
validate: bool = Query(False),
353+
validation: bool = Query(False),
354+
gts_ref_validation: GtsRefValidationMode = GTS_REF_VALIDATION_QUERY,
348355
) -> JSONResponse:
349-
result = self.ops.add_entity(body, validate=validate)
356+
result = self.ops.add_entity(
357+
body,
358+
validate=validate is True or validation is True,
359+
gts_ref_validation=gts_ref_validation,
360+
)
350361
status_code = 200 if result.ok else 409 if result.conflict else 422
351362
return JSONResponse(result.to_dict(), status_code=status_code)
352363

353364
async def add_entities(
354365
self,
355-
body: list[dict[str, Any]] = Body(...), # noqa: B008 - FastAPI dependency pattern
366+
body: list[dict[str, Any]] = Body(...),
356367
) -> JSONResponse:
357368
return JSONResponse(self.ops.add_entities(body).to_dict())
358369

@@ -367,7 +378,7 @@ async def validate_id(self, id: str = Query(..., alias="gts_id")) -> dict[str, A
367378

368379
async def extract_id(
369380
self,
370-
body: dict[str, Any] = Body(...), # noqa: B008 - FastAPI dependency pattern
381+
body: dict[str, Any] = Body(...),
371382
) -> dict[str, Any]:
372383
return self.ops.extract_id(body).to_dict()
373384

@@ -384,29 +395,41 @@ async def match_id_pattern(
384395
async def id_to_uuid(self, id: str = Query(..., alias="gts_id")) -> dict[str, Any]:
385396
return self.ops.uuid(id).to_dict()
386397

387-
async def validate_instance(self, body: ValidateInstanceRequest) -> dict[str, Any]:
388-
return self.ops.validate_instance(body.instance_id).to_dict()
398+
async def validate_instance(
399+
self,
400+
body: ValidateInstanceRequest,
401+
gts_ref_validation: GtsRefValidationMode = GTS_REF_VALIDATION_QUERY,
402+
) -> dict[str, Any]:
403+
return self.ops.validate_instance(
404+
body.instance_id, gts_ref_validation
405+
).to_dict()
389406

390407
async def validate_json(
391408
self,
392-
body: dict[str, Any] = Body(...), # noqa: B008 - FastAPI dependency pattern
409+
body: dict[str, Any] = Body(...),
393410
) -> dict[str, Any]:
394411
return self.ops.validate_json(body).to_dict()
395412

396413
async def validate_json_as_type(
397414
self,
398415
gts_type: str,
399-
body: dict[str, Any] = Body(...), # noqa: B008 - FastAPI dependency pattern
416+
body: dict[str, Any] = Body(...),
400417
) -> dict[str, Any]:
401418
return self.ops.validate_json(body, explicit_type_id=gts_type).to_dict()
402419

403420
async def validate_type_schema(
404-
self, body: ValidateTypeSchemaRequest
421+
self,
422+
body: ValidateTypeSchemaRequest,
423+
gts_ref_validation: GtsRefValidationMode = GTS_REF_VALIDATION_QUERY,
405424
) -> dict[str, Any]:
406-
return self.ops.validate_schema(body.type_id).to_dict()
425+
return self.ops.validate_schema(body.type_id, gts_ref_validation).to_dict()
407426

408-
async def validate_entity(self, body: ValidateEntityRequest) -> dict[str, Any]:
409-
return self.ops.validate_entity(body.resolved_id).to_dict()
427+
async def validate_entity(
428+
self,
429+
body: ValidateEntityRequest,
430+
gts_ref_validation: GtsRefValidationMode = GTS_REF_VALIDATION_QUERY,
431+
) -> dict[str, Any]:
432+
return self.ops.validate_entity(body.resolved_id, gts_ref_validation).to_dict()
410433

411434
async def schema_graph(
412435
self, id: str = Query(..., alias="gts_id")

‎gts/src/gts/gts_ref_validation.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from enum import Enum
2+
3+
4+
class GtsRefValidationMode(str, Enum):
5+
NONE = "none"
6+
ANY_PRESENT = "any-present"
7+
ANY_VALID = "any-valid"

‎gts/src/gts/ops.py‎

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,23 @@
1010
from .entities import DEFAULT_GTS_CONFIG, GtsConfig, GtsEntity
1111
from .files_reader import GtsFileReader
1212
from .gts import GtsID, GtsWildcard
13+
from .gts_ref_validation import GtsRefValidationMode
1314
from .path_resolver import GtsPathResolver
1415
from .schema_cast import GtsEntityCastResult
1516
from .store import GtsStore, GtsStoreQueryResult
1617

1718
# Interface helpers
1819

1920

21+
def _normalize_gts_ref_validation(value: Any) -> GtsRefValidationMode:
22+
if isinstance(value, GtsRefValidationMode):
23+
return value
24+
try:
25+
return GtsRefValidationMode(value)
26+
except (TypeError, ValueError):
27+
return GtsRefValidationMode.ANY_VALID
28+
29+
2030
@dataclass
2131
class GtsIdValidationResult:
2232
"""Result of validating a GTS ID format."""
@@ -384,8 +394,12 @@ def reload_from_path(self, path: str | builtins.list[str]) -> None:
384394
self.store = GtsStore(self._reader)
385395

386396
def add_entity(
387-
self, content: dict[str, Any], validate: bool = False
397+
self,
398+
content: dict[str, Any],
399+
validate: bool = False,
400+
gts_ref_validation: GtsRefValidationMode = GtsRefValidationMode.ANY_VALID,
388401
) -> GtsAddEntityResult:
402+
gts_ref_validation = _normalize_gts_ref_validation(gts_ref_validation)
389403
entity = GtsEntity(content=content, cfg=self.cfg)
390404

391405
# For instances (non-schemas), require an id field from entity_id_fields
@@ -429,9 +443,11 @@ def add_entity(
429443
if entity.is_schema:
430444
self.store.validate_schema_basic(entity.gts_id.id)
431445
if validate:
432-
self.store.validate_schema(entity.gts_id.id)
446+
self.store.validate_schema(entity.gts_id.id, gts_ref_validation)
433447
elif validate:
434-
self.store.validate_instance(entity.raw_id or entity.gts_id.id)
448+
self.store.validate_instance(
449+
entity.raw_id or entity.gts_id.id, gts_ref_validation
450+
)
435451
except Exception as e: # noqa: BLE001 - converted to a result object at API boundary
436452
self.store.unregister(store_key)
437453
if previous:
@@ -652,34 +668,52 @@ def validate_json(
652668
is_type_schema=entity.is_schema,
653669
)
654670

655-
def validate_instance(self, gts_id: str) -> GtsValidationResult:
671+
def validate_instance(
672+
self,
673+
gts_id: str,
674+
gts_ref_validation: GtsRefValidationMode = GtsRefValidationMode.ANY_VALID,
675+
) -> GtsValidationResult:
676+
gts_ref_validation = _normalize_gts_ref_validation(gts_ref_validation)
656677
try:
657-
self.store.validate_instance(gts_id)
678+
self.store.validate_instance(gts_id, gts_ref_validation)
658679
return GtsValidationResult(id=gts_id, ok=True)
659680
except Exception as e: # noqa: BLE001 - converted to a result object at API boundary
660681
return GtsValidationResult(id=gts_id, ok=False, error=str(e))
661682

662-
def validate_schema(self, gts_id: str) -> GtsValidationResult:
683+
def validate_schema(
684+
self,
685+
gts_id: str,
686+
gts_ref_validation: GtsRefValidationMode = GtsRefValidationMode.ANY_VALID,
687+
) -> GtsValidationResult:
688+
gts_ref_validation = _normalize_gts_ref_validation(gts_ref_validation)
663689
try:
664-
self.store.validate_schema(gts_id)
690+
self.store.validate_schema(gts_id, gts_ref_validation)
665691
return GtsValidationResult(id=gts_id, ok=True)
666692
except Exception as e: # noqa: BLE001 - converted to a result object at API boundary
667693
return GtsValidationResult(id=gts_id, ok=False, error=str(e))
668694

669-
def validate_entity(self, gts_id: str) -> GtsEntityValidationResult:
670-
try:
671-
parsed = GtsID(gts_id)
672-
except Exception as e: # noqa: BLE001 - converted to a result object at API boundary
673-
return GtsEntityValidationResult(
674-
id=gts_id, ok=False, entity_type="", error=str(e)
675-
)
695+
def validate_entity(
696+
self,
697+
gts_id: str,
698+
gts_ref_validation: GtsRefValidationMode = GtsRefValidationMode.ANY_VALID,
699+
) -> GtsEntityValidationResult:
700+
gts_ref_validation = _normalize_gts_ref_validation(gts_ref_validation)
701+
entity = self.store.get(gts_id)
702+
if entity:
703+
entity_type = "schema" if entity.is_schema else "instance"
704+
else:
705+
try:
706+
parsed = GtsID(gts_id)
707+
entity_type = "schema" if parsed.is_type else "instance"
708+
except Exception as e: # noqa: BLE001 - converted at API boundary
709+
return GtsEntityValidationResult(
710+
id=gts_id, ok=False, entity_type="", error=str(e)
711+
)
676712

677-
if parsed.is_type:
678-
entity_type = "schema"
679-
result = self.validate_schema(gts_id)
713+
if entity_type == "schema":
714+
result = self.validate_schema(gts_id, gts_ref_validation)
680715
else:
681-
entity_type = "instance"
682-
result = self.validate_instance(gts_id)
716+
result = self.validate_instance(gts_id, gts_ref_validation)
683717

684718
return GtsEntityValidationResult(
685719
id=result.id, ok=result.ok, entity_type=entity_type, error=result.error

0 commit comments

Comments
 (0)