From 48c2a755ceb6f4f4ba9c440a17f52a87bdf9b491 Mon Sep 17 00:00:00 2001 From: wubin1989 <328454505@qq.com> Date: Sat, 18 Nov 2023 23:41:54 +0800 Subject: [PATCH] add http2grpc flag to support expose RESTful endpoints to grpc service --- cmd/grpc.go | 9 +- cmd/internal/svc/codegen/grpcannotation.go | 6 +- cmd/internal/svc/codegen/grpchttpmain.go | 126 +++++++++++ cmd/internal/svc/codegen/grpcproto.go | 2 +- cmd/internal/svc/codegen/http2grpc.go | 211 ++++++++++++++++++ cmd/internal/svc/codegen/httphandlerimpl.go | 8 +- .../svc/codegen/httphandlerimpl_test.go | 2 +- cmd/internal/svc/svc.go | 32 ++- cmd/internal/svc/validate/validate.go | 80 ++++++- framework/rest/bizerror.go | 14 ++ framework/rest/form.go | 12 + version/version.go | 2 +- 12 files changed, 481 insertions(+), 23 deletions(-) create mode 100644 cmd/internal/svc/codegen/grpchttpmain.go create mode 100644 cmd/internal/svc/codegen/http2grpc.go diff --git a/cmd/grpc.go b/cmd/grpc.go index c9e467ef..b633fe79 100644 --- a/cmd/grpc.go +++ b/cmd/grpc.go @@ -9,6 +9,7 @@ import ( ) var naming string +var http2grpc bool var grpcCmd = &cobra.Command{ Use: "grpc", @@ -20,7 +21,11 @@ var grpcCmd = &cobra.Command{ case "snake": fn = strcase.ToSnake } - s := svc.NewSvc("", svc.WithProtoGenerator(v3.NewProtoGenerator(v3.WithFieldNamingFunc(fn)))) + s := svc.NewSvc("", + svc.WithProtoGenerator(v3.NewProtoGenerator(v3.WithFieldNamingFunc(fn))), + svc.WithHttp2Grpc(http2grpc), + svc.WithAllowGetWithReqBody(allowGetWithReqBody), + ) s.Grpc() }, } @@ -28,4 +33,6 @@ var grpcCmd = &cobra.Command{ func init() { svcCmd.AddCommand(grpcCmd) grpcCmd.Flags().StringVar(&naming, "case", "lowerCamel", `protobuf message field naming strategy, only support "lowerCamel" and "snake"`) + grpcCmd.Flags().BoolVar(&http2grpc, "http2grpc", false, `whether need RESTful api for your grpc service`) + grpcCmd.Flags().BoolVar(&allowGetWithReqBody, "allowGetWithReqBody", false, "Whether allow get http request with request body.") } diff --git a/cmd/internal/svc/codegen/grpcannotation.go b/cmd/internal/svc/codegen/grpcannotation.go index 49766fb7..b6d73adf 100644 --- a/cmd/internal/svc/codegen/grpcannotation.go +++ b/cmd/internal/svc/codegen/grpcannotation.go @@ -55,7 +55,7 @@ func GenMethodAnnotationStore(dir string, ic astutils.InterfaceCollector) { sqlBuf bytes.Buffer fi os.FileInfo ) - grpcDir = filepath.Join(dir, "transport/grpc") + grpcDir = filepath.Join(dir, "transport", "grpc") if err = os.MkdirAll(grpcDir, os.ModePerm); err != nil { panic(err) } @@ -65,14 +65,14 @@ func GenMethodAnnotationStore(dir string, ic astutils.InterfaceCollector) { panic(err) } if fi != nil { - logrus.Warningln("file handler.go will be overwritten") + logrus.Warningln("file annotation.go will be overwritten") } if f, err = os.Create(annotationFile); err != nil { panic(err) } defer f.Close() - if tpl, err = template.New("annotation.go.tmpl").Parse(annotationTmpl); err != nil { + if tpl, err = template.New(annotationTmpl).Parse(annotationTmpl); err != nil { panic(err) } if err = tpl.Execute(&sqlBuf, struct { diff --git a/cmd/internal/svc/codegen/grpchttpmain.go b/cmd/internal/svc/codegen/grpchttpmain.go new file mode 100644 index 00000000..e1b09d2b --- /dev/null +++ b/cmd/internal/svc/codegen/grpchttpmain.go @@ -0,0 +1,126 @@ +package codegen + +import ( + "bytes" + "github.com/unionj-cloud/go-doudou/v2/cmd/internal/templates" + "github.com/unionj-cloud/go-doudou/v2/toolkit/astutils" + v3 "github.com/unionj-cloud/go-doudou/v2/toolkit/protobuf/v3" + "github.com/unionj-cloud/go-doudou/v2/version" + "os" + "path/filepath" + "strings" + "text/template" +) + +var mainTmplGrpcHttp = templates.EditableHeaderTmpl + `package main + +import ( + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpczerolog "github.com/grpc-ecosystem/go-grpc-middleware/providers/zerolog/v2" + grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" + grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" + grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/unionj-cloud/go-doudou/v2/toolkit/zlogger" + "google.golang.org/grpc" + "github.com/unionj-cloud/go-doudou/v2/framework/grpcx" + {{.ServiceAlias}} "{{.ServicePackage}}" + "{{.ConfigPackage}}" + pb "{{.PbPackage}}" + "{{.HttpPackage}}" + "github.com/unionj-cloud/go-doudou/v2/framework/rest" +) + +func main() { + conf := config.LoadFromEnv() + svc := {{.ServiceAlias}}.New{{.SvcName}}(conf) + + go func() { + grpcServer := grpcx.NewGrpcServer( + grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( + grpc_ctxtags.StreamServerInterceptor(), + grpc_opentracing.StreamServerInterceptor(), + grpc_prometheus.StreamServerInterceptor, + tags.StreamServerInterceptor(tags.WithFieldExtractor(tags.CodeGenRequestFieldExtractor)), + logging.StreamServerInterceptor(grpczerolog.InterceptorLogger(zlogger.Logger)), + grpc_recovery.StreamServerInterceptor(), + )), + grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( + grpc_ctxtags.UnaryServerInterceptor(), + grpc_opentracing.UnaryServerInterceptor(), + grpc_prometheus.UnaryServerInterceptor, + tags.UnaryServerInterceptor(tags.WithFieldExtractor(tags.CodeGenRequestFieldExtractor)), + logging.UnaryServerInterceptor(grpczerolog.InterceptorLogger(zlogger.Logger)), + grpc_recovery.UnaryServerInterceptor(), + )), + ) + pb.Register{{.GrpcSvcName}}Server(grpcServer, svc) + grpcServer.Run() + }() + + handler := httpsrv.New{{.SvcName}}Http2Grpc(svc) + srv := rest.NewRestServer() + srv.AddRoute(httpsrv.Routes(handler)...) + srv.Run() +} +` + +// GenMainGrpcHttp generates main function for grpc service +func GenMainGrpcHttp(dir string, ic astutils.InterfaceCollector, grpcSvc v3.Service) { + var ( + err error + mainfile string + f *os.File + tpl *template.Template + cmdDir string + svcName string + alias string + source string + ) + cmdDir = filepath.Join(dir, "cmd") + if err = MkdirAll(cmdDir, os.ModePerm); err != nil { + panic(err) + } + svcName = ic.Interfaces[0].Name + alias = ic.Package.Name + mainfile = filepath.Join(cmdDir, "main.go") + servicePkg := astutils.GetPkgPath(dir) + cfgPkg := astutils.GetPkgPath(filepath.Join(dir, "config")) + pbPkg := astutils.GetPkgPath(filepath.Join(dir, "transport", "grpc")) + httpsrvPkg := astutils.GetPkgPath(filepath.Join(dir, "transport", "httpsrv")) + if _, err = Stat(mainfile); os.IsNotExist(err) { + if f, err = Create(mainfile); err != nil { + panic(err) + } + defer f.Close() + if tpl, err = template.New(mainTmplGrpcHttp).Parse(mainTmplGrpcHttp); err != nil { + panic(err) + } + var buf bytes.Buffer + if err = tpl.Execute(&buf, struct { + ServicePackage string + ConfigPackage string + PbPackage string + SvcName string + ServiceAlias string + Version string + GrpcSvcName string + HttpPackage string + }{ + ServicePackage: servicePkg, + ConfigPackage: cfgPkg, + PbPackage: pbPkg, + SvcName: svcName, + ServiceAlias: alias, + Version: version.Release, + GrpcSvcName: grpcSvc.Name, + HttpPackage: httpsrvPkg, + }); err != nil { + panic(err) + } + source = strings.TrimSpace(buf.String()) + astutils.FixImport([]byte(source), mainfile) + } +} diff --git a/cmd/internal/svc/codegen/grpcproto.go b/cmd/internal/svc/codegen/grpcproto.go index e8f04398..5979bd05 100644 --- a/cmd/internal/svc/codegen/grpcproto.go +++ b/cmd/internal/svc/codegen/grpcproto.go @@ -85,7 +85,7 @@ func GenGrpcProto(dir string, ic astutils.InterfaceCollector, p protov3.ProtoGen f *os.File grpcDir string ) - grpcDir = filepath.Join(dir, "transport/grpc") + grpcDir = filepath.Join(dir, "transport", "grpc") if err = os.MkdirAll(grpcDir, os.ModePerm); err != nil { panic(err) } diff --git a/cmd/internal/svc/codegen/http2grpc.go b/cmd/internal/svc/codegen/http2grpc.go new file mode 100644 index 00000000..4af38ccd --- /dev/null +++ b/cmd/internal/svc/codegen/http2grpc.go @@ -0,0 +1,211 @@ +package codegen + +import ( + "bytes" + "github.com/iancoleman/strcase" + "github.com/sirupsen/logrus" + "github.com/unionj-cloud/go-doudou/v2/cmd/internal/templates" + "github.com/unionj-cloud/go-doudou/v2/toolkit/astutils" + "github.com/unionj-cloud/go-doudou/v2/toolkit/copier" + "github.com/unionj-cloud/go-doudou/v2/version" + "io/ioutil" + "os" + "path/filepath" + "strings" + "text/template" +) + +var appendHttp2GrpcTmpl = ` +{{- range $m := .Meta.Methods }} + func (receiver *{{$.Meta.Name}}Http2Grpc) {{$m.Name}}(_writer http.ResponseWriter, _req *http.Request) { + var ( + {{- range $p := $m.Params }} + {{- if eq $p.Type "context.Context"}} + {{ $p.Name }} {{ $p.Type }} + {{- else }} + {{$p.Name}} = new({{ trimLeft $p.Type "*" | replacePkg }}) + {{- end }} + {{- end }} + {{- range $r := $m.Results }} + {{- if eq $r.Type "error"}} + {{ $r.Name }} {{ $r.Type }} + {{- else }} + {{ $r.Name }} = new({{ trimLeft $r.Type "*" | replacePkg }}) + {{- end }} + {{- end }} + ) + {{- range $p := $m.Params }} + {{- if eq $p.Type "context.Context" }} + {{$p.Name}} = _req.Context() + {{- else if and (eq $m.HttpMethod "GET") (not $.Config.AllowGetWithReqBody)}} + if _err := _req.ParseForm(); _err != nil { + rest.PanicBadRequestErr(_err) + } + if _err := rest.DecodeForm({{ $p.Name }}, _req.Form); _err != nil { + rest.PanicBadRequestErr(_err) + } + if _err := rest.ValidateStruct({{ $p.Name }}); _err != nil { + rest.PanicBadRequestErr(_err) + } + {{- else }} + if _err := json.NewDecoder(_req.Body).Decode({{$p.Name}}); _err != nil { + rest.PanicBadRequestErr(_err) + } + if _err := rest.ValidateStruct({{$p.Name}}); _err != nil { + rest.PanicBadRequestErr(_err) + } + {{- end }} + {{- end }} + {{- if eq (len $m.Results) 1 }} + _, {{ range $i, $r := $m.Results }}{{- $r.Name }}{{- end }} = receiver.{{$.Meta.Name | toLowerCamel}}.{{$m.Name}}Rpc( + {{- else }} + {{ range $i, $r := $m.Results }}{{- if $i}},{{- end}}{{- $r.Name }}{{- end }} = receiver.{{$.Meta.Name | toLowerCamel}}.{{$m.Name}}Rpc( + {{- end }} + {{- if eq (len $m.Params) 1 }} + {{- range $p := $m.Params }} + {{ $p.Name }}, + {{- end }} + new(emptypb.Empty), + {{- else }} + {{- range $p := $m.Params }} + {{- if eq $p.Type "context.Context"}} + {{ $p.Name }}, + {{- else }} + {{$p.Name}}, + {{- end }} + {{- end }} + {{- end }} + ) + {{- range $r := $m.Results }} + {{- if eq $r.Type "error" }} + if {{ $r.Name }} != nil { + panic({{ $r.Name }}) + } + {{- end }} + {{- end }} + {{- if eq (len $m.Results) 1 }} + if _err := json.NewEncoder(_writer).Encode(struct {}{}); _err != nil { + rest.HandleInternalServerError(_err) + } + {{- else }} + if _err := json.NewEncoder(_writer).Encode({{- range $r := $m.Results }}{{- if ne $r.Type "error" }}{{- $r.Name }}{{- end }}{{- end }}); _err != nil { + rest.HandleInternalServerError(_err) + } + {{- end }} + } +{{- end }} +` + +var initHttp2GrpcTmpl = templates.EditableHeaderTmpl + `package httpsrv + +import () + +type {{.Meta.Name}}Http2Grpc struct{ + {{.Meta.Name | toLowerCamel}} pb.{{.Meta.Name}}ServiceServer +} + +` + appendHttp2GrpcTmpl + ` + +func New{{.Meta.Name}}Http2Grpc({{.Meta.Name | toLowerCamel}} pb.{{.Meta.Name}}ServiceServer) {{.Meta.Name}}Handler { + return &{{.Meta.Name}}Http2Grpc{ + {{.Meta.Name | toLowerCamel}}, + } +} +` + +var importHttp2GrpcTmpl = ` + "context" + "encoding/json" + "github.com/unionj-cloud/go-doudou/v2/framework/rest" + "net/http" + pb "{{.TransportGrpcPackage}}" +` + +type GenHttp2GrpcConfig struct { + AllowGetWithReqBody bool +} + +// GenHttp2Grpc generates http handler implementation +// Parsed value from query string parameters or application/x-www-form-urlencoded form will be string type. +// You may need to convert the type by yourself. +func GenHttp2Grpc(dir string, ic astutils.InterfaceCollector, config GenHttp2GrpcConfig) { + var ( + err error + handlerimplfile string + f *os.File + tpl *template.Template + buf bytes.Buffer + httpDir string + fi os.FileInfo + tmpl string + meta astutils.InterfaceMeta + importBuf bytes.Buffer + ) + httpDir = filepath.Join(dir, "transport", "httpsrv") + if err = os.MkdirAll(httpDir, os.ModePerm); err != nil { + panic(err) + } + handlerimplfile = filepath.Join(httpDir, "http2grpc.go") + fi, err = os.Stat(handlerimplfile) + if err != nil && !os.IsNotExist(err) { + panic(err) + } + err = copier.DeepCopy(ic.Interfaces[0], &meta) + if err != nil { + panic(err) + } + unimplementedMethods(&meta, httpDir, meta.Name+"Http2Grpc") + if fi != nil { + logrus.Warningln("New content will be append to http2grpc.go file") + if f, err = os.OpenFile(handlerimplfile, os.O_APPEND, os.ModePerm); err != nil { + panic(err) + } + defer f.Close() + tmpl = appendHttp2GrpcTmpl + } else { + if f, err = os.Create(handlerimplfile); err != nil { + panic(err) + } + defer f.Close() + tmpl = initHttp2GrpcTmpl + } + + funcMap := make(map[string]interface{}) + funcMap["toLowerCamel"] = strcase.ToLowerCamel + funcMap["replacePkg"] = func(input string) string { + return "pb" + input[strings.LastIndex(input, "."):] + } + funcMap["trimLeft"] = strings.TrimLeft + if tpl, err = template.New(tmpl).Funcs(funcMap).Parse(tmpl); err != nil { + panic(err) + } + if err = tpl.Execute(&buf, struct { + Meta astutils.InterfaceMeta + Config GenHttp2GrpcConfig + Version string + }{ + Meta: meta, + Config: config, + Version: version.Release, + }); err != nil { + panic(err) + } + original, err := ioutil.ReadAll(f) + if err != nil { + panic(err) + } + original = append(original, buf.Bytes()...) + if tpl, err = template.New(importHttp2GrpcTmpl).Parse(importHttp2GrpcTmpl); err != nil { + panic(err) + } + transGrpcPkg := astutils.GetPkgPath(filepath.Join(dir, "transport", "grpc")) + if err = tpl.Execute(&importBuf, struct { + TransportGrpcPackage string + }{ + TransportGrpcPackage: transGrpcPkg, + }); err != nil { + panic(err) + } + original = astutils.AppendImportStatements(original, importBuf.Bytes()) + astutils.FixImport(original, handlerimplfile) +} diff --git a/cmd/internal/svc/codegen/httphandlerimpl.go b/cmd/internal/svc/codegen/httphandlerimpl.go index ef00b2b4..66bfacd6 100644 --- a/cmd/internal/svc/codegen/httphandlerimpl.go +++ b/cmd/internal/svc/codegen/httphandlerimpl.go @@ -443,7 +443,7 @@ func GenHttpHandlerImpl(dir string, ic astutils.InterfaceCollector, config GenHt if err != nil { panic(err) } - unimplementedMethods(&meta, httpDir) + unimplementedMethods(&meta, httpDir, meta.Name+"HandlerImpl") if fi != nil { logrus.Warningln("New content will be append to handlerimpl.go file") if f, err = os.OpenFile(handlerimplfile, os.O_APPEND, os.ModePerm); err != nil { @@ -517,7 +517,7 @@ func GenHttpHandlerImpl(dir string, ic astutils.InterfaceCollector, config GenHt }{ ServicePackage: servicePkg, ServiceAlias: ic.Package.Name, - DtoPackage: dtoPkg, + DtoPackage: dtoPkg, }); err != nil { panic(err) } @@ -525,10 +525,10 @@ func GenHttpHandlerImpl(dir string, ic astutils.InterfaceCollector, config GenHt astutils.FixImport(original, handlerimplfile) } -func unimplementedMethods(meta *astutils.InterfaceMeta, httpDir string) { +func unimplementedMethods(meta *astutils.InterfaceMeta, httpDir string, structName string) { sc := astutils.NewStructCollector(astutils.ExprString) astutils.CollectStructsInFolder(httpDir, sc) - if handlers, exists := sc.Methods[meta.Name+"HandlerImpl"]; exists { + if handlers, exists := sc.Methods[structName]; exists { var notimplemented []astutils.MethodMeta for _, item := range meta.Methods { for _, handler := range handlers { diff --git a/cmd/internal/svc/codegen/httphandlerimpl_test.go b/cmd/internal/svc/codegen/httphandlerimpl_test.go index 18efa767..0527ef97 100644 --- a/cmd/internal/svc/codegen/httphandlerimpl_test.go +++ b/cmd/internal/svc/codegen/httphandlerimpl_test.go @@ -12,6 +12,6 @@ func Test_unimplementedMethods(t *testing.T) { ic := astutils.BuildInterfaceCollector(filepath.Join(testDir, "svc.go"), astutils.ExprString) var meta astutils.InterfaceMeta _ = copier.DeepCopy(ic.Interfaces[0], &meta) - unimplementedMethods(&meta, filepath.Join(testDir, "transport/httpsrv")) + unimplementedMethods(&meta, filepath.Join(testDir, "transport/httpsrv"), meta.Name+"HandlerImpl") fmt.Println(len(meta.Methods)) } diff --git a/cmd/internal/svc/svc.go b/cmd/internal/svc/svc.go index 140fef34..7d0070e1 100644 --- a/cmd/internal/svc/svc.go +++ b/cmd/internal/svc/svc.go @@ -93,6 +93,8 @@ type Svc struct { JsonCase string CaseConverter func(string) string + + http2grpc bool } type DbConfig struct { @@ -126,7 +128,6 @@ func (receiver *Svc) SetRunner(runner executils.Runner) { // from the result of ast parsing svc.go file in the project root. It may panic if validation failed func (receiver *Svc) Http() { dir := receiver.dir - parser.ParseDto(dir, "vo") parser.ParseDto(dir, "dto") validate.DataType(dir) @@ -207,6 +208,18 @@ func (receiver *Svc) Init() { type SvcOption func(svc *Svc) +func WithHttp2Grpc(http2grpc bool) SvcOption { + return func(svc *Svc) { + svc.http2grpc = http2grpc + } +} + +func WithAllowGetWithReqBody(allowGetWithReqBody bool) SvcOption { + return func(svc *Svc) { + svc.AllowGetWithReqBody = allowGetWithReqBody + } +} + func WithRunner(runner executils.Runner) SvcOption { return func(svc *Svc) { svc.runner = runner @@ -500,9 +513,8 @@ func (receiver *Svc) Grpc() { dir := receiver.dir validate.DataType(dir) ic := astutils.BuildInterfaceCollector(filepath.Join(dir, "svc.go"), astutils.ExprString) - validate.RestApi(dir, ic) + validate.GrpcApi(dir, ic, receiver.http2grpc) codegen.GenConfig(dir) - parser.ParseDtoGrpc(dir, receiver.protoGenerator, "vo") parser.ParseDtoGrpc(dir, receiver.protoGenerator, "dto") grpcSvc, protoFile := codegen.GenGrpcProto(dir, ic, receiver.protoGenerator) // protoc --proto_path=. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative transport/grpc/helloworld.proto @@ -515,7 +527,19 @@ func (receiver *Svc) Grpc() { panic(err) } codegen.GenSvcImplGrpc(dir, ic, grpcSvc) - codegen.GenMainGrpc(dir, ic, grpcSvc) + if receiver.http2grpc { + codegen.GenHttpHandler(dir, ic, receiver.RoutePatternStrategy) + codegen.GenHttp2Grpc(dir, ic, codegen.GenHttp2GrpcConfig{ + AllowGetWithReqBody: receiver.AllowGetWithReqBody, + }) + codegen.GenMainGrpcHttp(dir, ic, grpcSvc) + parser.GenDoc(dir, ic, parser.GenDocConfig{ + RoutePatternStrategy: receiver.RoutePatternStrategy, + AllowGetWithReqBody: receiver.AllowGetWithReqBody, + }) + } else { + codegen.GenMainGrpc(dir, ic, grpcSvc) + } codegen.FixModGrpc(dir) codegen.GenMethodAnnotationStore(dir, ic) runner := receiver.runner diff --git a/cmd/internal/svc/validate/validate.go b/cmd/internal/svc/validate/validate.go index 47ff89ef..efe10d6e 100644 --- a/cmd/internal/svc/validate/validate.go +++ b/cmd/internal/svc/validate/validate.go @@ -6,6 +6,7 @@ import ( "github.com/unionj-cloud/go-doudou/v2/cmd/internal/svc/parser" "github.com/unionj-cloud/go-doudou/v2/toolkit/astutils" v3helper "github.com/unionj-cloud/go-doudou/v2/toolkit/openapi/v3" + "github.com/unionj-cloud/go-doudou/v2/toolkit/sliceutils" "os" "path/filepath" "regexp" @@ -14,14 +15,7 @@ import ( func DataType(dir string) { astutils.BuildInterfaceCollector(filepath.Join(dir, "svc.go"), parser.ExprStringP) - vodir := filepath.Join(dir, "vo") var files []string - if _, err := os.Stat(vodir); !os.IsNotExist(err) { - _ = filepath.Walk(vodir, astutils.Visit(&files)) - for _, file := range files { - astutils.BuildStructCollector(file, parser.ExprStringP) - } - } dtodir := filepath.Join(dir, "dto") if _, err := os.Stat(dtodir); !os.IsNotExist(err) { files = nil @@ -43,7 +37,6 @@ func RestApi(dir string, ic astutils.InterfaceCollector) { panic(errors.New("no service interface found")) } if len(v3helper.SchemaNames) == 0 && len(v3helper.Enums) == 0 { - parser.ParseDto(dir, "vo") parser.ParseDto(dir, "dto") } svcInter := ic.Interfaces[0] @@ -61,6 +54,77 @@ func RestApi(dir string, ic astutils.InterfaceCollector) { } } +func GrpcApi(dir string, ic astutils.InterfaceCollector, http2grpc bool) { + if len(ic.Interfaces) == 0 { + panic(errors.New("no service interface found")) + } + if len(v3helper.SchemaNames) == 0 && len(v3helper.Enums) == 0 { + parser.ParseDto(dir, "dto") + } + svcInter := ic.Interfaces[0] + re := regexp.MustCompile(`anonystruct«(.*)»`) + for _, method := range svcInter.Methods { + if http2grpc { + pass := checkParams(method.Params) + if !pass { + panic("Only support pass one context.Context and at most one struct from dto package as parameters. A context.Context is required.") + } + pass = checkResults(method.Results) + if !pass { + panic("Only support pass one struct from dto package and one error as results. An error is required.") + } + } else { + nonBasicTypes := getNonBasicTypes(method.Params) + if len(nonBasicTypes) > 1 { + panic(fmt.Sprintf("Too many golang non-builtin type parameters in method %s, can't decide which one should be put into request body!", method)) + } + for _, param := range method.Results { + if re.MatchString(param.Type) { + panic("not support anonymous struct as parameter") + } + } + } + } +} + +func checkResults(params []astutils.FieldMeta) bool { + pass := true + var passedParams []string + for _, param := range params { + if param.Type == "error" || strings.HasPrefix(strings.TrimLeft(param.Type, "*"), "dto.") { + passedParams = append(passedParams, param.Type) + continue + } + return false + } + if len(passedParams) > 2 { + return false + } + if !sliceutils.StringContains(passedParams, "error") { + return false + } + return pass +} + +func checkParams(params []astutils.FieldMeta) bool { + pass := true + var passedParams []string + for _, param := range params { + if param.Type == "context.Context" || strings.HasPrefix(strings.TrimLeft(param.Type, "*"), "dto.") { + passedParams = append(passedParams, param.Type) + continue + } + return false + } + if len(passedParams) > 2 { + return false + } + if !sliceutils.StringContains(passedParams, "context.Context") { + return false + } + return pass +} + func getNonBasicTypes(params []astutils.FieldMeta) []string { var nonBasicTypes []string cpmap := make(map[string]int) diff --git a/framework/rest/bizerror.go b/framework/rest/bizerror.go index 329c245f..68802e7e 100644 --- a/framework/rest/bizerror.go +++ b/framework/rest/bizerror.go @@ -69,3 +69,17 @@ func HandleBadRequestErr(err error) { func HandleInternalServerError(err error) { panic(NewBizError(err)) } + +func PanicBadRequestErr(err error) { + if err == nil { + return + } + panic(NewBizError(err, WithStatusCode(http.StatusBadRequest))) +} + +func PanicInternalServerError(err error) { + if err == nil { + return + } + panic(NewBizError(err)) +} diff --git a/framework/rest/form.go b/framework/rest/form.go index 379bbea0..2a5f1500 100644 --- a/framework/rest/form.go +++ b/framework/rest/form.go @@ -3,17 +3,29 @@ package rest import ( "github.com/go-playground/form/v4" "net/url" + "reflect" + "strings" ) var decoder = form.NewDecoder() var encoder = form.NewEncoder() +func tagNameFunc(fld reflect.StructField) string { + name := fld.Tag.Get("json") + if commaIndex := strings.Index(name, ","); commaIndex != -1 { + name = name[:commaIndex] + } + return name +} + func init() { // frontend axios.js use [] by default decoder.SetNamespacePrefix("[") decoder.SetNamespaceSuffix("]") + decoder.RegisterTagNameFunc(tagNameFunc) encoder.SetNamespacePrefix("[") encoder.SetNamespaceSuffix("]") + encoder.RegisterTagNameFunc(tagNameFunc) } func GetFormDecoder() *form.Decoder { diff --git a/version/version.go b/version/version.go index 453a1931..ad0e362b 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,3 @@ package version -const Release = "v2.1.8" +const Release = "v2.1.9"