Skip to content

Commit df8e6df

Browse files
committed
feat: ts api jinja generator
1 parent 2541afe commit df8e6df

File tree

229 files changed

+6362
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

229 files changed

+6362
-5
lines changed

.github/workflows/api_ci.yml

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- "tools/api_tools/**"
1313
- "crates/algod_client/**"
1414
- "crates/indexer_client/**"
15+
- "packages/typescript/**"
1516
- ".github/workflows/api_ci.yml"
1617
pull_request:
1718
branches:
@@ -21,6 +22,7 @@ on:
2122
- "tools/api_tools/**"
2223
- "crates/algod_client/**"
2324
- "crates/indexer_client/**"
25+
- "packages/typescript/**"
2426
- ".github/workflows/api_ci.yml"
2527
workflow_dispatch:
2628

@@ -33,10 +35,10 @@ jobs:
3335
steps:
3436
- uses: actions/checkout@v4
3537
- uses: ./.github/actions/setup-api-tools
36-
38+
3739
- name: Run OAS generator linting
3840
run: cargo api lint-oas
39-
41+
4042
- name: Run OAS generator tests
4143
run: cargo api test-oas
4244

@@ -54,10 +56,10 @@ jobs:
5456
steps:
5557
- uses: actions/checkout@v4
5658
- uses: ./.github/actions/setup-api-tools
57-
59+
5860
- name: Generate ${{ matrix.client }} API client
5961
run: cargo api generate-${{ matrix.client }}
60-
62+
6163
- name: Check for output stability
6264
run: |
6365
git add -N ${{ matrix.output_dir }}
@@ -66,6 +68,43 @@ jobs:
6668
echo "🔧 To fix: Run 'cargo api generate-${{ matrix.client }}' locally and commit the changes"
6769
exit 1
6870
fi
69-
71+
7072
- name: Verify generated code compiles
7173
run: cargo check --manifest-path Cargo.toml -p ${{ matrix.client }}_client
74+
75+
ts_api_client_generation:
76+
runs-on: ubuntu-latest
77+
needs: oas_lint_and_test
78+
strategy:
79+
matrix:
80+
pkg: [algod_client, indexer_client]
81+
steps:
82+
- uses: actions/checkout@v4
83+
- uses: ./.github/actions/setup-api-tools
84+
- uses: actions/setup-node@v4
85+
with:
86+
node-version: "20"
87+
88+
- name: Generate TypeScript API clients
89+
run: cargo api generate-ts-all
90+
91+
- name: Check output stability for ${{ matrix.pkg }}
92+
run: |
93+
git add -N packages/typescript/${{ matrix.pkg }}
94+
if ! git diff --exit-code --minimal packages/typescript/${{ matrix.pkg }}; then
95+
echo "❌ Generated ${{ matrix.pkg }} TS client differs from committed version!"
96+
echo "🔧 To fix: Run 'cargo api generate-ts-all' locally and commit the changes"
97+
exit 1
98+
fi
99+
100+
- name: Install dependencies (${{ matrix.pkg }})
101+
working-directory: packages/typescript/${{ matrix.pkg }}
102+
run: bun install
103+
104+
- name: Format check (${{ matrix.pkg }})
105+
working-directory: packages/typescript/${{ matrix.pkg }}
106+
run: npx prettier --check .
107+
108+
- name: Build (tsc) ${{ matrix.pkg }}
109+
working-directory: packages/typescript/${{ matrix.pkg }}
110+
run: bun run build

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ AGENTS.md
2323
.references/
2424
.kiro/
2525

26+
27+
packages/typescript/algod_client/dist/
28+
packages/typescript/indexer_client/dist/

api/oas_generator/ts_oas_generator/__init__.py

Whitespace-only changes.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python3
2+
"""Command-line interface for the TypeScript OAS Generator (Phase 2)."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import contextlib
8+
import json
9+
import shutil
10+
import sys
11+
import tempfile
12+
import traceback
13+
from collections.abc import Generator
14+
from pathlib import Path
15+
16+
from ts_oas_generator.generator.template_engine import TsCodeGenerator
17+
from ts_oas_generator.utils.file_utils import write_files_to_disk
18+
19+
# Exit codes for better error reporting
20+
EXIT_SUCCESS = 0
21+
EXIT_FILE_NOT_FOUND = 1
22+
EXIT_INVALID_JSON = 2
23+
EXIT_GENERATION_ERROR = 3
24+
25+
26+
def parse_command_line_args(args: list[str] | None = None) -> argparse.Namespace:
27+
"""Create and configure the command line argument parser for TS generator."""
28+
parser = argparse.ArgumentParser(
29+
description="Generate TypeScript client from OpenAPI specification",
30+
formatter_class=argparse.RawDescriptionHelpFormatter,
31+
epilog="""
32+
Examples:
33+
%(prog)s --runtime-only --output ./packages/algod_client --package-name algod_client
34+
%(prog)s ../specs/algod.oas3.json --output ./packages/algod_client --package-name algod_client
35+
%(prog)s ../specs/indexer.oas3.json -o ./packages/indexer_client -p indexer_client
36+
""",
37+
)
38+
39+
parser.add_argument(
40+
"spec_file",
41+
nargs="?",
42+
type=Path,
43+
help="Path to OpenAPI specification file (JSON or YAML)",
44+
metavar="SPEC_FILE",
45+
)
46+
parser.add_argument(
47+
"--runtime-only",
48+
action="store_true",
49+
help="Generate only the base runtime files (no models/APIs); does not require a spec file",
50+
)
51+
parser.add_argument(
52+
"--output",
53+
"-o",
54+
type=Path,
55+
default=Path("./generated_ts"),
56+
help="Output directory for generated files (default: %(default)s)",
57+
dest="output_dir",
58+
)
59+
parser.add_argument(
60+
"--package-name",
61+
"-p",
62+
default="api_ts_client",
63+
help="Name for the generated TypeScript package (default: %(default)s)",
64+
dest="package_name",
65+
)
66+
parser.add_argument(
67+
"--template-dir",
68+
"-t",
69+
type=Path,
70+
help="Custom template directory (optional)",
71+
dest="template_dir",
72+
)
73+
parser.add_argument(
74+
"--verbose",
75+
"-v",
76+
action="store_true",
77+
help="Enable verbose output",
78+
)
79+
parser.add_argument(
80+
"--description",
81+
"-d",
82+
help="Custom description for the generated package (overrides spec description)",
83+
dest="custom_description",
84+
)
85+
86+
parsed_args = parser.parse_args(args)
87+
88+
# Validate inputs
89+
if not parsed_args.runtime_only and parsed_args.spec_file is None:
90+
parser.error("SPEC_FILE is required unless --runtime-only is provided")
91+
92+
if parsed_args.spec_file is not None and not parsed_args.spec_file.exists():
93+
parser.error(f"Specification file not found: {parsed_args.spec_file}")
94+
95+
return parsed_args
96+
97+
98+
def print_generation_summary(*, file_count: int, files: dict[Path, str], output_dir: Path) -> None:
99+
"""Print summary of generated files."""
100+
print(f"Generated {file_count} files:")
101+
for file_path in sorted(files.keys()):
102+
print(f" {file_path}")
103+
print(f"\nTypeScript client generated successfully in {output_dir}")
104+
105+
106+
@contextlib.contextmanager
107+
def backup_and_prepare_output_dir(output_dir: Path) -> Generator[None, None, None]:
108+
"""Backup and ensure the output directory exists before generation."""
109+
backup_dir: Path | None = None
110+
111+
# Create a backup of the existing directory if it exists and is non-empty
112+
if output_dir.exists() and any(output_dir.iterdir()):
113+
backup_dir = Path(tempfile.mkdtemp(prefix="tsgen_bak_"))
114+
shutil.copytree(output_dir, backup_dir, dirs_exist_ok=True)
115+
116+
# Ensure directory exists
117+
output_dir.mkdir(parents=True, exist_ok=True)
118+
119+
try:
120+
yield
121+
except Exception:
122+
if backup_dir:
123+
print(
124+
"Error: Generation failed. Restoring original content.",
125+
file=sys.stderr,
126+
)
127+
# Restore backup
128+
if output_dir.exists():
129+
shutil.rmtree(output_dir)
130+
shutil.copytree(backup_dir, output_dir, dirs_exist_ok=True)
131+
raise
132+
finally:
133+
if backup_dir and backup_dir.exists():
134+
shutil.rmtree(backup_dir)
135+
136+
137+
def main(args: list[str] | None = None) -> int:
138+
parsed_args = parse_command_line_args(args)
139+
140+
try:
141+
with backup_and_prepare_output_dir(parsed_args.output_dir):
142+
generator = TsCodeGenerator(template_dir=parsed_args.template_dir)
143+
144+
if parsed_args.runtime_only:
145+
generated_files = generator.generate_runtime(
146+
parsed_args.output_dir,
147+
parsed_args.package_name,
148+
custom_description=parsed_args.custom_description,
149+
)
150+
else:
151+
generated_files = generator.generate_full(
152+
parsed_args.spec_file,
153+
parsed_args.output_dir,
154+
parsed_args.package_name,
155+
custom_description=parsed_args.custom_description,
156+
)
157+
158+
# Write files to disk (overwrite safely)
159+
write_files_to_disk(generated_files)
160+
161+
if parsed_args.verbose:
162+
print_generation_summary(
163+
file_count=len(generated_files),
164+
files=generated_files,
165+
output_dir=parsed_args.output_dir,
166+
)
167+
else:
168+
print(f"TypeScript client generated successfully in {parsed_args.output_dir}")
169+
170+
return EXIT_SUCCESS
171+
172+
except FileNotFoundError:
173+
print(
174+
f"Error: Specification file not found: {parsed_args.spec_file}",
175+
file=sys.stderr,
176+
)
177+
return EXIT_FILE_NOT_FOUND
178+
except json.JSONDecodeError as e:
179+
print(f"Error: Invalid JSON in specification file: {e}", file=sys.stderr)
180+
return EXIT_INVALID_JSON
181+
except Exception as e:
182+
print(f"Error: {e}", file=sys.stderr)
183+
if parsed_args.verbose:
184+
traceback.print_exc()
185+
return EXIT_GENERATION_ERROR
186+
187+
188+
if __name__ == "__main__":
189+
sys.exit(main())

api/oas_generator/ts_oas_generator/generator/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)