From 4064d4c9f954509fcd2c7e19e276eb7a6b17251f Mon Sep 17 00:00:00 2001 From: hushengquan <45221305+hushengquan@users.noreply.github.com> Date: Mon, 19 Sep 2022 11:34:10 +0800 Subject: [PATCH 01/11] fix: fix some tsr bugs (#232) Co-authored-by: hushengquan --- pkg/route/tree.go | 7 ++++--- pkg/route/tree_test.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/route/tree.go b/pkg/route/tree.go index 774c8a3b6..4c2d9de86 100644 --- a/pkg/route/tree.go +++ b/pkg/route/tree.go @@ -361,7 +361,8 @@ func (r *router) find(path string, paramsPointer *param.Params, unescape bool) ( searchIndex = searchIndex + len(cn.prefix) } else { // not equal - if (len(cn.prefix) == len(search)+1) && (cn.prefix[len(search)]) == '/' && (cn.handlers != nil || cn.anyChild != nil) { + if (len(cn.prefix) == len(search)+1) && + (cn.prefix[len(search)]) == '/' && cn.prefix[:len(search)] == search && (cn.handlers != nil || cn.anyChild != nil) { res.tsr = true } // No matching prefix, let's backtrack to the first possible alternative node of the decision path @@ -394,7 +395,7 @@ func (r *router) find(path string, paramsPointer *param.Params, unescape bool) ( } if search == nilString { - if cd := cn.findChild('/'); cd != nil && cd.handlers != nil { + if cd := cn.findChild('/'); cd != nil && (cd.handlers != nil || cd.anyChild != nil) { res.tsr = true } } @@ -419,7 +420,7 @@ func (r *router) find(path string, paramsPointer *param.Params, unescape bool) ( search = search[i:] searchIndex = searchIndex + i if search == nilString { - if cd := cn.findChild('/'); cd != nil && cd.handlers != nil { + if cd := cn.findChild('/'); cd != nil && (cd.handlers != nil || cd.anyChild != nil) { res.tsr = true } } diff --git a/pkg/route/tree_test.go b/pkg/route/tree_test.go index cdb190bef..946bfd626 100644 --- a/pkg/route/tree_test.go +++ b/pkg/route/tree_test.go @@ -471,6 +471,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/no/a", "/no/b", "/api/hello/:name", + "/user/:name/*id", + "/resource", + "/r/*id", + "/book/biz/:name", + "/book/biz/abc", + "/book/biz/abc/bar", + "/book/:page/:name", + "/book/hello/:name/biz/", } for _, route := range routes { recv := catchPanic(func() { @@ -496,6 +504,11 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/admin/config/", "/admin/config/permissions/", "/doc/", + "/user/name", + "/r", + "/book/hello/a/biz", + "/book/biz/foo/", + "/book/biz/abc/bar/", } v := make(param.Params, 0, 10) for _, route := range tsrRoutes { @@ -514,6 +527,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/_", "/_/", "/api/world/abc", + "/book", + "/book/", + "/book/hello/a/abc", + "/book/biz/abc/biz", } for _, route := range noTsrRoutes { value := tree.find(route, &v, false) From dc570bac8a3dc72d6993b715806b59afd8d8f5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B3=BD?= <45022150+BaiZe1998@users.noreply.github.com> Date: Mon, 19 Sep 2022 14:53:23 +0800 Subject: [PATCH 02/11] feat: add a startup log to display the name of the loaded network library (#251) --- pkg/route/engine.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/route/engine.go b/pkg/route/engine.go index 06f1f6218..a3e1baa31 100644 --- a/pkg/route/engine.go +++ b/pkg/route/engine.go @@ -340,6 +340,7 @@ func (engine *Engine) alpnEnable() bool { } func (engine *Engine) listenAndServe() error { + hlog.Infof("HERTZ: Using network library=%s", GetTransporterName()) return engine.transport.ListenAndServe(engine.onData) } From 8e8baf374dee982dd9ff0efa750018734e6dd36a Mon Sep 17 00:00:00 2001 From: Xuran <37136584+Duslia@users.noreply.github.com> Date: Mon, 19 Sep 2022 16:05:14 +0800 Subject: [PATCH 03/11] refactor: client init m and ms logic (#238) --- pkg/app/client/client.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pkg/app/client/client.go b/pkg/app/client/client.go index 9800638fa..dccfede12 100644 --- a/pkg/app/client/client.go +++ b/pkg/app/client/client.go @@ -458,20 +458,12 @@ func (c *Client) do(ctx context.Context, req *protocol.Request, resp *protocol.R startCleaner := false c.mLock.Lock() + m := c.m if isTLS { m = c.ms } - if m == nil { - m = make(map[string]client.HostClient) - if isTLS { - c.ms = m - } else { - c.m = m - } - } - h := string(host) hc := m[h] if hc == nil { @@ -548,6 +540,8 @@ func NewClient(opts ...config.ClientOption) (*Client, error) { } c := &Client{ options: opt, + m: make(map[string]client.HostClient), + ms: make(map[string]client.HostClient), } return c, nil From 0bebcc5e07385f06d646331a967b85be433ab688 Mon Sep 17 00:00:00 2001 From: Wenju Gao Date: Mon, 19 Sep 2022 17:08:00 +0800 Subject: [PATCH 04/11] optimize: add a warning log for illegal status code (#226) --- pkg/protocol/header.go | 11 +++++++++++ pkg/protocol/header_test.go | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/pkg/protocol/header.go b/pkg/protocol/header.go index 0babea54c..bb2e0338b 100644 --- a/pkg/protocol/header.go +++ b/pkg/protocol/header.go @@ -52,6 +52,7 @@ import ( "github.com/cloudwego/hertz/internal/bytestr" "github.com/cloudwego/hertz/internal/nocopy" errs "github.com/cloudwego/hertz/pkg/common/errors" + "github.com/cloudwego/hertz/pkg/common/hlog" "github.com/cloudwego/hertz/pkg/common/utils" "github.com/cloudwego/hertz/pkg/protocol/consts" ) @@ -447,9 +448,19 @@ func (h *RequestHeader) SetHost(host string) { // SetStatusCode sets response status code. func (h *ResponseHeader) SetStatusCode(statusCode int) { + checkWriteHeaderCode(statusCode) h.statusCode = statusCode } +func checkWriteHeaderCode(code int) { + // For now, we only emit a warning for bad codes. + // In the future we might block things over 599 or under 100 + if code < 100 || code > 599 { + hlog.Warnf("Invalid StatusCode code %v, status code should not be under 100 or over 599.\n"+ + "For more info: https://www.rfc-editor.org/rfc/rfc9110.html#name-status-codes", code) + } +} + func (h *ResponseHeader) ResetSkipNormalize() { h.noHTTP11 = false h.connectionClose = false diff --git a/pkg/protocol/header_test.go b/pkg/protocol/header_test.go index 301de8aa1..9a75b3d3e 100644 --- a/pkg/protocol/header_test.go +++ b/pkg/protocol/header_test.go @@ -42,13 +42,14 @@ package protocol import ( + "bytes" "fmt" "strings" "testing" - "github.com/cloudwego/hertz/pkg/common/test/assert" - "github.com/cloudwego/hertz/internal/bytestr" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/common/test/assert" "github.com/cloudwego/hertz/pkg/protocol/consts" ) @@ -193,6 +194,22 @@ func TestResponseHeaderDelClientCookie(t *testing.T) { ReleaseCookie(c) } +func TestCheckWriteHeaderCode(t *testing.T) { + buffer := bytes.NewBuffer(make([]byte, 0, 1024)) + hlog.SetOutput(buffer) + checkWriteHeaderCode(99) + assert.True(t, strings.Contains(buffer.String(), "[Warn] Invalid StatusCode code")) + buffer.Reset() + checkWriteHeaderCode(600) + assert.True(t, strings.Contains(buffer.String(), "[Warn] Invalid StatusCode code")) + buffer.Reset() + checkWriteHeaderCode(100) + assert.False(t, strings.Contains(buffer.String(), "[Warn] Invalid StatusCode code")) + buffer.Reset() + checkWriteHeaderCode(599) + assert.False(t, strings.Contains(buffer.String(), "[Warn] Invalid StatusCode code")) +} + func TestResponseHeaderAdd(t *testing.T) { t.Parallel() From dd2cc8d12861373debb7a1f5a97994f059cf0155 Mon Sep 17 00:00:00 2001 From: kinggo Date: Mon, 19 Sep 2022 19:13:33 +0800 Subject: [PATCH 05/11] fix: add channel signal judge to allow onShutdownHook to complete or timeout (#249) --- pkg/app/server/hertz_test.go | 2 +- pkg/route/engine.go | 35 +++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pkg/app/server/hertz_test.go b/pkg/app/server/hertz_test.go index 0d2790164..de6b359d5 100644 --- a/pkg/app/server/hertz_test.go +++ b/pkg/app/server/hertz_test.go @@ -94,6 +94,7 @@ func TestHertz_GracefulShutdown(t *testing.T) { atomic.StoreUint32(&testint2, 2) }) engine.Engine.OnShutdown = append(engine.OnShutdown, func(ctx context.Context) { + time.Sleep(2 * time.Second) atomic.StoreUint32(&testint3, 3) }) @@ -196,7 +197,6 @@ func TestHertz_Spin(t *testing.T) { t.Fatal(err) } t.Logf("[%v]end SIGHUP\n", time.Now()) - <-ch assert.Nil(t, err) assert.NotNil(t, resp) diff --git a/pkg/route/engine.go b/pkg/route/engine.go index a3e1baa31..52849d434 100644 --- a/pkg/route/engine.go +++ b/pkg/route/engine.go @@ -251,7 +251,7 @@ func (engine *Engine) NewContext() *app.RequestContext { // Shutdown starts the server's graceful exit by next steps: // -// 1. Trigger OnShutdown hooks concurrently, but don't wait them +// 1. Trigger OnShutdown hooks concurrently and wait them until wait timeout or finish // 2. Close the net listener, which means new connection won't be accepted // 3. Wait all connections get closed: // One connection gets closed after reaching out the shorter time of processing @@ -265,12 +265,21 @@ func (engine *Engine) Shutdown(ctx context.Context) (err error) { return } + ch := make(chan struct{}) // trigger hooks if any - for i := range engine.OnShutdown { - go func(index int) { - engine.OnShutdown[index](ctx) - }(i) - } + go engine.executeOnShutdownHooks(ctx, ch) + + defer func() { + // ensure that the hook is executed until wait timeout or finish + select { + case <-ctx.Done(): + hlog.Infof("HERTZ: Execute OnShutdownHooks timeout: error=%v", ctx.Err()) + return + case <-ch: + hlog.Info("HERTZ: Execute OnShutdownHooks finish") + return + } + }() if opt := engine.options; opt != nil && opt.Registry != nil { if err = opt.Registry.Deregister(opt.RegistryInfo); err != nil { @@ -283,9 +292,23 @@ func (engine *Engine) Shutdown(ctx context.Context) (err error) { if err := engine.transport.Shutdown(ctx); err != ctx.Err() { return err } + return } +func (engine *Engine) executeOnShutdownHooks(ctx context.Context, ch chan struct{}) { + wg := sync.WaitGroup{} + for i := range engine.OnShutdown { + wg.Add(1) + go func(index int) { + defer wg.Done() + engine.OnShutdown[index](ctx) + }(i) + } + wg.Wait() + ch <- struct{}{} +} + func (engine *Engine) Run() (err error) { if err = engine.Init(); err != nil { return err From 0b0c9981262a79cc4e3c130ae9fe878f4248785c Mon Sep 17 00:00:00 2001 From: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> Date: Tue, 20 Sep 2022 11:10:17 +0800 Subject: [PATCH 06/11] feat: get dialer name (#198) --- pkg/app/client/client.go | 26 +++++++++++++++++++ pkg/app/client/client_test.go | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/pkg/app/client/client.go b/pkg/app/client/client.go index dccfede12..92514ae1f 100644 --- a/pkg/app/client/client.go +++ b/pkg/app/client/client.go @@ -45,6 +45,8 @@ import ( "bytes" "context" "fmt" + "reflect" + "strings" "sync" "time" @@ -532,6 +534,30 @@ func (c *Client) SetClientFactory(cf suite.ClientFactory) { c.clientFactory = cf } +// GetDialerName returns the name of the dialer +func (c *Client) GetDialerName() (dName string, err error) { + defer func() { + err := recover() + if err != nil { + dName = "unknown" + } + }() + + opt := c.GetOptions() + if opt == nil || opt.Dialer == nil { + return "", fmt.Errorf("abnormal process: there is no client options or dialer") + } + + dName = reflect.TypeOf(opt.Dialer).String() + dSlice := strings.Split(dName, ".") + dName = dSlice[0] + if dName[0] == '*' { + dName = dName[1:] + } + + return +} + // NewClient return a client with options func NewClient(opts ...config.ClientOption) (*Client, error) { opt := config.NewClientOptions(opts) diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go index 20c25e762..bd5464ca7 100644 --- a/pkg/app/client/client_test.go +++ b/pkg/app/client/client_test.go @@ -1879,3 +1879,52 @@ func newMockDialerWithCustomFunc(network, address string, timeout time.Duration, timeout: timeout, } } + +func TestClientDialerName(t *testing.T) { + client, _ := NewClient() + dName, err := client.GetDialerName() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Depending on the operating system, + // the default dialer has a different network library, either "netpoll" or "standard" + if !(dName == "netpoll" || dName == "standard") { + t.Errorf("expected 'netpoll', but get %s", dName) + } + + client, _ = NewClient(WithDialer(netpoll.NewDialer())) + dName, err = client.GetDialerName() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dName != "netpoll" { + t.Errorf("expected 'standard', but get %s", dName) + } + + client, _ = NewClient(WithDialer(standard.NewDialer())) + dName, err = client.GetDialerName() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dName != "standard" { + t.Errorf("expected 'standard', but get %s", dName) + } + + client, _ = NewClient(WithDialer(&mockDialer{})) + dName, err = client.GetDialerName() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dName != "client" { + t.Errorf("expected 'client', but get %s", dName) + } + + client.options.Dialer = nil + dName, err = client.GetDialerName() + if err == nil { + t.Errorf("expected an err for abnormal process") + } + if dName != "" { + t.Errorf("expected 'empty string', but get %s", dName) + } +} From 02f9247392f1ed4ac99887ddda8f43b97deccd4e Mon Sep 17 00:00:00 2001 From: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> Date: Tue, 20 Sep 2022 14:55:25 +0800 Subject: [PATCH 07/11] feat: tidy the generated go tag (#179) --- cmd/hz/internal/config/cmd.go | 29 +++- cmd/hz/internal/protobuf/tag_test.go | 153 ++++++++++++++++++ cmd/hz/internal/protobuf/tags.go | 34 +++- .../protobuf/test_data/protobuf_tag_test.out | Bin 0 -> 56955 bytes .../protobuf/test_data/test_tag.proto | 32 ++++ cmd/hz/internal/thrift/plugin.go | 21 +-- cmd/hz/internal/thrift/tag_test.go | 129 +++++++++++++++ cmd/hz/internal/thrift/tags.go | 45 +++++- .../internal/thrift/test_data/test_tag.thrift | 26 +++ .../thrift/test_data/thrift_tag_test.out | Bin 0 -> 3017 bytes cmd/hz/internal/util/tool_install.go | 151 +++++++++++++++++ cmd/hz/internal/util/tool_install_test.go | 36 +++++ 12 files changed, 622 insertions(+), 34 deletions(-) create mode 100644 cmd/hz/internal/protobuf/tag_test.go create mode 100755 cmd/hz/internal/protobuf/test_data/protobuf_tag_test.out create mode 100644 cmd/hz/internal/protobuf/test_data/test_tag.proto create mode 100644 cmd/hz/internal/thrift/tag_test.go create mode 100644 cmd/hz/internal/thrift/test_data/test_tag.thrift create mode 100755 cmd/hz/internal/thrift/test_data/thrift_tag_test.out create mode 100644 cmd/hz/internal/util/tool_install.go create mode 100644 cmd/hz/internal/util/tool_install_test.go diff --git a/cmd/hz/internal/config/cmd.go b/cmd/hz/internal/config/cmd.go index 1c1623137..3c93e65d4 100644 --- a/cmd/hz/internal/config/cmd.go +++ b/cmd/hz/internal/config/cmd.go @@ -38,20 +38,37 @@ func lookupTool(idlType string) (string, error) { path, err := exec.LookPath(tool) logs.Debugf("[DEBUG]path:%v", path) if err != nil { - logs.Warnf("Failed to find %q from $PATH: %s. Try $GOPATH/bin/%s instead\n", path, err.Error(), tool) - p, err := exec.LookPath(tool) + goPath, err := util.GetGOPATH() if err != nil { - return "", fmt.Errorf("failed to find %q from $PATH or $GOPATH/bin: %s", tool, err) + return "", fmt.Errorf("get 'GOPATH' failed for find %s : %v", tool, path) } - path = filepath.Join(p, "bin", tool) + path = filepath.Join(goPath, "bin", tool) } isExist, err := util.PathExist(path) if err != nil { + return "", fmt.Errorf("check '%s' path error: %v", path, err) } if !isExist { - return "", fmt.Errorf("%s is not installed, please install it first", tool) + if tool == meta.TpCompilerThrift { + // If thriftgo does not exist, the latest version will be installed automatically. + err := util.InstallAndCheckThriftgo() + if err != nil { + return "", fmt.Errorf("can't install '%s' automatically, please install it manually for https://github.com/cloudwego/thriftgo, err : %v", tool, err) + } + } else { + // todo: protoc automatic installation + return "", fmt.Errorf("%s is not installed, please install it first", tool) + } + } + + if tool == meta.TpCompilerThrift { + // If thriftgo exists, the version is detected; if the version is lower than v0.2.0 then the latest version of thriftgo is automatically installed. + err := util.CheckAndUpdateThriftgo() + if err != nil { + return "", fmt.Errorf("update thriftgo version failed, please install it manually for https://github.com/cloudwego/thriftgo, err: %v", err) + } } exe, err := os.Executable() @@ -183,6 +200,6 @@ func (arg *Argument) GetThriftgoOptions() (string, error) { if arg.JSONEnumStr { arg.ThriftOptions = append(arg.ThriftOptions, "json_enum_as_text") } - gas := "go:" + strings.Join(arg.ThriftOptions, ",") + ",reserve_comments" + gas := "go:" + strings.Join(arg.ThriftOptions, ",") + ",reserve_comments,gen_json_tag=false" return gas, nil } diff --git a/cmd/hz/internal/protobuf/tag_test.go b/cmd/hz/internal/protobuf/tag_test.go new file mode 100644 index 000000000..7e6528b16 --- /dev/null +++ b/cmd/hz/internal/protobuf/tag_test.go @@ -0,0 +1,153 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package protobuf + +import ( + "io/ioutil" + "strings" + "testing" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/pluginpb" +) + +func TestTagGenerate(t *testing.T) { + type TagStruct struct { + Annotation string + GeneratedTag string + ActualTag string + } + + tagList := []TagStruct{ + { + Annotation: "query", + GeneratedTag: "protobuf:\"bytes,1,opt,name=QueryTag\" json:\"QueryTag,omitempty\" query:\"query\"", + }, + { + Annotation: "raw_body", + GeneratedTag: "protobuf:\"bytes,2,opt,name=RawBodyTag\" json:\"RawBodyTag,omitempty\" raw_body:\"raw_body\"", + }, + { + Annotation: "path", + GeneratedTag: "protobuf:\"bytes,3,opt,name=PathTag\" json:\"PathTag,omitempty\" path:\"path\"", + }, + { + Annotation: "form", + GeneratedTag: "protobuf:\"bytes,4,opt,name=FormTag\" json:\"FormTag,omitempty\" form:\"form\"", + }, + { + Annotation: "cookie", + GeneratedTag: "protobuf:\"bytes,5,opt,name=CookieTag\" json:\"CookieTag,omitempty\" cookie:\"cookie\"", + }, + { + Annotation: "header", + GeneratedTag: "protobuf:\"bytes,6,opt,name=HeaderTag\" json:\"HeaderTag,omitempty\" header:\"header\"", + }, + { + Annotation: "body", + GeneratedTag: "bytes,7,opt,name=BodyTag\" form:\"body\" json:\"body,omitempty\"", + }, + { + Annotation: "go.tag", + GeneratedTag: "bytes,8,opt,name=GoTag\" json:\"json\" form:\"form\" goTag:\"tag\" header:\"header\" query:\"query\"", + }, + { + Annotation: "vd", + GeneratedTag: "bytes,9,opt,name=VdTag\" json:\"VdTag,omitempty\" form:\"VdTag\" query:\"VdTag\" vd:\"$!='?'\"", + }, + { + Annotation: "non", + GeneratedTag: "bytes,10,opt,name=DefaultTag\" json:\"DefaultTag,omitempty\" form:\"DefaultTag\" query:\"DefaultTag\"", + }, + { + Annotation: "query required", + GeneratedTag: "bytes,11,req,name=ReqQuery\" json:\"ReqQuery,required\" query:\"query,required\"", + }, + { + Annotation: "query optional", + GeneratedTag: "bytes,12,opt,name=OptQuery\" json:\"OptQuery,omitempty\" query:\"query\"", + }, + { + Annotation: "body required", + GeneratedTag: "protobuf:\"bytes,13,req,name=ReqBody\" form:\"body,required\" json:\"body,required\"", + }, + { + Annotation: "body optional", + GeneratedTag: "protobuf:\"bytes,14,opt,name=OptBody\" form:\"body\" json:\"body,omitempty\"", + }, + { + Annotation: "go.tag required", + GeneratedTag: "protobuf:\"bytes,15,req,name=ReqGoTag\" query:\"ReqGoTag,required\" form:\"ReqGoTag,required\" json:\"json\"", + }, + { + Annotation: "go.tag optional", + GeneratedTag: "bytes,16,opt,name=OptGoTag\" query:\"OptGoTag\" form:\"OptGoTag\" json:\"json\"", + }, + { + Annotation: "go tag cover query", + GeneratedTag: "bytes,17,req,name=QueryGoTag\" json:\"QueryGoTag,required\" query:\"queryTag\"", + }, + } + + in, err := ioutil.ReadFile("./test_data/protobuf_tag_test.out") + if err != nil { + t.Fatal(err) + } + + req := &pluginpb.CodeGeneratorRequest{} + err = proto.Unmarshal(in, req) + if err != nil { + t.Fatalf("unmarshal stdin request error: %v", err) + } + + opts := protogen.Options{} + gen, err := opts.New(req) + + for _, f := range gen.Files { + if f.Proto.GetName() == "test_tag.proto" { + fileInfo := newFileInfo(f) + for _, message := range fileInfo.allMessages { + for idx, field := range message.Fields { + tags := structTags{ + {"protobuf", fieldProtobufTagValue(field)}, + } + err = injectTagsToStructTags(field.Desc, &tags, true) + if err != nil { + t.Fatal(err) + } + var actualTag string + for i, tag := range tags { + if i == 0 { + actualTag = tag[0] + ":" + "\"" + tag[1] + "\"" + } else { + actualTag = actualTag + " " + tag[0] + ":" + "\"" + tag[1] + "\"" + } + } + tagList[idx].ActualTag = actualTag + + } + } + } + } + + for i := range tagList { + if !strings.Contains(tagList[i].ActualTag, tagList[i].GeneratedTag) { + t.Fatalf("expected tag: '%s', but autual tag: '%s'", tagList[i].GeneratedTag, tagList[i].ActualTag) + } + } +} diff --git a/cmd/hz/internal/protobuf/tags.go b/cmd/hz/internal/protobuf/tags.go index 068c23b43..c2b2b48e8 100644 --- a/cmd/hz/internal/protobuf/tags.go +++ b/cmd/hz/internal/protobuf/tags.go @@ -74,12 +74,13 @@ var ( } BindingTags = map[*protoimpl.ExtensionInfo]string{ - api.E_Path: "path", - api.E_Query: "query", + api.E_Path: "path", + api.E_Query: "query", + api.E_Header: "header", + api.E_Cookie: "cookie", + api.E_Body: "json", + // Do not change the relative order of "api.E_Form" and "api.E_Body", so that "api.E_Form" can overwrite the form tag generated by "api.E_Body" api.E_Form: "form", - api.E_Header: "header", - api.E_Cookie: "cookie", - api.E_Body: "json", api.E_RawBody: "raw_body", } @@ -272,6 +273,22 @@ func reflectJsonTag(f protoreflect.FieldDescriptor) (ret model.Tag) { func defaultBindingStructTags(f protoreflect.FieldDescriptor) []model.Tag { opts := f.Options() out := make([]model.Tag, 3) + bindingTags := []*protoimpl.ExtensionInfo{ + api.E_Path, + api.E_Query, + api.E_Form, + api.E_Header, + api.E_Cookie, + api.E_Body, + api.E_RawBody, + } + for _, tag := range bindingTags { + if vv := checkFirstOption(tag, opts); vv != nil { + out[0] = reflectJsonTag(f) + return out[:1] + } + } + if v := checkFirstOption(api.E_Body, opts); v != nil { val := getStructJsonValue(f, v.(string)) out[0] = tag("json", val) @@ -306,8 +323,12 @@ func injectTagsToStructTags(f protoreflect.FieldDescriptor, out *structTags, nee for k, v := range BindingTags { if vv := checkFirstOption(k, as); vv != nil { tags.Remove(v) + // body annotation will generate "json" & "form" tag for protobuf if v == "json" { + formVal := vv vv = getStructJsonValue(f, vv.(string)) + formVal = checkStructRequire(f, formVal.(string)) + tags = append(tags, tag("form", formVal)) } else { vv = checkStructRequire(f, vv.(string)) } @@ -342,7 +363,8 @@ func injectTagsToStructTags(f protoreflect.FieldDescriptor, out *structTags, nee } } - sort.Sort(tags) + // protobuf tag as first + sort.Sort(tags[1:]) for _, t := range tags { *out = append(*out, m2s(t)) } diff --git a/cmd/hz/internal/protobuf/test_data/protobuf_tag_test.out b/cmd/hz/internal/protobuf/test_data/protobuf_tag_test.out new file mode 100755 index 0000000000000000000000000000000000000000..0a4dff16645cd785f6190f92ae0f3386ed735eca GIT binary patch literal 56955 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 literal 0 HcmV?d00001 diff --git a/cmd/hz/internal/protobuf/test_data/test_tag.proto b/cmd/hz/internal/protobuf/test_data/test_tag.proto new file mode 100644 index 000000000..96cf90694 --- /dev/null +++ b/cmd/hz/internal/protobuf/test_data/test_tag.proto @@ -0,0 +1,32 @@ +syntax = "proto2"; + +package test; + +option go_package = "cloudwego.hertz.hz"; + +import "api.proto"; + +message MultiTagReq { + // basic feature + optional string QueryTag = 1 [(api.query)="query"]; + optional string RawBodyTag = 2 [(api.raw_body)="raw_body"]; + optional string PathTag = 3 [(api.path)="path"]; + optional string FormTag = 4 [(api.form)="form"]; + optional string CookieTag = 5 [(api.cookie)="cookie"]; + optional string HeaderTag = 6 [(api.header)="header"]; + optional string BodyTag = 7 [(api.body)="body"]; + optional string GoTag = 8 [(api.go_tag)="json:\"json\" query:\"query\" form:\"form\" header:\"header\" goTag:\"tag\""]; + optional string VdTag = 9 [(api.vd)="$!='?'"]; + optional string DefaultTag = 10; + + // optional / required + required string ReqQuery = 11 [(api.query)="query"]; + optional string OptQuery = 12 [(api.query)="query"]; + required string ReqBody = 13 [(api.body)="body"]; + optional string OptBody = 14 [(api.body)="body"]; + required string ReqGoTag = 15 [(api.go_tag)="json:\"json\""]; + optional string OptGoTag = 16 [(api.go_tag)="json:\"json\""]; + + // gotag cover feature + required string QueryGoTag = 17 [(api.query)="query", (api.go_tag)="query:\"queryTag\""]; +} diff --git a/cmd/hz/internal/thrift/plugin.go b/cmd/hz/internal/thrift/plugin.go index 97c8ffc9d..4ab2c39eb 100644 --- a/cmd/hz/internal/thrift/plugin.go +++ b/cmd/hz/internal/thrift/plugin.go @@ -178,6 +178,7 @@ func (plugin *Plugin) handleRequest() error { return fmt.Errorf("unmarshal request failed: %s", err.Error()) } plugin.req = req + return nil } @@ -285,13 +286,6 @@ func (plugin *Plugin) InsertTag() ([]*thriftgo_plugin.Generated, error) { stName := st.GetName() for _, f := range st.Fields { fieldName := f.GetName() - hasBodyTag := false - for _, t := range f.Annotations { - if t.Key == AnnotationBody { - hasBodyTag = true - break - } - } field := model.Field{} err := injectTags(f, &field, true, false) if err != nil { @@ -300,9 +294,6 @@ func (plugin *Plugin) InsertTag() ([]*thriftgo_plugin.Generated, error) { tags := field.Tags var tagString string for idx, tag := range tags { - if tag.Key == "json" && !hasBodyTag { - continue - } if idx == 0 { tagString += " " + tag.Key + ":\"" + tag.Value + ":\"" + " " } else if idx == len(tags)-1 { @@ -333,13 +324,6 @@ func (plugin *Plugin) InsertTag() ([]*thriftgo_plugin.Generated, error) { stName := st.GetName() for _, f := range st.Fields { fieldName := f.GetName() - hasBodyTag := false - for _, t := range f.Annotations { - if t.Key == AnnotationBody { - hasBodyTag = true - break - } - } field := model.Field{} err := injectTags(f, &field, true, false) if err != nil { @@ -348,9 +332,6 @@ func (plugin *Plugin) InsertTag() ([]*thriftgo_plugin.Generated, error) { tags := field.Tags var tagString string for idx, tag := range tags { - if tag.Key == "json" && !hasBodyTag { - continue - } if idx == 0 { tagString += " " + tag.Key + ":\"" + tag.Value + "\"" + " " } else if idx == len(tags)-1 { diff --git a/cmd/hz/internal/thrift/tag_test.go b/cmd/hz/internal/thrift/tag_test.go new file mode 100644 index 000000000..f9188e547 --- /dev/null +++ b/cmd/hz/internal/thrift/tag_test.go @@ -0,0 +1,129 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package thrift + +import ( + "io/ioutil" + "strings" + "testing" + + "github.com/cloudwego/hertz/cmd/hz/internal/config" + "github.com/cloudwego/thriftgo/plugin" +) + +func TestInsertTag(t *testing.T) { + data, err := ioutil.ReadFile("./test_data/thrift_tag_test.out") + if err != nil { + t.Fatal(err) + } + req, err := plugin.UnmarshalRequest(data) + if err != nil { + t.Fatal(err) + } + + plu := new(Plugin) + plu.req = req + plu.args = new(config.Argument) + + type TagStruct struct { + Annotation string + GeneratedTag string + ActualTag string + } + + tagList := []TagStruct{ + { + Annotation: "query", + GeneratedTag: "json:\"DefaultQueryTag\" query:\"query\"", + }, + { + Annotation: "raw_body", + GeneratedTag: "json:\"RawBodyTag\" raw_body:\"raw_body\"", + }, + { + Annotation: "path", + GeneratedTag: "json:\"PathTag\" path:\"path\"", + }, + { + Annotation: "form", + GeneratedTag: "form:\"form\" json:\"FormTag\"", + }, + { + Annotation: "cookie", + GeneratedTag: "cookie:\"cookie\" json:\"CookieTag\"", + }, + { + Annotation: "header", + GeneratedTag: "header:\"header\" json:\"HeaderTag\"", + }, + { + Annotation: "body", + GeneratedTag: "form:\"body\" json:\"body\"", + }, + { + Annotation: "go.tag", + GeneratedTag: "", + }, + { + Annotation: "vd", + GeneratedTag: "form:\"VdTag\" json:\"VdTag\" query:\"VdTag\" vd:\"$!='?'\"", + }, + { + Annotation: "non", + GeneratedTag: "form:\"DefaultTag\" json:\"DefaultTag\" query:\"DefaultTag\"", + }, + { + Annotation: "query required", + GeneratedTag: "json:\"ReqQuery,required\" query:\"query,required\"", + }, + { + Annotation: "query optional", + GeneratedTag: "json:\"OptQuery,omitempty\" query:\"query\"", + }, + { + Annotation: "body required", + GeneratedTag: "form:\"body,required\" json:\"body,required\"", + }, + { + Annotation: "body optional", + GeneratedTag: "form:\"body\" json:\"body,omitempty\"", + }, + { + Annotation: "go.tag required", + GeneratedTag: "form:\"ReqGoTag,required\" query:\"ReqGoTag,required\"", + }, + { + Annotation: "go.tag optional", + GeneratedTag: "form:\"OptGoTag\" query:\"OptGoTag\"", + }, + { + Annotation: "go tag cover query", + GeneratedTag: "form:\"QueryGoTag,required\" json:\"QueryGoTag,required\"", + }, + } + + tags, err := plu.InsertTag() + if err != nil { + t.Fatal(err) + } + for i, tag := range tags { + tagList[i].ActualTag = tag.Content + if !strings.Contains(tagList[i].ActualTag, tagList[i].GeneratedTag) { + t.Fatalf("expected tag: '%s', but autual tag: '%s'", tagList[i].GeneratedTag, tagList[i].ActualTag) + } + } +} diff --git a/cmd/hz/internal/thrift/tags.go b/cmd/hz/internal/thrift/tags.go index 8193388ed..6a4af4ff8 100644 --- a/cmd/hz/internal/thrift/tags.go +++ b/cmd/hz/internal/thrift/tags.go @@ -73,10 +73,10 @@ var ( BindingTags = map[string]string{ AnnotationPath: "path", AnnotationQuery: "query", - AnnotationForm: "form", AnnotationHeader: "header", AnnotationCookie: "cookie", AnnotationBody: "json", + AnnotationForm: "form", AnnotationRawBody: "raw_body", } @@ -152,6 +152,23 @@ func getAnnotations(input parser.Annotations, targets map[string]string) map[str func defaultBindingTags(f *parser.Field) []model.Tag { out := make([]model.Tag, 3) + bindingTags := []string{ + AnnotationQuery, + AnnotationForm, + AnnotationPath, + AnnotationHeader, + AnnotationCookie, + AnnotationBody, + AnnotationRawBody, + } + + for _, tag := range bindingTags { + if v := getAnnotation(f.Annotations, tag); len(v) > 0 { + out[0] = jsonTag(f) + return out[:1] + } + } + if v := getAnnotation(f.Annotations, AnnotationBody); len(v) > 0 { val := getJsonValue(f, v[0]) out[0] = tag("json", val) @@ -230,7 +247,10 @@ func injectTags(f *parser.Field, gf *model.Field, needDefault, needGoTag bool) e key := t.Key tags.Remove(key) if key == "json" { + formVal := t.Value t.Value = getJsonValue(f, t.Value) + formVal = checkRequire(f, formVal) + tags = append(tags, tag("form", formVal)) } else { t.Value = checkRequire(f, t.Value) } @@ -240,7 +260,10 @@ func injectTags(f *parser.Field, gf *model.Field, needDefault, needGoTag bool) e // validator tags tags = append(tags, annotationToTags(as, ValidatorTags)...) - // go.tags + // the tag defined by gotag with higher priority + checkGoTag(as, &tags) + + // go.tags for compiler mode if needGoTag { rets := getAnnotation(as, AnnotationGoTag) for _, v := range rets { @@ -289,3 +312,21 @@ func checkRequire(f *parser.Field, val string) string { return val } + +// checkGoTag removes the tag defined in gotag +func checkGoTag(as parser.Annotations, tags *model.Tags) error { + rets := getAnnotation(as, AnnotationGoTag) + for _, v := range rets { + gts := util.SplitGoTags(v) + for _, gt := range gts { + sp := strings.SplitN(gt, ":", 2) + if len(sp) != 2 { + return fmt.Errorf("invalid go tag: %s", v) + } + key := sp[0] + tags.Remove(key) + } + } + + return nil +} diff --git a/cmd/hz/internal/thrift/test_data/test_tag.thrift b/cmd/hz/internal/thrift/test_data/test_tag.thrift new file mode 100644 index 000000000..ab3c56a58 --- /dev/null +++ b/cmd/hz/internal/thrift/test_data/test_tag.thrift @@ -0,0 +1,26 @@ +namespace go cloudwego.hertz.hz + +struct MultiTagReq { + // basic feature + 1: string DefaultQueryTag (api.query="query"); + 2: string RawBodyTag (api.raw_body="raw_body"); + 3: string PathTag (api.path="path"); + 4: string FormTag (api.form="form"); + 5: string CookieTag (api.cookie="cookie"); + 6: string HeaderTag (api.header="header"); + 7: string BodyTag (api.body="body"); + 8: string GoTag (go.tag="json:\"json\" query:\"query\" form:\"form\" header:\"header\" goTag:\"tag\""); + 9: string VdTag (api.vd="$!='?'"); + 10: string DefaultTag; + + // optional / required + 11: required string ReqQuery (api.query="query"); + 12: optional string OptQuery (api.query="query"); + 13: required string ReqBody (api.body="body"); + 14: optional string OptBody (api.body="body"); + 15: required string ReqGoTag (go.tag="json:\"json\""); + 16: optional string OptGoTag (go.tag="json:\"json\""); + + // gotag cover feature + 17: required string QueryGoTag (apt.query="query", go.tag="query:\"queryTag\"") +} \ No newline at end of file diff --git a/cmd/hz/internal/thrift/test_data/thrift_tag_test.out b/cmd/hz/internal/thrift/test_data/thrift_tag_test.out new file mode 100755 index 0000000000000000000000000000000000000000..4e7968140c258accc039d202eea4563e7724ecec GIT binary patch literal 3017 zcmcJRQE$^Q5P-d{P1B~M+a5qdLJASmo=D&UX{uBq*hJw0jg7$*QspLY8rsI4@ov#EBf{ zeLv)rARO~Nnz5J%Lnh=r2q#f6oCp169!z+|#;|%{u#AZ-)`R&ImddOHg9kB7d%rWD z_9Trv13J!Fi|in<4v%v@i7u|Etdp`CbYCCOqE7Gwq+~(=TC#|yAq#++iBNNU11?4e z4h!C$=JF&FtFH+p%Oos(@+BH8upEkHfFr+P>k3BR5Fp$aiFCr;k}4Ts=vTu zep*vty=7JHe z3?wTm(>SNIk38B`k0$CN=&aWVSp`+>n=zveF?7gWq}MQ0HgtiFXiSL$wu1c<61=OB zpSYMj1QII9q=>|R30c+1`Z4FDgl!0Ipq-(TRKQw#qy)AM_%ovs6Au9%DoI7K9w~t# zo0XFvAf(j3mcprm{SwmF$ZD5wXr~o(U(&c>;|Dx&+8(|=S0z>3Q;p~1Znr&rd#?6G z+taP*#;8f#16f{4*SgX5XSAuV7z|fYL09Y5%g*8Z!xFHefelk~)}@xDI$Ej?748gc zkFH=)%pawKJ%tL!r!wJiLb`z~*kzsw7MV)YG`gHlbwSm-K;eHBU2f{vuBO(&0~qR? zIAGN{e`1he=n}b}ovR{S%Y80n*LwSWJG-1hc9jBbZ@H-j@OsAH3%swv4Heb}^gV5c z!xax=IGyFcPiLHQTe-broq>G4DIu5zF^8JshWv_&#c6IXHQ+$JjNxUcZf+ki% RB~8bM|41!fQJ}j^$Y1zP@4)~7 literal 0 HcmV?d00001 diff --git a/cmd/hz/internal/util/tool_install.go b/cmd/hz/internal/util/tool_install.go new file mode 100644 index 000000000..46a004eda --- /dev/null +++ b/cmd/hz/internal/util/tool_install.go @@ -0,0 +1,151 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/cloudwego/hertz/cmd/hz/internal/meta" + "github.com/cloudwego/hertz/cmd/hz/internal/util/logs" + gv "github.com/hashicorp/go-version" +) + +const ThriftgoMiniVersion = "v0.2.0" + +// QueryVersion will query the version of the corresponding executable. +func QueryVersion(exe string) (version string, err error) { + var buf strings.Builder + cmd := &exec.Cmd{ + Path: exe, + Args: []string{ + exe, "--version", + }, + Stdin: os.Stdin, + Stdout: &buf, + Stderr: &buf, + } + err = cmd.Run() + if err == nil { + version = strings.Split(buf.String(), " ")[1] + if strings.HasSuffix(version, "\n") { + version = version[:len(version)-1] + } + } + return +} + +// ShouldUpdate will return "true" when current is lower than latest. +func ShouldUpdate(current, latest string) bool { + cv, err := gv.NewVersion(current) + if err != nil { + return false + } + lv, err := gv.NewVersion(latest) + if err != nil { + return false + } + + return cv.Compare(lv) < 0 +} + +// InstallAndCheckThriftgo will automatically install thriftgo and judge whether it is installed successfully. +func InstallAndCheckThriftgo() error { + exe, err := exec.LookPath("go") + if err != nil { + return fmt.Errorf("can not find tool 'go': %v", err) + } + var buf strings.Builder + cmd := &exec.Cmd{ + Path: exe, + Args: []string{ + exe, "install", "github.com/cloudwego/thriftgo@latest", + }, + Stdin: os.Stdin, + Stdout: &buf, + Stderr: &buf, + } + + done := make(chan error) + logs.Infof("installing thriftgo automatically") + go func() { + done <- cmd.Run() + }() + select { + case err = <-done: + if err != nil { + return fmt.Errorf("can not install thriftgo, err: %v. Please install it manual, and make sure the version of thriftgo is greater than v0.2.0", cmd.Stderr) + } + case <-time.After(time.Second * 30): + return fmt.Errorf("install thriftgo time out.Please install it manual, and make sure the version of thriftgo is greater than v0.2.0") + } + + exist, err := CheckCompiler(meta.TpCompilerThrift) + if err != nil { + return fmt.Errorf("check %s exist failed, err: %v", meta.TpCompilerThrift, err) + } + if !exist { + return fmt.Errorf("install thriftgo failed. Please install it manual, and make sure the version of thriftgo is greater than v0.2.0") + } + + return nil +} + +// CheckCompiler will check if the tool exists. +func CheckCompiler(tool string) (bool, error) { + path, err := exec.LookPath(tool) + if err != nil { + goPath, err := GetGOPATH() + if err != nil { + return false, fmt.Errorf("get 'GOPATH' failed for find %s : %v", tool, path) + } + path = filepath.Join(goPath, "bin", tool) + } + + isExist, err := PathExist(path) + if err != nil { + return false, fmt.Errorf("can not check %s exist, err: %v", tool, err) + } + if !isExist { + return false, nil + } + + return true, nil +} + +// CheckAndUpdateThriftgo checks the version of thriftgo and updates the tool to the latest version if its version is less than v0.2.0. +func CheckAndUpdateThriftgo() error { + path, err := exec.LookPath(meta.TpCompilerThrift) + if err != nil { + return fmt.Errorf("can not find %s", meta.TpCompilerThrift) + } + curVersion, err := QueryVersion(path) + logs.Infof("current thriftgo version is %s", curVersion) + if ShouldUpdate(curVersion, ThriftgoMiniVersion) { + logs.Infof(" current thriftgo version is less than v0.2.0, so update thriftgo version") + err = InstallAndCheckThriftgo() + if err != nil { + return fmt.Errorf("update thriftgo version failed, err: %v", err) + } + } + + return nil +} diff --git a/cmd/hz/internal/util/tool_install_test.go b/cmd/hz/internal/util/tool_install_test.go new file mode 100644 index 000000000..946befdf1 --- /dev/null +++ b/cmd/hz/internal/util/tool_install_test.go @@ -0,0 +1,36 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package util + +import "testing" + +func TestQueryVersion(t *testing.T) { + lowVersion := "v0.1.0" + equalVersion := "v0.2.0" + highVersion := "v0.3.0" + + if ShouldUpdate(lowVersion, ThriftgoMiniVersion) { + } + + if ShouldUpdate(equalVersion, ThriftgoMiniVersion) { + t.Fatal("should not be updated") + } + + if ShouldUpdate(highVersion, ThriftgoMiniVersion) { + t.Fatal("should not be updated") + } +} From 419a448536e0623f1f02bd90096fcbad5ff6fa79 Mon Sep 17 00:00:00 2001 From: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> Date: Tue, 20 Sep 2022 14:55:43 +0800 Subject: [PATCH 08/11] optimize: softlink the plugin to $GOPATH/bin (#235) --- cmd/hz/internal/config/cmd.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/hz/internal/config/cmd.go b/cmd/hz/internal/config/cmd.go index 3c93e65d4..92c569a26 100644 --- a/cmd/hz/internal/config/cmd.go +++ b/cmd/hz/internal/config/cmd.go @@ -75,7 +75,12 @@ func lookupTool(idlType string) (string, error) { if err != nil { return "", fmt.Errorf("failed to get executable path: %s", err) } - dir := filepath.Dir(path) + gopath, err := util.GetGOPATH() + if err != nil { + return "", err + } + // softlink the plugin to "$GOPATH/bin" + dir := gopath + string(os.PathSeparator) + "bin" if tool == meta.TpCompilerProto { pgh, err := exec.LookPath(meta.ProtocPluginName) linkName := filepath.Join(dir, meta.ProtocPluginName) From 0745fe858b98e4a323371e8d5c744da49e260363 Mon Sep 17 00:00:00 2001 From: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> Date: Tue, 20 Sep 2022 14:56:01 +0800 Subject: [PATCH 09/11] fix: same package for self (#216) --- cmd/hz/internal/protobuf/resolver.go | 13 +++++++++---- cmd/hz/internal/thrift/resolver.go | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/cmd/hz/internal/protobuf/resolver.go b/cmd/hz/internal/protobuf/resolver.go index 074c23ad1..9b74cacfa 100644 --- a/cmd/hz/internal/protobuf/resolver.go +++ b/cmd/hz/internal/protobuf/resolver.go @@ -69,17 +69,22 @@ type PackageReference struct { func getReferPkgMap(pkgMap map[string]string, incs []*descriptorpb.FileDescriptorProto) (map[string]*PackageReference, error) { var err error out := make(map[string]*PackageReference, len(pkgMap)) + pkgAliasMap := make(map[string]string, len(incs)) for _, inc := range incs { pkg := getGoPackage(inc, pkgMap) path := inc.GetName() base := util.BaseName(path, ".proto") fileName := inc.GetName() pkgName := util.BaseName(pkg, "") - pkgName, err = util.GetPackageUniqueName(pkgName) - if err != nil { - return nil, fmt.Errorf("get package unique name failed, err: %v", err) + if pn, exist := pkgAliasMap[pkg]; exist { + pkgName = pn + } else { + pkgName, err = util.GetPackageUniqueName(pkgName) + pkgAliasMap[pkg] = pkgName + if err != nil { + return nil, fmt.Errorf("get package unique name failed, err: %v", err) + } } - out[fileName] = &PackageReference{base, path, &model.Model{ FilePath: path, Package: pkg, diff --git a/cmd/hz/internal/thrift/resolver.go b/cmd/hz/internal/thrift/resolver.go index b105cf034..98d77240c 100644 --- a/cmd/hz/internal/thrift/resolver.go +++ b/cmd/hz/internal/thrift/resolver.go @@ -57,16 +57,21 @@ type PackageReference struct { func getReferPkgMap(pkgMap map[string]string, incs []*parser.Include) (map[string]*PackageReference, error) { var err error out := make(map[string]*PackageReference, len(pkgMap)) + pkgAliasMap := make(map[string]string, len(incs)) for _, inc := range incs { pkg := getGoPackage(inc.Reference, pkgMap) impt := inc.GetPath() base := util.BaseNameAndTrim(impt) pkgName := util.SplitPackageName(pkg, "") - pkgName, err = util.GetPackageUniqueName(pkgName) - if err != nil { - return nil, fmt.Errorf("get package unique name failed, err: %v", err) + if pn, exist := pkgAliasMap[pkg]; exist { + pkgName = pn + } else { + pkgName, err = util.GetPackageUniqueName(pkgName) + pkgAliasMap[pkg] = pkgName + if err != nil { + return nil, fmt.Errorf("get package unique name failed, err: %v", err) + } } - out[base] = &PackageReference{base, impt, &model.Model{ FilePath: inc.Path, Package: pkg, From f4267ff0b39230d06d996cb1db6ddd5ee3a3cb0b Mon Sep 17 00:00:00 2001 From: GuangyuFan <97507466+FGYFFFF@users.noreply.github.com> Date: Tue, 20 Sep 2022 18:13:38 +0800 Subject: [PATCH 10/11] chore: update hz version v0.3.0 (#259) --- cmd/hz/internal/meta/const.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/hz/internal/meta/const.go b/cmd/hz/internal/meta/const.go index 8996510a4..d8a05890d 100644 --- a/cmd/hz/internal/meta/const.go +++ b/cmd/hz/internal/meta/const.go @@ -19,7 +19,7 @@ package meta import "runtime" // Version hz version -const Version = "v0.2.1" +const Version = "v0.3.0" // Mode hz run modes type Mode int From 8bd424c45f8325b55ef94f088ec35c1b3714d47a Mon Sep 17 00:00:00 2001 From: alice <90381261+alice-yyds@users.noreply.github.com> Date: Tue, 20 Sep 2022 18:14:48 +0800 Subject: [PATCH 11/11] chore: update version v0.3.2 --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 8119d3006..25fc0b2f5 100644 --- a/version.go +++ b/version.go @@ -19,5 +19,5 @@ package hertz // Name and Version info of this framework, used for statistics and debug const ( Name = "Hertz" - Version = "v0.3.1" + Version = "v0.3.2" )