(goctl): support nested struct (#4211)

This commit is contained in:
kesonan 2024-06-25 23:18:15 +08:00 committed by GitHub
parent 4ec9cac82b
commit a012a9138f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 318 additions and 76 deletions

View File

@ -1,6 +1,7 @@
package dartgen
import (
"bytes"
"os"
"strings"
"text/template"
@ -8,8 +9,8 @@ import (
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
)
const dataTemplate = `// --{{with .Info}}{{.Title}}{{end}}--
{{ range .Types}}
const dataTemplate = `// --{{with .APISpec.Info}}{{.Title}}{{end}}--
{{ range .APISpec.Types}}
class {{.Name}}{
{{range .Members}}
/// {{.Comment}}
@ -28,12 +29,16 @@ class {{.Name}}{
'{{getPropertyFromMember .}}': {{if isDirectType .Type.Name}}{{lowCamelCase .Name}}{{else if isClassListType .Type.Name}}{{lowCamelCase .Name}}.map((i) => i.toJson()){{else}}{{lowCamelCase .Name}}.toJson(){{end}},{{end}}
};
}
{{ range $.InnerClassList}}
{{.}}
{{end}}
}
{{end}}
`
const dataTemplateV2 = `// --{{with .Info}}{{.Title}}{{end}}--
{{ range .Types}}
const dataTemplateV2 = `// --{{with .APISpec.Info}}{{.Title}}{{end}}--
{{ range .APISpec.Types}}
class {{.Name}} {
{{range .Members}}
{{if .Comment}}{{.Comment}}{{end}}
@ -73,9 +78,18 @@ class {{.Name}} {
,{{end}}
};
}
{{ range $.InnerClassList}}
{{.}}
{{end}}
}
{{end}}`
type DartSpec struct {
APISpec *spec.ApiSpec
InnerClassList []string
}
func genData(dir string, api *spec.ApiSpec, isLegacy bool) error {
err := os.MkdirAll(dir, 0o755)
if err != nil {
@ -104,12 +118,12 @@ func genData(dir string, api *spec.ApiSpec, isLegacy bool) error {
return err
}
err = convertDataType(api)
err, dartSpec := convertDataType(api, isLegacy)
if err != nil {
return err
}
return t.Execute(file, api)
return t.Execute(file, dartSpec)
}
func genTokens(dir string, isLeagcy bool) error {
@ -132,24 +146,61 @@ func genTokens(dir string, isLeagcy bool) error {
return err
}
func convertDataType(api *spec.ApiSpec) error {
func convertDataType(api *spec.ApiSpec, isLegacy bool) (error, *DartSpec) {
var result DartSpec
types := api.Types
if len(types) == 0 {
return nil
return nil, &result
}
for _, ty := range types {
defineStruct, ok := ty.(spec.DefineStruct)
if ok {
for index, member := range defineStruct.Members {
tp, err := specTypeToDart(member.Type)
if err != nil {
return err
structMember, ok := member.Type.(spec.DefineStruct)
if ok && structMember.IsNestedStruct() {
defineStruct.Members[index].Type = spec.PrimitiveType{RawName: member.Name}
t := template.New("dataTemplate")
t = t.Funcs(funcMap)
tpl := dataTemplateV2
if isLegacy {
tpl = dataTemplate
}
t, err := t.Parse(tpl)
if err != nil {
return err, nil
}
var innerClassSpec = &spec.ApiSpec{
Types: []spec.Type{
spec.DefineStruct{
RawName: member.Name,
Members: structMember.Members,
},
},
}
err, dartSpec := convertDataType(innerClassSpec, isLegacy)
if err != nil {
return err, nil
}
writer := bytes.NewBuffer(nil)
err = t.Execute(writer, dartSpec)
if err != nil {
return err, nil
}
result.InnerClassList = append(result.InnerClassList, writer.String())
} else {
tp, err := specTypeToDart(member.Type)
if err != nil {
return err, nil
}
defineStruct.Members[index].Type = buildSpecType(member.Type, tp)
}
defineStruct.Members[index].Type = buildSpecType(member.Type, tp)
}
}
}
result.APISpec = api
return nil
return nil, &result
}

View File

@ -57,6 +57,8 @@ var (
importTwiceApi string
//go:embed testdata/another_import_api.api
anotherImportApi string
//go:embed testdata/example.api
exampleApi string
)
func TestParser(t *testing.T) {
@ -316,15 +318,32 @@ func TestCamelStyle(t *testing.T) {
validateWithCamel(t, filename, "GoZero")
}
func TestExampleGen(t *testing.T) {
env.Set(t, env.GoctlExperimental, env.ExperimentalOn)
filename := "greet.api"
err := os.WriteFile(filename, []byte(exampleApi), os.ModePerm)
assert.Nil(t, err)
t.Cleanup(func() {
_ = os.Remove(filename)
})
spec, err := parser.Parse(filename)
assert.Nil(t, err)
assert.Equal(t, len(spec.Types), 10)
validate(t, filename)
}
func validate(t *testing.T, api string) {
validateWithCamel(t, api, "gozero")
}
func validateWithCamel(t *testing.T, api, camel string) {
dir := "workspace"
defer func() {
os.RemoveAll(dir)
}()
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
err := pathx.MkdirIfNotExist(dir)
assert.Nil(t, err)
err = initMod(dir)

View File

@ -74,8 +74,21 @@ func writeType(writer io.Writer, tp spec.Type) error {
return fmt.Errorf("unspport struct type: %s", tp.Name())
}
fmt.Fprintf(writer, "type %s struct {\n", util.Title(tp.Name()))
for _, member := range structType.Members {
_, err := fmt.Fprintf(writer, "type %s struct {\n", util.Title(tp.Name()))
if err != nil {
return err
}
if err := writeMember(writer, structType.Members); err != nil {
return err
}
_, err = fmt.Fprintf(writer, "}")
return err
}
func writeMember(writer io.Writer, members []spec.Member) error {
for _, member := range members {
if member.IsInline {
if _, err := fmt.Fprintf(writer, "%s\n", strings.Title(member.Type.Name())); err != nil {
return err
@ -88,6 +101,5 @@ func writeType(writer io.Writer, tp spec.Type) error {
return err
}
}
fmt.Fprintf(writer, "}")
return nil
}

View File

@ -0,0 +1,99 @@
syntax = "v1"
info(
title: "demo title"
desc: "demo desc"
author: "keson.an"
date: "2024-06-25"
version: "v1"
)
// empty structure
type Foo {
}
// type lit
type Bar {
Foo int `json:"foo"`
Bar bool `json:"bar"`
Baz []string `json:"baz"`
Qux map[string]string `json:"qux"`
}
type Baz {
Foo `json:"foo"`
// array type
Arr [2]int `json:"arr"`
// nested type
Bar {
Foo string `json:"foo"`
Bar bool `json:"bar"`
Baz {
Foo string `json:"foo"`
Bar bool `json:"bar"`
}
Qux {
Foo string `json:"foo"`
Bar bool `json:"bar"`
} `json:"qux"`
} `json:"bar"`
}
type UpdateReq {
Arg1 string `json:"arg1"`
}
type ListItem {
Value1 string `json:"value1"`
}
type LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResp {
Name string `json:"name"`
}
type FormExampleReq {
Name string `form:"name"`
}
type PathExampleReq {
ID string `path:"id"`
}
type PathExampleResp {
Name string `json:"name"`
}
@server(
jwt: Auth
prefix: /v1
group: g1
timeout: 3s
middleware: AuthInterceptor
maxBytes: 1048576
)
service Foo {
@handler ping
get /ping
@handler update
post /update (UpdateReq)
@handler list
get /list returns ([]ListItem)
@handler login
post /login (LoginReq) returns (LoginResp)
@handler formExample
post /form/example (FormExampleReq)
@handler pathExample
get /path/example/:id (PathExampleReq) returns (PathExampleResp)
}

View File

@ -59,16 +59,59 @@ func genFile(c fileGenConfig) error {
func writeProperty(writer io.Writer, name, tag, comment string, tp spec.Type, indent int) error {
util.WriteIndent(writer, indent)
var err error
var (
err error
isNestedStruct bool
)
structType, ok := tp.(spec.DefineStruct)
if ok && structType.IsNestedStruct() {
isNestedStruct = true
}
if len(comment) > 0 {
comment = strings.TrimPrefix(comment, "//")
comment = "//" + comment
_, err = fmt.Fprintf(writer, "%s %s %s %s\n", strings.Title(name), tp.Name(), tag, comment)
} else {
_, err = fmt.Fprintf(writer, "%s %s %s\n", strings.Title(name), tp.Name(), tag)
}
return err
if isNestedStruct {
_, err = fmt.Fprintf(writer, "%s struct {\n", strings.Title(name))
if err != nil {
return err
}
if err := writeMember(writer, structType.Members); err != nil {
return err
}
_, err := fmt.Fprintf(writer, "} %s", tag)
if err != nil {
return err
}
if len(comment) > 0 {
_, err = fmt.Fprintf(writer, " %s", comment)
if err != nil {
return err
}
}
_, err = fmt.Fprint(writer, "\n")
if err != nil {
return err
}
} else {
if len(comment) > 0 {
_, err = fmt.Fprintf(writer, "%s %s %s %s\n", strings.Title(name), tp.Name(), tag, comment)
if err != nil {
return err
}
} else {
_, err = fmt.Fprintf(writer, "%s %s %s\n", strings.Title(name), tp.Name(), tag)
if err != nil {
return err
}
}
}
return nil
}
func getAuths(api *spec.ApiSpec) []string {

View File

@ -1,8 +1,6 @@
package ast
import (
"github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
)
import "github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
// ImportExpr defines import syntax for api
type ImportExpr struct {

View File

@ -1,8 +1,6 @@
package ast
import (
"github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
)
import "github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
// InfoExpr defines info syntax for api
type InfoExpr struct {

View File

@ -1,8 +1,6 @@
package ast
import (
"github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
)
import "github.com/zeromicro/go-zero/tools/goctl/api/parser/g4/gen/api"
// SyntaxExpr describes syntax for api
type SyntaxExpr struct {

View File

@ -30,6 +30,11 @@ func (t DefineStruct) Documents() []string {
return t.Docs
}
// IsNestedStruct returns whether the structure is nested.
func (t DefineStruct) IsNestedStruct() bool {
return len(t.Members) > 0
}
// Name returns a map string, such as map[string]int
func (t MapType) Name() string {
return t.RawName

View File

@ -1,6 +1,7 @@
package tsgen
import (
"bytes"
"errors"
"fmt"
"io"
@ -19,7 +20,7 @@ const (
func writeProperty(writer io.Writer, member spec.Member, indent int) error {
writeIndent(writer, indent)
ty, err := genTsType(member)
ty, err := genTsType(member, indent)
if err != nil {
return err
}
@ -40,7 +41,7 @@ func writeProperty(writer io.Writer, member spec.Member, indent int) error {
}
if len(member.Docs) > 0 {
fmt.Fprintf(writer, "%s\n", strings.Join(member.Docs, ""))
writeIndent(writer, 1)
writeIndent(writer, indent)
}
_, err = fmt.Fprintf(writer, "%s%s: %s%s\n", name, optionalTag, ty, comment)
return err
@ -52,7 +53,27 @@ func writeIndent(writer io.Writer, indent int) {
}
}
func genTsType(m spec.Member) (ty string, err error) {
func genTsType(m spec.Member, indent int) (ty string, err error) {
v, ok := m.Type.(spec.DefineStruct)
if ok && v.IsNestedStruct() {
writer := bytes.NewBuffer(nil)
_, err := fmt.Fprintf(writer, "{\n")
if err != nil {
return "", err
}
if err := writeMembers(writer, v, false, indent+1); err != nil {
return "", err
}
writeIndent(writer, indent)
_, err = fmt.Fprintf(writer, "}")
if err != nil {
return "", err
}
return writer.String(), nil
}
ty, err = goTypeToTs(m.Type, false)
if enums := m.GetEnumOptions(); enums != nil {
if ty == "string" {
@ -130,7 +151,7 @@ func primitiveType(tp string) (string, bool) {
func writeType(writer io.Writer, tp spec.Type) error {
fmt.Fprintf(writer, "export interface %s {\n", util.Title(tp.Name()))
if err := writeMembers(writer, tp, false); err != nil {
if err := writeMembers(writer, tp, false, 1); err != nil {
return err
}
@ -166,12 +187,12 @@ func genParamsTypesIfNeed(writer io.Writer, tp spec.Type) error {
return nil
}
func writeMembers(writer io.Writer, tp spec.Type, isParam bool) error {
func writeMembers(writer io.Writer, tp spec.Type, isParam bool, indent int) error {
definedType, ok := tp.(spec.DefineStruct)
if !ok {
pointType, ok := tp.(spec.PointerType)
if ok {
return writeMembers(writer, pointType.Type, isParam)
return writeMembers(writer, pointType.Type, isParam, indent)
}
return fmt.Errorf("type %s not supported", tp.Name())
@ -183,13 +204,13 @@ func writeMembers(writer io.Writer, tp spec.Type, isParam bool) error {
}
for _, member := range members {
if member.IsInline {
if err := writeMembers(writer, member.Type, isParam); err != nil {
if err := writeMembers(writer, member.Type, isParam, indent); err != nil {
return err
}
continue
}
if err := writeProperty(writer, member, 1); err != nil {
if err := writeProperty(writer, member, indent); err != nil {
return apiutil.WrapErr(err, " type "+tp.Name())
}
}

View File

@ -16,14 +16,14 @@ func TestGenTsType(t *testing.T) {
Docs: nil,
IsInline: false,
}
ty, err := genTsType(member)
ty, err := genTsType(member, 1)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, `'foo' | 'bar' | 'options' | '123'`, ty)
member.IsInline = true
ty, err = genTsType(member)
ty, err = genTsType(member, 1)
if err != nil {
t.Fatal(err)
}
@ -31,7 +31,7 @@ func TestGenTsType(t *testing.T) {
member.Type = spec.PrimitiveType{RawName: "int"}
member.Tag = `json:"foo,options=1|3|4|123"`
ty, err = genTsType(member)
ty, err = genTsType(member, 1)
if err != nil {
t.Fatal(err)
}

View File

@ -7,7 +7,6 @@ import (
"context"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/pb/hello"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)

View File

@ -4,13 +4,12 @@ import (
"flag"
"fmt"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/internal/config"
greetServer "github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/internal/server/greet"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/internal/svc"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/pb/hello"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"

View File

@ -3,10 +3,9 @@ package greetlogic
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/internal/svc"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hello/pb/hello"
"github.com/zeromicro/go-zero/core/logx"
)
type SayHelloLogic struct {

View File

@ -7,7 +7,6 @@ import (
"context"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/pb/hi"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)

View File

@ -7,7 +7,6 @@ import (
"context"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/pb/hi"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)

View File

@ -4,14 +4,13 @@ import (
"flag"
"fmt"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/config"
eventServer "github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/server/event"
greetServer "github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/server/greet"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/svc"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/pb/hi"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"

View File

@ -3,10 +3,9 @@ package eventlogic
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/svc"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/pb/hi"
"github.com/zeromicro/go-zero/core/logx"
)
type AskQuestionLogic struct {

View File

@ -3,10 +3,9 @@ package greetlogic
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/svc"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/pb/hi"
"github.com/zeromicro/go-zero/core/logx"
)
type SayHelloLogic struct {

View File

@ -3,10 +3,9 @@ package greetlogic
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/internal/svc"
"github.com/zeromicro/go-zero/tools/goctl/example/rpc/hi/pb/hi"
"github.com/zeromicro/go-zero/core/logx"
)
type SayHiLogic struct {

View File

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

View File

@ -1,8 +1,6 @@
package builderx
import (
"github.com/zeromicro/go-zero/core/stores/builder"
)
import "github.com/zeromicro/go-zero/core/stores/builder"
// Deprecated: Use github.com/zeromicro/go-zero/core/stores/builder.RawFieldNames instead.
func FieldNames(in any) []string {

View File

@ -7,7 +7,6 @@ import (
"github.com/go-sql-driver/mysql"
"github.com/spf13/cobra"
"github.com/zeromicro/go-zero/core/collection"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/postgres"

View File

@ -9,7 +9,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/model/sql/gen"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"

View File

@ -12,7 +12,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/builder"
"github.com/zeromicro/go-zero/core/stringx"

View File

@ -7,7 +7,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/tools/goctl/model/sql/model"
"github.com/zeromicro/go-zero/tools/goctl/model/sql/util"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"

View File

@ -42,7 +42,18 @@ func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
case *ast.AnyDataType:
return nil, ast.SyntaxError(v.Pos(), "unsupported any type")
case *ast.StructDataType:
// TODO(keson) feature: can be extended
var members []spec.Member
for _, item := range v.Elements {
m, err := a.fieldToMember(item)
if err != nil {
return nil, err
}
members = append(members, m)
}
return spec.DefineStruct{
RawName: v.RawText(),
Members: members,
}, nil
case *ast.InterfaceDataType:
return spec.InterfaceType{RawName: v.RawText()}, nil
case *ast.MapDataType:
@ -323,12 +334,14 @@ func (a *Analyzer) fillTypes() error {
for _, member := range v.Members {
switch v := member.Type.(type) {
case spec.DefineStruct:
tp, err := a.findDefinedType(v.RawName)
if err != nil {
return err
}
if !v.IsNestedStruct() {
tp, err := a.findDefinedType(v.RawName)
if err != nil {
return err
}
member.Type = tp
member.Type = tp
}
}
members = append(members, member)
}

View File

@ -1,13 +1,13 @@
package generator
import (
"github.com/zeromicro/go-zero/tools/goctl/util/format"
"path/filepath"
"strings"
conf "github.com/zeromicro/go-zero/tools/goctl/config"
"github.com/zeromicro/go-zero/tools/goctl/rpc/parser"
"github.com/zeromicro/go-zero/tools/goctl/util/ctx"
"github.com/zeromicro/go-zero/tools/goctl/util/format"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
"github.com/zeromicro/go-zero/tools/goctl/util/stringx"
)