Skip to content

Commit b1ff09d

Browse files
authored
Merge pull request #7 from Artifizer/main
feat: support GTS spec v0.5 - x-gts-ref, get entity by ID, validation parameter
2 parents 035fa5b + c2bd327 commit b1ff09d

6 files changed

Lines changed: 649 additions & 9 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ See details in [gts/README.md](gts/README.md)
2727

2828
Other GTS spec [Reference Implementation](https://github.com/globaltypesystem/gts-spec/blob/main/README.md#9-reference-implementation-recommendations) recommended features support:
2929

30-
- [ ] **In-memory entities registry** - simple GTS entities registry with optional GTS references validation on entity registration
30+
- [x] **In-memory entities registry** - simple GTS entities registry with optional GTS references validation on entity registration
3131
- [x] **CLI** - command-line interface for all GTS operations
3232
- [x] **Web server** - a non-production web-server with REST API for the operations processing and testing
33-
- [ ] **x-gts-ref support** - to support special GTS entity reference annotation in schemas
33+
- [x] **x-gts-ref support** - to support special GTS entity reference annotation in schemas
3434
- [ ] **YAML support** - to support YAML files (*.yml, *.yaml) as input files
3535
- [ ] **TypeSpec support** - add [typespec.io](https://typespec.io/) files (*.tsp) support
3636
- [ ] **UUID for instances** - to support UUID as ID in JSON instances

gts/openapi.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@
5050
"post": {
5151
"summary": "Register a single entity (object or schema)",
5252
"operationId": "add_entity_entities_post",
53+
"parameters": [
54+
{
55+
"required": false,
56+
"schema": {
57+
"type": "boolean",
58+
"title": "Validate",
59+
"default": false
60+
},
61+
"name": "validate",
62+
"in": "query"
63+
}
64+
],
5365
"requestBody": {
5466
"content": {
5567
"application/json": {
@@ -83,6 +95,46 @@
8395
}
8496
}
8597
},
98+
"/entities/{gts_id}": {
99+
"get": {
100+
"summary": "Get a specific entity by GTS ID",
101+
"operationId": "get_entity_entities__gts_id__get",
102+
"parameters": [
103+
{
104+
"required": true,
105+
"schema": {
106+
"type": "string",
107+
"title": "Gts Id"
108+
},
109+
"name": "gts_id",
110+
"in": "path"
111+
}
112+
],
113+
"responses": {
114+
"200": {
115+
"description": "Successful Response",
116+
"content": {
117+
"application/json": {
118+
"schema": {
119+
"type": "object",
120+
"title": "Response Get Entity Entities Gts Id Get"
121+
}
122+
}
123+
}
124+
},
125+
"422": {
126+
"description": "Validation Error",
127+
"content": {
128+
"application/json": {
129+
"schema": {
130+
"$ref": "#/components/schemas/HTTPValidationError"
131+
}
132+
}
133+
}
134+
}
135+
}
136+
}
137+
},
86138
"/entities/bulk": {
87139
"post": {
88140
"summary": "Register multiple entities",

gts/src/gts/ops.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,28 @@ def to_dict(self) -> Dict[str, Any]:
134134
}
135135

136136

137+
@dataclass
138+
class GtsGetEntityResult:
139+
"""Result of getting a single entity."""
140+
ok: bool
141+
id: str = ""
142+
schema_id: Optional[str] = None
143+
is_schema: bool = False
144+
content: Any = None
145+
error: str = ""
146+
147+
def to_dict(self) -> Dict[str, Any]:
148+
result: Dict[str, Any] = {"ok": self.ok}
149+
if self.ok:
150+
result["id"] = self.id
151+
result["schema_id"] = self.schema_id
152+
result["is_schema"] = self.is_schema
153+
result["content"] = self.content
154+
else:
155+
result["error"] = self.error
156+
return result
157+
158+
137159
@dataclass
138160
class GtsEntitiesListResult:
139161
"""Result of listing entities."""
@@ -265,11 +287,41 @@ def reload_from_path(self, path: str | List[str]) -> None:
265287
self._reader = GtsFileReader(self.path, cfg=self.cfg)
266288
self.store = GtsStore(self._reader)
267289

268-
def add_entity(self, content: Dict[str, Any]) -> GtsAddEntityResult:
290+
def add_entity(
291+
self,
292+
content: Dict[str, Any],
293+
validate: bool = False
294+
) -> GtsAddEntityResult:
269295
entity = JsonEntity(content=content, cfg=self.cfg)
270296
if not entity.gts_id:
271-
return GtsAddEntityResult(ok=False, error="Unable to detect GTS ID in entity")
297+
return GtsAddEntityResult(
298+
ok=False,
299+
error="Unable to detect GTS ID in entity"
300+
)
301+
302+
# Register the entity first
272303
self.store.register(entity)
304+
305+
# Always validate schemas
306+
if entity.is_schema:
307+
try:
308+
self.store.validate_schema(entity.gts_id.id)
309+
except Exception as e:
310+
return GtsAddEntityResult(
311+
ok=False,
312+
error=f"Validation failed: {str(e)}"
313+
)
314+
315+
# If validation is requested, validate the instance as well
316+
if validate and not entity.is_schema:
317+
try:
318+
self.store.validate_instance(entity.gts_id.id)
319+
except Exception as e:
320+
return GtsAddEntityResult(
321+
ok=False,
322+
error=f"Validation failed: {str(e)}"
323+
)
324+
273325
return GtsAddEntityResult(
274326
ok=True,
275327
id=entity.gts_id.id,
@@ -337,6 +389,23 @@ def validate_instance(self, gts_id: str) -> GtsValidationResult:
337389
except Exception as e:
338390
return GtsValidationResult(id=gts_id, ok=False, error=str(e))
339391

392+
def validate_schema(self, gts_id: str) -> GtsValidationResult:
393+
try:
394+
self.store.validate_schema(gts_id)
395+
return GtsValidationResult(id=gts_id, ok=True)
396+
except Exception as e:
397+
return GtsValidationResult(id=gts_id, ok=False, error=str(e))
398+
399+
def validate_entity(self, gts_id: str) -> GtsValidationResult:
400+
try:
401+
if gts_id.endswith("~"):
402+
self.store.validate_schema(gts_id)
403+
else:
404+
self.store.validate_instance(gts_id)
405+
return GtsValidationResult(id=gts_id, ok=True)
406+
except Exception as e:
407+
return GtsValidationResult(id=gts_id, ok=False, error=str(e))
408+
340409
def schema_graph(self, gts_id: str) -> GtsSchemaGraphResult:
341410
graph = self.store.build_schema_graph(gts_id)
342411
return GtsSchemaGraphResult(graph=graph)
@@ -372,6 +441,35 @@ def extract_id(self, content: Dict[str, Any]) -> GtsExtractIdResult:
372441
is_schema=entity.is_schema,
373442
)
374443

444+
def get_entity(self, gts_id: str) -> GtsGetEntityResult:
445+
"""Get a single entity by its GTS ID.
446+
447+
Args:
448+
gts_id: The GTS ID of the entity to retrieve
449+
450+
Returns:
451+
GtsGetEntityResult with entity details or error
452+
"""
453+
try:
454+
entity = self.store.get(gts_id)
455+
if not entity:
456+
return GtsGetEntityResult(
457+
ok=False,
458+
error=f"Entity '{gts_id}' not found"
459+
)
460+
return GtsGetEntityResult(
461+
ok=True,
462+
id=entity.gts_id.id if entity.gts_id else gts_id,
463+
schema_id=entity.schemaId,
464+
is_schema=entity.is_schema,
465+
content=entity.content
466+
)
467+
except Exception as e:
468+
return GtsGetEntityResult(
469+
ok=False,
470+
error=str(e)
471+
)
472+
375473
def get_entities(self, limit: int = 100) -> GtsEntitiesListResult:
376474
"""Get all entities in the registry.
377475
@@ -391,7 +489,9 @@ def get_entities(self, limit: int = 100) -> GtsEntitiesListResult:
391489
)
392490
for entity_id, entity in all_entities[:limit]
393491
]
394-
return GtsEntitiesListResult(entities=entities, count=len(entities), total=total)
492+
return GtsEntitiesListResult(
493+
entities=entities, count=len(entities), total=total
494+
)
395495

396496
def list(self, limit: int = 100) -> GtsEntitiesListResult:
397497
"""Alias for get_entities. List all discovered entities.

gts/src/gts/server.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ def _register_routes(self) -> None:
178178
summary="Get all entities in the registry",
179179
response_class=JSONResponse,
180180
)
181+
app.add_api_route(
182+
"/entities/{gts_id:path}",
183+
self.get_entity,
184+
methods=["GET"],
185+
summary="Get a specific entity by GTS ID",
186+
response_class=JSONResponse,
187+
)
181188
app.add_api_route(
182189
"/entities",
183190
self.add_entity,
@@ -287,8 +294,14 @@ def _register_routes(self) -> None:
287294
)
288295

289296
# Handlers as methods (no free functions)
290-
async def add_entity(self, body: Dict[str, Any] = Body(...)) -> JSONResponse:
291-
return JSONResponse(self.ops.add_entity(body).to_dict())
297+
async def add_entity(
298+
self,
299+
body: Dict[str, Any] = Body(...),
300+
validate: bool = Query(False)
301+
) -> JSONResponse:
302+
return JSONResponse(
303+
self.ops.add_entity(body, validate=validate).to_dict()
304+
)
292305

293306
async def add_entities(self, body: List[Dict[str, Any]] = Body(...)) -> JSONResponse:
294307
return JSONResponse(self.ops.add_entities(body).to_dict())
@@ -339,5 +352,10 @@ async def query(self, expr: str = Query(...), limit: int = Query(100, ge=1, le=1
339352
async def attr(self, gts_with_path: str = Query(...)) -> Dict[str, Any]:
340353
return self.ops.attr(gts_with_path).to_dict()
341354

342-
async def get_entities(self, limit: int = Query(100, ge=1, le=1000)) -> Dict[str, Any]:
355+
async def get_entity(self, gts_id: str) -> Dict[str, Any]:
356+
return self.ops.get_entity(gts_id).to_dict()
357+
358+
async def get_entities(
359+
self, limit: int = Query(100, ge=1, le=1000)
360+
) -> Dict[str, Any]:
343361
return self.ops.get_entities(limit=limit).to_dict()

0 commit comments

Comments
 (0)