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

Feature/issue 3051 swagger preserve rpc order #3500

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
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
johanbrandhorst marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
Loading