Skip to content

Commit 5fbaad1

Browse files
authored
feat: add go-zero framework support (#8)
* docs: add go-zero framework support design and implementation plan - Design doc with architecture and component details - Implementation plan with 9 tasks (including goctl bug patches) - Bug fixes for goctl issues #5426, #5427, #5428 Signed-off-by: spencercjh <spencercjh@gmail.com> * feat(gozero): add go-zero extractor package stubs Add initial package structure for go-zero framework support: - detector.go: Project detection with go.mod parsing (placeholder) - generator.go: OpenAPI spec generation via goctl (placeholder) - patcher.go: Project patching for goctl compatibility (placeholder) - gozero.go: Constants for build tool and goctl versions - Test files for all components verifying instantiation Task: Task 1 - Create gozero package structure Signed-off-by: spencercjh <spencercjh@gmail.com> * feat(gozero): implement Detector with go.mod and .api file detection Signed-off-by: spencercjh <spencercjh@gmail.com> * feat(extractor): extend ProjectInfo with go-zero fields Signed-off-by: spencercjh <spencercjh@gmail.com> * feat(gozero): implement Patcher with goctl version check Signed-off-by: spencercjh <spencercjh@gmail.com> * fix(gozero): patch goctl swagger generation bugs (#5426-5428) Signed-off-by: spencercjh <spencercjh@gmail.com> * fix(gozero): add API file patcher for goctl bug #5425 - Detect and patch unquoted multi-hyphen prefix values in .api files - Automatically wrap values in quotes before passing to goctl - Cleanup patched files after swagger generation - Add ValidateAPIFile for manual validation with helpful error messages Signed-off-by: spencercjh <spencercjh@gmail.com> * test(gozero): update demo API to match Spring Boot demo Add user.api with equivalent endpoints: - GET /users/:id - Get user by ID - GET /users - List users with pagination - POST /users - Create user - POST /users/upload - Upload file - POST /users/:id/profile - Update profile Includes all DTOs: User, ApiResponse, PageResult, FileUploadResult Signed-off-by: spencercjh <spencercjh@gmail.com> * fix(gozero): resolve all linter issues (gci, gocritic, gosec, govet, modernize, perfsprint) Signed-off-by: spencercjh <spencercjh@gmail.com> * feat(extractor): add framework registration mechanism Add Extractor interface and global registry for framework plugins. Each framework (spring, gozero) registers itself via init(). Generate command now loops through all registered extractors. - Add Extractor interface in internal/extractor/types.go - Add Register/Get/GetAll/DetectFramework functions - Add PatchResult type for uniform patch results - Implement Extractor for spring package - Implement Extractor for gozero package - Update generate.go to use registration mechanism Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor(extractor): use Framework constants in detectors Use FrameworkSpringBoot and FrameworkGoZero constants when setting ProjectInfo.Framework in both spring and gozero detectors. Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor(extractor): compose ProjectInfo with framework-specific structs Split ProjectInfo into common fields and framework-specific sub-structs: - ProjectInfo: Framework, BuildTool, BuildFilePath - SpringInfo: Spring Boot specific fields (HasSpringdocDeps, IsMultiModule, etc.) - GoZeroInfo: go-zero specific fields (HasGoZeroDeps, HasGoctl, APIFiles, etc.) This eliminates field pollution and makes the data model cleaner. Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor: decouple extractor types and move registration to builtin package - Create internal/extractor/builtin/ package with registry - Move SpringInfo to spring package as Info struct - Move GoZeroInfo to gozero package as Info struct - Change ProjectInfo.FrameworkData to 'any' type for framework-specific data - Update all type assertions to use local Info types - Remove empty init() functions from generator files - Update all test files to use new type structure - Fix linter issues (errcheck, govet shadowing) Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor: move framework constants to framework-specific packages - Add FrameworkGoZero constant to gozero package - Add FrameworkSpringBoot constant to spring package - Update detectors to use local framework constants - Remove framework constants from extractor/types.go Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor(gozero): remove duplicate ProjectInfo struct - Remove ProjectInfo from gozero.go (duplicate of Info in info.go) - Update NeedsPatch to use *Info instead of *ProjectInfo - Update test to use *Info Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor(gozero): use golang.org/x/mod/modfile to parse go.mod - Replace manual scanner-based parsing with modfile.Parse - Remove parseRequireLine helper function - Simplify parseGoMod logic using modfile API - Update go.mod dependency Signed-off-by: spencercjh <spencercjh@gmail.com> * fix: address PR #8 review feedback - Remove empty const block from types.go - Add ErrNotGoZeroProject custom error type for better error handling - Fix detector to reject projects without go-zero deps or .api files - Use exact module path match for go-zero dependency - Fix generator to respect OutputDir option - Add nil checks in swagger_patch.go to prevent panics - Update tests to use errors.As for error type checking Signed-off-by: spencercjh <spencercjh@gmail.com> * feat: add comprehensive logging to go-zero extractor - Add debug/info/warn/error logging to detector for detection flow - Add logging to patcher for goctl availability check - Add logging to generator for swagger generation and conversion - Add logging to api_patch for file patching operations - Add logging to swagger_patch for document patching - Enhance builtin.DetectFramework logging with detailed progress Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor(spring): reduce cyclomatic complexity of patchGradle function Signed-off-by: spencercjh <spencercjh@gmail.com> * feat(gozero-demo): generate complete go-zero framework code with goctl Signed-off-by: spencercjh <spencercjh@gmail.com> * fix(go-zero): fix goctl swagger output path and add missing path params - Fix goctl swagger filename extension (.json auto-added by goctl) - Add -dir flag to specify output directory - Add missing path params when converting Swagger to OpenAPI 3 - Remove old goctl-openapi.json and add new openapi.yaml output Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: spencercjh <spencercjh@gmail.com> * style(go-zero): fix lint issues and format gozero demo code - Combine append calls in generator.go (gocritic appendCombine) - Remove unnecessary blank lines in gozero demo imports - Fix bare return statements to explicit returns Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: spencercjh <spencercjh@gmail.com> * refactor(executor): decouple executor from specific tools - Remove Hint field from CommandNotFoundError - Remove getInstallHint function from executor package - Move install hints to each caller (spring, publisher) - Update tests to remove Hint field references - Add nolint comments for errors.AsType calls This eliminates coupling between executor package and specific tools like mvn, gradle, rdme, goctl. Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: spencercjh <spencercjh@gmail.com> * fix: address all remaining PR #8 review comments 1. spring/patcher.go: Fix error handling in Gradle patch helpers - addGradleDependencyIfNeeded now returns (string, error) - addGradlePluginIfNeeded now returns (string, error) - Errors are propagated instead of silently ignored 2. gozero/generator.go: Fix api directory matching - Change strings.HasPrefix(relPath, "api") to strings.HasPrefix(normalizedPath, "api/") - Prevents matching directories like "api2/" 3. gozero/generator.go: Fix patched files logging - Use apiPatcher.HasPatchedFiles() instead of len(patchedFiles) - Only logs when files are actually modified 4. gozero/detector.go: Fix vendor directory check - Change strings.Contains(path, "vendor") to info.Name() == "vendor" - Prevents skipping unrelated directories like "myvendor" 5. gozero/detector.go: Fix goctl PATH check - Remove os.Stat("goctl") which only checks current directory - Now relies solely on exec.LookPath for proper PATH lookup 6. gozero/swagger_patch.go: Fix path parameter regex - Change from \w+ to [^\}]+ to support hyphens in param names - Now supports paths like /users/{user-id} Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: spencercjh <spencercjh@gmail.com> --------- Signed-off-by: spencercjh <spencercjh@gmail.com> Signed-off-by: Claude <noreply@anthropic.com>
1 parent fa3f248 commit 5fbaad1

51 files changed

Lines changed: 6300 additions & 166 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/generate.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"github.com/spencercjh/spec-forge/internal/enricher/processor"
2020
"github.com/spencercjh/spec-forge/internal/enricher/provider"
2121
"github.com/spencercjh/spec-forge/internal/extractor"
22-
"github.com/spencercjh/spec-forge/internal/extractor/spring"
22+
"github.com/spencercjh/spec-forge/internal/extractor/builtin" // registers built-in extractors
2323
"github.com/spencercjh/spec-forge/internal/publisher"
2424
"github.com/spencercjh/spec-forge/internal/validator"
2525
)
@@ -70,54 +70,51 @@ func runGenerate(cmd *cobra.Command, args []string) error { //nolint:gocyclo //
7070

7171
slog.InfoContext(ctx, "Generating OpenAPI spec", "path", path)
7272

73-
// Step 1: Detect project
74-
detector := spring.NewDetector()
75-
info, err := detector.Detect(path)
73+
// Step 1: Detect framework - try all registered extractors
74+
extractorImpl, info, err := builtin.DetectFramework(path)
7675
if err != nil {
77-
return errWrap("detection failed", err)
76+
return errWrap("no supported framework detected", err)
7877
}
7978

8079
slog.InfoContext(ctx, "Detected project",
80+
"framework", extractorImpl.Name(),
8181
"tool", info.BuildTool,
8282
"build_file", info.BuildFilePath,
83-
"multi_module", info.IsMultiModule,
8483
)
8584

8685
// Step 2: Patch project if needed
87-
patcher := spring.NewPatcher()
8886
patchOpts := &extractor.PatchOptions{
8987
KeepPatched: generateKeepPatched,
9088
}
9189

92-
result, err := patcher.Patch(path, patchOpts)
90+
patchResult, err := extractorImpl.Patch(path, patchOpts)
9391
if err != nil {
9492
return errWrap("patch failed", err)
9593
}
9694

9795
// Step 3: If we patched the file and should restore later, defer the restore
98-
if !generateKeepPatched && result.OriginalContent != "" {
96+
if !generateKeepPatched && patchResult.OriginalContent != "" {
9997
defer func() {
10098
slog.InfoContext(ctx, "Restoring original build file...")
101-
if restoreErr := patcher.Restore(result.BuildFilePath, result.OriginalContent); restoreErr != nil {
99+
if restoreErr := extractorImpl.Restore(patchResult.BuildFilePath, patchResult.OriginalContent); restoreErr != nil {
102100
slog.WarnContext(ctx, "failed to restore original file", "error", restoreErr)
103101
} else {
104102
slog.InfoContext(ctx, "Original build file restored", "status", "✅")
105103
}
106104
}()
107105
}
108106

109-
if result.DependencyAdded {
110-
slog.InfoContext(ctx, "springdoc dependency added temporarily", "status", "✅")
107+
if patchResult.DependencyAdded {
108+
slog.InfoContext(ctx, "dependencies added temporarily", "status", "✅")
111109
}
112-
if result.PluginAdded {
113-
slog.InfoContext(ctx, "springdoc plugin added temporarily", "status", "✅")
110+
if patchResult.PluginAdded {
111+
slog.InfoContext(ctx, "plugin added temporarily", "status", "✅")
114112
}
115-
if result.SpringBootConfigured {
113+
if patchResult.SpringBootConfigured {
116114
slog.InfoContext(ctx, "spring-boot-maven-plugin configured with start/stop goals", "status", "✅")
117115
}
118116

119117
// Step 4: Generate OpenAPI spec
120-
generator := spring.NewGenerator()
121118

122119
// Determine output directory
123120
outputDir := generateOutput
@@ -132,7 +129,7 @@ func runGenerate(cmd *cobra.Command, args []string) error { //nolint:gocyclo //
132129
SkipTests: true,
133130
}
134131

135-
genResult, err := generator.Generate(ctx, path, info, genOpts)
132+
genResult, err := extractorImpl.Generate(ctx, path, info, genOpts)
136133
if err != nil {
137134
return errWrap("generation failed", err)
138135
}

cmd/spring.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,26 +64,32 @@ func printProjectInfo(ctx context.Context, info *extractor.ProjectInfo) {
6464
slog.InfoContext(ctx, "Spring Project Detection Results")
6565
slog.InfoContext(ctx, "Build Tool", "tool", info.BuildTool)
6666
slog.InfoContext(ctx, "Build File", "path", info.BuildFilePath)
67-
slog.InfoContext(ctx, "Spring Boot", "version", info.SpringBootVersion)
6867

69-
if info.IsMultiModule {
68+
springInfo, ok := info.FrameworkData.(*spring.Info)
69+
if !ok || springInfo == nil {
70+
springInfo = &spring.Info{}
71+
}
72+
73+
slog.InfoContext(ctx, "Spring Boot", "version", springInfo.SpringBootVersion)
74+
75+
if springInfo.IsMultiModule {
7076
slog.InfoContext(ctx, "Multi-Module", "enabled", "✅ Yes")
71-
slog.InfoContext(ctx, "Modules", "list", info.Modules)
72-
if info.MainModule != "" {
73-
slog.InfoContext(ctx, "Main Module", "name", info.MainModule)
74-
slog.InfoContext(ctx, "Main Module Path", "path", info.MainModulePath)
77+
slog.InfoContext(ctx, "Modules", "list", springInfo.Modules)
78+
if springInfo.MainModule != "" {
79+
slog.InfoContext(ctx, "Main Module", "name", springInfo.MainModule)
80+
slog.InfoContext(ctx, "Main Module Path", "path", springInfo.MainModulePath)
7581
}
7682
} else {
7783
slog.InfoContext(ctx, "Multi-Module", "enabled", "❌ No")
7884
}
7985

80-
if info.HasSpringdocDeps {
81-
slog.InfoContext(ctx, "springdoc Dependency", "status", "✅ Present", "version", info.SpringdocVersion)
86+
if springInfo.HasSpringdocDeps {
87+
slog.InfoContext(ctx, "springdoc Dependency", "status", "✅ Present", "version", springInfo.SpringdocVersion)
8288
} else {
8389
slog.InfoContext(ctx, "springdoc Dependency", "status", "❌ Not found")
8490
}
8591

86-
if info.HasSpringdocPlugin {
92+
if springInfo.HasSpringdocPlugin {
8793
slog.InfoContext(ctx, "springdoc Plugin", "status", "✅ Configured")
8894
} else {
8995
slog.InfoContext(ctx, "springdoc Plugin", "status", "❌ Not configured")

0 commit comments

Comments
 (0)