From b27183c380ee16686ce08fb4b4dad5826ef26993 Mon Sep 17 00:00:00 2001 From: Zhe Zhang Date: Thu, 24 Oct 2024 19:38:46 +0800 Subject: [PATCH] feat: add include path for idl (#14) * feat: add include path for idl * feat: add include path for idl * doc: add include path for idl * doc: add include path for idl --- README.md | 2 + README_cn.md | 2 + internal/test/client_test.go | 89 ++++++++++++++++++++++++++++++--- internal/test/example2.thrift | 34 +++++++++++++ internal/test/idl/common.thrift | 29 +++++++++++ pkg/argparse/args.go | 25 ++++++--- pkg/client/generic_client.go | 2 +- pkg/config/config.go | 1 + 8 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 internal/test/example2.thrift create mode 100644 internal/test/idl/common.thrift diff --git a/README.md b/README.md index cb9fd76..55d41f5 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Output: - `-help` or `-h`: Outputs the usage instructions. - `-type` or `-t`: Specifies the IDL type: thrift or protobuf. It supports inference based on the IDL file type. The default is thrift.. - `-idl-path` or `-p`: Specifies the path to the IDL file. +- `-include-path`: Add a search path for the IDL. Multiple paths can be added and will be searched in the order they are added. - `-method` or `-m`: Required, specifies the method name in the format IDLServiceName/MethodName or just MethodName. When the server side has MultiService mode enabled, IDLServiceName must be specified, and the transport protocol must be TTHeader or TTHeaderFramed. - `-file` or `-f`: Specifies the input file path, which must be in JSON format. - `-data` or `-d`: Specifies the data to be sent, in JSON string format. @@ -118,6 +119,7 @@ Output: - `-meta`: Specifies one-way metadata passed to the server. Multiple can be specified, in the format key=value. - `-meta-persistent`: Specifies persistent metadata passed to the server. Multiple can be specified, in the format key=value. - `-meta-backward`: Enables receiving backward metadata (Backward) returned by the server. +- `-q`: Only output JSON response, no other information. - `-verbose` or `-v`: Enables verbose mode for more detailed output information. ### Detailed Description diff --git a/README_cn.md b/README_cn.md index a4c4c3a..524b0ba 100644 --- a/README_cn.md +++ b/README_cn.md @@ -111,6 +111,7 @@ kitexcall -idl-path echo.thrift -m echo -d '{"message": "hello"}' -e 127.0.0.1:9 - `-help` 或 `-h`:输出使用说明。 - `-type` 或 `-t`:指定 IDL 类型:`thrift` 或 `protobuf`,支持通过 IDL 文件类型推测,默认是 `thrift`。 - `-idl-path` 或 `-p`:指定 IDL 文件的路径。 +- `-include-path`:添加一个 IDL 里 include 的其他文件的搜索路径。支持添加多个,会按照添加的路径顺序搜索。 - `-method` 或 `-m`:必选,指定方法名,格式为 `IDLServiceName/MethodName` 或仅为 `MethodName`。当 server 端开启了 MultiService 模式时,必须指定 `IDLServiceName`,同时指定传输协议为 TTHeader 或 TTHeaderFramed 。 - `-file` 或 `-f`:指定输入文件路径,必须是 JSON 格式。 - `-data` 或 `-d`:指定要发送的数据,格式为 JSON 字符串。 @@ -120,6 +121,7 @@ kitexcall -idl-path echo.thrift -m echo -d '{"message": "hello"}' -e 127.0.0.1:9 - `-meta`:指定传递给 server 的单跳透传元信息。可以指定多个,格式为 key=value。 - `-meta-persistent`:指定传递给 server 的持续透传元信息。可以指定多个,格式为 key=value。 - `-meta-backward`:启用从服务器接收反向透传元信息。 +- `-q`: 只输出Json响应,不输出其他提示信息。 - `-verbose` 或 `-v`:启用详细模式。 ### 详细描述 diff --git a/internal/test/client_test.go b/internal/test/client_test.go index 76e00cd..a9dc94f 100644 --- a/internal/test/client_test.go +++ b/internal/test/client_test.go @@ -21,6 +21,8 @@ import ( "encoding/json" "errors" "net" + "os" + "path/filepath" "testing" "time" @@ -38,14 +40,17 @@ import ( ) var ( - thriftGenericServer server.Server - pbGenericServer server.Server - bizErrorGenericServer server.Server - thriftServerHost = "127.0.0.1:9919" - pbServerHostPort = "127.0.0.1:9199" - bizErrorServerHost = "127.0.0.1:9109" - pbFilePath = "./example_service.proto" - thriftFilePath = "./example_service.thrift" + thriftGenericServer server.Server + pbGenericServer server.Server + bizErrorGenericServer server.Server + thriftGenericServerWithImportPath server.Server + thriftServerHost = "127.0.0.1:9919" + pbServerHostPort = "127.0.0.1:9199" + bizErrorServerHost = "127.0.0.1:9109" + pbFilePath = "./example_service.proto" + thriftFilePath = "./example_service.thrift" + example2ServerHost = "127.0.0.1:9100" + example2FilePath = "./example2.thrift" ) func InitPbGenericServer() { @@ -100,6 +105,30 @@ func InitThriftGenericServer() { WaitServerStart(thriftServerHost) } +func InitThriftGenericServerWithImportPath() { + p, err := generic.NewThriftFileProvider(example2FilePath, "./idl") + if err != nil { + panic(err) + } + g, err := generic.JSONThriftGeneric(p) + if err != nil { + panic(err) + } + + go func() { + addr, _ := net.ResolveTCPAddr("tcp", example2ServerHost) + klog.Infof("Starting Example2 service on %s", addr.String()) + + thriftGenericServerWithImportPath = genericserver.NewServer(new(GenericServiceImpl), g, server.WithServiceAddr(addr)) + + if err := thriftGenericServerWithImportPath.Run(); err != nil { + klog.Fatalf("Failed to run Example2 service: %v", err) + } + }() + + WaitServerStart(example2ServerHost) +} + // WaitServerStart waits for server to start for at most 1 second func WaitServerStart(addr string) { for begin := time.Now(); time.Since(begin) < time.Second; { @@ -292,3 +321,47 @@ func TestBizErrorGenericServer_invokeRPC(t *testing.T) { t.Errorf("Expected BizMessage %s, got %s", expectedMessage, bizErr.BizMessage()) } } + +func TestExample2Service_withImportPath(t *testing.T) { + InitThriftGenericServerWithImportPath() + defer thriftGenericServerWithImportPath.Stop() + + currentPath, _ := os.Getwd() + includePath := filepath.Join(currentPath, "idl") + + conf := &config.Config{ + Type: config.Thrift, + Endpoint: []string{example2ServerHost}, + IDLPath: example2FilePath, + IDLServiceName: "Example2Service", + Method: "Example2Method", + Data: "{\"Msg\": \"hello\", \"User\": {\"UserID\": \"123\", \"UserName\": \"Alice\", \"Age\": 25}}", + Transport: config.TTHeader, + MetaBackward: true, + IncludePath: []string{includePath}, + } + + cli, err := client.InvokeRPC(conf) + if err != nil { + t.Fatalf("InvokeRPC failed: %v", err) + } + + resp := cli.GetResponse() + if resp == nil { + t.Fatalf("Response is nil") + } + + expectedResponse := `{"Msg":"world", "BaseResp": {"StatusCode": 0, "StatusMessage": ""}}` + + var serverData, expectedData interface{} + json.Unmarshal([]byte(resp.(string)), &serverData) + json.Unmarshal([]byte(expectedResponse), &expectedData) + DeepEqual(t, serverData, expectedData) + + // MetaBackward + if conf.MetaBackward { + if res := cli.GetMetaBackward(); res == nil { + t.Errorf("Expected meta backward not found in response") + } + } +} diff --git a/internal/test/example2.thrift b/internal/test/example2.thrift new file mode 100644 index 0000000..bc3a99d --- /dev/null +++ b/internal/test/example2.thrift @@ -0,0 +1,34 @@ +// Copyright 2024 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. + +namespace go kitex.test.server + +include "common.thrift" + +// Request data for Example2Service +struct Example2Req { + 1: required string Msg, + 2: required common.UserData User, +} + +// Response for Example2Service +struct Example2Resp { + 1: required string Msg, + 2: required common.CommonResponse BaseResp, +} + +// Service definition that uses imported common types +service Example2Service { + Example2Resp Example2Method(1: Example2Req req), +} diff --git a/internal/test/idl/common.thrift b/internal/test/idl/common.thrift new file mode 100644 index 0000000..c07e549 --- /dev/null +++ b/internal/test/idl/common.thrift @@ -0,0 +1,29 @@ +// Copyright 2024 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. + +namespace go kitex.test.common + +// Commonly shared struct in multiple services +struct CommonResponse { + 1: string StatusMessage = "", + 2: i32 StatusCode = 0, + 3: optional map Extra, +} + +// Common data shared across services +struct UserData { + 1: string UserID, + 2: string UserName, + 3: i32 Age, +} diff --git a/pkg/argparse/args.go b/pkg/argparse/args.go index 8ca3bb9..015d7aa 100644 --- a/pkg/argparse/args.go +++ b/pkg/argparse/args.go @@ -43,14 +43,14 @@ func NewArgument() *Argument { } } -type EndpointList []string +type StringList []string -func (e *EndpointList) String() string { - return strings.Join(*e, ",") +func (s *StringList) String() string { + return strings.Join(*s, ",") } -func (e *EndpointList) Set(value string) error { - *e = append(*e, value) +func (s *StringList) Set(value string) error { + *s = append(*s, value) return nil } @@ -90,8 +90,8 @@ func (a *Argument) buildFlags() *flag.FlagSet { f.StringVar(&a.Data, "data", "", "Specify the data to be sent as a JSON formatted string.") f.StringVar(&a.Data, "d", "", "Specify the data to be sent as a JSON formatted string. (shorthand)") - f.Var((*EndpointList)(&a.Endpoint), "endpoint", "Specify the server endpoints. Can be repeated.") - f.Var((*EndpointList)(&a.Endpoint), "e", "Specify the server endpoints. Can be repeated. (shorthand)") + f.Var((*StringList)(&a.Endpoint), "endpoint", "Specify the server endpoints. Can be repeated.") + f.Var((*StringList)(&a.Endpoint), "e", "Specify the server endpoints. Can be repeated. (shorthand)") f.BoolVar(&a.Verbose, "verbose", false, "Enable verbose mode.") f.BoolVar(&a.Verbose, "v", false, "Enable verbose mode. (shorthand)") @@ -109,6 +109,8 @@ func (a *Argument) buildFlags() *flag.FlagSet { f.BoolVar(&a.Quiet, "quiet", false, "Enable only print rpc response.") + f.Var((*StringList)(&a.IncludePath), "include-path", "Specify additional paths for imported IDL files. Can be repeated.") + return f } @@ -242,6 +244,14 @@ func (a *Argument) checkIDL() error { return errors.New(errors.ArgParseError, "IDL file does not exist") } + if a.IncludePath != nil { + for _, path := range a.IncludePath { + if _, err := os.Stat(path); os.IsNotExist(err) { + return errors.New(errors.ArgParseError, "Include path does not exist") + } + } + } + switch a.Type { case "thrift", "protobuf": case "unknown": @@ -282,6 +292,7 @@ func (a *Argument) BuildConfig() *config.Config { MetaBackward: a.MetaBackward, BizError: a.BizError, Quiet: a.Quiet, + IncludePath: a.IncludePath, } } diff --git a/pkg/client/generic_client.go b/pkg/client/generic_client.go index c572122..4dfa69a 100644 --- a/pkg/client/generic_client.go +++ b/pkg/client/generic_client.go @@ -217,7 +217,7 @@ func NewThriftGeneric() *ThriftGeneric { func (c *ThriftGeneric) Init(Conf *config.Config) error { // Waiting for server reflection - p, err := generic.NewThriftFileProvider(Conf.IDLPath) + p, err := generic.NewThriftFileProvider(Conf.IDLPath, Conf.IncludePath...) if err != nil { return err } diff --git a/pkg/config/config.go b/pkg/config/config.go index d662d45..e0fb67c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -47,6 +47,7 @@ type Config struct { MetaBackward bool BizError bool Quiet bool + IncludePath []string } type ConfigBuilder interface {