-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: generator options are parsed into a standalone struct #479
Changes from all commits
bdf669f
e025034
b6cda62
d2d2391
f10420b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,8 +26,6 @@ import ( | |
"unicode" | ||
"unicode/utf8" | ||
|
||
yaml "gopkg.in/yaml.v2" | ||
|
||
"github.com/golang/protobuf/proto" | ||
"github.com/golang/protobuf/protoc-gen-go/descriptor" | ||
plugin "github.com/golang/protobuf/protoc-gen-go/plugin" | ||
|
@@ -37,6 +35,7 @@ import ( | |
"github.com/googleapis/gapic-generator-go/internal/pbinfo" | ||
"github.com/googleapis/gapic-generator-go/internal/printer" | ||
"google.golang.org/genproto/googleapis/api/annotations" | ||
"gopkg.in/yaml.v2" | ||
) | ||
|
||
const ( | ||
|
@@ -52,82 +51,154 @@ const ( | |
|
||
var headerParamRegexp = regexp.MustCompile(`{([_.a-z]+)`) | ||
|
||
func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, error) { | ||
var pkgPath, pkgName, outDir string | ||
var g generator | ||
type Transport int | ||
|
||
const ( | ||
grpc Transport = iota | ||
rest | ||
) | ||
|
||
type options struct { | ||
pkgPath string | ||
pkgName string | ||
outDir string | ||
relLvl string | ||
modulePrefix string | ||
grpcConfPath string | ||
serviceConfigPath string | ||
sampleOnly bool | ||
transports []Transport | ||
} | ||
|
||
// ParseOptions takes a string and parses it into a struct defining | ||
// customizations on the target gapic surface. | ||
// Options are comma-separated key/value pairs which are in turn delimited with '='. | ||
// Valid options include: | ||
// * go-gapic-package (package and module naming info) | ||
// * sample-only (only checked for presence) | ||
// * gapic-service-config (filepath) | ||
// * grpc-service-config (filepath) | ||
// * module (name) | ||
// * release-level (one of 'alpha', 'beta', or empty) | ||
// * transport ('+' separated list of transport backends to generate) | ||
// The only required option is 'go-gapic-package'. | ||
// | ||
// Valid parameter example: | ||
// go-gapic-package=path/to/out;pkg,module=path,transport=rest+grpc,gapic-service-config=gapic_cfg.json,release-level=alpha | ||
// | ||
// It returns a pointer to a populated options if no errors were encountered while parsing. | ||
// If errors were encountered, it returns a nil pointer and the first error. | ||
func ParseOptions(parameter *string) (*options, error) { | ||
opts := options{sampleOnly: false} | ||
|
||
if genReq.Parameter == nil { | ||
return &g.resp, errors.E(nil, paramError) | ||
if parameter == nil { | ||
return nil, errors.E(nil, "empty options parameter") | ||
} | ||
|
||
// parse plugin params, ignoring unknown values | ||
for _, s := range strings.Split(*genReq.Parameter, ",") { | ||
for _, s := range strings.Split(*parameter, ",") { | ||
// check for the boolean flag, sample-only, that disables client generation | ||
if s == "sample-only" { | ||
return &g.resp, nil | ||
return &options{sampleOnly: true}, nil | ||
} | ||
|
||
e := strings.IndexByte(s, '=') | ||
if e < 0 { | ||
return &g.resp, errors.E(nil, "invalid plugin option format, must be key=value: %s", s) | ||
return nil, errors.E(nil, "invalid plugin option format, must be key=value: %s", s) | ||
} | ||
|
||
key, val := s[:e], s[e+1:] | ||
if val == "" { | ||
return &g.resp, errors.E(nil, "invalid plugin option value, missing value in key=value: %s", s) | ||
return nil, errors.E(nil, "invalid plugin option value, missing value in key=value: %s", s) | ||
} | ||
|
||
switch key { | ||
case "go-gapic-package": | ||
p := strings.IndexByte(s, ';') | ||
|
||
if p < 0 { | ||
return &g.resp, errors.E(nil, paramError) | ||
return nil, errors.E(nil, paramError) | ||
} | ||
|
||
pkgPath = s[e+1 : p] | ||
pkgName = s[p+1:] | ||
outDir = filepath.FromSlash(pkgPath) | ||
opts.pkgPath = s[e+1 : p] | ||
opts.pkgName = s[p+1:] | ||
opts.outDir = filepath.FromSlash(opts.pkgPath) | ||
case "gapic-service-config": | ||
f, err := os.Open(val) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error opening service config: %v", err) | ||
} | ||
|
||
err = yaml.NewDecoder(f).Decode(&g.serviceConfig) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error decoding service config: %v", err) | ||
} | ||
opts.serviceConfigPath = val | ||
case "grpc-service-config": | ||
f, err := os.Open(val) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error opening gRPC service config: %v", err) | ||
} | ||
|
||
g.grpcConf, err = conf.New(f) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error parsing gPRC service config: %v", err) | ||
} | ||
opts.grpcConfPath = val | ||
case "module": | ||
g.modulePrefix = val | ||
opts.modulePrefix = val | ||
case "release-level": | ||
g.relLvl = strings.ToLower(val) | ||
opts.relLvl = strings.ToLower(val) | ||
case "transport": | ||
for _, t := range strings.Split(val, "+") { | ||
switch t { | ||
case "grpc": | ||
opts.transports = append(opts.transports, grpc) | ||
case "rest": | ||
opts.transports = append(opts.transports, rest) | ||
default: | ||
return nil, errors.E(nil, "invalid transport option: %s", t) | ||
} | ||
} | ||
} | ||
} | ||
|
||
if pkgPath == "" || pkgName == "" || outDir == "" { | ||
return &g.resp, errors.E(nil, paramError) | ||
if opts.pkgPath == "" || opts.pkgName == "" || opts.outDir == "" { | ||
return nil, errors.E(nil, paramError) | ||
} | ||
|
||
if g.modulePrefix != "" { | ||
if !strings.HasPrefix(outDir, g.modulePrefix) { | ||
return &g.resp, errors.E(nil, "go-gapic-package %q does not match prefix %q", outDir, g.modulePrefix) | ||
if opts.modulePrefix != "" { | ||
if !strings.HasPrefix(opts.outDir, opts.modulePrefix) { | ||
return nil, errors.E(nil, "go-gapic-package %q does not match prefix %q", opts.outDir, opts.modulePrefix) | ||
} | ||
outDir = strings.TrimPrefix(outDir, g.modulePrefix+"/") | ||
opts.outDir = strings.TrimPrefix(opts.outDir, opts.modulePrefix+"/") | ||
} | ||
|
||
// Default is just grpc for now. | ||
if opts.transports == nil { | ||
opts.transports = []Transport{grpc} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider moving this assignment to line 102 to better group the initialization together. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the current placement better communicates the intention that this is a default value if the user doesn't provide a |
||
} | ||
|
||
return &opts, nil | ||
} | ||
|
||
func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, error) { | ||
var g generator | ||
g.init(genReq.ProtoFile) | ||
|
||
opts, err := ParseOptions(genReq.Parameter) | ||
if err != nil { | ||
return &g.resp, err | ||
} | ||
|
||
if opts.serviceConfigPath != "" { | ||
f, err := os.Open(opts.serviceConfigPath) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error opening service config: %v", err) | ||
} | ||
defer f.Close() | ||
|
||
err = yaml.NewDecoder(f).Decode(&g.serviceConfig) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error decoding service config: %v", err) | ||
} | ||
} | ||
if opts.grpcConfPath != "" { | ||
f, err := os.Open(opts.grpcConfPath) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error opening gRPC service config: %v", err) | ||
} | ||
defer f.Close() | ||
|
||
g.grpcConf, err = conf.New(f) | ||
if err != nil { | ||
return &g.resp, errors.E(nil, "error parsing gPRC service config: %v", err) | ||
} | ||
} | ||
g.opts = opts | ||
|
||
var genServs []*descriptor.ServiceDescriptorProto | ||
for _, f := range genReq.ProtoFile { | ||
if !strContains(genReq.FileToGenerate, f.GetName()) { | ||
|
@@ -149,30 +220,30 @@ func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, er | |
// Keep the current behavior for now, but we could revisit this later. | ||
outFile := pbinfo.ReduceServName(s.GetName(), "") | ||
outFile = camelToSnake(outFile) | ||
outFile = filepath.Join(outDir, outFile) | ||
outFile = filepath.Join(g.opts.outDir, outFile) | ||
|
||
g.reset() | ||
if err := g.gen(s, pkgName); err != nil { | ||
if err := g.gen(s); err != nil { | ||
return &g.resp, err | ||
} | ||
g.commit(outFile+"_client.go", pkgName) | ||
g.commit(outFile+"_client.go", g.opts.pkgName) | ||
|
||
g.reset() | ||
if err := g.genExampleFile(s, pkgName); err != nil { | ||
if err := g.genExampleFile(s, g.opts.pkgName); err != nil { | ||
return &g.resp, errors.E(err, "example: %s", s.GetName()) | ||
} | ||
g.imports[pbinfo.ImportSpec{Name: pkgName, Path: pkgPath}] = true | ||
g.commit(outFile+"_client_example_test.go", pkgName+"_test") | ||
g.imports[pbinfo.ImportSpec{Name: g.opts.pkgName, Path: g.opts.pkgPath}] = true | ||
g.commit(outFile+"_client_example_test.go", g.opts.pkgName+"_test") | ||
} | ||
|
||
g.reset() | ||
scopes, err := collectScopes(genServs, g.serviceConfig) | ||
if err != nil { | ||
return &g.resp, err | ||
} | ||
g.genDocFile(pkgPath, pkgName, time.Now().Year(), scopes) | ||
g.genDocFile(time.Now().Year(), scopes) | ||
g.resp.File = append(g.resp.File, &plugin.CodeGeneratorResponse_File{ | ||
Name: proto.String(filepath.Join(outDir, "doc.go")), | ||
Name: proto.String(filepath.Join(g.opts.outDir, "doc.go")), | ||
Content: proto.String(g.pt.String()), | ||
}) | ||
|
||
|
@@ -218,6 +289,10 @@ type generator struct { | |
// The Go module prefix to strip from the go-gapic-package | ||
// used as the generated file name. | ||
modulePrefix string | ||
|
||
// Options for the generator determining module names, transports, | ||
// config file paths, etc. | ||
opts *options | ||
} | ||
|
||
func (g *generator) init(files []*descriptor.FileDescriptorProto) { | ||
|
@@ -330,8 +405,8 @@ func (g *generator) reset() { | |
} | ||
|
||
// gen generates client for the given service. | ||
func (g *generator) gen(serv *descriptor.ServiceDescriptorProto, pkgName string) error { | ||
servName := pbinfo.ReduceServName(*serv.Name, pkgName) | ||
func (g *generator) gen(serv *descriptor.ServiceDescriptorProto) error { | ||
servName := pbinfo.ReduceServName(*serv.Name, g.opts.pkgName) | ||
|
||
g.clientHook(servName) | ||
if err := g.clientOptions(serv, servName); err != nil { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe Java assumes that either gRPC or REST will be used at a particular time. Does this array imply that callers can pass in both transport types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the design doc, the generators will support specifying which transports to generate code for via the
transport
option, which takes a+
-separated list. The eventual default will begrpc+rest
. The end developer can then specify which of these generated transports to use when communicating with the server.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is the implication. Python allows multiple transports to be generated for the surface with runtime choice via dependency injection. This is supported by the spec: see this heading.