cli/docs/plans/2026-01-29-sdk-generation-design.md
Snider 53b3d2613d docs: add SDK generation design (S3.4)
Design document for OpenAPI SDK generation feature:
- Hybrid generators (native + openapi-generator fallback)
- Core 4 languages: TypeScript, Python, Go, PHP
- Auto-detection: config → common paths → Laravel Scramble
- Breaking change detection with oasdiff
- Monorepo publish support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 00:59:17 +00:00

7.4 KiB

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

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

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 for semantic OpenAPI comparison:

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

# 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:

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.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