github.com/Minish144/prototool-arm64@v1.3.0/internal/cmd/templates.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package cmd 22 23 import ( 24 "fmt" 25 "io" 26 "os" 27 "strings" 28 29 wordwrap "github.com/mitchellh/go-wordwrap" 30 "github.com/spf13/cobra" 31 "github.com/spf13/pflag" 32 "github.com/uber/prototool/internal/exec" 33 "go.uber.org/zap" 34 "go.uber.org/zap/zapcore" 35 ) 36 37 const wordWrapLength uint = 80 38 39 var ( 40 allCmdTemplate = &cmdTemplate{ 41 Use: "all [dirOrFile]", 42 Short: "Compile, then format and overwrite, then re-compile and generate, then lint, stopping if any step fails.", 43 Args: cobra.MaximumNArgs(1), 44 Run: func(runner exec.Runner, args []string, flags *flags) error { 45 return runner.All(args, flags.disableFormat, flags.disableLint, flags.fix) 46 }, 47 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 48 flags.bindConfigData(flagSet) 49 flags.bindDisableFormat(flagSet) 50 flags.bindDisableLint(flagSet) 51 flags.bindJSON(flagSet) 52 flags.bindFix(flagSet) 53 flags.bindProtocURL(flagSet) 54 flags.bindProtocBinPath(flagSet) 55 flags.bindProtocWKTPath(flagSet) 56 }, 57 } 58 59 binaryToJSONCmdTemplate = &cmdTemplate{ 60 Use: "binary-to-json [dirOrFile] messagePath data", 61 Short: "Convert the data from json to binary for the message path and data.", 62 Args: cobra.RangeArgs(2, 3), 63 Run: func(runner exec.Runner, args []string, flags *flags) error { 64 return runner.BinaryToJSON(args) 65 }, 66 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 67 flags.bindConfigData(flagSet) 68 }, 69 } 70 71 cleanCmdTemplate = &cmdTemplate{ 72 Use: "clean", 73 Short: "Delete the cache.", 74 Args: cobra.NoArgs, 75 Run: func(runner exec.Runner, args []string, flags *flags) error { 76 return runner.Clean() 77 }, 78 } 79 80 compileCmdTemplate = &cmdTemplate{ 81 Use: "compile [dirOrFile]", 82 Short: "Compile with protoc to check for failures.", 83 Long: `Stubs will not be generated. To generate stubs, use the "gen" command. Calling "compile" has the effect of calling protoc with "-o /dev/null".`, 84 Args: cobra.MaximumNArgs(1), 85 Run: func(runner exec.Runner, args []string, flags *flags) error { 86 return runner.Compile(args, flags.dryRun) 87 }, 88 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 89 flags.bindConfigData(flagSet) 90 flags.bindDryRun(flagSet) 91 flags.bindJSON(flagSet) 92 flags.bindProtocURL(flagSet) 93 flags.bindProtocBinPath(flagSet) 94 flags.bindProtocWKTPath(flagSet) 95 }, 96 } 97 98 createCmdTemplate = &cmdTemplate{ 99 Use: "create files...", 100 Short: "Create the given Protobuf files according to a template that passes default prototool lint.", 101 Long: `Assuming the filename "example_create_file.proto", the file will look like the following: 102 103 syntax = "proto3"; 104 105 package SOME.PKG; 106 107 option go_package = "PKGpb"; 108 option java_multiple_files = true; 109 option java_outer_classname = "ExampleCreateFileProto"; 110 option java_package = "com.SOME.PKG.pb"; 111 112 This matches what the linter expects. "SOME.PKG" will be computed as follows: 113 114 - If "--package" is specified, "SOME.PKG" will be the value passed to 115 "--package". 116 - Otherwise, if there is no "prototool.yaml" or "prototool.json" that would 117 apply to the new file, use "uber.prototool.generated". 118 - Otherwise, if there is a "prototool.yaml" or "prototool.json" file, check if 119 it has a "packages" setting under the "create" section. If it does, this 120 package, concatenated with the relative path from the directory with the 121 "prototool.yaml" or "prototool.json" will be used. 122 - Otherwise, if there is no "packages" directive, just use the 123 relative path from the directory with the "prototool.yaml" or 124 "prototool.json" file. If the file is in the same directory as the 125 "prototool.yaml" or "prototool.json" file, use "uber.prototool.generated". 126 127 For example, assume you have the following file at "repo/prototool.yaml": 128 129 create: 130 packages: 131 - directory: idl 132 name: uber 133 - directory: idl/baz 134 name: special 135 136 - "prototool create repo/idl/foo/bar/bar.proto" will have the package 137 "uber.foo.bar". 138 - "prototool create repo/idl/bar.proto" will have the package "uber". 139 - "prototool create repo/idl/baz/baz.proto" will have the package "special". 140 - "prototool create repo/idl/baz/bat/bat.proto" will have the package 141 "special.bat". 142 - "prototool create repo/another/dir/bar.proto" will have the package 143 "another.dir". 144 - "prototool create repo/bar.proto" will have the package 145 "uber.prototool.generated". 146 147 This is meant to mimic what you generally want - a base package for your idl directory, followed by packages matching the directory structure. 148 149 Note you can override the directory that the "prototool.yaml" or "prototool.json" file is in as well. If we update our file at "repo/prototool.yaml" to this: 150 151 create: 152 packages: 153 - directory: . 154 name: foo.bar 155 156 Then "prototool create repo/bar.proto" will have the package "foo.bar", and "prototool create repo/another/dir/bar.proto" will have the package "foo.bar.another.dir". 157 158 If Vim integration is set up, files will be generated when you open a new Protobuf file.`, 159 Args: cobra.MinimumNArgs(1), 160 Run: func(runner exec.Runner, args []string, flags *flags) error { 161 return runner.Create(args, flags.pkg) 162 }, 163 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 164 flags.bindConfigData(flagSet) 165 flags.bindPackage(flagSet) 166 }, 167 } 168 169 descriptorProtoCmdTemplate = &cmdTemplate{ 170 Use: "descriptor-proto [dirOrFile] messagePath", 171 Short: "Get the descriptor proto for the message path.", 172 Args: cobra.MaximumNArgs(2), 173 Run: func(runner exec.Runner, args []string, flags *flags) error { 174 return runner.DescriptorProto(args) 175 }, 176 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 177 flags.bindConfigData(flagSet) 178 }, 179 } 180 181 downloadCmdTemplate = &cmdTemplate{ 182 Use: "download", 183 Short: "Download the protobuf artifacts to a cache.", 184 Args: cobra.NoArgs, 185 Run: func(runner exec.Runner, args []string, flags *flags) error { 186 return runner.Download() 187 }, 188 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 189 flags.bindConfigData(flagSet) 190 }, 191 } 192 193 fieldDescriptorProtoCmdTemplate = &cmdTemplate{ 194 Use: "field-descriptor-proto [dirOrFile] fieldPath", 195 Short: "Get the field descriptor proto for the field path.", 196 Args: cobra.RangeArgs(1, 2), 197 Run: func(runner exec.Runner, args []string, flags *flags) error { 198 return runner.FieldDescriptorProto(args) 199 }, 200 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 201 flags.bindConfigData(flagSet) 202 }, 203 } 204 205 filesCmdTemplate = &cmdTemplate{ 206 Use: "files [dirOrFile]", 207 Short: "Print all files that match the input arguments.", 208 Args: cobra.MaximumNArgs(1), 209 Run: func(runner exec.Runner, args []string, flags *flags) error { 210 return runner.Files(args) 211 }, 212 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 213 flags.bindConfigData(flagSet) 214 }, 215 } 216 217 formatCmdTemplate = &cmdTemplate{ 218 Use: "format [dirOrFile]", 219 Short: "Format a proto file and compile with protoc to check for failures.", 220 Args: cobra.MaximumNArgs(1), 221 Run: func(runner exec.Runner, args []string, flags *flags) error { 222 return runner.Format(args, flags.overwrite, flags.diffMode, flags.lintMode, flags.fix) 223 }, 224 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 225 flags.bindConfigData(flagSet) 226 flags.bindDiffMode(flagSet) 227 flags.bindJSON(flagSet) 228 flags.bindLintMode(flagSet) 229 flags.bindOverwrite(flagSet) 230 flags.bindFix(flagSet) 231 flags.bindProtocURL(flagSet) 232 flags.bindProtocBinPath(flagSet) 233 flags.bindProtocWKTPath(flagSet) 234 }, 235 } 236 237 generateCmdTemplate = &cmdTemplate{ 238 Use: "generate [dirOrFile]", 239 Short: "Generate with protoc.", 240 Args: cobra.MaximumNArgs(1), 241 Run: func(runner exec.Runner, args []string, flags *flags) error { 242 return runner.Gen(args, flags.dryRun) 243 }, 244 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 245 flags.bindConfigData(flagSet) 246 flags.bindDryRun(flagSet) 247 flags.bindJSON(flagSet) 248 flags.bindProtocURL(flagSet) 249 flags.bindProtocBinPath(flagSet) 250 flags.bindProtocWKTPath(flagSet) 251 }, 252 } 253 254 grpcCmdTemplate = &cmdTemplate{ 255 Use: "grpc [dirOrFile]", 256 Short: "Call a gRPC endpoint. Be sure to set required flags address, method, and either data or stdin.", 257 Long: `This command compiles your proto files with "protoc", converts JSON input to binary and converts the result from binary to JSON. All these steps take on the order of milliseconds. For example, the overhead for a file with four dependencies is about 30ms, so there is little overhead for CLI calls to gRPC. 258 259 There is a full example for gRPC in the example directory of Prototool. Run "make init example" to make sure everything is installed and generated. 260 261 Start the example server in a separate terminal by doing "go run example/cmd/excited/main.go". 262 263 prototool grpc [dirOrFile] \ 264 --address serverAddress \ 265 --method package.service/Method \ 266 --data 'requestData' 267 268 Either use "--data 'requestData'" as the the JSON data to input, or "--stdin" which will result in the input being read from stdin as JSON. 269 270 $ make init example # make sure everything is built just in case 271 272 $ prototool grpc example \ 273 --address 0.0.0.0:8080 \ 274 --method foo.ExcitedService/Exclamation \ 275 --data '{"value":"hello"}' 276 { 277 "value": "hello!" 278 } 279 280 $ prototool grpc example \ 281 --address 0.0.0.0:8080 \ 282 --method foo.ExcitedService/ExclamationServerStream \ 283 --data '{"value":"hello"}' 284 { 285 "value": "h" 286 } 287 { 288 "value": "e" 289 } 290 { 291 "value": "l" 292 } 293 { 294 "value": "l" 295 } 296 { 297 "value": "o" 298 } 299 { 300 "value": "!" 301 } 302 303 $ cat input.json 304 {"value":"hello"} 305 {"value":"salutations"} 306 307 $ cat input.json | prototool grpc example \ 308 --address 0.0.0.0:8080 \ 309 --method foo.ExcitedService/ExclamationClientStream \ 310 --stdin 311 { 312 "value": "hellosalutations!" 313 } 314 315 $ cat input.json | prototool grpc example \ 316 --address 0.0.0.0:8080 \ 317 --method foo.ExcitedService/ExclamationBidiStream \ 318 --stdin 319 { 320 "value": "hello!" 321 } 322 { 323 "value": "salutations!" 324 }`, 325 Args: cobra.MaximumNArgs(1), 326 Run: func(runner exec.Runner, args []string, flags *flags) error { 327 return runner.GRPC(args, flags.headers, flags.address, flags.method, flags.data, flags.callTimeout, flags.connectTimeout, flags.keepaliveTime, flags.stdin) 328 }, 329 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 330 flags.bindConfigData(flagSet) 331 flags.bindAddress(flagSet) 332 flags.bindCallTimeout(flagSet) 333 flags.bindConnectTimeout(flagSet) 334 flags.bindData(flagSet) 335 flags.bindHeaders(flagSet) 336 flags.bindKeepaliveTime(flagSet) 337 flags.bindMethod(flagSet) 338 flags.bindStdin(flagSet) 339 flags.bindProtocURL(flagSet) 340 flags.bindProtocBinPath(flagSet) 341 flags.bindProtocWKTPath(flagSet) 342 }, 343 } 344 345 configInitCmdTemplate = &cmdTemplate{ 346 Use: "init [dirPath]", 347 Short: "Generate an initial config file in the current or given directory.", 348 Long: `All available options will be generated and commented out except for "protoc.version". Pass the "--uncomment" flag to uncomment all options.`, 349 Args: cobra.MaximumNArgs(1), 350 Run: func(runner exec.Runner, args []string, flags *flags) error { 351 return runner.Init(args, flags.uncomment) 352 }, 353 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 354 flags.bindUncomment(flagSet) 355 }, 356 } 357 358 jsonToBinaryCmdTemplate = &cmdTemplate{ 359 Use: "json-to-binary [dirOrFile] messagePath data", 360 Short: "Convert the data from json to binary for the message path and data.", 361 Args: cobra.RangeArgs(2, 3), 362 Run: func(runner exec.Runner, args []string, flags *flags) error { 363 return runner.JSONToBinary(args) 364 }, 365 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 366 flags.bindConfigData(flagSet) 367 }, 368 } 369 370 lintCmdTemplate = &cmdTemplate{ 371 Use: "lint [dirOrFile]", 372 Short: "Lint proto files and compile with protoc to check for failures.", 373 Long: `The default rule set follows the Style Guide at https://github.com/uber/prototool/blob/master/etc/style/uber/uber.proto. You can add or exclude lint rules in your configuration file. The default rule set is very strict and is meant to enforce consistent development patterns.`, 374 Args: cobra.MaximumNArgs(1), 375 Run: func(runner exec.Runner, args []string, flags *flags) error { 376 return runner.Lint(args, flags.listAllLinters, flags.listLinters) 377 }, 378 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 379 flags.bindConfigData(flagSet) 380 flags.bindJSON(flagSet) 381 flags.bindListAllLinters(flagSet) 382 flags.bindListLinters(flagSet) 383 flags.bindProtocURL(flagSet) 384 flags.bindProtocBinPath(flagSet) 385 flags.bindProtocWKTPath(flagSet) 386 }, 387 } 388 389 listAllLintGroupsCmdTemplate = &cmdTemplate{ 390 Use: "list-all-lint-groups", 391 Short: "List all the available lint groups.", 392 Args: cobra.NoArgs, 393 Run: func(runner exec.Runner, args []string, flags *flags) error { 394 return runner.ListAllLintGroups() 395 }, 396 } 397 398 listLintGroupCmdTemplate = &cmdTemplate{ 399 Use: "list-lint-group group", 400 Short: "List the linters in the given lint group.", 401 Args: cobra.ExactArgs(1), 402 Run: func(runner exec.Runner, args []string, flags *flags) error { 403 return runner.ListLintGroup(args[0]) 404 }, 405 } 406 407 serviceDescriptorProtoCmdTemplate = &cmdTemplate{ 408 Use: "service-descriptor-proto [dirOrFile] servicePath", 409 Short: "Get the service descriptor proto for the service path.", 410 Args: cobra.RangeArgs(1, 2), 411 Run: func(runner exec.Runner, args []string, flags *flags) error { 412 return runner.ServiceDescriptorProto(args) 413 }, 414 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 415 flags.bindConfigData(flagSet) 416 }, 417 } 418 419 versionCmdTemplate = &cmdTemplate{ 420 Use: "version", 421 Short: "Print the version.", 422 Args: cobra.NoArgs, 423 BindFlags: func(flagSet *pflag.FlagSet, flags *flags) { 424 flags.bindJSON(flagSet) 425 }, 426 Run: func(runner exec.Runner, args []string, flags *flags) error { 427 return runner.Version() 428 }, 429 } 430 ) 431 432 // cmdTemplate contains the static parts of a cobra.Command such as 433 // documentation that we want to store outside of runtime creation. 434 // 435 // We do not just store cobra.Commands as in theory they have fields 436 // with types such as slices that if we were to return a blind copy, 437 // would mean that both the global cmdTemplate and the runtime 438 // cobra.Command would point to the same location. By making a new 439 // struct, we can also do more fancy templating things like prepending 440 // the Short description to the Long description for consistency, and 441 // have our own abstractions for the Run command. 442 type cmdTemplate struct { 443 // Use is the one-line usage message. 444 // This field is required. 445 Use string 446 // Short is the short description shown in the 'help' output. 447 // This field is required. 448 Short string 449 // Long is the long message shown in the 'help <this-command>' output. 450 // The Short field will be prepended to the Long field with a newline 451 // when applied to a *cobra.Command. 452 // This field is optional. 453 Long string 454 // Expected arguments. 455 // This field is optional. 456 Args cobra.PositionalArgs 457 // Run is the command to run given an exec.Runner, args, and flags. 458 // This field is required. 459 Run func(exec.Runner, []string, *flags) error 460 // BindFlags binds flags to the *pflag.FlagSet on Build. 461 // There is no corollary to this on *cobra.Command. 462 // This field is optional, although usually will be set. 463 // We need to do this before run as the flags are populated 464 // before Run is called. 465 BindFlags func(*pflag.FlagSet, *flags) 466 } 467 468 // Build builds a *cobra.Command from the cmdTemplate. 469 func (c *cmdTemplate) Build(exitCodeAddr *int, stdin io.Reader, stdout io.Writer, stderr io.Writer, flags *flags) *cobra.Command { 470 command := &cobra.Command{} 471 command.Use = c.Use 472 command.Short = strings.TrimSpace(c.Short) 473 if c.Long != "" { 474 command.Long = wordwrap.WrapString(fmt.Sprintf("%s\n\n%s", strings.TrimSpace(c.Short), strings.TrimSpace(c.Long)), wordWrapLength) 475 } 476 command.Args = c.Args 477 command.Run = func(_ *cobra.Command, args []string) { 478 checkCmd(exitCodeAddr, stdin, stdout, stderr, args, flags, c.Run) 479 } 480 if c.BindFlags != nil { 481 c.BindFlags(command.PersistentFlags(), flags) 482 } 483 return command 484 } 485 486 func checkCmd(exitCodeAddr *int, stdin io.Reader, stdout io.Writer, stderr io.Writer, args []string, flags *flags, f func(exec.Runner, []string, *flags) error) { 487 runner, err := getRunner(stdin, stdout, stderr, flags) 488 if err != nil { 489 *exitCodeAddr = printAndGetErrorExitCode(err, stdout) 490 return 491 } 492 if err := f(runner, args, flags); err != nil { 493 *exitCodeAddr = printAndGetErrorExitCode(err, stdout) 494 } 495 } 496 497 func getRunner(stdin io.Reader, stdout io.Writer, stderr io.Writer, flags *flags) (exec.Runner, error) { 498 logger, err := getLogger(stderr, flags.debug) 499 if err != nil { 500 return nil, err 501 } 502 runnerOptions := []exec.RunnerOption{ 503 exec.RunnerWithLogger(logger), 504 } 505 if flags.cachePath != "" { 506 runnerOptions = append( 507 runnerOptions, 508 exec.RunnerWithCachePath(flags.cachePath), 509 ) 510 } 511 if flags.configData != "" { 512 runnerOptions = append( 513 runnerOptions, 514 exec.RunnerWithConfigData(flags.configData), 515 ) 516 } 517 if flags.json { 518 runnerOptions = append( 519 runnerOptions, 520 exec.RunnerWithJSON(), 521 ) 522 } 523 if flags.protocBinPath != "" { 524 runnerOptions = append( 525 runnerOptions, 526 exec.RunnerWithProtocBinPath(flags.protocBinPath), 527 ) 528 } 529 if flags.protocWKTPath != "" { 530 runnerOptions = append( 531 runnerOptions, 532 exec.RunnerWithProtocWKTPath(flags.protocWKTPath), 533 ) 534 } 535 if flags.printFields != "" { 536 runnerOptions = append( 537 runnerOptions, 538 exec.RunnerWithPrintFields(flags.printFields), 539 ) 540 } 541 if flags.protocURL != "" { 542 runnerOptions = append( 543 runnerOptions, 544 exec.RunnerWithProtocURL(flags.protocURL), 545 ) 546 } 547 workDirPath, err := os.Getwd() 548 if err != nil { 549 return nil, err 550 } 551 return exec.NewRunner(workDirPath, stdin, stdout, runnerOptions...), nil 552 } 553 554 func getLogger(stderr io.Writer, debug bool) (*zap.Logger, error) { 555 level := zapcore.InfoLevel 556 if debug { 557 level = zapcore.DebugLevel 558 } 559 return zap.New( 560 zapcore.NewCore( 561 zapcore.NewConsoleEncoder( 562 zap.NewDevelopmentEncoderConfig(), 563 ), 564 zapcore.Lock(zapcore.AddSync(stderr)), 565 zap.NewAtomicLevelAt(level), 566 ), 567 ), nil 568 }