feat: add SDK codegen wrapper for openapi-generator-cli

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-21 01:08:39 +00:00
parent 303779f527
commit a09a4e9db9
2 changed files with 190 additions and 0 deletions

96
codegen.go Normal file
View file

@ -0,0 +1,96 @@
// SPDX-License-Identifier: EUPL-1.2
package api
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
)
// Supported SDK target languages.
var supportedLanguages = map[string]string{
"go": "go",
"typescript-fetch": "typescript-fetch",
"typescript-axios": "typescript-axios",
"python": "python",
"java": "java",
"csharp": "csharp-netcore",
"ruby": "ruby",
"swift": "swift5",
"kotlin": "kotlin",
"rust": "rust",
"php": "php",
}
// SDKGenerator wraps openapi-generator-cli for SDK generation.
type SDKGenerator struct {
// SpecPath is the path to the OpenAPI spec file (JSON or YAML).
SpecPath string
// OutputDir is the base directory for generated SDK output.
OutputDir string
// PackageName is the name used for the generated package/module.
PackageName string
}
// Generate creates an SDK for the given language using openapi-generator-cli.
// The language must be one of the supported languages returned by SupportedLanguages().
func (g *SDKGenerator) Generate(ctx context.Context, language string) error {
generator, ok := supportedLanguages[language]
if !ok {
return fmt.Errorf("unsupported language %q: supported languages are %v", language, SupportedLanguages())
}
if _, err := os.Stat(g.SpecPath); os.IsNotExist(err) {
return fmt.Errorf("spec file not found: %s", g.SpecPath)
}
outputDir := filepath.Join(g.OutputDir, language)
if err := os.MkdirAll(outputDir, 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}
args := g.buildArgs(generator, outputDir)
cmd := exec.CommandContext(ctx, "openapi-generator-cli", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("openapi-generator-cli failed for %s: %w", language, err)
}
return nil
}
// buildArgs constructs the openapi-generator-cli command arguments.
func (g *SDKGenerator) buildArgs(generator, outputDir string) []string {
args := []string{
"generate",
"-i", g.SpecPath,
"-g", generator,
"-o", outputDir,
}
if g.PackageName != "" {
args = append(args, "--additional-properties", "packageName="+g.PackageName)
}
return args
}
// Available checks if openapi-generator-cli is installed and accessible.
func (g *SDKGenerator) Available() bool {
_, err := exec.LookPath("openapi-generator-cli")
return err == nil
}
// SupportedLanguages returns the list of supported SDK target languages.
func SupportedLanguages() []string {
langs := make([]string, 0, len(supportedLanguages))
for k := range supportedLanguages {
langs = append(langs, k)
}
return langs
}

94
codegen_test.go Normal file
View file

@ -0,0 +1,94 @@
// SPDX-License-Identifier: EUPL-1.2
package api_test
import (
"context"
"os"
"path/filepath"
"slices"
"strings"
"testing"
api "forge.lthn.ai/core/go-api"
)
// ── SDKGenerator tests ─────────────────────────────────────────────────────
func TestSDKGenerator_Good_SupportedLanguages(t *testing.T) {
langs := api.SupportedLanguages()
if len(langs) == 0 {
t.Fatal("expected at least one supported language")
}
expected := []string{"go", "typescript-fetch", "python", "java", "csharp"}
for _, lang := range expected {
if !slices.Contains(langs, lang) {
t.Errorf("expected %q in supported languages, got %v", lang, langs)
}
}
}
func TestSDKGenerator_Bad_UnsupportedLanguage(t *testing.T) {
gen := &api.SDKGenerator{
SpecPath: "spec.json",
OutputDir: t.TempDir(),
}
err := gen.Generate(context.Background(), "brainfuck")
if err == nil {
t.Fatal("expected error for unsupported language, got nil")
}
if !strings.Contains(err.Error(), "unsupported language") {
t.Fatalf("expected error to contain 'unsupported language', got: %v", err)
}
}
func TestSDKGenerator_Bad_MissingSpec(t *testing.T) {
gen := &api.SDKGenerator{
SpecPath: filepath.Join(t.TempDir(), "nonexistent.json"),
OutputDir: t.TempDir(),
}
err := gen.Generate(context.Background(), "go")
if err == nil {
t.Fatal("expected error for missing spec file, got nil")
}
if !strings.Contains(err.Error(), "spec file not found") {
t.Fatalf("expected error to contain 'spec file not found', got: %v", err)
}
}
func TestSDKGenerator_Good_OutputDirCreated(t *testing.T) {
// Write a minimal spec file so we pass the file-exists check.
specDir := t.TempDir()
specPath := filepath.Join(specDir, "spec.json")
if err := os.WriteFile(specPath, []byte(`{"openapi":"3.1.0"}`), 0o644); err != nil {
t.Fatalf("failed to write spec file: %v", err)
}
outputDir := filepath.Join(t.TempDir(), "nested", "sdk")
gen := &api.SDKGenerator{
SpecPath: specPath,
OutputDir: outputDir,
}
// Generate will fail at the exec step (openapi-generator-cli likely not installed),
// but the output directory should have been created before that.
_ = gen.Generate(context.Background(), "go")
expected := filepath.Join(outputDir, "go")
info, err := os.Stat(expected)
if err != nil {
t.Fatalf("expected output directory %s to exist, got error: %v", expected, err)
}
if !info.IsDir() {
t.Fatalf("expected %s to be a directory", expected)
}
}
func TestSDKGenerator_Good_Available(t *testing.T) {
gen := &api.SDKGenerator{}
// Just verify it returns a bool and does not panic.
_ = gen.Available()
}