re #22 add SDK emitters (typed TypeScript + Python clients from OpenAPI)#91
Merged
Conversation
`bin/altair spec:emit-sdk typescript|python` turns the merged OpenAPI
3.1 document into a production-quality, idiomatic client SDK. No
external code-gen runtime (no Java / openapi-generator) — the document
is parsed with symfony/yaml (already a dep) into a language-neutral
model, and each language emitter walks that model.
## What ships (src/Altair/Scaffold/Sdk/)
- Model/ — OpenApiParser + OpenApiDocument / OperationModel /
ResponseModel / SchemaType (handles inline schemas, $ref,
components.schemas, enums, OpenAPI-3.1 `type: [x, null]` nullability)
- Contracts/EmitterInterface, EmitterRegistry (language → emitter),
EmittedSdk value object, Exception/SdkException
- TypeScript/TypeScriptEmitter — fetch-based, zero runtime deps,
`interface` for objects + `type` for unions, status-discriminated
response unions, one tree-shakeable exported function per operation,
ApiOptions + ApiError + private request() helper
- Python/PythonEmitter — httpx + pydantic v2, str/Enum for enums,
snake_case fields with Field(alias=...) for wire mapping, sync
Client + AsyncClient
- Cli/EmitSdkCommand — `<language>`, `--list`, `--out`, `--multi-file`,
`--check` (CI drift gate), `--openapi=<path>` (else merges
docs/openapi/*.yaml)
operationId synthesis singularises POST-to-collection
(`POST /users` → createUser) and handles path params
(`GET /users/{id}` → getUsersById).
## Determinism
Named types alpha-sorted, operations in document order → byte-stable
output, so `--check` is a reliable drift gate for CI.
## Tests (32, 2 skipped)
- OpenApiParser: title/version/operations, components+enum, request
body + path params, response union, operationId synthesis, non-map
rejection
- TypeScript + Python emitters: golden-shape assertions per language
feature + determinism + multi-file split
- EmitterRegistry: get/has/available/case-insensitive/unknown-throws
- EmitSdkCommand: --list, unknown lang, stdout, file + --check pass,
drift detection, multi-file
- CompileIntegrationTest: runs real tsc/mypy against emitted output;
SKIPPED when the toolchain is absent (matches ext-redis pattern)
## Docs
- .agent/packages/scaffold.md SDK section
- AGENT.md §2 scaffold entry
- CLAUDE.md §3 "Client SDK emitters" subsection
No new composer deps — symfony/yaml was already a scaffold dependency.
Closes Phase 1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #22. This is the last Phase-1 issue — Phase 1 is complete.
Summary
bin/altair spec:emit-sdk typescript|pythonturns the merged OpenAPI 3.1 document into a production-quality, idiomatic client SDK. No external code-gen runtime — no Java, noopenapi-generator. The document is parsed withsymfony/yaml(already a scaffold dep) into a language-neutral model, then each emitter walks that model.The framework's pitch — "one spec, one consistent API for humans, clients, and AI" — now covers client SDKs too.
CLI
Input defaults to merging
docs/openapi/*.yamlfragments (same merge asspec:emit-openapi);--openapi=<path>overrides.TypeScript output
fetch-based, zero runtime dependenciesinterfacefor object shapes,typefor unions{ status: 201; data: ... } | { status: 422; data: ... }async functionper operationApiOptionstype,ApiErrorclass, privaterequest()helperPython output
httpx+pydantic v2str, Enumfor enums;BaseModelper object schemaField(alias=...)for wire-name mappingClient(sync) andAsyncClientArchitecture
CLI:
Cli/EmitSdkCommand. Add a language by implementingEmitterInterface+ registering it — no need to fork the existing emitters.Parser handles inline schemas,
$ref,components.schemas, enums, and OpenAPI-3.1type: [x, "null"]nullability.operationIdis honoured when present; synthesised otherwise (POST /users→createUsersingularised,GET /users/{id}→getUsersById).Determinism
Named types alpha-sorted, operations in document order → byte-stable output.
--checkis therefore a reliable CI drift gate.Tests (32, 2 skipped)
OpenApiParserTest— title/version/operations, components + enum, request body + path params, response union, operationId synthesis, non-map rejectionTypeScriptEmitterTest/PythonEmitterTest— golden-shape assertions per language feature, determinism, multi-file splitEmitterRegistryTest— get / has / available / case-insensitive / unknown-throwsEmitSdkCommandTest—--list, unknown language, stdout, file +--checkpass, drift detection, multi-fileCompileIntegrationTest— runs realtsc/mypyagainst emitted output; skipped when the toolchain is absent (same pattern as the ext-redis / ext-mongodb storage tests, so the framework suite stays green without Node/Python)Test plan
composer test --filter 'Altair\\Tests\\Scaffold\\Sdk\\\\'— 32 tests, 81 assertions, 0 failures, 2 skippedcomposer cs— cleancomposer stan— cleancomposer rector— clean (fixpoint)--checkexits non-zero on drift, zero when in syncDecisions / notes
cebe/php-openapi. The issue assumed it was already a dep (it wasn't). The framework's own OpenAPI is plain JSON-schema-shaped YAML, sosymfony/yaml(already present) is sufficient — keeps the dependency footprint minimal.tsc/mypynot added to CI here. The compile test is skip-on-absent; wiring Node + Python toolchains into.github/workflows/ci.ymlto un-skip it is a deliberate follow-up, not bundled into this PR.Sdk/Parser/OpenApiParser.php) was found mid-build and, per discussion, merged intoSdk/Model/OpenApiParser.php(kept its singularisation + richer operationId synthesis; kept my YAML/$ref/enum/error handling), then removed.Out of scope (follow-ups, per issue)
EmitterInterfacestructure)CompileIntegrationTest