codegen/client.go (243 lines of code) (raw):

// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package codegen import ( "path/filepath" "github.com/ghodss/yaml" "github.com/pkg/errors" validator2 "gopkg.in/validator.v2" ) type clientConfig interface { NewClientSpec( instance *ModuleInstance, h *PackageHelper) (*ClientSpec, error) } // ClientIDLConfig is the "config" field in the client-config.yaml for // HTTP/TChannel/gRPC clients. type ClientIDLConfig struct { ExposedMethods map[string]string `yaml:"exposedMethods" json:"exposedMethods" validate:"exposedMethods"` IDLFile string `yaml:"idlFile" json:"idlFile" validate:"nonzero"` IDLFileSha string `yaml:"idlFileSha,omitempty" json:"idlFileSha"` SidecarRouter string `yaml:"sidecarRouter" json:"sidecarRouter"` Fixture *Fixture `yaml:"fixture,omitempty" json:"fixture"` CustomInterface string `yaml:"customInterface,omitempty" json:"customInterface,omitempty"` } // Fixture specifies client fixture import path and all scenarios type Fixture struct { // ImportPath is the package where the user-defined Fixture global variable is contained. // The Fixture object defines, for a given client, the standardized list of fixture scenarios for that client ImportPath string `yaml:"importPath" json:"importPath" validate:"nonzero"` // Scenarios is a map from zanzibar's exposed method name to a list of user-defined fixture scenarios for a client Scenarios map[string][]string `yaml:"scenarios" json:"scenarios"` } // Validate the fixture configuration func (f *Fixture) Validate(methods map[string]interface{}) error { if f.ImportPath == "" { return errors.New("fixture importPath is empty") } for m := range f.Scenarios { if _, ok := methods[m]; !ok { return errors.Errorf("%q is not a valid method", m) } } return nil } func validateExposedMethods(v interface{}, param string) error { methods := v.(map[string]string) // Check duplication visited := make(map[string]string, len(methods)) for key, val := range methods { if _, ok := visited[val]; ok { return errors.Errorf( "value %q of the exposedMethods is not unique", val, ) } visited[val] = key } return nil } // HTTPClientConfig represents the "config" field for a HTTP client-config.yaml type HTTPClientConfig struct { ClassConfigBase `yaml:",inline" json:",inline"` Dependencies Dependencies `yaml:"dependencies,omitempty" json:"dependencies"` Config *ClientIDLConfig `yaml:"config" json:"config" validate:"nonzero"` } func newHTTPClientConfig(raw []byte, validator *validator2.Validator) (*HTTPClientConfig, error) { config := &HTTPClientConfig{} if errUnmarshal := yaml.Unmarshal(raw, config); errUnmarshal != nil { return nil, errors.Wrap( errUnmarshal, "Could not parse HTTP client config data") } if errValidate := validator.Validate(config); errValidate != nil { return nil, errors.Wrap( errValidate, "http client config validation failed") } return config, nil } func newClientSpec( clientType string, config *ClientIDLConfig, instance *ModuleInstance, h *PackageHelper, annotate bool, ) (*ClientSpec, error) { thriftFile := filepath.Join(h.IdlPath(), h.GetModuleIdlSubDir(false), config.IDLFile) mspec, err := NewModuleSpec(thriftFile, annotate, false, h) if err != nil { return nil, err } mspec.PackageName = mspec.PackageName + "client" cspec := &ClientSpec{ ModuleSpec: mspec, YAMLFile: instance.YAMLFileName, JSONFile: instance.JSONFileName, ClientType: clientType, ImportPackagePath: instance.PackageInfo.ImportPackagePath(), ImportPackageAlias: instance.PackageInfo.ImportPackageAlias(), ExportName: instance.PackageInfo.ExportName, ExportType: instance.PackageInfo.ExportType, ThriftFile: thriftFile, ClientID: instance.InstanceName, ClientName: instance.PackageInfo.QualifiedInstanceName, ExposedMethods: config.ExposedMethods, SidecarRouter: config.SidecarRouter, CustomInterface: config.CustomInterface, } return cspec, nil } // NewClientSpec creates a client spec from a client module instance func (c *HTTPClientConfig) NewClientSpec( instance *ModuleInstance, h *PackageHelper) (*ClientSpec, error) { return newClientSpec(c.Type, c.Config, instance, h, true) } // TChannelClientConfig represents the "config" field for a TChannel client-config.yaml type TChannelClientConfig struct { ClassConfigBase `yaml:",inline" json:",inline"` Dependencies Dependencies `yaml:"dependencies,omitempty" json:"dependencies"` Config *ClientIDLConfig `yaml:"config" json:"config" validate:"nonzero"` } func newTChannelClientConfig(raw []byte, validator *validator2.Validator) (*TChannelClientConfig, error) { config := &TChannelClientConfig{} if errUnmarshal := yaml.Unmarshal(raw, config); errUnmarshal != nil { return nil, errors.Wrap( errUnmarshal, "Could not parse TChannel client config data") } if errValidate := validator.Validate(config); errValidate != nil { return nil, errors.Wrap( errValidate, "tchannel client config validation failed") } return config, nil } // NewClientSpec creates a client spec from a client module instance func (c *TChannelClientConfig) NewClientSpec( instance *ModuleInstance, h *PackageHelper) (*ClientSpec, error) { return newClientSpec(c.Type, c.Config, instance, h, false) } // CustomClientConfig represents the config for a custom client. type CustomClientConfig struct { ClassConfigBase `yaml:",inline" json:",inline"` Dependencies Dependencies `yaml:"dependencies" json:"dependencies"` Config *struct { Fixture *Fixture `yaml:"fixture" json:"fixture"` CustomImportPath string `yaml:"customImportPath" json:"customImportPath" validate:"nonzero"` CustomInterface string `yaml:"customInterface,omitempty" json:"customInterface,omitempty"` } `yaml:"config" json:"config" validate:"nonzero"` } func newCustomClientConfig(raw []byte, validator *validator2.Validator) (*CustomClientConfig, error) { config := &CustomClientConfig{} if errUnmarshal := yaml.Unmarshal(raw, config); errUnmarshal != nil { return nil, errors.Wrap( errUnmarshal, "Could not parse Custom client config data") } if errValidate := validator.Validate(config); errValidate != nil { return nil, errors.Wrap( errValidate, "custom client config validation failed") } return config, nil } // NewClientSpec creates a client spec from a http client module instance func (c *CustomClientConfig) NewClientSpec( instance *ModuleInstance, h *PackageHelper) (*ClientSpec, error) { spec := &ClientSpec{ YAMLFile: instance.YAMLFileName, JSONFile: instance.JSONFileName, ImportPackagePath: instance.PackageInfo.ImportPackagePath(), ImportPackageAlias: instance.PackageInfo.ImportPackageAlias(), ExportName: instance.PackageInfo.ExportName, ExportType: instance.PackageInfo.ExportType, ClientType: c.Type, ClientID: c.Name, ClientName: instance.PackageInfo.QualifiedInstanceName, CustomImportPath: c.Config.CustomImportPath, CustomInterface: c.Config.CustomInterface, } return spec, nil } // GRPCClientConfig represents the "config" field for a gRPC client-config.yaml. type GRPCClientConfig struct { ClassConfigBase `yaml:",inline" json:",inline"` Dependencies Dependencies `yaml:"dependencies,omitempty" json:"dependencies"` Config *ClientIDLConfig `yaml:"config" json:"config" validate:"nonzero"` } func newGRPCClientConfig(raw []byte, validator *validator2.Validator) (*GRPCClientConfig, error) { config := &GRPCClientConfig{} if errUnmarshal := yaml.Unmarshal(raw, config); errUnmarshal != nil { return nil, errors.Wrap( errUnmarshal, "could not parse gRPC client config data") } if errValidate := validator.Validate(config); errValidate != nil { return nil, errors.Wrap( errValidate, "grpc client config validation failed") } return config, nil } func newGRPCClientSpec( clientType string, config *ClientIDLConfig, instance *ModuleInstance, h *PackageHelper, ) (*ClientSpec, error) { protoFile := filepath.Join(h.IdlPath(), h.GetModuleIdlSubDir(false), config.IDLFile) protoSpec, err := NewProtoModuleSpec(protoFile, false, h) if err != nil { return nil, errors.Wrapf( err, "could not build proto spec for proto file %s: ", protoFile, ) } cspec := &ClientSpec{ ModuleSpec: protoSpec, YAMLFile: instance.YAMLFileName, JSONFile: instance.JSONFileName, ClientType: clientType, ImportPackagePath: instance.PackageInfo.ImportPackagePath(), ImportPackageAlias: instance.PackageInfo.ImportPackageAlias(), ExportName: instance.PackageInfo.ExportName, ExportType: instance.PackageInfo.ExportType, ThriftFile: protoFile, ClientID: instance.InstanceName, ClientName: instance.PackageInfo.QualifiedInstanceName, ExposedMethods: config.ExposedMethods, SidecarRouter: config.SidecarRouter, } return cspec, nil } // NewClientSpec creates a client spec from a client module instance func (c *GRPCClientConfig) NewClientSpec( instance *ModuleInstance, h *PackageHelper, ) (*ClientSpec, error) { return newGRPCClientSpec(c.Type, c.Config, instance, h) } func clientType(raw []byte) (string, error) { clientConfig := ClassConfigBase{} if err := yaml.Unmarshal(raw, &clientConfig); err != nil { return "", errors.Wrap( err, "Could not parse client config data to determine client type") } return clientConfig.Type, nil } func newClientConfig(raw []byte, validator *validator2.Validator) (clientConfig, error) { clientType, errType := clientType(raw) if errType != nil { return nil, errors.Wrap( errType, "Could not determine client type") } switch clientType { case "http": return newHTTPClientConfig(raw, validator) case "tchannel": return newTChannelClientConfig(raw, validator) case "grpc": return newGRPCClientConfig(raw, validator) case "custom": return newCustomClientConfig(raw, validator) default: return nil, errors.Errorf( "Unknown client type %q", clientType) } }