Skip to content
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

protoc-gen-openapiv2: support YAML OpenAPI/Swagger v2 definition generation #2579

Merged
merged 11 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6
google.golang.org/grpc v1.45.0
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/yaml v1.3.0
)

Expand All @@ -21,5 +22,4 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
11 changes: 11 additions & 0 deletions protoc-gen-openapiv2/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def _run_proto_gen_openapi(
disable_default_errors,
enums_as_ints,
omit_enum_default_value,
output_format,
simple_operation_ids,
proto3_optional_nullable,
openapi_configuration,
Expand Down Expand Up @@ -116,6 +117,9 @@ def _run_proto_gen_openapi(
if omit_enum_default_value:
args.add("--openapiv2_opt", "omit_enum_default_value=true")

if output_format:
args.add("--openapiv2_opt", "output_format=%s" % output_format)

if proto3_optional_nullable:
args.add("--openapiv2_opt", "proto3_optional_nullable=true")

Expand Down Expand Up @@ -214,6 +218,7 @@ def _proto_gen_openapi_impl(ctx):
disable_default_errors = ctx.attr.disable_default_errors,
enums_as_ints = ctx.attr.enums_as_ints,
omit_enum_default_value = ctx.attr.omit_enum_default_value,
output_format = ctx.attr.output_format,
simple_operation_ids = ctx.attr.simple_operation_ids,
proto3_optional_nullable = ctx.attr.proto3_optional_nullable,
openapi_configuration = ctx.file.openapi_configuration,
Expand Down Expand Up @@ -301,6 +306,12 @@ protoc_gen_openapiv2 = rule(
mandatory = False,
doc = "if set, omit default enum value",
),
"output_format": attr.string(
default = "json",
mandatory = False,
values = ["json", "yaml"],
doc = "output content format. Allowed values are: `json`, `yaml`",
),
"simple_operation_ids": attr.bool(
default = False,
mandatory = False,
Expand Down
6 changes: 6 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go_library(
name = "genopenapi",
srcs = [
"doc.go",
"format.go",
"generator.go",
"helpers.go",
"helpers_go111_old.go",
Expand All @@ -23,6 +24,7 @@ go_library(
"@com_github_golang_protobuf//descriptor:go_default_library_gen",
"@go_googleapis//google/api:annotations_go_proto",
"@go_googleapis//google/rpc:status_go_proto",
"@in_gopkg_yaml_v2//:yaml_v2",
"@io_bazel_rules_go//proto/wkt:any_go_proto",
"@org_golang_google_protobuf//encoding/protojson",
"@org_golang_google_protobuf//proto",
Expand All @@ -37,8 +39,11 @@ go_test(
size = "small",
srcs = [
"cycle_test.go",
"format_test.go",
"generator_test.go",
"naming_test.go",
"template_test.go",
"types_test.go",
],
embed = [":genopenapi"],
deps = [
Expand All @@ -49,6 +54,7 @@ go_test(
"//runtime",
"@com_github_google_go_cmp//cmp",
"@go_googleapis//google/api:annotations_go_proto",
"@in_gopkg_yaml_v2//:yaml_v2",
"@io_bazel_rules_go//proto/wkt:field_mask_go_proto",
"@org_golang_google_protobuf//proto",
"@org_golang_google_protobuf//reflect/protodesc",
Expand Down
43 changes: 43 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package genopenapi

import (
"encoding/json"
"errors"
"io"

"gopkg.in/yaml.v2"
)

type Format string

const (
FormatJSON Format = "json"
FormatYAML Format = "yaml"
)

type ContentEncoder interface {
Encode(v interface{}) (err error)
}

func (f Format) Validate() error {
switch f {
case FormatJSON, FormatYAML:
return nil
default:
return errors.New("unknown format: " + string(f))
}
}

func (f Format) NewEncoder(w io.Writer) (ContentEncoder, error) {
switch f {
case FormatYAML:
return yaml.NewEncoder(w), nil
case FormatJSON:
enc := json.NewEncoder(w)
enc.SetIndent("", " ")

return enc, nil
default:
return nil, errors.New("unknown format: " + string(f))
}
}
105 changes: 105 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package genopenapi_test

import (
"bytes"
"encoding/json"
"io"
"reflect"
"testing"

"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/internal/genopenapi"
"gopkg.in/yaml.v2"
)

func TestFormatValidate(t *testing.T) {
t.Parallel()

testCases := [...]struct {
Format genopenapi.Format
Valid bool
}{{
Format: genopenapi.FormatJSON,
Valid: true,
}, {
Format: genopenapi.FormatYAML,
Valid: true,
}, {
Format: genopenapi.Format("unknown"),
Valid: false,
}, {
Format: genopenapi.Format(""),
Valid: false,
}}

for _, tc := range testCases {
tc := tc

t.Run(string(tc.Format), func(t *testing.T) {
t.Parallel()

err := tc.Format.Validate()
switch {
case tc.Valid && err != nil:
t.Fatalf("expect no validation error, got: %s", err)
case !tc.Valid && err == nil:
t.Fatal("expect validation error, got nil")
}
})
}
}

func TestFormatEncode(t *testing.T) {
t.Parallel()

type contentDecoder interface {
Decode(v interface{}) error
}

testCases := [...]struct {
Format genopenapi.Format
NewDecoder func(r io.Reader) contentDecoder
}{{
Format: genopenapi.FormatJSON,
NewDecoder: func(r io.Reader) contentDecoder {
return json.NewDecoder(r)
},
}, {
Format: genopenapi.FormatYAML,
NewDecoder: func(r io.Reader) contentDecoder {
return yaml.NewDecoder(r)
},
}}

for _, tc := range testCases {
tc := tc

t.Run(string(tc.Format), func(t *testing.T) {
t.Parallel()

expParams := map[string]string{
"hello": "world",
}

var buf bytes.Buffer
enc, err := tc.Format.NewEncoder(&buf)
if err != nil {
t.Fatalf("expect no encoder creating error, got: %s", err)
}

err = enc.Encode(expParams)
if err != nil {
t.Fatalf("expect no encoding error, got: %s", err)
}

gotParams := make(map[string]string)
err = tc.NewDecoder(&buf).Decode(&gotParams)
if err != nil {
t.Fatalf("expect no decoding error, got: %s", err)
}

if !reflect.DeepEqual(expParams, gotParams) {
t.Fatalf("expected: %+v, actual: %+v", expParams, gotParams)
}
})
}
}
30 changes: 19 additions & 11 deletions protoc-gen-openapiv2/internal/genopenapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ var (
)

type generator struct {
reg *descriptor.Registry
reg *descriptor.Registry
format Format
}

type wrapper struct {
Expand All @@ -42,8 +43,11 @@ type GeneratorOptions struct {
}

// New returns a new generator which generates grpc gateway files.
func New(reg *descriptor.Registry) gen.Generator {
return &generator{reg: reg}
func New(reg *descriptor.Registry, format Format) gen.Generator {
return &generator{
reg: reg,
format: format,
}
}

// Merge a lot of OpenAPI file (wrapper) to single one OpenAPI file
Expand Down Expand Up @@ -143,21 +147,25 @@ func extensionMarshalJSON(so interface{}, extensions []extension) ([]byte, error
}

// encodeOpenAPI converts OpenAPI file obj to pluginpb.CodeGeneratorResponse_File
func encodeOpenAPI(file *wrapper) (*descriptor.ResponseFile, error) {
var formatted bytes.Buffer
enc := json.NewEncoder(&formatted)
enc.SetIndent("", " ")
func encodeOpenAPI(file *wrapper, format Format) (*descriptor.ResponseFile, error) {
var contentBuf bytes.Buffer
enc, err := format.NewEncoder(&contentBuf)
if err != nil {
return nil, err
}

if err := enc.Encode(*file.swagger); err != nil {
return nil, err
}

name := file.fileName
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
output := fmt.Sprintf("%s.swagger.json", base)
output := fmt.Sprintf("%s.swagger."+string(format), base)
return &descriptor.ResponseFile{
CodeGeneratorResponse_File: &pluginpb.CodeGeneratorResponse_File{
Name: proto.String(output),
Content: proto.String(formatted.String()),
Content: proto.String(contentBuf.String()),
},
}, nil
}
Expand Down Expand Up @@ -207,15 +215,15 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response

if g.reg.IsAllowMerge() {
targetOpenAPI := mergeTargetFile(openapis, g.reg.GetMergeFileName())
f, err := encodeOpenAPI(targetOpenAPI)
f, err := encodeOpenAPI(targetOpenAPI, g.format)
if err != nil {
return nil, fmt.Errorf("failed to encode OpenAPI for %s: %s", g.reg.GetMergeFileName(), err)
}
files = append(files, f)
glog.V(1).Infof("New OpenAPI file will emit")
} else {
for _, file := range openapis {
f, err := encodeOpenAPI(file)
f, err := encodeOpenAPI(file, g.format)
if err != nil {
return nil, fmt.Errorf("failed to encode OpenAPI for %s: %s", file.fileName, err)
}
Expand Down
59 changes: 59 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package genopenapi_test

import (
"testing"

"github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor"
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/internal/genopenapi"
"gopkg.in/yaml.v2"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/pluginpb"
)

func TestGenerate_YAML(t *testing.T) {
t.Parallel()

reg := descriptor.NewRegistry()
req := &pluginpb.CodeGeneratorRequest{
ProtoFile: []*descriptorpb.FileDescriptorProto{{
Name: proto.String("file.proto"),
Package: proto.String("example"),
Options: &descriptorpb.FileOptions{
GoPackage: proto.String("goexample/v1;goexample"),
},
}},
FileToGenerate: []string{
"file.proto",
},
}

if err := reg.Load(req); err != nil {
t.Fatalf("failed to load request: %s", err)
}

var targets []*descriptor.File
for _, target := range req.FileToGenerate {
f, err := reg.LookupFile(target)
if err != nil {
t.Fatalf("failed to lookup file: %s", err)
}
targets = append(targets, f)
}

g := genopenapi.New(reg, genopenapi.FormatYAML)
resp, err := g.Generate(targets)
switch {
case err != nil:
t.Fatalf("failed to generate targets: %s", err)
case len(resp) != 1:
t.Fatalf("invalid count, expected: 1, actual: %d", len(resp))
}

var p map[string]interface{}
err = yaml.Unmarshal([]byte(resp[0].GetContent()), &p)
if err != nil {
t.Fatalf("failed to unmarshall yaml: %s", err)
}
}
6 changes: 3 additions & 3 deletions protoc-gen-openapiv2/internal/genopenapi/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -1891,7 +1891,7 @@ func processHeaders(inputHdrs map[string]*openapi_options.Header) (openapiHeader
if err != nil {
return nil, err
}
ret.Default = json.RawMessage(v.Default)
ret.Default = RawExample(v.Default)
}
hdrs[header] = ret
}
Expand Down Expand Up @@ -2396,7 +2396,7 @@ func updateswaggerObjectFromJSONSchema(s *openapiSchemaObject, j *openapi_option
s.Type = strings.ToLower(overrideType[0].String())
}
if j != nil && j.GetExample() != "" {
s.Example = json.RawMessage(j.GetExample())
s.Example = RawExample(j.GetExample())
}
if j != nil && j.GetFormat() != "" {
s.Format = j.GetFormat()
Expand Down Expand Up @@ -2438,7 +2438,7 @@ func openapiSchemaFromProtoSchema(s *openapi_options.Schema, reg *descriptor.Reg
updateswaggerObjectFromJSONSchema(&ret, s.GetJsonSchema(), reg, data)

if s != nil && s.Example != "" {
ret.Example = json.RawMessage(s.Example)
ret.Example = RawExample(s.Example)
}

return ret
Expand Down
Loading