protogen: support passing command-line parameters to plugins

Add a protogen.Options struct for future expansion. Include a FlagSet
which will be populated with parameters passed to the plugin.

Change-Id: I26a13bbde7ce011135b9c151edd160f3b51b7f9a
Reviewed-on: https://go-review.googlesource.com/134696
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
This commit is contained in:
Damien Neil 2018-09-11 13:53:14 -07:00
parent 46abb57549
commit 3cf6e62f69
3 changed files with 100 additions and 18 deletions

View File

@ -11,6 +11,7 @@ import (
"compress/gzip" "compress/gzip"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"flag"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -24,7 +25,13 @@ import (
const protoPackage = "github.com/golang/protobuf/proto" const protoPackage = "github.com/golang/protobuf/proto"
func main() { func main() {
protogen.Run(func(gen *protogen.Plugin) error { var flags flag.FlagSet
// TODO: Decide what to do for backwards compatibility with plugins=grpc.
flags.String("plugins", "", "")
opts := &protogen.Options{
ParamFunc: flags.Set,
}
protogen.Run(opts, func(gen *protogen.Plugin) error {
for _, f := range gen.Files { for _, f := range gen.Files {
if !f.Generate { if !f.Generate {
continue continue

View File

@ -41,14 +41,16 @@ import (
// //
// If a failure occurs while reading or writing, Run prints an error to // If a failure occurs while reading or writing, Run prints an error to
// os.Stderr and calls os.Exit(1). // os.Stderr and calls os.Exit(1).
func Run(f func(*Plugin) error) { //
if err := run(f); err != nil { // Passing a nil options is equivalent to passing a zero-valued one.
func Run(opts *Options, f func(*Plugin) error) {
if err := run(opts, f); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", filepath.Base(os.Args[0]), err) fmt.Fprintf(os.Stderr, "%s: %v\n", filepath.Base(os.Args[0]), err)
os.Exit(1) os.Exit(1)
} }
} }
func run(f func(*Plugin) error) error { func run(opts *Options, f func(*Plugin) error) error {
in, err := ioutil.ReadAll(os.Stdin) in, err := ioutil.ReadAll(os.Stdin)
if err != nil { if err != nil {
return err return err
@ -57,7 +59,7 @@ func run(f func(*Plugin) error) error {
if err := proto.Unmarshal(in, req); err != nil { if err := proto.Unmarshal(in, req); err != nil {
return err return err
} }
gen, err := New(req) gen, err := New(req, opts)
if err != nil { if err != nil {
return err return err
} }
@ -98,15 +100,47 @@ type Plugin struct {
err error err error
} }
// Options are optional parameters to New.
type Options struct {
// If ParamFunc is non-nil, it will be called with each unknown
// generator parameter.
//
// Plugins for protoc can accept parameters from the command line,
// passed in the --<lang>_out protoc, separated from the output
// directory with a colon; e.g.,
//
// --go_out=<param1>=<value1>,<param2>=<value2>:<output_directory>
//
// Parameters passed in this fashion as a comma-separated list of
// key=value pairs will be passed to the ParamFunc.
//
// The (flag.FlagSet).Set method matches this function signature,
// so parameters can be converted into flags as in the following:
//
// var flags flag.FlagSet
// value := flags.Bool("param", false, "")
// opts := &protogen.Options{
// ParamFunc: flags.Set,
// }
// protogen.Run(opts, func(p *protogen.Plugin) error {
// if *value { ... }
// })
ParamFunc func(name, value string) error
}
// New returns a new Plugin. // New returns a new Plugin.
func New(req *pluginpb.CodeGeneratorRequest) (*Plugin, error) { //
// Passing a nil Options is equivalent to passing a zero-valued one.
func New(req *pluginpb.CodeGeneratorRequest, opts *Options) (*Plugin, error) {
if opts == nil {
opts = &Options{}
}
gen := &Plugin{ gen := &Plugin{
Request: req, Request: req,
filesByName: make(map[string]*File), filesByName: make(map[string]*File),
fileReg: protoregistry.NewFiles(), fileReg: protoregistry.NewFiles(),
} }
// TODO: Figure out how to pass parameters to the generator.
packageNames := make(map[string]GoPackageName) // filename -> package name packageNames := make(map[string]GoPackageName) // filename -> package name
importPaths := make(map[string]GoImportPath) // filename -> import path importPaths := make(map[string]GoImportPath) // filename -> import path
var packageImportPath GoImportPath var packageImportPath GoImportPath
@ -132,15 +166,18 @@ func New(req *pluginpb.CodeGeneratorRequest) (*Plugin, error) {
default: default:
return nil, fmt.Errorf(`unknown path type %q: want "import" or "source_relative"`, value) return nil, fmt.Errorf(`unknown path type %q: want "import" or "source_relative"`, value)
} }
case "plugins":
// TODO
case "annotate_code": case "annotate_code":
// TODO // TODO
default: default:
if param[0] != 'M' { if param[0] == 'M' {
return nil, fmt.Errorf("unknown parameter %q", param) importPaths[param[1:]] = GoImportPath(value)
continue
}
if opts.ParamFunc != nil {
if err := opts.ParamFunc(param, value); err != nil {
return nil, err
}
} }
importPaths[param[1:]] = GoImportPath(value)
} }
} }

View File

@ -5,6 +5,7 @@
package protogen package protogen
import ( import (
"flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -18,8 +19,45 @@ import (
pluginpb "github.com/golang/protobuf/protoc-gen-go/plugin" pluginpb "github.com/golang/protobuf/protoc-gen-go/plugin"
) )
func TestPluginParameters(t *testing.T) {
var flags flag.FlagSet
value := flags.Int("integer", 0, "")
opts := &Options{
ParamFunc: flags.Set,
}
const params = "integer=2"
_, err := New(&pluginpb.CodeGeneratorRequest{
Parameter: proto.String(params),
}, opts)
if err != nil {
t.Errorf("New(generator parameters %q): %v", params, err)
}
if *value != 2 {
t.Errorf("New(generator parameters %q): integer=%v, want 2", params, *value)
}
}
func TestPluginParameterErrors(t *testing.T) {
for _, parameter := range []string{
"unknown=1",
"boolean=error",
} {
var flags flag.FlagSet
flags.Bool("boolean", false, "")
opts := &Options{
ParamFunc: flags.Set,
}
_, err := New(&pluginpb.CodeGeneratorRequest{
Parameter: proto.String(parameter),
}, opts)
if err == nil {
t.Errorf("New(generator parameters %q): want error, got nil", parameter)
}
}
}
func TestFiles(t *testing.T) { func TestFiles(t *testing.T) {
gen, err := New(makeRequest(t, "testdata/go_package/no_go_package_import.proto")) gen, err := New(makeRequest(t, "testdata/go_package/no_go_package_import.proto"), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -144,7 +182,7 @@ TEST: %v
if test.generate { if test.generate {
req.FileToGenerate = []string{filename} req.FileToGenerate = []string{filename}
} }
gen, err := New(req) gen, err := New(req, nil)
if err != nil { if err != nil {
t.Errorf("%vNew(req) = %v", context, err) t.Errorf("%vNew(req) = %v", context, err)
continue continue
@ -182,7 +220,7 @@ func TestPackageNameInference(t *testing.T) {
}, },
}, },
FileToGenerate: []string{"dir/file1.proto", "dir/file2.proto"}, FileToGenerate: []string{"dir/file1.proto", "dir/file2.proto"},
}) }, nil)
if err != nil { if err != nil {
t.Fatalf("New(req) = %v", err) t.Fatalf("New(req) = %v", err)
} }
@ -212,14 +250,14 @@ func TestInconsistentPackageNames(t *testing.T) {
}, },
}, },
FileToGenerate: []string{"dir/file1.proto", "dir/file2.proto"}, FileToGenerate: []string{"dir/file1.proto", "dir/file2.proto"},
}) }, nil)
if err == nil { if err == nil {
t.Fatalf("inconsistent package names for the same import path: New(req) = nil, want error") t.Fatalf("inconsistent package names for the same import path: New(req) = nil, want error")
} }
} }
func TestImports(t *testing.T) { func TestImports(t *testing.T) {
gen, err := New(&pluginpb.CodeGeneratorRequest{}) gen, err := New(&pluginpb.CodeGeneratorRequest{}, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -309,7 +347,7 @@ func makeRequest(t *testing.T, args ...string) *pluginpb.CodeGeneratorRequest {
func init() { func init() {
if os.Getenv("RUN_AS_PROTOC_PLUGIN") != "" { if os.Getenv("RUN_AS_PROTOC_PLUGIN") != "" {
Run(func(p *Plugin) error { Run(nil, func(p *Plugin) error {
g := p.NewGeneratedFile("request", "") g := p.NewGeneratedFile("request", "")
return proto.MarshalText(g, p.Request) return proto.MarshalText(g, p.Request)
}) })