Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions gts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ for entity in reader:
print(f"{entity.gts_id.id}: {entity.file.name}")
```

### TypeSpec
TypeSpec (`.tsp`) schemas must be pre-compiled to JSON Schema before use with gts-python.
### TypeSpec (Native Compilation)

**Setup:**
TypeSpec (`.tsp`) files are now natively supported. When a `.tsp` file is encountered, gts-python automatically compiles it to JSON Schema using the TypeSpec compiler.

**Requirements:**
```bash
# Install TypeSpec compiler
# Install TypeSpec compiler (one-time setup)
npm install -g @typespec/compiler @typespec/json-schema

# Compile TypeSpec to JSON Schema
tsp compile --emit @typespec/json-schema your-schemas/
```

**Usage:**
```python
from gts import GtsFileReader

# Point to the generated JSON Schema output directory
reader = GtsFileReader("tsp-output/@typespec/json-schema/")
entities = list(reader)
# Reads JSON, YAML, and TypeSpec files directly
reader = GtsFileReader("path/to/schemas/")
for entity in reader:
print(f"{entity.gts_id.id}: {entity.file.name}")
```

TypeSpec files are automatically compiled on-the-fly. If the TypeSpec compiler is not available, `.tsp` files are skipped with a warning.

See [gts-spec TypeSpec examples](https://github.com/globaltypesystem/gts-spec/tree/main/examples/typespec) for sample TypeSpec definitions.

## Featureset
Expand Down
113 changes: 105 additions & 8 deletions gts/src/gts/files_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import json
import yaml
import subprocess
import tempfile
import shutil
from pathlib import Path
import os
from typing import Iterator, List, Optional, Any
Expand All @@ -16,7 +19,7 @@


class GtsFileReader(GtsReader):
"""Reads GTS entities from JSON and YAML files in directories specified by path."""
"""Reads GTS entities from JSON, YAML, and TypeSpec files in directories specified by path."""

def __init__(self, path: str | List[str], cfg: Optional[GtsConfig] = None) -> None:
"""
Expand All @@ -38,10 +41,96 @@ def __init__(self, path: str | List[str], cfg: Optional[GtsConfig] = None) -> No
self._current_file_entities: List[GtsEntity] = []
self._current_entity_index = 0
self._initialized = False
self._tsp_available: Optional[bool] = None

def _check_tsp_available(self) -> bool:
"""Check if TypeSpec compiler (tsp) is available in the system."""
if self._tsp_available is not None:
return self._tsp_available

self._tsp_available = shutil.which("tsp") is not None
if not self._tsp_available:
# Also check npx tsp
try:
result = subprocess.run(
["npx", "--yes", "@typespec/compiler", "--version"],
capture_output=True,
timeout=30
)
self._tsp_available = result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
self._tsp_available = False

return self._tsp_available

def _compile_tsp(self, file_path: Path) -> Optional[Any]:
"""
Compile TypeSpec file to JSON Schema and return the parsed content.

Args:
file_path: Path to .tsp file

Returns:
Parsed JSON Schema content or None if compilation fails
"""
if not self._check_tsp_available():
logging.warning(f"TypeSpec compiler not available, skipping: {file_path}")
return None

try:
with tempfile.TemporaryDirectory() as tmpdir:
output_dir = Path(tmpdir) / "output"
output_dir.mkdir()

# Try using tsp directly, fall back to npx
tsp_cmd = ["tsp"] if shutil.which("tsp") else ["npx", "--yes", "@typespec/compiler"]

cmd = [
*tsp_cmd,
"compile",
str(file_path),
"--emit", "@typespec/json-schema",
"--output-dir", str(output_dir)
]

logging.debug(f"Compiling TypeSpec: {' '.join(cmd)}")

result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60,
cwd=file_path.parent
)

if result.returncode != 0:
logging.warning(f"TypeSpec compilation failed for {file_path}: {result.stderr}")
return None

# Find generated JSON Schema files
schema_files = list(output_dir.rglob("*.json"))
if not schema_files:
logging.warning(f"No JSON Schema generated for {file_path}")
return None

# If multiple schemas generated, return as array
schemas = []
for schema_file in schema_files:
with schema_file.open("r", encoding="utf-8") as f:
schemas.append(json.load(f))

return schemas if len(schemas) > 1 else schemas[0] if schemas else None

except subprocess.TimeoutExpired:
logging.warning(f"TypeSpec compilation timed out for {file_path}")
return None
except Exception as e:
logging.warning(f"Error compiling TypeSpec {file_path}: {e}")
return None

def _collect_files(self) -> None:
"""Collect all JSON and YAML files from the specified paths, following symlinks."""
valid_extensions = {'.json', '.jsonc', '.gts', '.yaml', '.yml'}
"""Collect all JSON, YAML, and TypeSpec files from the specified paths, following symlinks."""
valid_extensions = {'.json', '.jsonc', '.gts', '.yaml', '.yml', '.tsp'}
seen: set[str] = set()
collected: List[Path] = []

Expand Down Expand Up @@ -75,19 +164,27 @@ def _collect_files(self) -> None:
self._files = collected

def _load_file(self, file_path: Path) -> Any:
"""Load content from JSON or YAML file."""
with file_path.open("r", encoding="utf-8") as f:
if file_path.suffix.lower() in {'.yaml', '.yml'}:
"""Load content from JSON, YAML, or TypeSpec file."""
suffix = file_path.suffix.lower()

if suffix == '.tsp':
return self._compile_tsp(file_path)
elif suffix in {'.yaml', '.yml'}:
with file_path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f)
else:
else:
with file_path.open("r", encoding="utf-8") as f:
return json.load(f)

def _process_file(self, file_path: Path) -> List[GtsEntity]:
"""Process a single JSON or YAML file and return list of GtsEntity objects."""
"""Process a single JSON, YAML, or TypeSpec file and return list of GtsEntity objects."""
entities: List[GtsEntity] = []

try:
content = self._load_file(file_path)
if content is None:
return entities

json_file = GtsFile(
path=str(file_path),
name=file_path.name,
Expand Down