Skip to content

Commit f3a72ed

Browse files
authored
Feature/issue 3051 swagger preserve rpc order (#3500)
* 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
1 parent 0651476 commit f3a72ed

File tree

8 files changed

+2429
-82
lines changed

8 files changed

+2429
-82
lines changed

docs/docs/mapping/customizing_openapi_output.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,5 +847,32 @@ or with `protoc`:
847847
protoc --openapiv2_out=. --openapiv2_opt=ignore_comments=true ./path/to/file.proto
848848
```
849849

850+
### Preserve RPC Path Order
851+
852+
By default, generated Swagger files emit paths found in proto files in alphabetical order. If you would like to
853+
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.
854+
855+
This option will also ensure path ordering is preserved in the following scenarios:
856+
857+
1. When using additional bindings, paths will preserve their ordering within an RPC.
858+
2. When using multiple services, paths will preserve their ordering between RPCs in the whole protobuf file.
859+
3. When merging protobuf files, paths will preserve their ordering depending on the order of files specified on the command line.
860+
861+
`preserve_rpc_order` can be passed via the `protoc` CLI:
862+
863+
```sh
864+
protoc --openapiv2_out=. --openapiv2_opt=preserve_rpc_order=true ./path/to/file.proto
865+
```
866+
867+
Or, with `buf` in `buf.gen.yaml`:
868+
869+
```yaml
870+
version: v1
871+
plugins:
872+
- name: openapiv2
873+
out: .
874+
opt:
875+
- preserve_rpc_order=true
876+
```
850877

851878
{% endraw %}

internal/descriptor/registry.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ type Registry struct {
146146

147147
// allowPatchFeature determines whether to use PATCH feature involving update masks (using google.protobuf.FieldMask).
148148
allowPatchFeature bool
149+
150+
// preserveRPCOrder, if true, will ensure the order of paths emitted in openapi swagger files mirror
151+
// the order of RPC methods found in proto files. If false, emitted paths will be ordered alphabetically.
152+
preserveRPCOrder bool
149153
}
150154

151155
type repeatedFieldSeparator struct {
@@ -811,3 +815,13 @@ func (r *Registry) SetAllowPatchFeature(allow bool) {
811815
func (r *Registry) GetAllowPatchFeature() bool {
812816
return r.allowPatchFeature
813817
}
818+
819+
// SetPreserveRPCOrder sets preserveRPCOrder
820+
func (r *Registry) SetPreserveRPCOrder(preserve bool) {
821+
r.preserveRPCOrder = preserve
822+
}
823+
824+
// IsPreserveRPCOrder returns preserveRPCOrder
825+
func (r *Registry) IsPreserveRPCOrder() bool {
826+
return r.preserveRPCOrder
827+
}

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"path/filepath"
99
"reflect"
10+
"sort"
1011
"strings"
1112

1213
"github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor"
@@ -19,6 +20,7 @@ import (
1920
"google.golang.org/protobuf/types/descriptorpb"
2021
"google.golang.org/protobuf/types/known/anypb"
2122
"google.golang.org/protobuf/types/pluginpb"
23+
"gopkg.in/yaml.v3"
2224
)
2325

2426
var errNoTargetService = errors.New("no target service defined in the file")
@@ -59,12 +61,10 @@ func mergeTargetFile(targets []*wrapper, mergeFileName string) *wrapper {
5961
for k, v := range f.swagger.Definitions {
6062
mergedTarget.swagger.Definitions[k] = v
6163
}
62-
for k, v := range f.swagger.Paths {
63-
mergedTarget.swagger.Paths[k] = v
64-
}
6564
for k, v := range f.swagger.SecurityDefinitions {
6665
mergedTarget.swagger.SecurityDefinitions[k] = v
6766
}
67+
copy(mergedTarget.swagger.Paths, f.swagger.Paths)
6868
mergedTarget.swagger.Security = append(mergedTarget.swagger.Security, f.swagger.Security...)
6969
}
7070
}
@@ -112,6 +112,58 @@ func (so openapiSwaggerObject) MarshalYAML() (interface{}, error) {
112112
}, nil
113113
}
114114

115+
// Custom json marshaller for openapiPathsObject. Ensures
116+
// openapiPathsObject is marshalled into expected format in generated
117+
// swagger.json.
118+
func (po openapiPathsObject) MarshalJSON() ([]byte, error) {
119+
var buf bytes.Buffer
120+
121+
buf.WriteString("{")
122+
for i, pd := range po {
123+
if i != 0 {
124+
buf.WriteString(",")
125+
}
126+
// marshal key
127+
key, err := json.Marshal(pd.Path)
128+
if err != nil {
129+
return nil, err
130+
}
131+
buf.Write(key)
132+
buf.WriteString(":")
133+
// marshal value
134+
val, err := json.Marshal(pd.PathItemObject)
135+
if err != nil {
136+
return nil, err
137+
}
138+
buf.Write(val)
139+
}
140+
141+
buf.WriteString("}")
142+
return buf.Bytes(), nil
143+
}
144+
145+
// Custom yaml marshaller for openapiPathsObject. Ensures
146+
// openapiPathsObject is marshalled into expected format in generated
147+
// swagger.yaml.
148+
func (po openapiPathsObject) MarshalYAML() (interface{}, error) {
149+
var pathObjectNode yaml.Node
150+
pathObjectNode.Kind = yaml.MappingNode
151+
152+
for _, pathData := range po {
153+
var pathNode, pathItemObjectNode yaml.Node
154+
155+
pathNode.SetString(pathData.Path)
156+
b, err := yaml.Marshal(pathData.PathItemObject)
157+
if err != nil {
158+
return nil, err
159+
}
160+
pathItemObjectNode.SetString(string(b))
161+
pathObjectNode.Content = append(pathObjectNode.Content, &pathNode, &pathItemObjectNode)
162+
}
163+
164+
return pathObjectNode, nil
165+
}
166+
115167
func (so openapiInfoObject) MarshalJSON() ([]byte, error) {
116168
type alias openapiInfoObject
117169
return extensionMarshalJSON(alias(so), so.extensions)
@@ -341,6 +393,9 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
341393

342394
if g.reg.IsAllowMerge() {
343395
targetOpenAPI := mergeTargetFile(openapis, g.reg.GetMergeFileName())
396+
if !g.reg.IsPreserveRPCOrder() {
397+
targetOpenAPI.swagger.sortPathsAlphabetically()
398+
}
344399
f, err := encodeOpenAPI(targetOpenAPI, g.format)
345400
if err != nil {
346401
return nil, fmt.Errorf("failed to encode OpenAPI for %s: %w", g.reg.GetMergeFileName(), err)
@@ -351,6 +406,9 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
351406
}
352407
} else {
353408
for _, file := range openapis {
409+
if !g.reg.IsPreserveRPCOrder() {
410+
file.swagger.sortPathsAlphabetically()
411+
}
354412
f, err := encodeOpenAPI(file, g.format)
355413
if err != nil {
356414
return nil, fmt.Errorf("failed to encode OpenAPI for %s: %w", file.fileName, err)
@@ -364,6 +422,12 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
364422
return files, nil
365423
}
366424

425+
func (so openapiSwaggerObject) sortPathsAlphabetically() {
426+
sort.Slice(so.Paths, func(i, j int) bool {
427+
return so.Paths[i].Path < so.Paths[j].Path
428+
})
429+
}
430+
367431
// AddErrorDefs Adds google.rpc.Status and google.protobuf.Any
368432
// to registry (used for error-related API responses)
369433
func AddErrorDefs(reg *descriptor.Registry) error {

0 commit comments

Comments
 (0)