codegen/package.go (281 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 ( "fmt" "path" "path/filepath" "strings" "github.com/pkg/errors" "go.uber.org/thriftrw/compile" ) const ( thriftExtension = ".thrift" protoExtension = ".proto" endpointModuleName = "endpoints" defaultModuleFallback = "default" ) // PackageHelper manages the mapping from idl file to generated type code and service code. type PackageHelper struct { // The project package name packageRoot string // The absolute root dir path for all configs, i.e., clients, endpoints, idl, etc configRoot string // The absolute root directory containing idl files idlRootDir string // moduleIdlSubDir defines subdir for idl per module moduleIdlSubDir map[string]string // The map of idl type to go package name of where the generated structs are // map is keyed with idl file extension, e.g., .thrift, .proto genCodePackage map[string]string // The absolute directory to put the generated service code targetGenDir string // The go package name where all the generated code is goGatewayNamespace string // The root directory for the gateway test config files testConfigsRootDir string // String containing copyright header to add to generated code. copyrightHeader string // annotation prefix to parse for thrift schema annotationPrefix string // The middlewares available for the endpoints middlewareSpecs map[string]*MiddlewareSpec // The default middlewares for all endpoints defaultMiddlewareSpecs map[string]*MiddlewareSpec // The default headers to forward with all requests when present defaultHeaders []string // Use deputy client when this header is set deputyReqHeader string // traceKey is the key for unique trace id that identifies request / response pair traceKey string // moduleSearchPaths is a dictionary of glob patterns for folders that contain *-config.yaml files moduleSearchPaths map[string][]string // defaultDependencies is a dictionary of glob patterns for default dependencies defaultDependencies map[string][]string } // NewDefaultPackageHelperOptions returns a new default PackageHelperOptions, all optional fields are set as default. func NewDefaultPackageHelperOptions() *PackageHelperOptions { return &PackageHelperOptions{} } // PackageHelperOptions set the optional configurations for the project package. // Each field is optional, if not set, it is set to the corresponding default value. type PackageHelperOptions struct { // relative path to the idl dir, defaults to "./idl" RelIdlRootDir string // subdir for idl per module ModuleIdlSubDir map[string]string // relative path to the target dir that will contain generated code, defaults to "./build" RelTargetGenDir string // relative path to the middleware config dir, defaults to "" RelMiddlewareConfigDir string // relative path to the default middleware config dir, defaults to "" RelDefaultMiddlewareConfigDir string // map of idl type to package path of the generated code // map should be keyed with idl file extension, e.g., .thrift, .proto GenCodePackage map[string]string // thrift http annotation prefix, defaults to "zanzibar" AnnotationPrefix string // copyright header in generated code, defaults to "" CopyrightHeader string // header key to redirect client requests to staging environment, defaults to "X-Zanzibar-Use-Staging" StagingReqHeader string // The default headers to forward with all requests when present DefaultHeaders []string // header key to redirect client requests to local environment, defaults to "x-deputy-forwarded" DeputyReqHeader string // header key to uniquely identifies request/response pair, defaults to "x-trace-id" TraceKey string // ModuleSearchPaths is a dictionary of glob patterns for folders that contain *-config.yaml files ModuleSearchPaths map[string][]string // DefaultDependencies is a dictionary of glob patterns for folders that contain default dependencies DefaultDependencies map[string][]string // key to read qps levels or not in the endpoint levels QPSLevelsEnabled bool // key to enable custom initialisation CustomInitialisationEnabled bool } func (p *PackageHelperOptions) relTargetGenDir() string { if p.RelTargetGenDir != "" { return p.RelTargetGenDir } return "./build" } func (p *PackageHelperOptions) relIdlRootDir() string { if p.RelIdlRootDir != "" { return p.RelIdlRootDir } return "./idl" } func (p *PackageHelperOptions) moduleIdlSubDir() map[string]string { return p.ModuleIdlSubDir } func (p *PackageHelperOptions) relMiddlewareConfigDir() string { return p.RelMiddlewareConfigDir } func (p *PackageHelperOptions) relDefaultMiddlewareConfigDir() string { return p.RelDefaultMiddlewareConfigDir } func (p *PackageHelperOptions) genCodePackage(packageRoot string) map[string]string { if p.GenCodePackage != nil { return p.GenCodePackage } defaultGenCodePath := path.Join(packageRoot, p.relTargetGenDir(), "gen-code") return map[string]string{ "thrift": defaultGenCodePath, "proto": defaultGenCodePath, } } func (p *PackageHelperOptions) annotationPrefix() string { if p.AnnotationPrefix != "" { return p.AnnotationPrefix } return "zanzibar" } func (p *PackageHelperOptions) copyrightHeader() string { if p.CopyrightHeader != "" { return p.CopyrightHeader } return "" } func (p *PackageHelperOptions) deputyReqHeader() string { if p.DeputyReqHeader != "" { return p.DeputyReqHeader } return "x-deputy-forwarded" } func (p *PackageHelperOptions) traceKey() string { if p.TraceKey != "" { return p.TraceKey } return "x-uber-id" } // NewPackageHelper creates a package helper. // packageRoot is the project root package path, configRoot is the project config dir path. // options can be nil, in which case all optional settings for the package are set to default values. func NewPackageHelper( packageRoot string, configRoot string, options *PackageHelperOptions, ) (*PackageHelper, error) { absConfigRoot, err := filepath.Abs(configRoot) if err != nil { return nil, errors.Errorf( "%s is not valid path: %s", configRoot, err, ) } if options == nil { options = NewDefaultPackageHelperOptions() } goGatewayNamespace := path.Join(packageRoot, options.relTargetGenDir()) middlewareSpecs, err := parseMiddlewareConfig(options.relMiddlewareConfigDir(), absConfigRoot) if err != nil { return nil, errors.Wrapf(err, "cannot load middlewares") } defaultMiddlewareSpecs, err := parseDefaultMiddlewareConfig( options.relDefaultMiddlewareConfigDir(), absConfigRoot) if err != nil { return nil, errors.Wrapf(err, "cannot load default middlewares") } moduleIdlSubDir := options.moduleIdlSubDir() if len(moduleIdlSubDir) == 0 { moduleIdlSubDir = map[string]string{endpointModuleName: ".", defaultModuleFallback: "."} } p := &PackageHelper{ packageRoot: packageRoot, configRoot: absConfigRoot, idlRootDir: filepath.Join(absConfigRoot, options.relIdlRootDir()), genCodePackage: options.genCodePackage(packageRoot), goGatewayNamespace: goGatewayNamespace, targetGenDir: filepath.Join(absConfigRoot, options.relTargetGenDir()), copyrightHeader: options.copyrightHeader(), middlewareSpecs: middlewareSpecs, defaultMiddlewareSpecs: defaultMiddlewareSpecs, annotationPrefix: options.annotationPrefix(), deputyReqHeader: options.deputyReqHeader(), traceKey: options.traceKey(), moduleSearchPaths: options.ModuleSearchPaths, defaultDependencies: options.DefaultDependencies, defaultHeaders: options.DefaultHeaders, moduleIdlSubDir: moduleIdlSubDir, } return p, nil } // PackageRoot returns the service's root package name func (p PackageHelper) PackageRoot() string { return p.packageRoot } // ConfigRoot returns the service's absolute config root path func (p PackageHelper) ConfigRoot() string { return p.configRoot } // MiddlewareSpecs returns a map of middlewares available func (p PackageHelper) MiddlewareSpecs() map[string]*MiddlewareSpec { return p.middlewareSpecs } // DefaultMiddlewareSpecs returns a map of default middlewares func (p PackageHelper) DefaultMiddlewareSpecs() map[string]*MiddlewareSpec { return p.defaultMiddlewareSpecs } // GenCodePackage returns map of idl type to the file path of the idl generated code folder func (p PackageHelper) GenCodePackage() map[string]string { return p.genCodePackage } // TypeImportPath returns the Go import path for types defined in a idlFile file. func (p PackageHelper) TypeImportPath(idlFile string) (string, error) { if !strings.HasSuffix(idlFile, thriftExtension) && !strings.HasSuffix(idlFile, protoExtension) { return "", errors.Errorf("idl file %s is not %s or %s", idlFile, thriftExtension, protoExtension) } var suffix string // for a filepath: a/b/c.(thrift|proto) // - thrift generates code in path: a/b/c/c.go // - proto generates code in path: a/b/c.go if strings.HasSuffix(idlFile, thriftExtension) { suffix = strings.TrimSuffix(idlFile, thriftExtension) } else { suffix = filepath.Dir(idlFile) } idx := strings.Index(idlFile, p.idlRootDir) if idx == -1 { return "", errors.Errorf( "file %s is not in IDL dir (%s)", idlFile, p.idlRootDir, ) } ext := filepath.Ext(idlFile) genCodePkg, ok := p.genCodePackage[ext] if !ok { return "", errors.Errorf("genCodePackage for %q idl file is not configured in build.yaml", ext) } return path.Join( genCodePkg, idlFile[idx+len(p.idlRootDir):len(suffix)], ), nil } // GoGatewayPackageName returns the name of the gateway package func (p PackageHelper) GoGatewayPackageName() string { return p.goGatewayNamespace } // IdlPath returns the file path to the thrift idl folder func (p PackageHelper) IdlPath() string { return p.idlRootDir } // CodeGenTargetPath returns the file path where the code should // be generated. func (p PackageHelper) CodeGenTargetPath() string { return p.targetGenDir } // TypePackageName returns the package name that defines the type. func (p PackageHelper) TypePackageName(idlFile string) (string, error) { if !strings.HasSuffix(idlFile, thriftExtension) && !strings.HasSuffix(idlFile, protoExtension) { return "", errors.Errorf("file %s is not %s or %s", idlFile, thriftExtension, protoExtension) } idx := strings.Index(idlFile, p.idlRootDir) if idx == -1 { return "", errors.Errorf( "file %s is not in IDL dir (%s)", idlFile, p.idlRootDir, ) } var prefix string if strings.HasSuffix(idlFile, thriftExtension) { prefix = strings.TrimSuffix(idlFile, thriftExtension) } else { prefix = strings.TrimSuffix(idlFile, protoExtension) } // Strip the leading / and the trailing .thrift/.proto suffix. segment := idlFile[idx+len(p.idlRootDir)+1 : len(prefix)] packageName := strings.Replace(segment, "/", "_", -1) return CamelCase(packageName), nil } func (p PackageHelper) getRelativeFileName(idlFile string) (string, error) { if !strings.HasSuffix(idlFile, thriftExtension) && !strings.HasSuffix(idlFile, protoExtension) { return "", errors.Errorf("file %s is not %s or %s", idlFile, thriftExtension, protoExtension) } idx := strings.Index(idlFile, p.idlRootDir) if idx == -1 { return "", errors.Errorf( "file %s is not in IDL dir (%s)", idlFile, p.idlRootDir, ) } return idlFile[idx+len(p.idlRootDir):], nil } // GetModuleIdlSubDir returns subdir for idl per module func (p PackageHelper) GetModuleIdlSubDir(isEndpoint bool) string { className := p.getModuleClass(isEndpoint) if subDir, ok := p.moduleIdlSubDir[className]; ok { return subDir } panic(fmt.Sprintf("unrecognized module %s", className)) } func (p PackageHelper) getModuleClass(isEndpoint bool) string { if isEndpoint { return endpointModuleName } return defaultModuleFallback } // TargetClientsInitPath returns where the clients init should go func (p PackageHelper) TargetClientsInitPath() string { return path.Join(p.targetGenDir, "clients", "clients.go") } // TargetEndpointsRegisterPath returns where the endpoints register file // should be written to func (p PackageHelper) TargetEndpointsRegisterPath() string { return path.Join(p.targetGenDir, "endpoints", "register.go") } // EndpointTestConfigPath returns the path for the endpoint test configs func (p PackageHelper) EndpointTestConfigPath( serviceName, methodName string, ) string { fileName := strings.ToLower(methodName) + "_test.yaml" return path.Join(p.testConfigsRootDir, strings.ToLower(serviceName), fileName) } // TypeFullName returns the referred Go type name in generated code. func (p PackageHelper) TypeFullName(typeSpec compile.TypeSpec) (string, error) { if typeSpec == nil { return "", nil } return GoType(p, typeSpec) } // DeputyReqHeader returns the header name that will be checked to determine // if a request should go to the deputy downstream client func (p PackageHelper) DeputyReqHeader() string { return p.deputyReqHeader } // DefaultHeaders returns a list of headers that will always be forwarded // when present in the incoming request. func (p PackageHelper) DefaultHeaders() []string { return p.defaultHeaders }