From a09a4e9db9f21ee37c3b909cbfe85e0859a871c9 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Feb 2026 01:08:39 +0000 Subject: [PATCH] feat: add SDK codegen wrapper for openapi-generator-cli Co-Authored-By: Virgil Co-Authored-By: Claude Opus 4.6 --- codegen.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ codegen_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 codegen.go create mode 100644 codegen_test.go diff --git a/codegen.go b/codegen.go new file mode 100644 index 0000000..1c068e0 --- /dev/null +++ b/codegen.go @@ -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 +} diff --git a/codegen_test.go b/codegen_test.go new file mode 100644 index 0000000..3ba5210 --- /dev/null +++ b/codegen_test.go @@ -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() +}