Skip to content

Commit

Permalink
Feature/issue 3051 swagger preserve rpc order (#3500)
Browse files Browse the repository at this point in the history
* add preserveRPCOrder option to registry options

* add preserve_rpc_order flag to protoc-gen-openapiv2 main

* convert openapiPathsObject from map to ordered data structure

* alphabetically sort paths if preserveRPCOrder is false during swagger file gen

* add tests for path order preservation in generate function

* add tests for path preservation in the renderServices function

* fix bugs in generator.go tests

* regenerate files

* fix incorrect boolean argument to require generate

* fix test error log in template_test.go not reflecting openapiPathItemObject change

* create custom json and yaml marshallers for openapiPathsObject

* marshal PathItemObject in custom yaml marshaller, instead of encode

* regenerate files

* further test template.go functions

* fix golangci warnings

* document `preserve_rpc_order` option

* regenerate files after rebase
  • Loading branch information
CemGurhan committed Aug 22, 2023
1 parent 0651476 commit f3a72ed
Show file tree
Hide file tree
Showing 8 changed files with 2,429 additions and 82 deletions.
27 changes: 27 additions & 0 deletions docs/docs/mapping/customizing_openapi_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -847,5 +847,32 @@ or with `protoc`:
protoc --openapiv2_out=. --openapiv2_opt=ignore_comments=true ./path/to/file.proto
```

### Preserve RPC Path Order

By default, generated Swagger files emit paths found in proto files in alphabetical order. If you would like to
preserve the order of emitted paths to mirror the path order found in proto files, you can use the `preserve_rpc_order` option. If set to `true`, this option will ensure path ordering is preserved for Swagger files with both json and yaml formats.

This option will also ensure path ordering is preserved in the following scenarios:

1. When using additional bindings, paths will preserve their ordering within an RPC.
2. When using multiple services, paths will preserve their ordering between RPCs in the whole protobuf file.
3. When merging protobuf files, paths will preserve their ordering depending on the order of files specified on the command line.

`preserve_rpc_order` can be passed via the `protoc` CLI:

```sh
protoc --openapiv2_out=. --openapiv2_opt=preserve_rpc_order=true ./path/to/file.proto
```

Or, with `buf` in `buf.gen.yaml`:

```yaml
version: v1
plugins:
- name: openapiv2
out: .
opt:
- preserve_rpc_order=true
```

{% endraw %}
14 changes: 14 additions & 0 deletions internal/descriptor/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ type Registry struct {

// allowPatchFeature determines whether to use PATCH feature involving update masks (using google.protobuf.FieldMask).
allowPatchFeature bool

// preserveRPCOrder, if true, will ensure the order of paths emitted in openapi swagger files mirror
// the order of RPC methods found in proto files. If false, emitted paths will be ordered alphabetically.
preserveRPCOrder bool
}

type repeatedFieldSeparator struct {
Expand Down Expand Up @@ -811,3 +815,13 @@ func (r *Registry) SetAllowPatchFeature(allow bool) {
func (r *Registry) GetAllowPatchFeature() bool {
return r.allowPatchFeature
}

// SetPreserveRPCOrder sets preserveRPCOrder
func (r *Registry) SetPreserveRPCOrder(preserve bool) {
r.preserveRPCOrder = preserve
}

// IsPreserveRPCOrder returns preserveRPCOrder
func (r *Registry) IsPreserveRPCOrder() bool {
return r.preserveRPCOrder
}
70 changes: 67 additions & 3 deletions protoc-gen-openapiv2/internal/genopenapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"path/filepath"
"reflect"
"sort"
"strings"

"github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor"
Expand All @@ -19,6 +20,7 @@ import (
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/pluginpb"
"gopkg.in/yaml.v3"
)

var errNoTargetService = errors.New("no target service defined in the file")
Expand Down Expand Up @@ -59,12 +61,10 @@ func mergeTargetFile(targets []*wrapper, mergeFileName string) *wrapper {
for k, v := range f.swagger.Definitions {
mergedTarget.swagger.Definitions[k] = v
}
for k, v := range f.swagger.Paths {
mergedTarget.swagger.Paths[k] = v
}
for k, v := range f.swagger.SecurityDefinitions {
mergedTarget.swagger.SecurityDefinitions[k] = v
}
copy(mergedTarget.swagger.Paths, f.swagger.Paths)
mergedTarget.swagger.Security = append(mergedTarget.swagger.Security, f.swagger.Security...)
}
}
Expand Down Expand Up @@ -112,6 +112,58 @@ func (so openapiSwaggerObject) MarshalYAML() (interface{}, error) {
}, nil
}

// Custom json marshaller for openapiPathsObject. Ensures
// openapiPathsObject is marshalled into expected format in generated
// swagger.json.
func (po openapiPathsObject) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer

buf.WriteString("{")
for i, pd := range po {
if i != 0 {
buf.WriteString(",")
}
// marshal key
key, err := json.Marshal(pd.Path)
if err != nil {
return nil, err
}
buf.Write(key)
buf.WriteString(":")
// marshal value
val, err := json.Marshal(pd.PathItemObject)
if err != nil {
return nil, err
}
buf.Write(val)
}

buf.WriteString("}")
return buf.Bytes(), nil
}

// Custom yaml marshaller for openapiPathsObject. Ensures
// openapiPathsObject is marshalled into expected format in generated
// swagger.yaml.
func (po openapiPathsObject) MarshalYAML() (interface{}, error) {
var pathObjectNode yaml.Node
pathObjectNode.Kind = yaml.MappingNode

for _, pathData := range po {
var pathNode, pathItemObjectNode yaml.Node

pathNode.SetString(pathData.Path)
b, err := yaml.Marshal(pathData.PathItemObject)
if err != nil {
return nil, err
}
pathItemObjectNode.SetString(string(b))
pathObjectNode.Content = append(pathObjectNode.Content, &pathNode, &pathItemObjectNode)
}

return pathObjectNode, nil
}

func (so openapiInfoObject) MarshalJSON() ([]byte, error) {
type alias openapiInfoObject
return extensionMarshalJSON(alias(so), so.extensions)
Expand Down Expand Up @@ -341,6 +393,9 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response

if g.reg.IsAllowMerge() {
targetOpenAPI := mergeTargetFile(openapis, g.reg.GetMergeFileName())
if !g.reg.IsPreserveRPCOrder() {
targetOpenAPI.swagger.sortPathsAlphabetically()
}
f, err := encodeOpenAPI(targetOpenAPI, g.format)
if err != nil {
return nil, fmt.Errorf("failed to encode OpenAPI for %s: %w", g.reg.GetMergeFileName(), err)
Expand All @@ -351,6 +406,9 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
}
} else {
for _, file := range openapis {
if !g.reg.IsPreserveRPCOrder() {
file.swagger.sortPathsAlphabetically()
}
f, err := encodeOpenAPI(file, g.format)
if err != nil {
return nil, fmt.Errorf("failed to encode OpenAPI for %s: %w", file.fileName, err)
Expand All @@ -364,6 +422,12 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
return files, nil
}

func (so openapiSwaggerObject) sortPathsAlphabetically() {
sort.Slice(so.Paths, func(i, j int) bool {
return so.Paths[i].Path < so.Paths[j].Path
})
}

// AddErrorDefs Adds google.rpc.Status and google.protobuf.Any
// to registry (used for error-related API responses)
func AddErrorDefs(reg *descriptor.Registry) error {
Expand Down
Loading

0 comments on commit f3a72ed

Please sign in to comment.