From c2b1e5dc61cecf0e585f6e784bb978afbd998d50 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 01:11:15 +0000 Subject: [PATCH] feat(sdk): add OpenAPI spec detection Detects OpenAPI spec via: 1. Configured spec path 2. Common paths (api/openapi.yaml, openapi.yaml, etc.) 3. Laravel Scramble (stub for now) Co-Authored-By: Claude Opus 4.5 --- pkg/sdk/detect.go | 78 ++++++++++++++++++++++++++++++++++++++++++ pkg/sdk/detect_test.go | 49 ++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 pkg/sdk/detect.go create mode 100644 pkg/sdk/detect_test.go diff --git a/pkg/sdk/detect.go b/pkg/sdk/detect.go new file mode 100644 index 00000000..aeb221f1 --- /dev/null +++ b/pkg/sdk/detect.go @@ -0,0 +1,78 @@ +package sdk + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// commonSpecPaths are checked in order when no spec is configured. +var commonSpecPaths = []string{ + "api/openapi.yaml", + "api/openapi.json", + "openapi.yaml", + "openapi.json", + "docs/api.yaml", + "docs/api.json", + "swagger.yaml", + "swagger.json", +} + +// DetectSpec finds the OpenAPI spec file. +// Priority: config path -> common paths -> Laravel Scramble. +func (s *SDK) DetectSpec() (string, error) { + // 1. Check configured path + if s.config.Spec != "" { + specPath := filepath.Join(s.projectDir, s.config.Spec) + if _, err := os.Stat(specPath); err == nil { + return specPath, nil + } + return "", fmt.Errorf("sdk.DetectSpec: configured spec not found: %s", s.config.Spec) + } + + // 2. Check common paths + for _, p := range commonSpecPaths { + specPath := filepath.Join(s.projectDir, p) + if _, err := os.Stat(specPath); err == nil { + return specPath, nil + } + } + + // 3. Try Laravel Scramble detection + specPath, err := s.detectScramble() + if err == nil { + return specPath, nil + } + + return "", fmt.Errorf("sdk.DetectSpec: no OpenAPI spec found (checked config, common paths, Scramble)") +} + +// detectScramble checks for Laravel Scramble and exports the spec. +func (s *SDK) detectScramble() (string, error) { + composerPath := filepath.Join(s.projectDir, "composer.json") + if _, err := os.Stat(composerPath); err != nil { + return "", fmt.Errorf("no composer.json") + } + + // Check for scramble in composer.json + data, err := os.ReadFile(composerPath) + if err != nil { + return "", err + } + + // Simple check for scramble package + if !containsScramble(data) { + return "", fmt.Errorf("scramble not found in composer.json") + } + + // TODO: Run php artisan scramble:export + return "", fmt.Errorf("scramble export not implemented") +} + +// containsScramble checks if composer.json includes scramble. +func containsScramble(data []byte) bool { + content := string(data) + return strings.Contains(content, "dedoc/scramble") || + strings.Contains(content, "\"scramble\"") +} diff --git a/pkg/sdk/detect_test.go b/pkg/sdk/detect_test.go new file mode 100644 index 00000000..d12a0480 --- /dev/null +++ b/pkg/sdk/detect_test.go @@ -0,0 +1,49 @@ +package sdk + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDetectSpec_Good_ConfigPath(t *testing.T) { + // Create temp directory with spec at configured path + tmpDir := t.TempDir() + specPath := filepath.Join(tmpDir, "api", "spec.yaml") + os.MkdirAll(filepath.Dir(specPath), 0755) + os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644) + + sdk := New(tmpDir, &Config{Spec: "api/spec.yaml"}) + got, err := sdk.DetectSpec() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != specPath { + t.Errorf("got %q, want %q", got, specPath) + } +} + +func TestDetectSpec_Good_CommonPath(t *testing.T) { + // Create temp directory with spec at common path + tmpDir := t.TempDir() + specPath := filepath.Join(tmpDir, "openapi.yaml") + os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644) + + sdk := New(tmpDir, nil) + got, err := sdk.DetectSpec() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != specPath { + t.Errorf("got %q, want %q", got, specPath) + } +} + +func TestDetectSpec_Bad_NotFound(t *testing.T) { + tmpDir := t.TempDir() + sdk := New(tmpDir, nil) + _, err := sdk.DetectSpec() + if err == nil { + t.Fatal("expected error for missing spec") + } +}