diff --git a/protoc-gen-openapiv2/internal/genopenapi/BUILD.bazel b/protoc-gen-openapiv2/internal/genopenapi/BUILD.bazel index 6af63ffc8af..66ea1948725 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/BUILD.bazel +++ b/protoc-gen-openapiv2/internal/genopenapi/BUILD.bazel @@ -49,6 +49,7 @@ go_test( "template_test.go", "types_test.go", ], + data = glob(["testdata/**"]), embed = [":genopenapi"], deps = [ "//internal/descriptor", diff --git a/protoc-gen-openapiv2/internal/genopenapi/generator.go b/protoc-gen-openapiv2/internal/genopenapi/generator.go index 5087af77614..5ec31de63c7 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/generator.go +++ b/protoc-gen-openapiv2/internal/genopenapi/generator.go @@ -150,20 +150,45 @@ func (po openapiPathsObject) MarshalYAML() (interface{}, error) { pathObjectNode.Kind = yaml.MappingNode for _, pathData := range po { - var pathNode, pathItemObjectNode yaml.Node + var pathNode yaml.Node pathNode.SetString(pathData.Path) - b, err := yaml.Marshal(pathData.PathItemObject) + pathItemObjectNode, err := pathData.PathItemObject.toYAMLNode() if err != nil { return nil, err } - pathItemObjectNode.SetString(string(b)) - pathObjectNode.Content = append(pathObjectNode.Content, &pathNode, &pathItemObjectNode) + pathObjectNode.Content = append(pathObjectNode.Content, &pathNode, pathItemObjectNode) } return pathObjectNode, nil } +// We can simplify this implementation once the go-yaml bug is resolved. See: https://github.com/go-yaml/yaml/issues/643. +// +// func (pio *openapiPathItemObject) toYAMLNode() (*yaml.Node, error) { +// var node yaml.Node +// if err := node.Encode(pio); err != nil { +// return nil, err +// } +// return &node, nil +// } +func (pio *openapiPathItemObject) toYAMLNode() (*yaml.Node, error) { + var doc yaml.Node + var buf bytes.Buffer + ec := yaml.NewEncoder(&buf) + ec.SetIndent(2) + if err := ec.Encode(pio); err != nil { + return nil, err + } + if err := yaml.Unmarshal(buf.Bytes(), &doc); err != nil { + return nil, err + } + if len(doc.Content) == 0 { + return nil, errors.New("unexpected number of yaml nodes") + } + return doc.Content[0], nil +} + func (so openapiInfoObject) MarshalJSON() ([]byte, error) { type alias openapiInfoObject return extensionMarshalJSON(alias(so), so.extensions) diff --git a/protoc-gen-openapiv2/internal/genopenapi/generator_test.go b/protoc-gen-openapiv2/internal/genopenapi/generator_test.go index 59194b3f38f..559259ca2ea 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/generator_test.go +++ b/protoc-gen-openapiv2/internal/genopenapi/generator_test.go @@ -1,11 +1,13 @@ package genopenapi_test import ( + "os" "reflect" "sort" "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor" "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/internal/genopenapi" "gopkg.in/yaml.v3" @@ -120,6 +122,54 @@ func TestGenerateExtension(t *testing.T) { } } +func TestGenerateYAML(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + inputProtoText string + wantYAML string + }{ + { + // It tests https://github.com/grpc-ecosystem/grpc-gateway/issues/3557. + name: "path item object", + inputProtoText: "testdata/generator/path_item_object.prototext", + wantYAML: "testdata/generator/path_item_object.swagger.yaml", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + b, err := os.ReadFile(tt.inputProtoText) + if err != nil { + t.Fatal(err) + } + var req pluginpb.CodeGeneratorRequest + if err := prototext.Unmarshal(b, &req); err != nil { + t.Fatal(err) + } + + resp := requireGenerate(t, &req, genopenapi.FormatYAML, false, true) + if len(resp) != 1 { + t.Fatalf("invalid count, expected: 1, actual: %d", len(resp)) + } + got := resp[0].GetContent() + + want, err := os.ReadFile(tt.wantYAML) + if err != nil { + t.Fatal(err) + } + diff := cmp.Diff(string(want), got) + if diff != "" { + t.Fatalf("content not match\n%s", diff) + } + }) + } +} + func requireGenerate( tb testing.TB, req *pluginpb.CodeGeneratorRequest, diff --git a/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/path_item_object.prototext b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/path_item_object.prototext new file mode 100644 index 00000000000..63a641be2b9 --- /dev/null +++ b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/path_item_object.prototext @@ -0,0 +1,32 @@ +file_to_generate: "your/service/v1/your_service.proto" +proto_file: { + name: "your/service/v1/your_service.proto" + package: "your.service.v1" + message_type: { + name: "StringMessage" + field: { + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "value" + } + } + service: { + name: "YourService" + method: { + name: "Echo" + input_type: ".your.service.v1.StringMessage" + output_type: ".your.service.v1.StringMessage" + options: { + [google.api.http]: { + post: "/api/echo" + } + } + } + } + options: { + go_package: "github.com/yourorg/yourprotos/gen/go/your/service/v1" + } + syntax: "proto3" +} diff --git a/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/path_item_object.swagger.yaml b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/path_item_object.swagger.yaml new file mode 100644 index 00000000000..217ea3b248e --- /dev/null +++ b/protoc-gen-openapiv2/internal/genopenapi/testdata/generator/path_item_object.swagger.yaml @@ -0,0 +1,32 @@ +swagger: "2.0" +info: + title: your/service/v1/your_service.proto + version: version not set +tags: + - name: YourService +consumes: + - application/json +produces: + - application/json +paths: + /api/echo: + post: + operationId: YourService_Echo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/StringMessage' + parameters: + - name: value + in: query + required: false + type: string + tags: + - YourService +definitions: + StringMessage: + type: object + properties: + value: + type: string