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 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-29 01:11:15 +00:00
parent 64a5b0e605
commit c2b1e5dc61
2 changed files with 127 additions and 0 deletions

78
pkg/sdk/detect.go Normal file
View file

@ -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\"")
}

49
pkg/sdk/detect_test.go Normal file
View file

@ -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")
}
}