/* * 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 app import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/urfave/cli/v2" "std.gaore.com/Gaore-Go/gr_hz/config" "std.gaore.com/Gaore-Go/gr_hz/generator" "std.gaore.com/Gaore-Go/gr_hz/meta" "std.gaore.com/Gaore-Go/gr_hz/protobuf" "std.gaore.com/Gaore-Go/gr_hz/thrift" "std.gaore.com/Gaore-Go/gr_hz/util" "std.gaore.com/Gaore-Go/gr_hz/util/logs" ) // global args. MUST fork it when use var globalArgs = config.NewArgument() func New(c *cli.Context) error { args, err := globalArgs.Parse(c, meta.CmdNew) if err != nil { return cli.Exit(err, meta.LoadError) } setLogVerbose(args.Verbose) logs.Debugf("args: %#v\n", args) exist, err := util.PathExist(filepath.Join(args.OutDir, meta.ManifestFile)) if err != nil { return cli.Exit(err, meta.LoadError) } if exist && !args.ForceNew { return cli.Exit(fmt.Errorf("the current is already a hertz project, if you want to regenerate it you can specify \"-force\""), meta.LoadError) } err = GenerateLayout(args) if err != nil { return cli.Exit(err, meta.GenerateLayoutError) } err = TriggerPlugin(args) if err != nil { return cli.Exit(err, meta.PluginError) } // ".hz" file converges to the hz tool manifest := new(meta.Manifest) args.InitManifest(manifest) err = manifest.Persist(args.OutDir) if err != nil { return cli.Exit(fmt.Errorf("persist manifest failed: %v", err), meta.PersistError) } if !args.NeedGoMod && args.IdlType == meta.IdlThrift { logs.Warn(meta.AddThriftReplace) } return nil } func Update(c *cli.Context) error { // begin to update args, err := globalArgs.Parse(c, meta.CmdUpdate) if err != nil { return cli.Exit(err, meta.LoadError) } setLogVerbose(args.Verbose) logs.Debugf("Args: %#v\n", args) manifest := new(meta.Manifest) err = manifest.InitAndValidate(args.OutDir) if err != nil { return cli.Exit(err, meta.LoadError) } // update argument by ".hz", can automatically get "handler_dir"/"model_dir"/"router_dir" args.UpdateByManifest(manifest) err = TriggerPlugin(args) if err != nil { return cli.Exit(err, meta.PluginError) } // If the "handler_dir"/"model_dir" is updated, write it back to ".hz" args.UpdateManifest(manifest) err = manifest.Persist(args.OutDir) if err != nil { return cli.Exit(fmt.Errorf("persist manifest failed: %v", err), meta.PersistError) } return nil } func Model(c *cli.Context) error { args, err := globalArgs.Parse(c, meta.CmdModel) if err != nil { return cli.Exit(err, meta.LoadError) } setLogVerbose(args.Verbose) logs.Debugf("Args: %#v\n", args) err = TriggerPlugin(args) if err != nil { return cli.Exit(err, meta.PluginError) } return nil } func Client(c *cli.Context) error { args, err := globalArgs.Parse(c, meta.CmdClient) if err != nil { return cli.Exit(err, meta.LoadError) } setLogVerbose(args.Verbose) logs.Debugf("Args: %#v\n", args) err = TriggerPlugin(args) if err != nil { return cli.Exit(err, meta.PluginError) } return nil } func PluginMode() { mode := os.Getenv(meta.EnvPluginMode) if len(os.Args) <= 1 && mode != "" { switch mode { case meta.ThriftPluginName: plugin := new(thrift.Plugin) os.Exit(plugin.Run()) case meta.ProtocPluginName: plugin := new(protobuf.Plugin) os.Exit(plugin.Run()) } } } func Init() *cli.App { // flags verboseFlag := cli.BoolFlag{Name: "verbose,vv", Usage: "turn on verbose mode", Destination: &globalArgs.Verbose} idlFlag := cli.StringSliceFlag{Name: "idl", Usage: "Specify the IDL file path. (.thrift or .proto)"} moduleFlag := cli.StringFlag{Name: "module", Aliases: []string{"mod"}, Usage: "Specify the Go module name.", Destination: &globalArgs.Gomod} serviceNameFlag := cli.StringFlag{Name: "service", Usage: "Specify the service name.", Destination: &globalArgs.ServiceName} outDirFlag := cli.StringFlag{Name: "out_dir", Usage: "Specify the project path.", Destination: &globalArgs.OutDir} handlerDirFlag := cli.StringFlag{Name: "handler_dir", Usage: "Specify the handler relative path (based on \"out_dir\").", Destination: &globalArgs.HandlerDir} modelDirFlag := cli.StringFlag{Name: "model_dir", Usage: "Specify the model relative path (based on \"out_dir\").", Destination: &globalArgs.ModelDir} routerDirFlag := cli.StringFlag{Name: "router_dir", Usage: "Specify the router relative path (based on \"out_dir\").", Destination: &globalArgs.RouterDir} useFlag := cli.StringFlag{Name: "use", Usage: "Specify the model package to import for handler.", Destination: &globalArgs.Use} baseDomainFlag := cli.StringFlag{Name: "base_domain", Usage: "Specify the request domain.", Destination: &globalArgs.BaseDomain} clientDirFlag := cli.StringFlag{Name: "client_dir", Usage: "Specify the client path. If not specified, IDL generated path is used for 'client' command; no client code is generated for 'new' command", Destination: &globalArgs.ClientDir} forceClientDirFlag := cli.StringFlag{Name: "force_client_dir", Usage: "Specify the client path, and won't use namespaces as subpaths", Destination: &globalArgs.ForceClientDir} optPkgFlag := cli.StringSliceFlag{Name: "option_package", Aliases: []string{"P"}, Usage: "Specify the package path. ({include_path}={import_path})"} includesFlag := cli.StringSliceFlag{Name: "proto_path", Aliases: []string{"I"}, Usage: "Add an IDL search path for includes. (Valid only if idl is protobuf)"} excludeFilesFlag := cli.StringSliceFlag{Name: "exclude_file", Aliases: []string{"E"}, Usage: "Specify the files that do not need to be updated."} thriftOptionsFlag := cli.StringSliceFlag{Name: "thriftgo", Aliases: []string{"t"}, Usage: "Specify arguments for the thriftgo. ({flag}={value})"} protoOptionsFlag := cli.StringSliceFlag{Name: "protoc", Aliases: []string{"p"}, Usage: "Specify arguments for the protoc. ({flag}={value})"} thriftPluginsFlag := cli.StringSliceFlag{Name: "thrift-plugins", Usage: "Specify plugins for the thriftgo. ({plugin_name}:{options})"} protoPluginsFlag := cli.StringSliceFlag{Name: "protoc-plugins", Usage: "Specify plugins for the protoc. ({plugin_name}:{options}:{out_dir})"} noRecurseFlag := cli.BoolFlag{Name: "no_recurse", Usage: "Generate master model only.", Destination: &globalArgs.NoRecurse} forceNewFlag := cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "Force new a project, which will overwrite the generated files", Destination: &globalArgs.ForceNew} enableExtendsFlag := cli.BoolFlag{Name: "enable_extends", Usage: "Parse 'extends' for thrift IDL", Destination: &globalArgs.EnableExtends} jsonEnumStrFlag := cli.BoolFlag{Name: "json_enumstr", Usage: "Use string instead of num for json enums when idl is thrift.", Destination: &globalArgs.JSONEnumStr} unsetOmitemptyFlag := cli.BoolFlag{Name: "unset_omitempty", Usage: "Remove 'omitempty' tag for generated struct.", Destination: &globalArgs.UnsetOmitempty} protoCamelJSONTag := cli.BoolFlag{Name: "pb_camel_json_tag", Usage: "Convert Name style for json tag to camel(Only works protobuf).", Destination: &globalArgs.ProtobufCamelJSONTag} snakeNameFlag := cli.BoolFlag{Name: "snake_tag", Usage: "Use snake_case style naming for tags. (Only works for 'form', 'query', 'json')", Destination: &globalArgs.SnakeName} rmTagFlag := cli.StringSliceFlag{Name: "rm_tag", Usage: "Remove the default tag(json/query/form). If the annotation tag is set explicitly, it will not be removed."} customLayout := cli.StringFlag{Name: "customize_layout", Usage: "Specify the path for layout template.", Destination: &globalArgs.CustomizeLayout} customLayoutData := cli.StringFlag{Name: "customize_layout_data_path", Usage: "Specify the path for layout template render data.", Destination: &globalArgs.CustomizeLayoutData} customPackage := cli.StringFlag{Name: "customize_package", Usage: "Specify the path for package template.", Destination: &globalArgs.CustomizePackage} handlerByMethod := cli.BoolFlag{Name: "handler_by_method", Usage: "Generate a separate handler file for each method.", Destination: &globalArgs.HandlerByMethod} // app app := cli.NewApp() app.Name = "hz" app.Usage = "A idl parser and code generator for Hertz projects" app.Version = meta.Version // The default separator for multiple parameters is modified to ";" app.SliceFlagSeparator = ";" // global flags app.Flags = []cli.Flag{ &verboseFlag, } // Commands app.Commands = []*cli.Command{ { Name: meta.CmdNew, Usage: "Generate a new Hertz project", Flags: []cli.Flag{ &idlFlag, &serviceNameFlag, &moduleFlag, &outDirFlag, &handlerDirFlag, &modelDirFlag, &routerDirFlag, &clientDirFlag, &useFlag, &includesFlag, &thriftOptionsFlag, &protoOptionsFlag, &optPkgFlag, &noRecurseFlag, &forceNewFlag, &enableExtendsFlag, &jsonEnumStrFlag, &unsetOmitemptyFlag, &protoCamelJSONTag, &snakeNameFlag, &rmTagFlag, &excludeFilesFlag, &customLayout, &customLayoutData, &customPackage, &handlerByMethod, &protoPluginsFlag, &thriftPluginsFlag, }, Action: New, }, { Name: meta.CmdUpdate, Usage: "Update an existing Hertz project", Flags: []cli.Flag{ &idlFlag, &moduleFlag, &outDirFlag, &handlerDirFlag, &modelDirFlag, &clientDirFlag, &useFlag, &includesFlag, &thriftOptionsFlag, &protoOptionsFlag, &optPkgFlag, &noRecurseFlag, &enableExtendsFlag, &jsonEnumStrFlag, &unsetOmitemptyFlag, &protoCamelJSONTag, &snakeNameFlag, &rmTagFlag, &excludeFilesFlag, &customPackage, &handlerByMethod, &protoPluginsFlag, &thriftPluginsFlag, }, Action: Update, }, { Name: meta.CmdModel, Usage: "Generate model code only", Flags: []cli.Flag{ &idlFlag, &moduleFlag, &outDirFlag, &modelDirFlag, &includesFlag, &thriftOptionsFlag, &protoOptionsFlag, &noRecurseFlag, &jsonEnumStrFlag, &unsetOmitemptyFlag, &protoCamelJSONTag, &snakeNameFlag, &rmTagFlag, &excludeFilesFlag, }, Action: Model, }, { Name: meta.CmdClient, Usage: "Generate hertz client based on IDL", Flags: []cli.Flag{ &idlFlag, &moduleFlag, &baseDomainFlag, &modelDirFlag, &clientDirFlag, &useFlag, &forceClientDirFlag, &includesFlag, &thriftOptionsFlag, &protoOptionsFlag, &noRecurseFlag, &enableExtendsFlag, &jsonEnumStrFlag, &unsetOmitemptyFlag, &protoCamelJSONTag, &snakeNameFlag, &rmTagFlag, &excludeFilesFlag, &customPackage, &protoPluginsFlag, &thriftPluginsFlag, }, Action: Client, }, } return app } func setLogVerbose(verbose bool) { if verbose { logs.SetLevel(logs.LevelDebug) } else { logs.SetLevel(logs.LevelWarn) } } func GenerateLayout(args *config.Argument) error { lg := &generator.LayoutGenerator{ TemplateGenerator: generator.TemplateGenerator{ OutputDir: args.OutDir, Excludes: args.Excludes, }, } layout := generator.Layout{ GoModule: args.Gomod, ServiceName: args.ServiceName, UseApacheThrift: args.IdlType == meta.IdlThrift, HasIdl: 0 != len(args.IdlPaths), ModelDir: args.ModelDir, HandlerDir: args.HandlerDir, RouterDir: args.RouterDir, NeedGoMod: args.NeedGoMod, } if args.CustomizeLayout == "" { // generate by default err := lg.GenerateByService(layout) if err != nil { return fmt.Errorf("generating layout failed: %v", err) } } else { // generate by customized layout configPath, dataPath := args.CustomizeLayout, args.CustomizeLayoutData logs.Infof("get customized layout info, layout_config_path: %s, template_data_path: %s", configPath, dataPath) exist, err := util.PathExist(configPath) if err != nil { return fmt.Errorf("check customized layout config file exist failed: %v", err) } if !exist { return errors.New("layout_config_path doesn't exist") } lg.ConfigPath = configPath // generate by service info if dataPath == "" { err := lg.GenerateByService(layout) if err != nil { return fmt.Errorf("generating layout failed: %v", err) } } else { // generate by customized data err := lg.GenerateByConfig(dataPath) if err != nil { return fmt.Errorf("generating layout failed: %v", err) } } } err := lg.Persist() if err != nil { return fmt.Errorf("generating layout failed: %v", err) } return nil } func TriggerPlugin(args *config.Argument) error { if len(args.IdlPaths) == 0 { return nil } cmd, err := config.BuildPluginCmd(args) if err != nil { return fmt.Errorf("build plugin command failed: %v", err) } compiler, err := config.IdlTypeToCompiler(args.IdlType) if err != nil { return fmt.Errorf("get compiler failed: %v", err) } logs.Debugf("begin to trigger plugin, compiler: %s, idl_paths: %v", compiler, args.IdlPaths) buf, err := cmd.CombinedOutput() if err != nil { out := strings.TrimSpace(string(buf)) if !strings.HasSuffix(out, meta.TheUseOptionMessage) { return fmt.Errorf("plugin %s_gen_hertz returns error: %v, cause:\n%v", compiler, err, string(buf)) } } // If len(buf) != 0, the plugin returned the log. if len(buf) != 0 { fmt.Println(string(buf)) } logs.Debugf("end run plugin %s_gen_hertz", compiler) return nil }