diff --git a/docs/plans/2026-01-29-sdk-generation-design.md b/docs/plans/2026-01-29-sdk-generation-design.md new file mode 100644 index 0000000..ee189fc --- /dev/null +++ b/docs/plans/2026-01-29-sdk-generation-design.md @@ -0,0 +1,291 @@ +# SDK Generation Design + +## Summary + +Generate typed API clients from OpenAPI specs for TypeScript, Python, Go, and PHP. Includes breaking change detection via semantic diff. + +## Design Decisions + +- **Generator approach**: Hybrid - native generators where available, openapi-generator fallback +- **Languages**: TypeScript, Python, Go, PHP (Core 4) +- **Detection**: Config → common paths → Laravel Scramble +- **Output**: Local `sdk/` + optional monorepo publish +- **Diff**: Semantic with oasdiff, CI-friendly exit codes +- **Priority**: DX (developer experience) + +## Package Structure + +``` +pkg/sdk/ +├── sdk.go # Main SDK type, orchestration +├── detect.go # OpenAPI spec detection +├── diff.go # Breaking change detection (oasdiff) +├── generators/ +│ ├── generator.go # Generator interface +│ ├── typescript.go # openapi-typescript-codegen +│ ├── python.go # openapi-python-client +│ ├── go.go # oapi-codegen +│ └── php.go # openapi-generator (Docker) +└── templates/ # Package scaffolding templates + ├── typescript/ + │ └── package.json.tmpl + ├── python/ + │ └── setup.py.tmpl + ├── go/ + │ └── go.mod.tmpl + └── php/ + └── composer.json.tmpl +``` + +## OpenAPI Detection Flow + +``` +1. Check config: sdk.spec in .core/release.yaml + ↓ not found +2. Check common paths: + - api/openapi.yaml + - api/openapi.json + - openapi.yaml + - openapi.json + - docs/api.yaml + - swagger.yaml + ↓ not found +3. Laravel Scramble detection: + - Check for scramble/scramble in composer.json + - Run: php artisan scramble:export --path=api/openapi.json + - Use generated spec + ↓ not found +4. Error: No OpenAPI spec found +``` + +## Generator Interface + +```go +type Generator interface { + // Language returns the generator's target language + Language() string + + // Generate creates SDK from OpenAPI spec + Generate(ctx context.Context, opts GenerateOptions) error + + // Available checks if generator dependencies are installed + Available() bool + + // Install provides installation instructions + Install() string +} + +type GenerateOptions struct { + SpecPath string // OpenAPI spec file + OutputDir string // Where to write SDK + PackageName string // Package/module name + Version string // SDK version +} +``` + +### Native Generators + +| Language | Tool | Install | +|------------|----------------------------|--------------------------------| +| TypeScript | openapi-typescript-codegen | `npm i -g openapi-typescript-codegen` | +| Python | openapi-python-client | `pip install openapi-python-client` | +| Go | oapi-codegen | `go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest` | +| PHP | openapi-generator (Docker) | Requires Docker | + +### Fallback Strategy + +```go +func (g *TypeScriptGenerator) Generate(ctx context.Context, opts GenerateOptions) error { + if g.Available() { + return g.generateNative(ctx, opts) + } + return g.generateDocker(ctx, opts) // openapi-generator in Docker +} +``` + +## Breaking Change Detection + +Using [oasdiff](https://github.com/Tufin/oasdiff) for semantic OpenAPI comparison: + +```go +import "github.com/tufin/oasdiff/diff" +import "github.com/tufin/oasdiff/checker" + +func (s *SDK) Diff(base, revision string) (*DiffResult, error) { + // Load specs + baseSpec, _ := load.From(loader, base) + revSpec, _ := load.From(loader, revision) + + // Compute diff + d, _ := diff.Get(diff.NewConfig(), baseSpec, revSpec) + + // Check for breaking changes + breaks := checker.CheckBackwardCompatibility( + checker.GetDefaultChecks(), + d, + baseSpec, + revSpec, + ) + + return &DiffResult{ + Breaking: len(breaks) > 0, + Changes: breaks, + Summary: formatSummary(d), + }, nil +} +``` + +### Exit Codes for CI + +| Exit Code | Meaning | +|-----------|---------| +| 0 | No breaking changes | +| 1 | Breaking changes detected | +| 2 | Error (invalid spec, etc.) | + +### Breaking Change Categories + +- Removed endpoints +- Changed required parameters +- Modified response schemas +- Changed authentication requirements + +## CLI Commands + +```bash +# Generate SDKs from OpenAPI spec +core sdk generate # Uses .core/release.yaml config +core sdk generate --spec api.yaml # Explicit spec file +core sdk generate --lang typescript # Single language + +# Check for breaking changes +core sdk diff # Compare current vs last release +core sdk diff --spec api.yaml --base v1.0.0 + +# Validate spec before generation +core sdk validate +core sdk validate --spec api.yaml +``` + +## Config Schema + +In `.core/release.yaml`: + +```yaml +sdk: + # OpenAPI spec source (auto-detected if omitted) + spec: api/openapi.yaml + + # Languages to generate + languages: + - typescript + - python + - go + - php + + # Output directory (default: sdk/) + output: sdk/ + + # Package naming + package: + name: myapi # Base name + version: "{{.Version}}" + + # Breaking change detection + diff: + enabled: true + fail_on_breaking: true # CI fails on breaking changes + + # Optional: publish to monorepo + publish: + repo: myorg/sdks + path: packages/myapi +``` + +## Output Structure + +Each generator outputs to `sdk/{lang}/`: + +``` +sdk/ +├── typescript/ +│ ├── package.json +│ ├── src/ +│ │ ├── index.ts +│ │ ├── client.ts +│ │ └── models/ +│ └── tsconfig.json +├── python/ +│ ├── setup.py +│ ├── myapi/ +│ │ ├── __init__.py +│ │ ├── client.py +│ │ └── models/ +│ └── requirements.txt +├── go/ +│ ├── go.mod +│ ├── client.go +│ └── models.go +└── php/ + ├── composer.json + ├── src/ + │ ├── Client.php + │ └── Models/ + └── README.md +``` + +## Publishing Workflow + +SDK publishing integrates with the existing release pipeline: + +``` +core release + → build artifacts + → generate SDKs (if sdk: configured) + → run diff check (warns or fails on breaking) + → publish to GitHub release + → publish SDKs (optional) +``` + +### Monorepo Publishing + +For projects using a shared SDK monorepo: + +1. Clone target repo (shallow) +2. Update `packages/{name}/{lang}/` +3. Commit with version tag +4. Push (triggers downstream CI) + +The SDK tarball is also attached to GitHub releases for direct download. + +## Implementation Steps + +1. Create `pkg/sdk/` package structure +2. Implement OpenAPI detection (`detect.go`) +3. Define Generator interface (`generators/generator.go`) +4. Implement TypeScript generator (native + fallback) +5. Implement Python generator (native + fallback) +6. Implement Go generator (native) +7. Implement PHP generator (Docker-based) +8. Add package templates (`templates/`) +9. Implement diff with oasdiff (`diff.go`) +10. Add CLI commands (`cmd/core/sdk.go`) +11. Integrate with release pipeline +12. Add monorepo publish support + +## Dependencies + +```go +// go.mod additions +require ( + github.com/tufin/oasdiff v1.x.x + github.com/getkin/kin-openapi v0.x.x +) +``` + +## Testing + +- Unit tests for each generator +- Integration tests with sample OpenAPI specs +- Diff tests with known breaking/non-breaking changes +- E2E test generating SDKs for a real API