Skip to content

Commit 0faa079

Browse files
authored
Merge pull request #226 from gofiber/codex/2025-11-29-15-54-44
2 parents 3e5769d + 8d4e0c1 commit 0faa079

File tree

6 files changed

+513
-1
lines changed

6 files changed

+513
-1
lines changed

cmd/internal/migrations/lists.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ var Migrations = []Migration{
5555
v3migrations.MigrateCORSConfig,
5656
v3migrations.MigrateCSRFConfig,
5757
v3migrations.MigrateMonitorImport,
58+
v3migrations.MigrateSwaggerPackages,
5859
v3migrations.MigrateContribPackages,
5960
v3migrations.MigrateUtilsImport,
6061
v3migrations.MigrateHealthcheckConfig,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package v3
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"sync"
9+
"time"
10+
11+
"golang.org/x/sync/singleflight"
12+
)
13+
14+
const contribV3ProxyPrefix = "https://proxy.golang.org/github.com/gofiber/contrib/v3/"
15+
16+
var (
17+
contribV3VersionMu sync.Mutex
18+
contribV3VersionCache = make(map[string]string)
19+
contribV3VersionFetcher = fetchContribV3Version
20+
contribV3VersionGroup singleflight.Group
21+
contribHTTPClient = &http.Client{}
22+
)
23+
24+
func contribV3Version(module string) (string, error) {
25+
contribV3VersionMu.Lock()
26+
if v, ok := contribV3VersionCache[module]; ok {
27+
contribV3VersionMu.Unlock()
28+
return v, nil
29+
}
30+
fetcher := contribV3VersionFetcher
31+
contribV3VersionMu.Unlock()
32+
33+
res, err, _ := contribV3VersionGroup.Do(module, func() (any, error) {
34+
v, fetchErr := fetcher(module)
35+
if fetchErr != nil {
36+
return "", fetchErr
37+
}
38+
39+
contribV3VersionMu.Lock()
40+
contribV3VersionCache[module] = v
41+
contribV3VersionMu.Unlock()
42+
return v, nil
43+
})
44+
if err != nil {
45+
return "", fmt.Errorf("fetch contrib version: %w", err)
46+
}
47+
48+
v, ok := res.(string)
49+
if !ok {
50+
return "", fmt.Errorf("unexpected contrib version type %T", res)
51+
}
52+
53+
return v, nil
54+
}
55+
56+
func fetchContribV3Version(module string) (version string, err error) {
57+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
58+
defer cancel()
59+
60+
url := contribV3ProxyPrefix + module + "/@latest"
61+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
62+
if err != nil {
63+
return "", fmt.Errorf("create request: %w", err)
64+
}
65+
66+
res, err := contribHTTPClient.Do(req)
67+
if err != nil {
68+
return "", fmt.Errorf("fetch latest version: %w", err)
69+
}
70+
defer func() {
71+
if cerr := res.Body.Close(); cerr != nil && err == nil {
72+
err = cerr
73+
}
74+
}()
75+
76+
if res.StatusCode != http.StatusOK {
77+
return "", fmt.Errorf("fetch latest version: unexpected status %d", res.StatusCode)
78+
}
79+
80+
var data struct {
81+
Version string `json:"Version"` //nolint:tagliatelle // field name defined by proxy
82+
}
83+
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
84+
return "", fmt.Errorf("parse latest version: %w", err)
85+
}
86+
if data.Version == "" {
87+
return "", fmt.Errorf("latest version not found for %s", module)
88+
}
89+
90+
return data.Version, nil
91+
}
92+
93+
// SetContribV3VersionFetcher overrides the function used to fetch contrib module versions.
94+
// It resets the cached versions and returns a restore function to revert the change.
95+
func SetContribV3VersionFetcher(fn func(string) (string, error)) func() {
96+
contribV3VersionMu.Lock()
97+
prev := contribV3VersionFetcher
98+
contribV3VersionFetcher = fn
99+
contribV3VersionCache = make(map[string]string)
100+
contribV3VersionMu.Unlock()
101+
return func() {
102+
contribV3VersionMu.Lock()
103+
contribV3VersionFetcher = prev
104+
contribV3VersionCache = make(map[string]string)
105+
contribV3VersionMu.Unlock()
106+
}
107+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package v3
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"go/ast"
7+
"go/format"
8+
"go/parser"
9+
"go/token"
10+
"io/fs"
11+
"os"
12+
"path/filepath"
13+
"regexp"
14+
"strings"
15+
16+
semver "github.com/Masterminds/semver/v3"
17+
"github.com/spf13/cobra"
18+
19+
"github.com/gofiber/cli/cmd/internal"
20+
)
21+
22+
const (
23+
contribSwaggerOld = "github.com/gofiber/contrib/swagger"
24+
contribSwaggerNew = "github.com/gofiber/contrib/v3/swaggo"
25+
fiberSwaggerOld = "github.com/gofiber/swagger"
26+
fiberSwaggerNew = "github.com/gofiber/contrib/v3/swaggerui"
27+
goModVersionPattern = `v[a-zA-Z0-9.+-]+`
28+
)
29+
30+
func MigrateSwaggerPackages(cmd *cobra.Command, cwd string, _, _ *semver.Version) error {
31+
changedImports, err := internal.ChangeFileContent(cwd, func(content string) string {
32+
updated, changed := rewriteSwaggerImports(content)
33+
if changed {
34+
return updated
35+
}
36+
return content
37+
})
38+
if err != nil {
39+
return fmt.Errorf("failed to migrate swagger imports: %w", err)
40+
}
41+
42+
modChanged, err := migrateSwaggerModules(cwd)
43+
if err != nil {
44+
return err
45+
}
46+
47+
if !changedImports && !modChanged {
48+
return nil
49+
}
50+
51+
cmd.Println("Migrating swagger packages")
52+
return nil
53+
}
54+
55+
func migrateSwaggerModules(cwd string) (bool, error) {
56+
modChanged := false
57+
var swaggoVersion, swaggerUIVersion string
58+
59+
walkErr := filepath.WalkDir(cwd, func(path string, d fs.DirEntry, walkErr error) error {
60+
if walkErr != nil {
61+
return walkErr
62+
}
63+
if d.IsDir() {
64+
if d.Name() == "vendor" {
65+
return filepath.SkipDir
66+
}
67+
return nil
68+
}
69+
if d.Name() != "go.mod" {
70+
return nil
71+
}
72+
73+
info, err := d.Info()
74+
if err != nil {
75+
return fmt.Errorf("stat %s: %w", path, err)
76+
}
77+
78+
b, err := os.ReadFile(path) // #nosec G304 -- reading module files
79+
if err != nil {
80+
return fmt.Errorf("read %s: %w", path, err)
81+
}
82+
content := string(b)
83+
84+
needsSwaggo := strings.Contains(content, contribSwaggerOld) || strings.Contains(content, contribSwaggerNew)
85+
needsSwaggerUI := strings.Contains(content, fiberSwaggerOld) || strings.Contains(content, fiberSwaggerNew)
86+
if !needsSwaggo && !needsSwaggerUI {
87+
return nil
88+
}
89+
90+
if needsSwaggo && swaggoVersion == "" {
91+
swaggoVersion, err = contribV3Version("swaggo")
92+
if err != nil {
93+
return fmt.Errorf("fetch swaggo version: %w", err)
94+
}
95+
}
96+
if needsSwaggerUI && swaggerUIVersion == "" {
97+
swaggerUIVersion, err = contribV3Version("swaggerui")
98+
if err != nil {
99+
return fmt.Errorf("fetch swaggerui version: %w", err)
100+
}
101+
}
102+
103+
updated := content
104+
if needsSwaggo {
105+
updated = updateGoModModule(updated, contribSwaggerOld, contribSwaggerNew, swaggoVersion)
106+
}
107+
if needsSwaggerUI {
108+
updated = updateGoModModule(updated, fiberSwaggerOld, fiberSwaggerNew, swaggerUIVersion)
109+
}
110+
111+
if updated == content {
112+
return nil
113+
}
114+
115+
if err := os.WriteFile(path, []byte(updated), info.Mode().Perm()); err != nil {
116+
return fmt.Errorf("write %s: %w", path, err)
117+
}
118+
modChanged = true
119+
return nil
120+
})
121+
if walkErr != nil {
122+
return false, fmt.Errorf("failed to migrate swagger go.mod entries: %w", walkErr)
123+
}
124+
125+
return modChanged, nil
126+
}
127+
128+
func rewriteSwaggerImports(content string) (string, bool) {
129+
fset := token.NewFileSet()
130+
f, err := parser.ParseFile(fset, "", content, parser.ParseComments)
131+
if err != nil {
132+
return content, false
133+
}
134+
135+
changed := false
136+
for _, imp := range f.Imports {
137+
path := strings.Trim(imp.Path.Value, "\"`")
138+
var (
139+
newPath string
140+
wasMigrated bool
141+
)
142+
143+
switch path {
144+
case contribSwaggerOld:
145+
newPath = contribSwaggerNew
146+
wasMigrated = true
147+
case fiberSwaggerOld:
148+
newPath = fiberSwaggerNew
149+
wasMigrated = true
150+
case contribSwaggerNew, fiberSwaggerNew:
151+
newPath = path
152+
default:
153+
continue
154+
}
155+
156+
if path != newPath {
157+
imp.Path.Value = fmt.Sprintf("%q", newPath)
158+
changed = true
159+
}
160+
161+
if wasMigrated && (imp.Name == nil || imp.Name.Name == "") {
162+
imp.Name = ast.NewIdent("swagger")
163+
changed = true
164+
}
165+
}
166+
167+
if !changed {
168+
return content, false
169+
}
170+
171+
var buf bytes.Buffer
172+
if err := format.Node(&buf, fset, f); err != nil {
173+
return content, false
174+
}
175+
176+
return buf.String(), true
177+
}
178+
179+
func updateGoModModule(content, oldPath, newPath, version string) string {
180+
if version == "" {
181+
return content
182+
}
183+
184+
reRequireOld := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)%s\s+%s`, regexp.QuoteMeta(oldPath), goModVersionPattern))
185+
content = reRequireOld.ReplaceAllString(content, fmt.Sprintf(`${1}%s %s`, newPath, version))
186+
187+
reRequireNew := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)%s\s+%s`, regexp.QuoteMeta(newPath), goModVersionPattern))
188+
content = reRequireNew.ReplaceAllString(content, fmt.Sprintf(`${1}%s %s`, newPath, version))
189+
190+
reReplaceOld := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*replace\s+)%s(\s+%s)?(\s+=>\s+)`, regexp.QuoteMeta(oldPath), goModVersionPattern))
191+
content = reReplaceOld.ReplaceAllString(content, fmt.Sprintf(`${1}%s${2}${3}`, newPath))
192+
193+
reReplaceNew := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*replace\s+)%s(\s+%s)?(\s+=>\s+)`, regexp.QuoteMeta(newPath), goModVersionPattern))
194+
content = reReplaceNew.ReplaceAllString(content, fmt.Sprintf(`${1}%s${2}${3}`, newPath))
195+
196+
return content
197+
}

0 commit comments

Comments
 (0)