Compare commits

..

9 Commits

Author SHA1 Message Date
kesonan
91ab1f6d2b goctl features of 1.8.4-alpha (#4849) 2025-05-15 13:59:48 +00:00
Kevin Wan
5048c350ae chore: fix test failure in profilecenter_test.go 2025-05-15 13:31:53 +00:00
Kevin Wan
94edc32f3e chore: optimize profile center and remove tablewriter dependency 2025-05-15 13:22:27 +00:00
Kevin Wan
ec989b2e2a chore: for backward compatibility (#4852)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-05-11 20:19:00 +08:00
me-cs
82fe802e81 update:Use the official sync.OnceFunc (#4840) 2025-05-11 12:08:43 +00:00
me-cs
072d68f897 update:Use the official slice operate func (#4841) 2025-05-11 11:48:54 +00:00
Kevin Wan
2e91ba5811 chore: refactor rest file server (#4851)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-05-11 12:44:43 +08:00
shaouai
5564c43197 feat: serve files using embed.FS (#4847) 2025-05-10 15:43:13 +00:00
Kevin Wan
e55158b0f7 chore: update deps in goctl (#4830) 2025-05-04 16:18:02 +08:00
48 changed files with 11399 additions and 348 deletions

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"sync"
@@ -15,7 +16,6 @@ import (
"github.com/zeromicro/go-zero/core/jsonx"
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/stringx"
)
const (
@@ -894,7 +894,7 @@ func (u *Unmarshaler) processNamedFieldWithValueFromString(fieldType reflect.Typ
valueKind.String())
}
if !stringx.Contains(options, checkValue) {
if !slices.Contains(options, checkValue) {
return fmt.Errorf(`value "%s" for field %q is not defined in options "%v"`,
mapValue, key, options)
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"reflect"
"slices"
"strconv"
"strings"
"sync"
@@ -634,11 +635,11 @@ func validateValueInOptions(val any, options []string) error {
if len(options) > 0 {
switch v := val.(type) {
case string:
if !stringx.Contains(options, v) {
if !slices.Contains(options, v) {
return fmt.Errorf(`error: value %q is not defined in options "%v"`, v, options)
}
default:
if !stringx.Contains(options, Repr(v)) {
if !slices.Contains(options, Repr(v)) {
return fmt.Errorf(`error: value "%v" is not defined in options "%v"`, val, options)
}
}

View File

@@ -1,13 +1,12 @@
package prof
import (
"bytes"
"strconv"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/olekukonko/tablewriter"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/threading"
)
@@ -28,46 +27,15 @@ type (
const flushInterval = 5 * time.Minute
var (
pc = &profileCenter{
slots: make(map[string]*profileSlot),
}
once sync.Once
)
func report(name string, duration time.Duration) {
updated := func() bool {
pc.lock.RLock()
defer pc.lock.RUnlock()
slot, ok := pc.slots[name]
if ok {
atomic.AddInt64(&slot.lifecount, 1)
atomic.AddInt64(&slot.lastcount, 1)
atomic.AddInt64(&slot.lifecycle, int64(duration))
atomic.AddInt64(&slot.lastcycle, int64(duration))
}
return ok
}()
if !updated {
func() {
pc.lock.Lock()
defer pc.lock.Unlock()
pc.slots[name] = &profileSlot{
lifecount: 1,
lastcount: 1,
lifecycle: int64(duration),
lastcycle: int64(duration),
}
}()
}
once.Do(flushRepeatly)
var pc = &profileCenter{
slots: make(map[string]*profileSlot),
}
func flushRepeatly() {
func init() {
flushRepeatedly()
}
func flushRepeatedly() {
threading.GoSafe(func() {
for {
time.Sleep(flushInterval)
@@ -76,42 +44,64 @@ func flushRepeatly() {
})
}
func report(name string, duration time.Duration) {
slot := loadOrStoreSlot(name, duration)
atomic.AddInt64(&slot.lifecount, 1)
atomic.AddInt64(&slot.lastcount, 1)
atomic.AddInt64(&slot.lifecycle, int64(duration))
atomic.AddInt64(&slot.lastcycle, int64(duration))
}
func loadOrStoreSlot(name string, duration time.Duration) *profileSlot {
pc.lock.RLock()
slot, ok := pc.slots[name]
pc.lock.RUnlock()
if ok {
return slot
}
pc.lock.Lock()
defer pc.lock.Unlock()
// double-check
if slot, ok = pc.slots[name]; ok {
return slot
}
slot = &profileSlot{}
pc.slots[name] = slot
return slot
}
func generateReport() string {
var buffer bytes.Buffer
buffer.WriteString("Profiling report\n")
var data [][]string
var builder strings.Builder
builder.WriteString("Profiling report\n")
builder.WriteString("QUEUE,LIFECOUNT,LIFECYCLE,LASTCOUNT,LASTCYCLE\n")
calcFn := func(total, count int64) string {
if count == 0 {
return "-"
}
return (time.Duration(total) / time.Duration(count)).String()
}
func() {
pc.lock.Lock()
defer pc.lock.Unlock()
pc.lock.Lock()
for key, slot := range pc.slots {
builder.WriteString(fmt.Sprintf("%s,%d,%s,%d,%s\n",
key,
slot.lifecount,
calcFn(slot.lifecycle, slot.lifecount),
slot.lastcount,
calcFn(slot.lastcycle, slot.lastcount),
))
for key, slot := range pc.slots {
data = append(data, []string{
key,
strconv.FormatInt(slot.lifecount, 10),
calcFn(slot.lifecycle, slot.lifecount),
strconv.FormatInt(slot.lastcount, 10),
calcFn(slot.lastcycle, slot.lastcount),
})
// reset last cycle stats
atomic.StoreInt64(&slot.lastcount, 0)
atomic.StoreInt64(&slot.lastcycle, 0)
}
pc.lock.Unlock()
// reset the data for last cycle
slot.lastcount = 0
slot.lastcycle = 0
}
}()
table := tablewriter.NewWriter(&buffer)
table.SetHeader([]string{"QUEUE", "LIFECOUNT", "LIFECYCLE", "LASTCOUNT", "LASTCYCLE"})
table.SetBorder(false)
table.AppendBulk(data)
table.Render()
return buffer.String()
return builder.String()
}

View File

@@ -8,7 +8,6 @@ import (
)
func TestReport(t *testing.T) {
once.Do(func() {})
assert.NotContains(t, generateReport(), "foo")
report("foo", time.Second)
assert.Contains(t, generateReport(), "foo")

View File

@@ -1,9 +1,10 @@
package service
import (
"sync"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/threading"
)
@@ -35,7 +36,7 @@ type (
// NewServiceGroup returns a ServiceGroup.
func NewServiceGroup() *ServiceGroup {
sg := new(ServiceGroup)
sg.stopOnce = syncx.Once(sg.doStop)
sg.stopOnce = sync.OnceFunc(sg.doStop)
return sg
}

View File

@@ -2,6 +2,7 @@ package stringx
import (
"errors"
"slices"
"unicode"
"github.com/zeromicro/go-zero/core/lang"
@@ -15,14 +16,9 @@ var (
)
// Contains checks if str is in list.
// Deprecated: use slices.Contains instead.
func Contains(list []string, str string) bool {
for _, each := range list {
if each == str {
return true
}
}
return false
return slices.Contains(list, str)
}
// Filter filters chars from s with given filter function.
@@ -123,11 +119,7 @@ func Remove(strings []string, strs ...string) []string {
// Reverse reverses s.
func Reverse(s string) string {
runes := []rune(s)
for from, to := 0, len(runes)-1; from < to; from, to = from+1, to-1 {
runes[from], runes[to] = runes[to], runes[from]
}
slices.Reverse(runes)
return string(runes)
}

View File

@@ -7,6 +7,28 @@ import (
"github.com/stretchr/testify/assert"
)
func TestContainsString(t *testing.T) {
cases := []struct {
slice []string
value string
expect bool
}{
{[]string{"1"}, "1", true},
{[]string{"1"}, "2", false},
{[]string{"1", "2"}, "1", true},
{[]string{"1", "2"}, "3", false},
{nil, "3", false},
{nil, "", false},
}
for _, each := range cases {
t.Run(path.Join(each.slice...), func(t *testing.T) {
actual := Contains(each.slice, each.value)
assert.Equal(t, each.expect, actual)
})
}
}
func TestNotEmpty(t *testing.T) {
cases := []struct {
args []string
@@ -41,28 +63,6 @@ func TestNotEmpty(t *testing.T) {
}
}
func TestContainsString(t *testing.T) {
cases := []struct {
slice []string
value string
expect bool
}{
{[]string{"1"}, "1", true},
{[]string{"1"}, "2", false},
{[]string{"1", "2"}, "1", true},
{[]string{"1", "2"}, "3", false},
{nil, "3", false},
{nil, "", false},
}
for _, each := range cases {
t.Run(path.Join(each.slice...), func(t *testing.T) {
actual := Contains(each.slice, each.value)
assert.Equal(t, each.expect, actual)
})
}
}
func TestFilter(t *testing.T) {
cases := []struct {
input string

View File

@@ -3,9 +3,7 @@ package syncx
import "sync"
// Once returns a func that guarantees fn can only called once.
// Deprecated: use sync.OnceFunc instead.
func Once(fn func()) func() {
once := new(sync.Once)
return func() {
once.Do(fn)
}
return sync.OnceFunc(fn)
}

1
go.mod
View File

@@ -14,7 +14,6 @@ require (
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.4
github.com/jhump/protoreflect v1.17.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml/v2 v2.2.2
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.8.0

3
go.sum
View File

@@ -121,7 +121,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -135,8 +134,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=

View File

@@ -2,15 +2,16 @@ package fileserver
import (
"net/http"
"path"
"strings"
"sync"
)
// Middleware returns a middleware that serves files from the given file system.
func Middleware(path string, fs http.FileSystem) func(http.HandlerFunc) http.HandlerFunc {
func Middleware(upath string, fs http.FileSystem) func(http.HandlerFunc) http.HandlerFunc {
fileServer := http.FileServer(fs)
pathWithoutTrailSlash := ensureNoTrailingSlash(path)
canServe := createServeChecker(path, fs)
pathWithoutTrailSlash := ensureNoTrailingSlash(upath)
canServe := createServeChecker(upath, fs)
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -28,9 +29,22 @@ func createFileChecker(fs http.FileSystem) func(string) bool {
var lock sync.RWMutex
fileChecker := make(map[string]bool)
return func(path string) bool {
return func(upath string) bool {
// Emulate http.Dir.Opens path normalization for embed.FS.Open.
// http.FileServer redirects any request ending in "/index.html"
// to the same path without the final "index.html".
// So the path here may be empty or end with a "/".
// http.Dir.Open uses this logic to clean the path,
// correctly handling those two cases.
// embed.FS doesnt perform this normalization, so we apply the same logic here.
upath = path.Clean("/" + upath)[1:]
if len(upath) == 0 {
// if the path is empty, we use "." to open the current directory
upath = "."
}
lock.RLock()
exist, ok := fileChecker[path]
exist, ok := fileChecker[upath]
lock.RUnlock()
if ok {
return exist
@@ -39,9 +53,9 @@ func createFileChecker(fs http.FileSystem) func(string) bool {
lock.Lock()
defer lock.Unlock()
file, err := fs.Open(path)
file, err := fs.Open(upath)
exist = err == nil
fileChecker[path] = exist
fileChecker[upath] = exist
if err != nil {
return false
}
@@ -51,8 +65,8 @@ func createFileChecker(fs http.FileSystem) func(string) bool {
}
}
func createServeChecker(path string, fs http.FileSystem) func(r *http.Request) bool {
pathWithTrailSlash := ensureTrailingSlash(path)
func createServeChecker(upath string, fs http.FileSystem) func(r *http.Request) bool {
pathWithTrailSlash := ensureTrailingSlash(upath)
fileChecker := createFileChecker(fs)
return func(r *http.Request) bool {
@@ -62,18 +76,18 @@ func createServeChecker(path string, fs http.FileSystem) func(r *http.Request) b
}
}
func ensureTrailingSlash(path string) string {
if strings.HasSuffix(path, "/") {
return path
func ensureTrailingSlash(upath string) string {
if strings.HasSuffix(upath, "/") {
return upath
}
return path + "/"
return upath + "/"
}
func ensureNoTrailingSlash(path string) string {
if strings.HasSuffix(path, "/") {
return path[:len(path)-1]
func ensureNoTrailingSlash(upath string) string {
if strings.HasSuffix(upath, "/") {
return upath[:len(upath)-1]
}
return path
return upath
}

View File

@@ -1,6 +1,8 @@
package fileserver
import (
"embed"
"io/fs"
"net/http"
"net/http/httptest"
"testing"
@@ -61,6 +63,46 @@ func TestMiddleware(t *testing.T) {
requestPath: "/ws",
expectedStatus: http.StatusAlreadyReported,
},
// http.FileServer redirects any request ending in "/index.html"
// to the same path, without the final "index.html".
{
name: "Serve index.html",
path: "/static",
dir: "testdata",
requestPath: "/static/index.html",
expectedStatus: http.StatusMovedPermanently,
},
{
name: "Serve index.html with path with trailing slash",
path: "/static/",
dir: "testdata",
requestPath: "/static/index.html",
expectedStatus: http.StatusMovedPermanently,
},
{
name: "Serve index.html in a nested directory",
path: "/static",
dir: "testdata",
requestPath: "/static/nested/index.html",
expectedStatus: http.StatusMovedPermanently,
},
{
name: "Request index.html indirectly",
path: "/static",
dir: "testdata",
requestPath: "/static/",
expectedStatus: http.StatusOK,
expectedContent: "hello",
},
{
name: "Request index.html in a nested directory indirectly",
path: "/static",
dir: "testdata",
requestPath: "/static/nested/",
expectedStatus: http.StatusOK,
expectedContent: "hello",
},
}
for _, tt := range tests {
@@ -87,6 +129,128 @@ func TestMiddleware(t *testing.T) {
}
}
var (
//go:embed testdata
testdataFS embed.FS
)
func TestMiddleware_embedFS(t *testing.T) {
tests := []struct {
name string
path string
requestPath string
expectedStatus int
expectedContent string
}{
{
name: "Serve static file",
path: "/static",
requestPath: "/static/example.txt",
expectedStatus: http.StatusOK,
expectedContent: "1",
},
{
name: "Path with trailing slash",
path: "/static/",
requestPath: "/static/example.txt",
expectedStatus: http.StatusOK,
expectedContent: "1",
},
{
name: "Root path",
path: "/",
requestPath: "/example.txt",
expectedStatus: http.StatusOK,
expectedContent: "1",
},
{
name: "Pass through non-matching path",
path: "/static/",
requestPath: "/other/path",
expectedStatus: http.StatusAlreadyReported,
},
{
name: "Not exist file",
path: "/assets",
requestPath: "/assets/not-exist.txt",
expectedStatus: http.StatusAlreadyReported,
},
{
name: "Not exist file in root",
path: "/",
requestPath: "/not-exist.txt",
expectedStatus: http.StatusAlreadyReported,
},
{
name: "websocket request",
path: "/",
requestPath: "/ws",
expectedStatus: http.StatusAlreadyReported,
},
// http.FileServer redirects any request ending in "/index.html"
// to the same path, without the final "index.html".
{
name: "Serve index.html",
path: "/static",
requestPath: "/static/index.html",
expectedStatus: http.StatusMovedPermanently,
},
{
name: "Serve index.html with path with trailing slash",
path: "/static/",
requestPath: "/static/index.html",
expectedStatus: http.StatusMovedPermanently,
},
{
name: "Serve index.html in a nested directory",
path: "/static",
requestPath: "/static/nested/index.html",
expectedStatus: http.StatusMovedPermanently,
},
{
name: "Request index.html indirectly",
path: "/static",
requestPath: "/static/",
expectedStatus: http.StatusOK,
expectedContent: "hello",
},
{
name: "Request index.html in a nested directory indirectly",
path: "/static",
requestPath: "/static/nested/",
expectedStatus: http.StatusOK,
expectedContent: "hello",
},
}
subFS, err := fs.Sub(testdataFS, "testdata")
assert.Nil(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
middleware := Middleware(tt.path, http.FS(subFS))
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAlreadyReported)
})
handlerToTest := middleware(nextHandler)
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
rr := httptest.NewRecorder()
handlerToTest.ServeHTTP(rr, req)
assert.Equal(t, tt.expectedStatus, rr.Code)
if len(tt.expectedContent) > 0 {
assert.Equal(t, tt.expectedContent, rr.Body.String())
}
}
})
}
}
func TestEnsureTrailingSlash(t *testing.T) {
tests := []struct {
input string

View File

@@ -0,0 +1 @@
hello

View File

@@ -0,0 +1 @@
hello

1
tools/goctl/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -77,6 +77,7 @@ func init() {
goCmdFlags.StringVar(&gogen.VarStringRemote, "remote")
goCmdFlags.StringVar(&gogen.VarStringBranch, "branch")
goCmdFlags.BoolVar(&gogen.VarBoolWithTest, "test")
goCmdFlags.BoolVar(&gogen.VarBoolTypeGroup, "type-group")
goCmdFlags.StringVarWithDefaultValue(&gogen.VarStringStyle, "style", config.DefaultFormat)
javaCmdFlags.StringVar(&javagen.VarStringDir, "dir")

View File

@@ -40,6 +40,8 @@ var (
// VarStringStyle describes the style of output files.
VarStringStyle string
VarBoolWithTest bool
// VarBoolTypeGroup describes whether to group types.
VarBoolTypeGroup bool
)
// GoCommand gen go project files from command line

View File

@@ -9,11 +9,11 @@ import (
"sort"
"strings"
"github.com/zeromicro/go-zero/core/collection"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
apiutil "github.com/zeromicro/go-zero/tools/goctl/api/util"
"github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
"github.com/zeromicro/go-zero/tools/goctl/pkg/env"
"github.com/zeromicro/go-zero/tools/goctl/util"
"github.com/zeromicro/go-zero/tools/goctl/util/format"
)
@@ -41,53 +41,116 @@ func BuildTypes(types []spec.Type) (string, error) {
return builder.String(), nil
}
func removeTypeFromDefault(tp spec.Type, group string, groupTypes map[string]map[string]spec.Type) map[string]map[string]spec.Type {
func getTypeName(tp spec.Type) string {
if tp == nil {
return ""
}
switch val := tp.(type) {
case spec.DefineStruct:
typeName := util.Title(tp.Name())
defaultGroups, ok := groupTypes[groupTypeDefault]
if ok {
delete(defaultGroups, typeName)
types, ok := groupTypes[group]
if !ok {
types = make(map[string]spec.Type)
}
types[typeName] = tp
groupTypes[group] = types
}
groupTypes[groupTypeDefault] = defaultGroups
return typeName
case spec.PointerType:
groupTypes = removeTypeFromDefault(val.Type, group, groupTypes)
return getTypeName(val.Type)
case spec.ArrayType:
groupTypes = removeTypeFromDefault(val.Value, group, groupTypes)
return getTypeName(val.Value)
}
return groupTypes
return ""
}
func genTypesWithGroup(dir string, cfg *config.Config, api *spec.ApiSpec) error {
groupTypes := make(map[string]map[string]spec.Type)
for _, v := range api.Types {
types, ok := groupTypes[groupTypeDefault]
if !ok {
types = make(map[string]spec.Type)
}
types[util.Title(v.Name())] = v
groupTypes[groupTypeDefault] = types
}
typesBelongToFiles := make(map[string]*collection.Set)
for _, v := range api.Service.Groups {
group := v.GetAnnotation(groupProperty)
if len(group) == 0 {
group = groupTypeDefault
}
// convert filepath to Identifier name spec.
group = strings.TrimPrefix(group, "/")
group = strings.TrimSuffix(group, "/")
group = util.SafeString(group)
for _, v := range v.Routes {
requestTypeName := getTypeName(v.RequestType)
responseTypeName := getTypeName(v.ResponseType)
requestTypeFileSet, ok := typesBelongToFiles[requestTypeName]
if !ok {
requestTypeFileSet = collection.NewSet()
}
if len(requestTypeName) > 0 {
requestTypeFileSet.AddStr(group)
typesBelongToFiles[requestTypeName] = requestTypeFileSet
}
responseTypeFileSet, ok := typesBelongToFiles[responseTypeName]
if !ok {
responseTypeFileSet = collection.NewSet()
}
if len(responseTypeName) > 0 {
responseTypeFileSet.AddStr(group)
typesBelongToFiles[responseTypeName] = responseTypeFileSet
}
}
}
typesInOneFile := make(map[string]*collection.Set)
for typeName, fileSet := range typesBelongToFiles {
count := fileSet.Count()
switch {
case count == 0: // it means there has no structure type or no request/response body
continue
case count == 1: // it means a structure type used in only one group.
groupName := fileSet.KeysStr()[0]
typeSet, ok := typesInOneFile[groupName]
if !ok {
typeSet = collection.NewSet()
}
typeSet.AddStr(typeName)
typesInOneFile[groupName] = typeSet
default: // it means this type is used in multiple groups.
continue
}
for _, v := range v.Routes {
if v.RequestType != nil {
groupTypes = removeTypeFromDefault(v.RequestType, group, groupTypes)
}
if v.ResponseType != nil {
groupTypes = removeTypeFromDefault(v.ResponseType, group, groupTypes)
}
}
for _, v := range api.Types {
typeName := util.Title(v.Name())
groupSet, ok := typesBelongToFiles[typeName]
var typeCount int
if !ok {
typeCount = 0
} else {
typeCount = groupSet.Count()
}
if typeCount == 0 { // not belong to any group
types, ok := groupTypes[groupTypeDefault]
if !ok {
types = make(map[string]spec.Type)
}
types[typeName] = v
groupTypes[groupTypeDefault] = types
continue
}
if typeCount == 1 { // belong to one group
groupName := groupSet.KeysStr()[0]
types, ok := groupTypes[groupName]
if !ok {
types = make(map[string]spec.Type)
}
types[typeName] = v
groupTypes[groupName] = types
continue
}
// belong to multiple groups
types, ok := groupTypes[groupTypeDefault]
if !ok {
types = make(map[string]spec.Type)
}
types[typeName] = v
groupTypes[groupTypeDefault] = types
}
for group, typeGroup := range groupTypes {
@@ -142,7 +205,7 @@ func writeTypes(dir, baseFilename string, cfg *config.Config, types []spec.Type)
}
func genTypes(dir string, cfg *config.Config, api *spec.ApiSpec) error {
if env.UseExperimental() {
if VarBoolTypeGroup {
return genTypesWithGroup(dir, cfg, api)
}
return writeTypes(dir, typesFile, cfg, api.Types)

View File

@@ -7,15 +7,6 @@ import (
"google.golang.org/grpc/metadata"
)
func hasKey(properties map[string]string, key string) bool {
if len(properties) == 0 {
return false
}
md := metadata.New(properties)
_, ok := md[key]
return ok
}
func getBoolFromKVOrDefault(properties map[string]string, key string, def bool) bool {
if len(properties) == 0 {
return def

View File

@@ -42,7 +42,7 @@ func Test_getListFromInfoOrDefault(t *testing.T) {
"empty": `""`,
}
assert.Equal(t, []string{"a", "b", "c"}, getListFromInfoOrDefault(properties, "list", []string{"default"}))
assert.Equal(t, []string{"a", " b", " c"}, getListFromInfoOrDefault(properties, "list", []string{"default"}))
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(properties, "empty", []string{"default"}))
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(properties, "missing", []string{"default"}))
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(nil, "nil", []string{"default"}))

View File

@@ -8,10 +8,9 @@ import (
"strings"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/parser"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
"gopkg.in/yaml.v2"
)
var (

View File

@@ -1,14 +1,15 @@
package swagger
const (
tagHeader = "header"
tagPath = "path"
tagForm = "form"
tagJson = "json"
defFlag = "default="
enumFlag = "options="
rangeFlag = "range="
exampleFlag = "example="
tagHeader = "header"
tagPath = "path"
tagForm = "form"
tagJson = "json"
defFlag = "default="
enumFlag = "options="
rangeFlag = "range="
exampleFlag = "example="
optionalFlag = "optional"
paramsInHeader = "header"
paramsInPath = "path"
@@ -27,6 +28,36 @@ const (
applicationJson = "application/json"
applicationForm = "application/x-www-form-urlencoded"
schemeHttps = "https"
defaultHost = "127.0.0.1"
defaultBasePath = "/"
)
const (
propertyKeyUseDefinitions = "useDefinitions"
propertyKeyExternalDocsDescription = "externalDocsDescription"
propertyKeyExternalDocsURL = "externalDocsURL"
propertyKeyTitle = "title"
propertyKeyTermsOfService = "termsOfService"
propertyKeyDescription = "description"
propertyKeyVersion = "version"
propertyKeyContactName = "contactName"
propertyKeyContactURL = "contactURL"
propertyKeyContactEmail = "contactEmail"
propertyKeyLicenseName = "licenseName"
propertyKeyLicenseURL = "licenseURL"
propertyKeyProduces = "produces"
propertyKeyConsumes = "consumes"
propertyKeySchemes = "schemes"
propertyKeyTags = "tags"
propertyKeySummary = "summary"
propertyKeyDeprecated = "deprecated"
propertyKeyPrefix = "prefix"
propertyKeyAuthType = "authType"
propertyKeyHost = "host"
propertyKeyBasePath = "basePath"
propertyKeyWrapCodeMsg = "wrapCodeMsg"
propertyKeyBizCodeEnumDescription = "bizCodeEnumDescription"
)
const (
defaultValueOfPropertyUseDefinition = false
)

View File

@@ -7,7 +7,7 @@ import (
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
func consumesFromTypeOrDef(method string, tp spec.Type) []string {
func consumesFromTypeOrDef(ctx Context, method string, tp spec.Type) []string {
if strings.EqualFold(method, http.MethodGet) {
return []string{}
}
@@ -18,7 +18,7 @@ func consumesFromTypeOrDef(method string, tp spec.Type) []string {
if !ok {
return []string{}
}
if typeContainsTag(structType, tagJson) {
if typeContainsTag(ctx, structType, tagJson) {
return []string{applicationJson}
}
return []string{applicationForm}

View File

@@ -61,7 +61,7 @@ func TestConsumesFromTypeOrDef(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := consumesFromTypeOrDef(tt.method, tt.tp)
result := consumesFromTypeOrDef(testingContext(t), tt.method, tt.tp)
assert.Equal(t, tt.expected, result)
})
}

View File

@@ -0,0 +1,28 @@
package swagger
import (
"testing"
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
type Context struct {
UseDefinitions bool
WrapCodeMsg bool
BizCodeEnumDescription string
}
func testingContext(_ *testing.T) Context {
return Context{}
}
func contextFromApi(info spec.Info) Context {
if len(info.Properties) == 0 {
return Context{}
}
return Context{
UseDefinitions: getBoolFromKVOrDefault(info.Properties, propertyKeyUseDefinitions, defaultValueOfPropertyUseDefinition),
WrapCodeMsg: getBoolFromKVOrDefault(info.Properties, propertyKeyWrapCodeMsg, false),
BizCodeEnumDescription: getStringFromKVOrDefault(info.Properties, propertyKeyBizCodeEnumDescription, "business code"),
}
}

View File

@@ -0,0 +1,32 @@
package swagger
import (
"github.com/go-openapi/spec"
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
func definitionsFromTypes(ctx Context, types []apiSpec.Type) spec.Definitions {
if !ctx.UseDefinitions {
return nil
}
definitions := make(spec.Definitions)
for _, tp := range types {
typeName := tp.Name()
definitions[typeName] = schemaFromType(ctx, tp)
}
return definitions
}
func schemaFromType(ctx Context, tp apiSpec.Type) spec.Schema {
p, r := propertiesFromType(ctx, tp)
props := spec.SchemaProps{
Type: typeFromGoType(ctx, tp),
Properties: p,
AdditionalProperties: mapFromGoType(ctx, tp),
Items: itemsFromGoType(ctx, tp),
Required: r,
}
return spec.Schema{
SchemaProps: props,
}
}

View File

@@ -12,15 +12,16 @@ info (
licenseURL: "https://github.com/zeromicro/go-zero" // licenseURL corresponding to Swagger
consumes: "application/json" // consumes corresponding to Swagger,default value is `application/json`
produces: "application/json" // produces corresponding to Swagger,default value is `application/json`
schemes: "https" // schemes corresponding to Swagger,default value is `https``
schemes: "http,https" // schemes corresponding to Swagger,default value is `https``
host: "example.com" // host corresponding to Swagger,default value is `127.0.0.1`
basePath: "/v1" // basePath corresponding to Swagger,default value is `/`
wrapCodeMsg: "true" // to wrap in the universal code-msg structure, like {"code":0,"msg":"OK","data":$data}
wrapCodeMsg: true // to wrap in the universal code-msg structure, like {"code":0,"msg":"OK","data":$data}
bizCodeEnumDescription: "1001-User not login<br>1002-User permission denied" // enums of business error codes, in JSON format, with the key being the business error code and the value being the description of that error code. This only takes effect when wrapCodeMsg is set to true.
// securityDefinitionsFromJson is a custom authentication configuration, and the JSON content will be directly inserted into the securityDefinitions of Swagger.
// Format reference: https://swagger.io/specification/v2/#security-definitions-object
// You can declare authType in the @server of the API to specify the authentication type used for its routes.
securityDefinitionsFromJson: `{"apiKey":{"description":"apiKey type description","type":"apiKey","name":"x-api-key","in":"header"}}`
useDefinitions: true // if set true, the definitions will be generated in the swagger.json for response body or json request body file, and the models will be referenced in the API.
)
type (

File diff suppressed because it is too large Load Diff

View File

@@ -12,15 +12,16 @@ info (
licenseURL: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 licenseURL
consumes: "application/json" // 对应 swagger 的 consumes,不填默认为 application/json
produces: "application/json" // 对应 swagger 的 produces,不填默认为 application/json
schemes: "https" // 对应 swagger 的 schemes,不填默认为 https
schemes: "http,https" // 对应 swagger 的 schemes,不填默认为 https
host: "example.com" // 对应 swagger 的 host,不填默认为 127.0.0.1
basePath: "/v1" // 对应 swagger 的 basePath,不填默认为 /
wrapCodeMsg: "true" // 是否用 code-msg 通用响应体,如果开启,则以格式 {"code":0,"msg":"OK","data":$data} 包括响应体
wrapCodeMsg: true // 是否用 code-msg 通用响应体,如果开启,则以格式 {"code":0,"msg":"OK","data":$data} 包括响应体
bizCodeEnumDescription: "1001-未登录<br>1002-无权限操作" // 全局业务错误码枚举描述json 格式,key 为业务错误码value 为该错误码的描述,仅当 wrapCodeMsg 为 true 时生效
// securityDefinitionsFromJson 为自定义鉴权配置json 内容将直接放入 swagger 的 securityDefinitions 中,
// 格式参考 https://swagger.io/specification/v2/#security-definitions-object
// 在 api 的 @server 中可声明 authType 来指定其路由使用的鉴权类型
securityDefinitionsFromJson: `{"apiKey":{"description":"apiKey 类型鉴权自定义","type":"apiKey","name":"x-api-key","in":"header"}}`
useDefinitions: true// 开启声明将生成models 进行关联definitions 仅对响应体和 json 请求体生效
)
type (
@@ -52,7 +53,7 @@ type (
service Swagger {
@doc (
description: "query 接口"
bizCodeEnumDescription: " 1003-用不存在<br>1004-非法操作" // 接口级别业务错误码枚举描述会覆盖全局的业务错误码json 格式,key 为业务错误码value 为该错误码的描述,仅当 wrapCodeMsg 为 true 时生效
bizCodeEnumDescription: " 1003-用不存在<br>1004-非法操作" // 接口级别业务错误码枚举描述会覆盖全局的业务错误码json 格式,key 为业务错误码value 为该错误码的描述,仅当 wrapCodeMsg 为 true 且 useDefinitions 为 false 时生效
)
@handler query
get /query (QueryReq) returns (QueryResp)

File diff suppressed because it is too large Load Diff

View File

@@ -81,21 +81,21 @@ func enumsValueFromOptions(options []string) []any {
return []any{}
}
func defValueFromOptions(options []string, apiType spec.Type) any {
tp := sampleTypeFromGoType(apiType)
return valueFromOptions(options, defFlag, tp)
func defValueFromOptions(ctx Context, options []string, apiType spec.Type) any {
tp := sampleTypeFromGoType(ctx, apiType)
return valueFromOptions(ctx, options, defFlag, tp)
}
func exampleValueFromOptions(options []string, apiType spec.Type) any {
tp := sampleTypeFromGoType(apiType)
val := valueFromOptions(options, exampleFlag, tp)
func exampleValueFromOptions(ctx Context, options []string, apiType spec.Type) any {
tp := sampleTypeFromGoType(ctx, apiType)
val := valueFromOptions(ctx, options, exampleFlag, tp)
if val != nil {
return val
}
return defValueFromOptions(options, apiType)
return defValueFromOptions(ctx, options, apiType)
}
func valueFromOptions(options []string, key string, tp string) any {
func valueFromOptions(_ Context, options []string, key string, tp string) any {
if len(options) == 0 {
return nil
}
@@ -103,16 +103,18 @@ func valueFromOptions(options []string, key string, tp string) any {
if strings.HasPrefix(option, key) {
s := option[len(key):]
switch tp {
case "integer":
case swaggerTypeInteger:
val, _ := strconv.ParseInt(s, 10, 64)
return val
case "boolean":
case swaggerTypeBoolean:
val, _ := strconv.ParseBool(s)
return val
case "number":
case swaggerTypeNumber:
val, _ := strconv.ParseFloat(s, 64)
return val
case "string":
case swaggerTypeArray:
return s
case swaggerTypeString:
return s
default:
return nil

View File

@@ -161,7 +161,7 @@ func TestDefValueFromOptions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := defValueFromOptions(tt.options, tt.apiType)
result := defValueFromOptions(testingContext(t), tt.options, tt.apiType)
assert.Equal(t, tt.expected, result)
})
}
@@ -202,7 +202,7 @@ func TestExampleValueFromOptions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exampleValueFromOptions(tt.options, tt.apiType)
exampleValueFromOptions(testingContext(t), tt.options, tt.apiType)
})
}
}
@@ -247,7 +247,7 @@ func TestValueFromOptions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := valueFromOptions(tt.options, tt.key, tt.tp)
result := valueFromOptions(testingContext(t), tt.options, tt.key, tt.tp)
assert.Equal(t, tt.expected, result)
})
}

View File

@@ -8,7 +8,25 @@ import (
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
func isPostJson(ctx Context, method string, tp apiSpec.Type) (string, bool) {
if strings.EqualFold(method, http.MethodPost) {
return "", false
}
structType, ok := tp.(apiSpec.DefineStruct)
if !ok {
return "", false
}
var isPostJson bool
rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
jsonTag, _ := tag.Get(tagJson)
if !isPostJson {
isPostJson = jsonTag != nil
}
})
return structType.RawName, isPostJson
}
func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Parameter {
if tp == nil {
return []spec.Parameter{}
}
@@ -16,12 +34,30 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
if !ok {
return []spec.Parameter{}
}
structName, ok := isPostJson(ctx, method, tp)
if ok {
return []spec.Parameter{
{
ParamProps: spec.ParamProps{
In: paramsInBody,
Name: paramsInBody,
Required: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef(getRefName(structName)),
},
},
},
},
}
}
var (
resp []spec.Parameter
properties = map[string]spec.Schema{}
requiredFields []string
)
rangeMemberAndDo(structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
headerTag, _ := tag.Get(tagHeader)
hasHeader := headerTag != nil
@@ -44,10 +80,9 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
Enum: enumsValueFromOptions(headerTag.Options),
},
SimpleSchema: spec.SimpleSchema{
Type: sampleTypeFromGoType(member.Type),
Default: defValueFromOptions(headerTag.Options, member.Type),
Example: exampleValueFromOptions(headerTag.Options, member.Type),
Items: sampleItemsFromGoType(member.Type),
Type: sampleTypeFromGoType(ctx, member.Type),
Default: defValueFromOptions(ctx, headerTag.Options, member.Type),
Items: sampleItemsFromGoType(ctx, member.Type),
},
ParamProps: spec.ParamProps{
In: paramsInHeader,
@@ -68,10 +103,9 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
Enum: enumsValueFromOptions(pathParameterTag.Options),
},
SimpleSchema: spec.SimpleSchema{
Type: sampleTypeFromGoType(member.Type),
Default: defValueFromOptions(pathParameterTag.Options, member.Type),
Example: exampleValueFromOptions(pathParameterTag.Options, member.Type),
Items: sampleItemsFromGoType(member.Type),
Type: sampleTypeFromGoType(ctx, member.Type),
Default: defValueFromOptions(ctx, pathParameterTag.Options, member.Type),
Items: sampleItemsFromGoType(ctx, member.Type),
},
ParamProps: spec.ParamProps{
In: paramsInPath,
@@ -93,10 +127,9 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
Enum: enumsValueFromOptions(formTag.Options),
},
SimpleSchema: spec.SimpleSchema{
Type: sampleTypeFromGoType(member.Type),
Default: defValueFromOptions(formTag.Options, member.Type),
Example: exampleValueFromOptions(formTag.Options, member.Type),
Items: sampleItemsFromGoType(member.Type),
Type: sampleTypeFromGoType(ctx, member.Type),
Default: defValueFromOptions(ctx, formTag.Options, member.Type),
Items: sampleItemsFromGoType(ctx, member.Type),
},
ParamProps: spec.ParamProps{
In: paramsInQuery,
@@ -116,10 +149,9 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
Enum: enumsValueFromOptions(formTag.Options),
},
SimpleSchema: spec.SimpleSchema{
Type: sampleTypeFromGoType(member.Type),
Default: defValueFromOptions(formTag.Options, member.Type),
Example: exampleValueFromOptions(formTag.Options, member.Type),
Items: sampleItemsFromGoType(member.Type),
Type: sampleTypeFromGoType(ctx, member.Type),
Default: defValueFromOptions(ctx, formTag.Options, member.Type),
Items: sampleItemsFromGoType(ctx, member.Type),
},
ParamProps: spec.ParamProps{
In: paramsInForm,
@@ -139,25 +171,25 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
}
var schema = spec.Schema{
SwaggerSchemaProps: spec.SwaggerSchemaProps{
Example: exampleValueFromOptions(jsonTag.Options, member.Type),
Example: exampleValueFromOptions(ctx, jsonTag.Options, member.Type),
},
SchemaProps: spec.SchemaProps{
Description: formatComment(member.Comment),
Type: typeFromGoType(member.Type),
Default: defValueFromOptions(jsonTag.Options, member.Type),
Type: typeFromGoType(ctx, member.Type),
Default: defValueFromOptions(ctx, jsonTag.Options, member.Type),
Maximum: maximum,
ExclusiveMaximum: exclusiveMaximum,
Minimum: minimum,
ExclusiveMinimum: exclusiveMinimum,
Enum: enumsValueFromOptions(jsonTag.Options),
AdditionalProperties: mapFromGoType(member.Type),
AdditionalProperties: mapFromGoType(ctx, member.Type),
},
}
switch sampleTypeFromGoType(member.Type) {
switch sampleTypeFromGoType(ctx, member.Type) {
case swaggerTypeArray:
schema.Items = itemsFromGoType(member.Type)
schema.Items = itemsFromGoType(ctx, member.Type)
case swaggerTypeObject:
p, r := propertiesFromType(member.Type)
p, r := propertiesFromType(ctx, member.Type)
schema.Properties = p
schema.Required = r
}
@@ -172,7 +204,7 @@ func parametersFromType(method string, tp apiSpec.Type) []spec.Parameter {
Required: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: typeFromGoType(structType),
Type: typeFromGoType(ctx, structType),
Properties: properties,
Required: requiredFields,
},

View File

@@ -9,18 +9,18 @@ import (
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
func spec2Paths(info apiSpec.Info, srv apiSpec.Service) *spec.Paths {
func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths {
paths := &spec.Paths{
Paths: make(map[string]spec.PathItem),
}
for _, group := range srv.Groups {
prefix := path.Clean(strings.TrimPrefix(group.GetAnnotation("prefix"), "/"))
prefix := path.Clean(strings.TrimPrefix(group.GetAnnotation(propertyKeyPrefix), "/"))
for _, route := range group.Routes {
routPath := pathVariable2SwaggerVariable(route.Path)
routPath := pathVariable2SwaggerVariable(ctx, route.Path)
if len(prefix) > 0 && prefix != "." {
routPath = "/" + path.Clean(prefix) + routPath
}
pathItem := spec2Path(info, group, route)
pathItem := spec2Path(ctx, group, route)
existPathItem, ok := paths.Paths[routPath]
if !ok {
paths.Paths[routPath] = pathItem
@@ -60,8 +60,8 @@ func mergePathItem(old, new spec.PathItem) spec.PathItem {
return old
}
func spec2Path(info apiSpec.Info, group apiSpec.Group, route apiSpec.Route) spec.PathItem {
authType := getStringFromKVOrDefault(group.Annotation.Properties, "authType", "")
func spec2Path(ctx Context, group apiSpec.Group, route apiSpec.Route) spec.PathItem {
authType := getStringFromKVOrDefault(group.Annotation.Properties, propertyKeyAuthType, "")
var security []map[string][]string
if len(authType) > 0 {
security = []map[string][]string{
@@ -72,20 +72,20 @@ func spec2Path(info apiSpec.Info, group apiSpec.Group, route apiSpec.Route) spec
}
op := &spec.Operation{
OperationProps: spec.OperationProps{
Description: getStringFromKVOrDefault(route.AtDoc.Properties, "description", ""),
Consumes: consumesFromTypeOrDef(route.Method, route.RequestType),
Produces: getListFromInfoOrDefault(route.AtDoc.Properties, "produces", []string{applicationJson}),
Schemes: getListFromInfoOrDefault(route.AtDoc.Properties, "schemes", []string{schemeHttps}),
Tags: getListFromInfoOrDefault(group.Annotation.Properties, "tags", []string{""}),
Summary: getStringFromKVOrDefault(route.AtDoc.Properties, "summary", getFirstUsableString(route.AtDoc.Text, route.Handler)),
Deprecated: getBoolFromKVOrDefault(route.AtDoc.Properties, "deprecated", false),
Parameters: parametersFromType(route.Method, route.RequestType),
Responses: jsonResponseFromType(info, route.AtDoc, route.ResponseType),
Description: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyDescription, ""),
Consumes: consumesFromTypeOrDef(ctx, route.Method, route.RequestType),
Produces: getListFromInfoOrDefault(route.AtDoc.Properties, propertyKeyProduces, []string{applicationJson}),
Schemes: getListFromInfoOrDefault(route.AtDoc.Properties, propertyKeySchemes, []string{schemeHttps}),
Tags: getListFromInfoOrDefault(group.Annotation.Properties, propertyKeyTags, getListFromInfoOrDefault(group.Annotation.Properties, propertyKeySummary, []string{})),
Summary: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeySummary, getFirstUsableString(route.AtDoc.Text, route.Handler)),
Deprecated: getBoolFromKVOrDefault(route.AtDoc.Properties, propertyKeyDeprecated, false),
Parameters: parametersFromType(ctx, route.Method, route.RequestType),
Responses: jsonResponseFromType(ctx, route.AtDoc, route.ResponseType),
Security: security,
},
}
externalDocsDescription := getStringFromKVOrDefault(route.AtDoc.Properties, "externalDocsDescription", "")
externalDocsURL := getStringFromKVOrDefault(route.AtDoc.Properties, "externalDocsURL", "")
externalDocsDescription := getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyExternalDocsDescription, "")
externalDocsURL := getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyExternalDocsURL, "")
if len(externalDocsDescription) > 0 || len(externalDocsURL) > 0 {
op.ExternalDocs = &spec.ExternalDocumentation{
Description: externalDocsDescription,

View File

@@ -5,18 +5,18 @@ import (
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
func propertiesFromType(tp apiSpec.Type) (spec.SchemaProperties, []string) {
func propertiesFromType(ctx Context, tp apiSpec.Type) (spec.SchemaProperties, []string) {
var (
properties = map[string]spec.Schema{}
requiredFields []string
)
switch val := tp.(type) {
case apiSpec.PointerType:
return propertiesFromType(val.Type)
return propertiesFromType(ctx, val.Type)
case apiSpec.ArrayType:
return propertiesFromType(val.Value)
return propertiesFromType(ctx, val.Value)
case apiSpec.DefineStruct, apiSpec.NestedStruct:
rangeMemberAndDo(val, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
rangeMemberAndDo(ctx, val, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
var (
jsonTagString = member.Name
minimum, maximum *float64
@@ -28,38 +28,46 @@ func propertiesFromType(tp apiSpec.Type) (spec.SchemaProperties, []string) {
if jsonTag != nil {
jsonTagString = jsonTag.Name
minimum, maximum, exclusiveMinimum, exclusiveMaximum = rangeValueFromOptions(jsonTag.Options)
example = exampleValueFromOptions(jsonTag.Options, member.Type)
defaultValue = defValueFromOptions(jsonTag.Options, member.Type)
example = exampleValueFromOptions(ctx, jsonTag.Options, member.Type)
defaultValue = defValueFromOptions(ctx, jsonTag.Options, member.Type)
enum = enumsValueFromOptions(jsonTag.Options)
}
if required {
requiredFields = append(requiredFields, jsonTagString)
}
var schema = spec.Schema{
schema := spec.Schema{
SwaggerSchemaProps: spec.SwaggerSchemaProps{
Example: example,
},
SchemaProps: spec.SchemaProps{
Description: formatComment(member.Comment),
Type: typeFromGoType(member.Type),
Type: typeFromGoType(ctx, member.Type),
Default: defaultValue,
Maximum: maximum,
ExclusiveMaximum: exclusiveMaximum,
Minimum: minimum,
ExclusiveMinimum: exclusiveMinimum,
Enum: enum,
AdditionalProperties: mapFromGoType(member.Type),
AdditionalProperties: mapFromGoType(ctx, member.Type),
},
}
switch sampleTypeFromGoType(member.Type) {
switch sampleTypeFromGoType(ctx, member.Type) {
case swaggerTypeArray:
schema.Items = itemsFromGoType(member.Type)
schema.Items = itemsFromGoType(ctx, member.Type)
case swaggerTypeObject:
p, r := propertiesFromType(member.Type)
p, r := propertiesFromType(ctx, member.Type)
schema.Properties = p
schema.Required = r
}
if ctx.UseDefinitions {
structName, containsStruct := containsStruct(member.Type)
if containsStruct {
schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName))
}
}
properties[jsonTagString] = schema
})
@@ -67,3 +75,22 @@ func propertiesFromType(tp apiSpec.Type) (spec.SchemaProperties, []string) {
return properties, requiredFields
}
func containsStruct(tp apiSpec.Type) (string, bool) {
switch val := tp.(type) {
case apiSpec.PointerType:
return containsStruct(val.Type)
case apiSpec.ArrayType:
return containsStruct(val.Value)
case apiSpec.DefineStruct:
return val.RawName, true
case apiSpec.MapType:
return containsStruct(val.Value)
default:
return "", false
}
}
func getRefName(typeName string) string {
return "#/definitions/" + typeName
}

View File

@@ -1,25 +1,62 @@
package swagger
import (
"net/http"
"github.com/go-openapi/spec"
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
func jsonResponseFromType(info apiSpec.Info, atDoc apiSpec.AtDoc, tp apiSpec.Type) *spec.Responses {
p, _ := propertiesFromType(tp)
func jsonResponseFromType(ctx Context, atDoc apiSpec.AtDoc, tp apiSpec.Type) *spec.Responses {
if tp == nil {
return &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
http.StatusOK: {
ResponseProps: spec.ResponseProps{
Description: "",
Schema: &spec.Schema{},
},
},
},
},
}
}
props := spec.SchemaProps{
Type: typeFromGoType(tp),
Properties: p,
AdditionalProperties: mapFromGoType(tp),
Items: itemsFromGoType(tp),
AdditionalProperties: mapFromGoType(ctx, tp),
Items: itemsFromGoType(ctx, tp),
}
if ctx.UseDefinitions {
structName, ok := containsStruct(tp)
if ok {
props.Ref = spec.MustCreateRef(getRefName(structName))
return &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
http.StatusOK: {
ResponseProps: spec.ResponseProps{
Schema: &spec.Schema{
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
},
},
},
},
},
}
}
}
p, _ := propertiesFromType(ctx, tp)
props.Type = typeFromGoType(ctx, tp)
props.Properties = p
return &spec.Responses{
ResponsesProps: spec.ResponsesProps{
Default: &spec.Response{
ResponseProps: spec.ResponseProps{
Schema: &spec.Schema{
SchemaProps: wrapCodeMsgProps(props, info, atDoc),
StatusCodeResponses: map[int]spec.Response{
http.StatusOK: {
ResponseProps: spec.ResponseProps{
Schema: &spec.Schema{
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
},
},
},
},

View File

@@ -8,12 +8,11 @@ import (
"github.com/go-openapi/spec"
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
"github.com/zeromicro/go-zero/tools/goctl/util"
)
func spec2Swagger(api *apiSpec.ApiSpec) (*spec.Swagger, error) {
ctx := contextFromApi(api.Info)
extensions, info := specExtensions(api.Info)
var securityDefinitions spec.SecurityDefinitions
securityDefinitionsFromJson := getStringFromKVOrDefault(api.Info.Properties, "securityDefinitionsFromJson", `{}`)
_ = json.Unmarshal([]byte(securityDefinitionsFromJson), &securityDefinitions)
@@ -22,14 +21,15 @@ func spec2Swagger(api *apiSpec.ApiSpec) (*spec.Swagger, error) {
Extensions: extensions,
},
SwaggerProps: spec.SwaggerProps{
Consumes: getListFromInfoOrDefault(api.Info.Properties, "consumes", []string{applicationJson}),
Produces: getListFromInfoOrDefault(api.Info.Properties, "produces", []string{applicationJson}),
Schemes: getListFromInfoOrDefault(api.Info.Properties, "schemes", []string{schemeHttps}),
Definitions: definitionsFromTypes(ctx, api.Types),
Consumes: getListFromInfoOrDefault(api.Info.Properties, propertyKeyConsumes, []string{applicationJson}),
Produces: getListFromInfoOrDefault(api.Info.Properties, propertyKeyProduces, []string{applicationJson}),
Schemes: getListFromInfoOrDefault(api.Info.Properties, propertyKeySchemes, []string{schemeHttps}),
Swagger: swaggerVersion,
Info: info,
Host: getStringFromKVOrDefault(api.Info.Properties, "host", defaultHost),
BasePath: getStringFromKVOrDefault(api.Info.Properties, "basePath", defaultBasePath),
Paths: spec2Paths(api.Info, api.Service),
Host: getStringFromKVOrDefault(api.Info.Properties, propertyKeyHost, ""),
BasePath: getStringFromKVOrDefault(api.Info.Properties, propertyKeyBasePath, defaultBasePath),
Paths: spec2Paths(ctx, api.Service),
SecurityDefinitions: securityDefinitions,
},
}
@@ -42,7 +42,7 @@ func formatComment(comment string) string {
return strings.TrimSpace(s)
}
func sampleItemsFromGoType(tp apiSpec.Type) *spec.Items {
func sampleItemsFromGoType(ctx Context, tp apiSpec.Type) *spec.Items {
val, ok := tp.(apiSpec.ArrayType)
if !ok {
return nil
@@ -52,14 +52,14 @@ func sampleItemsFromGoType(tp apiSpec.Type) *spec.Items {
case apiSpec.PrimitiveType:
return &spec.Items{
SimpleSchema: spec.SimpleSchema{
Type: sampleTypeFromGoType(item),
Type: sampleTypeFromGoType(ctx, item),
},
}
case apiSpec.ArrayType:
return &spec.Items{
SimpleSchema: spec.SimpleSchema{
Type: sampleTypeFromGoType(item),
Items: sampleItemsFromGoType(item),
Type: sampleTypeFromGoType(ctx, item),
Items: sampleItemsFromGoType(ctx, item),
},
}
default: // unsupported type
@@ -68,30 +68,30 @@ func sampleItemsFromGoType(tp apiSpec.Type) *spec.Items {
}
// itemsFromGoType returns the schema or array of the type, just for non json body parameters.
func itemsFromGoType(tp apiSpec.Type) *spec.SchemaOrArray {
func itemsFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrArray {
array, ok := tp.(apiSpec.ArrayType)
if !ok {
return nil
}
return itemFromGoType(array.Value)
return itemFromGoType(ctx, array.Value)
}
func mapFromGoType(tp apiSpec.Type) *spec.SchemaOrBool {
func mapFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrBool {
mapType, ok := tp.(apiSpec.MapType)
if !ok {
return nil
}
var schema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: typeFromGoType(mapType.Value),
AdditionalProperties: mapFromGoType(mapType.Value),
Type: typeFromGoType(ctx, mapType.Value),
AdditionalProperties: mapFromGoType(ctx, mapType.Value),
},
}
switch sampleTypeFromGoType(mapType.Value) {
switch sampleTypeFromGoType(ctx, mapType.Value) {
case swaggerTypeArray:
schema.Items = itemsFromGoType(mapType.Value)
schema.Items = itemsFromGoType(ctx, mapType.Value)
case swaggerTypeObject:
p, r := propertiesFromType(mapType.Value)
p, r := propertiesFromType(ctx, mapType.Value)
schema.Properties = p
schema.Required = r
}
@@ -102,37 +102,37 @@ func mapFromGoType(tp apiSpec.Type) *spec.SchemaOrBool {
}
// itemFromGoType returns the schema or array of the type, just for non json body parameters.
func itemFromGoType(tp apiSpec.Type) *spec.SchemaOrArray {
func itemFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrArray {
switch itemType := tp.(type) {
case apiSpec.PrimitiveType:
return &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: typeFromGoType(tp),
Type: typeFromGoType(ctx, tp),
},
},
}
case apiSpec.DefineStruct, apiSpec.NestedStruct:
properties, requiredFields := propertiesFromType(itemType)
case apiSpec.DefineStruct, apiSpec.NestedStruct, apiSpec.MapType:
properties, requiredFields := propertiesFromType(ctx, itemType)
return &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: typeFromGoType(itemType),
Items: itemsFromGoType(itemType),
Type: typeFromGoType(ctx, itemType),
Items: itemsFromGoType(ctx, itemType),
Properties: properties,
Required: requiredFields,
AdditionalProperties: mapFromGoType(itemType),
AdditionalProperties: mapFromGoType(ctx, itemType),
},
},
}
case apiSpec.PointerType:
return itemFromGoType(itemType.Type)
return itemFromGoType(ctx, itemType.Type)
case apiSpec.ArrayType:
return &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: typeFromGoType(itemType),
Items: itemsFromGoType(itemType),
Type: typeFromGoType(ctx, itemType),
Items: itemsFromGoType(ctx, itemType),
},
},
}
@@ -140,7 +140,7 @@ func itemFromGoType(tp apiSpec.Type) *spec.SchemaOrArray {
return nil
}
func typeFromGoType(tp apiSpec.Type) []string {
func typeFromGoType(ctx Context, tp apiSpec.Type) []string {
switch val := tp.(type) {
case apiSpec.PrimitiveType:
res, ok := tpMapper[val.RawName]
@@ -152,12 +152,12 @@ func typeFromGoType(tp apiSpec.Type) []string {
case apiSpec.DefineStruct, apiSpec.MapType:
return []string{swaggerTypeObject}
case apiSpec.PointerType:
return typeFromGoType(val.Type)
return typeFromGoType(ctx, val.Type)
}
return nil
}
func sampleTypeFromGoType(tp apiSpec.Type) string {
func sampleTypeFromGoType(ctx Context, tp apiSpec.Type) string {
switch val := tp.(type) {
case apiSpec.PrimitiveType:
return tpMapper[val.RawName]
@@ -166,13 +166,13 @@ func sampleTypeFromGoType(tp apiSpec.Type) string {
case apiSpec.DefineStruct, apiSpec.MapType, apiSpec.NestedStruct:
return swaggerTypeObject
case apiSpec.PointerType:
return sampleTypeFromGoType(val.Type)
return sampleTypeFromGoType(ctx, val.Type)
default:
return ""
}
}
func typeContainsTag(structType apiSpec.DefineStruct, tag string) bool {
func typeContainsTag(_ Context, structType apiSpec.DefineStruct, tag string) bool {
for _, field := range structType.Members {
tags, _ := apiSpec.Parse(field.Tag)
for _, t := range tags.Tags() {
@@ -184,13 +184,13 @@ func typeContainsTag(structType apiSpec.DefineStruct, tag string) bool {
return false
}
func expandMembers(tp apiSpec.Type) []apiSpec.Member {
func expandMembers(ctx Context, tp apiSpec.Type) []apiSpec.Member {
var members []apiSpec.Member
switch val := tp.(type) {
case apiSpec.DefineStruct:
for _, v := range val.Members {
if v.IsInline {
members = append(members, expandMembers(v.Type)...)
members = append(members, expandMembers(ctx, v.Type)...)
continue
}
members = append(members, v)
@@ -198,7 +198,7 @@ func expandMembers(tp apiSpec.Type) []apiSpec.Member {
case apiSpec.NestedStruct:
for _, v := range val.Members {
if v.IsInline {
members = append(members, expandMembers(v.Type)...)
members = append(members, expandMembers(ctx, v.Type)...)
continue
}
members = append(members, v)
@@ -208,42 +208,42 @@ func expandMembers(tp apiSpec.Type) []apiSpec.Member {
return members
}
func rangeMemberAndDo(structType apiSpec.Type, do func(tag *apiSpec.Tags, required bool, member apiSpec.Member)) {
var members = expandMembers(structType)
func rangeMemberAndDo(ctx Context, structType apiSpec.Type, do func(tag *apiSpec.Tags, required bool, member apiSpec.Member)) {
var members = expandMembers(ctx, structType)
for _, field := range members {
tags, _ := apiSpec.Parse(field.Tag)
required := isRequired(tags)
required := isRequired(ctx, tags)
do(tags, required, field)
}
}
func isRequired(tags *apiSpec.Tags) bool {
func isRequired(ctx Context, tags *apiSpec.Tags) bool {
tag, err := tags.Get(tagJson)
if err == nil {
return !isOptional(tag.Options)
return !isOptional(ctx, tag.Options)
}
tag, err = tags.Get(tagForm)
if err == nil {
return !isOptional(tag.Options)
return !isOptional(ctx, tag.Options)
}
tag, err = tags.Get(tagPath)
if err == nil {
return !isOptional(tag.Options)
return !isOptional(ctx, tag.Options)
}
return false
}
func isOptional(options []string) bool {
func isOptional(_ Context, options []string) bool {
for _, option := range options {
if option == "optional" {
if option == optionalFlag {
return true
}
}
return false
}
func pathVariable2SwaggerVariable(path string) string {
func pathVariable2SwaggerVariable(_ Context, path string) string {
pathItems := strings.FieldsFunc(path, slashRune)
var resp []string
for _, v := range pathItems {
@@ -256,13 +256,12 @@ func pathVariable2SwaggerVariable(path string) string {
return "/" + strings.Join(resp, "/")
}
func wrapCodeMsgProps(properties spec.SchemaProps, api apiSpec.Info, atDoc apiSpec.AtDoc) spec.SchemaProps {
wrapCodeMsg := getBoolFromKVOrDefault(api.Properties, "wrapCodeMsg", false)
if !wrapCodeMsg {
func wrapCodeMsgProps(ctx Context, properties spec.SchemaProps, atDoc apiSpec.AtDoc) spec.SchemaProps {
if !ctx.WrapCodeMsg {
return properties
}
globalCodeDesc := getStringFromKVOrDefault(api.Properties, "bizCodeEnumDescription", "business code")
methodCodeDesc := getStringFromKVOrDefault(atDoc.Properties, "bizCodeEnumDescription", globalCodeDesc)
globalCodeDesc := ctx.BizCodeEnumDescription
methodCodeDesc := getStringFromKVOrDefault(atDoc.Properties, propertyKeyBizCodeEnumDescription, globalCodeDesc)
return spec.SchemaProps{
Type: []string{swaggerTypeObject},
Properties: spec.SchemaProperties{
@@ -300,22 +299,22 @@ func specExtensions(api apiSpec.Info) (spec.Extensions, *spec.Info) {
ext.Add("x-go-zero-doc", "https://go-zero.dev/")
info := &spec.Info{}
info.Description = util.Unquote(api.Properties["description"])
info.Title = util.Unquote(api.Properties["title"])
info.TermsOfService = util.Unquote(api.Properties["termsOfService"])
info.Version = util.Unquote(api.Properties["version"])
info.Title = getStringFromKVOrDefault(api.Properties, propertyKeyTitle, "")
info.Description = getStringFromKVOrDefault(api.Properties, propertyKeyDescription, "")
info.TermsOfService = getStringFromKVOrDefault(api.Properties, propertyKeyTermsOfService, "")
info.Version = getStringFromKVOrDefault(api.Properties, propertyKeyVersion, "1.0")
contactInfo := spec.ContactInfo{}
contactInfo.Name = util.Unquote(api.Properties["contactName"])
contactInfo.URL = util.Unquote(api.Properties["contactURL"])
contactInfo.Email = util.Unquote(api.Properties["contactEmail"])
contactInfo.Name = getStringFromKVOrDefault(api.Properties, propertyKeyContactName, "")
contactInfo.URL = getStringFromKVOrDefault(api.Properties, propertyKeyContactURL, "")
contactInfo.Email = getStringFromKVOrDefault(api.Properties, propertyKeyContactEmail, "")
if len(contactInfo.Name) > 0 || len(contactInfo.URL) > 0 || len(contactInfo.Email) > 0 {
info.Contact = &contactInfo
}
license := &spec.License{}
license.Name = util.Unquote(api.Properties["licenseName"])
license.URL = util.Unquote(api.Properties["licenseURL"])
license.Name = getStringFromKVOrDefault(api.Properties, propertyKeyLicenseName, "")
license.URL = getStringFromKVOrDefault(api.Properties, propertyKeyLicenseURL, "")
if len(license.Name) > 0 || len(license.URL) > 0 {
info.License = license
}

View File

@@ -19,7 +19,7 @@ func Test_pathVariable2SwaggerVariable(t *testing.T) {
}
for _, tc := range testCases {
result := pathVariable2SwaggerVariable(tc.input)
result := pathVariable2SwaggerVariable(testingContext(t), tc.input)
assert.Equal(t, tc.expected, result)
}
}

2
tools/goctl/build.env Normal file
View File

@@ -0,0 +1,2 @@
APP_NAME=goctl
APP_VERSION=1.8.4-alpha

50
tools/goctl/build.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
source build.env
APP_NAME=$APP_NAME
VERSION=$APP_VERSION
BUILD_DIR="dist"
ZIP_DIR="${BUILD_DIR}/zips"
PLATFORMS=(
"linux/amd64"
"linux/arm64"
"darwin/amd64"
"darwin/arm64"
"windows/amd64"
"windows/arm64"
)
rm -rf "${BUILD_DIR}"
mkdir -p "${ZIP_DIR}"
for PLATFORM in "${PLATFORMS[@]}"; do
GOOS=${PLATFORM%/*}
GOARCH=${PLATFORM#*/}
OUTPUT="${BUILD_DIR}/${APP_NAME}-${VERSION}-${GOOS}-${GOARCH}"
if [ "${GOOS}" = "windows" ]; then
OUTPUT="${OUTPUT}.exe"
fi
echo "Building for ${GOOS}/${GOARCH}..."
env GOOS="${GOOS}" GOARCH="${GOARCH}" go build -o "${OUTPUT}" goctl.go
if [ $? -ne 0 ]; then
echo "Error building for ${GOOS}/${GOARCH}"
exit 1
fi
ZIP_OUTPUT="${ZIP_DIR}/$(basename "${OUTPUT}")"
if [ "${GOOS}" = "windows" ]; then
zip -j "${ZIP_OUTPUT%.exe}.zip" "${OUTPUT}"
else
zip -j "${ZIP_OUTPUT}.zip" "${OUTPUT}"
fi
echo "Created zip: ${ZIP_OUTPUT}.zip"
done
echo "All builds completed successfully. Zip files are in ${ZIP_DIR}/"

View File

@@ -16,7 +16,7 @@ require (
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
github.com/zeromicro/antlr v0.0.1
github.com/zeromicro/ddl-parser v1.0.5
github.com/zeromicro/go-zero v1.8.2
github.com/zeromicro/go-zero v1.8.3
golang.org/x/text v0.22.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.5
@@ -72,7 +72,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/redis/go-redis/v9 v9.8.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect

View File

@@ -146,8 +146,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -183,8 +183,8 @@ github.com/zeromicro/antlr v0.0.1 h1:CQpIn/dc0pUjgGQ81y98s/NGOm2Hfru2NNio2I9mQgk
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
github.com/zeromicro/go-zero v1.8.2 h1:AbJckBoojbr1lqCN1dkvURTIHOau7yvKReEd7ZmjuCk=
github.com/zeromicro/go-zero v1.8.2/go.mod h1:G5dF+jzCEuq0t1j8qdrtVAy30QMgctGcKSfqFIGsvSg=
github.com/zeromicro/go-zero v1.8.3 h1:AwpBJQLAsZAt4OOnK0eR8UU1Ja2RFBIXfKkHdnXQKfc=
github.com/zeromicro/go-zero v1.8.3/go.mod h1:EnuEA3XdIQvAvc4WWTskRTO0jM2/aQi7OXv1gKWRNJ0=
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=

View File

@@ -38,7 +38,8 @@
"remote": "{{.global.remote}}",
"branch": "{{.global.branch}}",
"style": "{{.global.style}}",
"test": "Generate test files"
"test": "Generate test files",
"type-group": "Generate type group files"
},
"new": {
"short": "Fast create api service",

View File

@@ -6,7 +6,7 @@ import (
)
// BuildVersion is the version of goctl.
const BuildVersion = "1.8.3"
const BuildVersion = "1.8.4-alpha"
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}

View File

@@ -1356,7 +1356,7 @@ func (p *Parser) parseKVExpression() *ast.KVExpr {
expr.Colon = p.curTokenNode()
// token STRING
if !p.advanceIfPeekTokenIs(token.STRING, token.RAW_STRING) {
if !p.advanceIfPeekTokenIs(token.STRING, token.RAW_STRING, token.IDENT) {
return nil
}

View File

@@ -130,6 +130,8 @@ func TestParser_Parse_infoStmt(t *testing.T) {
"author": `"type author here"`,
"email": `"type email here"`,
"version": `"type version here"`,
"enable": `true`,
"disable": `false`,
}
p := New("foo.api", infoTestAPI)
result := p.Parse()

View File

@@ -4,4 +4,6 @@ info(
author: "type author here"
email: "type email here"
version: "type version here"
enable: true
disable: false
)

View File

@@ -10,6 +10,8 @@ info ( // info stmt
author: "type author here"
email: "type email here"
version: "type version here"
enable: true
disable: false
)
type AliasInt int