Skip to content

Commit ce09567

Browse files
authored
3388 - Add feature to support passing values to go templates via cli (#3764)
1 parent c6ceb28 commit ce09567

File tree

6 files changed

+208
-0
lines changed

6 files changed

+208
-0
lines changed

docs/docs/mapping/customizing_openapi_output.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,61 @@ This is how the OpenAPI file would be rendered in [Postman](https://www.getpostm
231231

232232
For a more detailed example of a proto file that has Go, templates enabled, [see the examples](https://github.com/grpc-ecosystem/grpc-gateway/blob/main/examples/internal/proto/examplepb/use_go_template.proto).
233233

234+
### Using custom values
235+
236+
Custom values can be specified in the [Go templates](https://golang.org/pkg/text/template/) that generate your proto file comments.
237+
238+
A use case might be to interpolate different external documentation URLs when rendering documentation for different environments.
239+
240+
#### How to use it
241+
242+
The `use_go_templates` option has to be enabled as a prerequisite.
243+
244+
Provide customized values in the format of `go_template_args=my_key=my_value`. `{{arg "my_key"}}` will be replaced with `my_value` in the Go template.
245+
246+
Specify the `go_template_args` option multiple times if needed.
247+
248+
```sh
249+
--openapiv2_out . --openapiv2_opt use_go_templates=true --openapiv2_opt go_template_args=my_key1=my_value1 --openapiv2_opt go_template_args=my_key2=my_value2
250+
...
251+
```
252+
253+
#### Example script
254+
255+
Example of a bash script with the `use_go_templates` flag set to true and custom template values set:
256+
257+
```sh
258+
$ protoc -I. \
259+
--go_out . --go-grpc_out . \
260+
--grpc-gateway_out . \
261+
--openapiv2_out . \
262+
--openapiv2_opt use_go_templates=true \
263+
--openapiv2_opt go_template_args=environment=test1 \
264+
--openapiv2_opt go_template_args=environment_label=Test1 \
265+
path/to/my/proto/v1/myproto.proto
266+
```
267+
268+
#### Example proto file
269+
270+
Example of a proto file with Go templates and custom values:
271+
272+
```protobuf
273+
service LoginService {
274+
// Login (Environment: {{arg "environment_label"}})
275+
//
276+
// {{.MethodDescriptorProto.Name}} is a call with the method(s) {{$first := true}}{{range .Bindings}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{.HTTPMethod}}{{end}} within the "{{.Service.Name}}" service.
277+
// It takes in "{{.RequestType.Name}}" and returns a "{{.ResponseType.Name}}".
278+
// This only works in the {{arg "environment"}} domain.
279+
//
280+
rpc Login (LoginRequest) returns (LoginReply) {
281+
option (google.api.http) = {
282+
post: "/v1/example/login"
283+
body: "*"
284+
};
285+
}
286+
}
287+
```
288+
234289
## Other plugin options
235290

236291
A comprehensive list of OpenAPI plugin options can be found [here](https://github.com/grpc-ecosystem/grpc-gateway/blob/main/protoc-gen-openapiv2/main.go). Options can be passed via `protoc` CLI:

internal/descriptor/registry.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ type Registry struct {
7878
// in your protofile comments
7979
useGoTemplate bool
8080

81+
// goTemplateArgs specifies a list of key value pair inputs to be displayed in Go templates
82+
goTemplateArgs map[string]string
83+
8184
// ignoreComments determines whether all protofile comments should be excluded from output
8285
ignoreComments bool
8386

@@ -583,6 +586,19 @@ func (r *Registry) GetUseGoTemplate() bool {
583586
return r.useGoTemplate
584587
}
585588

589+
func (r *Registry) SetGoTemplateArgs(kvs []string) {
590+
r.goTemplateArgs = make(map[string]string)
591+
for _, kv := range kvs {
592+
if key, value, found := strings.Cut(kv, "="); found {
593+
r.goTemplateArgs[key] = value
594+
}
595+
}
596+
}
597+
598+
func (r *Registry) GetGoTemplateArgs() map[string]string {
599+
return r.goTemplateArgs
600+
}
601+
586602
// SetIgnoreComments sets ignoreComments
587603
func (r *Registry) SetIgnoreComments(ignore bool) {
588604
r.ignoreComments = ignore

protoc-gen-openapiv2/defs.bzl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def _run_proto_gen_openapi(
6060
fqn_for_openapi_name,
6161
openapi_naming_strategy,
6262
use_go_templates,
63+
go_template_args,
6364
ignore_comments,
6465
remove_internal_comments,
6566
disable_default_errors,
@@ -110,6 +111,9 @@ def _run_proto_gen_openapi(
110111
if use_go_templates:
111112
args.add("--openapiv2_opt", "use_go_templates=true")
112113

114+
for go_template_arg in go_template_args:
115+
args.add("--openapiv2_opt", "go_template_args=%s" % go_template_arg)
116+
113117
if ignore_comments:
114118
args.add("--openapiv2_opt", "ignore_comments=true")
115119

@@ -232,6 +236,7 @@ def _proto_gen_openapi_impl(ctx):
232236
fqn_for_openapi_name = ctx.attr.fqn_for_openapi_name,
233237
openapi_naming_strategy = ctx.attr.openapi_naming_strategy,
234238
use_go_templates = ctx.attr.use_go_templates,
239+
go_template_args = ctx.attr.go_template_args,
235240
ignore_comments = ctx.attr.ignore_comments,
236241
remove_internal_comments = ctx.attr.remove_internal_comments,
237242
disable_default_errors = ctx.attr.disable_default_errors,
@@ -312,6 +317,12 @@ protoc_gen_openapiv2 = rule(
312317
mandatory = False,
313318
doc = "if set, you can use Go templates in protofile comments",
314319
),
320+
"go_template_args": attr.string_list(
321+
mandatory = False,
322+
doc = "specify a key value pair as inputs to the Go template of the protofile" +
323+
" comments. Repeat this option to specify multiple template arguments." +
324+
" Requires the `use_go_templates` option to be set.",
325+
),
315326
"ignore_comments": attr.bool(
316327
default = False,
317328
mandatory = False,

protoc-gen-openapiv2/internal/genopenapi/template.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2486,6 +2486,12 @@ func goTemplateComments(comment string, data interface{}, reg *descriptor.Regist
24862486
"fieldcomments": func(msg *descriptor.Message, field *descriptor.Field) string {
24872487
return strings.ReplaceAll(fieldProtoComments(reg, msg, field), "\n", "<br>")
24882488
},
2489+
"arg": func(name string) string {
2490+
if v, f := reg.GetGoTemplateArgs()[name]; f {
2491+
return v
2492+
}
2493+
return fmt.Sprintf("goTemplateArg %s not found", name)
2494+
},
24892495
}).Parse(comment)
24902496
if err != nil {
24912497
// If there is an error parsing the templating insert the error as string in the comment

protoc-gen-openapiv2/internal/genopenapi/template_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5841,6 +5841,7 @@ func TestUpdateOpenAPIDataFromComments(t *testing.T) {
58415841
expectedError error
58425842
expectedOpenAPIObject interface{}
58435843
useGoTemplate bool
5844+
goTemplateArgs []string
58445845
}{
58455846
{
58465847
descr: "empty comments",
@@ -5968,6 +5969,33 @@ func TestUpdateOpenAPIDataFromComments(t *testing.T) {
59685969
expectedError: nil,
59695970
useGoTemplate: true,
59705971
},
5972+
{
5973+
descr: "template with use_go_template and go_template_args",
5974+
openapiSwaggerObject: &openapiSchemaObject{},
5975+
expectedOpenAPIObject: &openapiSchemaObject{
5976+
Title: "Template",
5977+
Description: `Description "which means nothing" for environment test with value my_value`,
5978+
},
5979+
comments: "Template\n\nDescription {{with \"which means nothing\"}}{{printf \"%q\" .}}{{end}} for " +
5980+
"environment {{arg \"environment\"}} with value {{arg \"my_key\"}}",
5981+
expectedError: nil,
5982+
useGoTemplate: true,
5983+
goTemplateArgs: []string{"my_key=my_value", "environment=test"},
5984+
},
5985+
{
5986+
descr: "template with use_go_template and undefined go_template_args",
5987+
openapiSwaggerObject: &openapiSchemaObject{},
5988+
expectedOpenAPIObject: &openapiSchemaObject{
5989+
Title: "Template",
5990+
Description: `Description "which means nothing" for environment test with value ` +
5991+
`goTemplateArg something_undefined not found`,
5992+
},
5993+
comments: "Template\n\nDescription {{with \"which means nothing\"}}{{printf \"%q\" .}}{{end}} for " +
5994+
"environment {{arg \"environment\"}} with value {{arg \"something_undefined\"}}",
5995+
expectedError: nil,
5996+
useGoTemplate: true,
5997+
goTemplateArgs: []string{"environment=test"},
5998+
},
59715999
}
59726000

59736001
for _, test := range tests {
@@ -5976,6 +6004,9 @@ func TestUpdateOpenAPIDataFromComments(t *testing.T) {
59766004
if test.useGoTemplate {
59776005
reg.SetUseGoTemplate(true)
59786006
}
6007+
if len(test.goTemplateArgs) > 0 {
6008+
reg.SetGoTemplateArgs(test.goTemplateArgs)
6009+
}
59796010
err := updateOpenAPIDataFromComments(reg, test.openapiSwaggerObject, nil, test.comments, false)
59806011
if test.expectedError == nil {
59816012
if err != nil {
@@ -6007,6 +6038,7 @@ func TestMessageOptionsWithGoTemplate(t *testing.T) {
60076038
defs openapiDefinitionsObject
60086039
openAPIOptions *openapiconfig.OpenAPIOptions
60096040
useGoTemplate bool
6041+
goTemplateArgs []string
60106042
}{
60116043
{
60126044
descr: "external docs option",
@@ -6068,6 +6100,39 @@ func TestMessageOptionsWithGoTemplate(t *testing.T) {
60686100
},
60696101
useGoTemplate: false,
60706102
},
6103+
{
6104+
descr: "external docs option with go template args",
6105+
msgDescs: []*descriptorpb.DescriptorProto{
6106+
{Name: proto.String("Message")},
6107+
},
6108+
schema: map[string]*openapi_options.Schema{
6109+
"Message": {
6110+
JsonSchema: &openapi_options.JSONSchema{
6111+
Title: "{{.Name}}",
6112+
Description: "Description {{with \"which means nothing\"}}{{printf \"%q\" .}}{{end}} " +
6113+
"{{arg \"my_key\"}}",
6114+
},
6115+
ExternalDocs: &openapi_options.ExternalDocumentation{
6116+
Description: "Description {{with \"which means nothing\"}}{{printf \"%q\" .}}{{end}} " +
6117+
"{{arg \"my_key\"}}",
6118+
},
6119+
},
6120+
},
6121+
defs: map[string]openapiSchemaObject{
6122+
"Message": {
6123+
schemaCore: schemaCore{
6124+
Type: "object",
6125+
},
6126+
Title: "Message",
6127+
Description: `Description "which means nothing" too`,
6128+
ExternalDocs: &openapiExternalDocumentationObject{
6129+
Description: `Description "which means nothing" too`,
6130+
},
6131+
},
6132+
},
6133+
useGoTemplate: true,
6134+
goTemplateArgs: []string{"my_key=too"},
6135+
},
60716136
{
60726137
descr: "registered OpenAPIOption",
60736138
msgDescs: []*descriptorpb.DescriptorProto{
@@ -6103,6 +6168,44 @@ func TestMessageOptionsWithGoTemplate(t *testing.T) {
61036168
},
61046169
useGoTemplate: true,
61056170
},
6171+
{
6172+
descr: "registered OpenAPIOption with go template args",
6173+
msgDescs: []*descriptorpb.DescriptorProto{
6174+
{Name: proto.String("Message")},
6175+
},
6176+
openAPIOptions: &openapiconfig.OpenAPIOptions{
6177+
Message: []*openapiconfig.OpenAPIMessageOption{
6178+
{
6179+
Message: "example.Message",
6180+
Option: &openapi_options.Schema{
6181+
JsonSchema: &openapi_options.JSONSchema{
6182+
Title: "{{.Name}}",
6183+
Description: "Description {{with \"which means nothing\"}}{{printf \"%q\" .}}{{end}} " +
6184+
"{{arg \"my_key\"}}",
6185+
},
6186+
ExternalDocs: &openapi_options.ExternalDocumentation{
6187+
Description: "Description {{with \"which means nothing\"}}{{printf \"%q\" .}}{{end}} " +
6188+
"{{arg \"my_key\"}}",
6189+
},
6190+
},
6191+
},
6192+
},
6193+
},
6194+
defs: map[string]openapiSchemaObject{
6195+
"Message": {
6196+
schemaCore: schemaCore{
6197+
Type: "object",
6198+
},
6199+
Title: "Message",
6200+
Description: `Description "which means nothing" too`,
6201+
ExternalDocs: &openapiExternalDocumentationObject{
6202+
Description: `Description "which means nothing" too`,
6203+
},
6204+
},
6205+
},
6206+
useGoTemplate: true,
6207+
goTemplateArgs: []string{"my_key=too"},
6208+
},
61066209
}
61076210

61086211
for _, test := range tests {
@@ -6116,6 +6219,7 @@ func TestMessageOptionsWithGoTemplate(t *testing.T) {
61166219

61176220
reg := descriptor.NewRegistry()
61186221
reg.SetUseGoTemplate(test.useGoTemplate)
6222+
reg.SetGoTemplateArgs(test.goTemplateArgs)
61196223
file := descriptor.File{
61206224
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
61216225
SourceCodeInfo: &descriptorpb.SourceCodeInfo{},
@@ -6174,6 +6278,7 @@ func TestMessageOptionsWithGoTemplate(t *testing.T) {
61746278
func TestTagsWithGoTemplate(t *testing.T) {
61756279
reg := descriptor.NewRegistry()
61766280
reg.SetUseGoTemplate(true)
6281+
reg.SetGoTemplateArgs([]string{"my_key=my_value"})
61776282

61786283
svc := &descriptorpb.ServiceDescriptorProto{
61796284
Name: proto.String("ExampleService"),
@@ -6220,6 +6325,10 @@ func TestTagsWithGoTemplate(t *testing.T) {
62206325
Name: "not a service tag 2",
62216326
Description: "{{ import \"file\" }}",
62226327
},
6328+
{
6329+
Name: "Service with my_key",
6330+
Description: "the {{arg \"my_key\"}}",
6331+
},
62236332
},
62246333
}
62256334
proto.SetExtension(proto.Message(file.FileDescriptorProto.Options), openapi_options.E_Openapiv2Swagger, &swagger)
@@ -6241,6 +6350,10 @@ func TestTagsWithGoTemplate(t *testing.T) {
62416350
Name: "not a service tag 2",
62426351
Description: "open file: no such file or directory",
62436352
},
6353+
{
6354+
Name: "Service with my_key",
6355+
Description: "the my_value",
6356+
},
62446357
}
62456358
if !reflect.DeepEqual(actual.Tags, expectedTags) {
62466359
t.Errorf("Expected tags %+v, not %+v", expectedTags, actual.Tags)

protoc-gen-openapiv2/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var (
3030
useFQNForOpenAPIName = flag.Bool("fqn_for_openapi_name", false, "if set, the object's OpenAPI names will use the fully qualified names from the proto definition (ie my.package.MyMessage.MyInnerMessage). DEPRECATED: prefer `openapi_naming_strategy=fqn`")
3131
openAPINamingStrategy = flag.String("openapi_naming_strategy", "", "use the given OpenAPI naming strategy. Allowed values are `legacy`, `fqn`, `simple`. If unset, either `legacy` or `fqn` are selected, depending on the value of the `fqn_for_openapi_name` flag")
3232
useGoTemplate = flag.Bool("use_go_templates", false, "if set, you can use Go templates in protofile comments")
33+
goTemplateArgs = utilities.StringArrayFlag(flag.CommandLine, "go_template_args", "provide a custom value that can override a key in the Go template. Requires the `use_go_templates` option to be set")
3334
ignoreComments = flag.Bool("ignore_comments", false, "if set, all protofile comments are excluded from output")
3435
removeInternalComments = flag.Bool("remove_internal_comments", false, "if set, removes all substrings in comments that start with `(--` and end with `--)` as specified in https://google.aip.dev/192#internal-comments")
3536
disableDefaultErrors = flag.Bool("disable_default_errors", false, "if set, disables generation of default errors. This is useful if you have defined custom error handling")
@@ -135,6 +136,12 @@ func main() {
135136
reg.SetIgnoreComments(*ignoreComments)
136137
reg.SetRemoveInternalComments(*removeInternalComments)
137138

139+
if len(*goTemplateArgs) > 0 && !*useGoTemplate {
140+
emitError(fmt.Errorf("`go_template_args` requires `use_go_templates` to be enabled"))
141+
return
142+
}
143+
reg.SetGoTemplateArgs(*goTemplateArgs)
144+
138145
reg.SetOpenAPINamingStrategy(namingStrategy)
139146
reg.SetEnumsAsInts(*enumsAsInts)
140147
reg.SetDisableDefaultErrors(*disableDefaultErrors)

0 commit comments

Comments
 (0)