Skip to content

Commit 8af97f8

Browse files
authored
feat(openapi): Add flatten_oneofs plugin option (#16)
1 parent 42f38ac commit 8af97f8

4 files changed

Lines changed: 149 additions & 4 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Generated with protoc-gen-openapi
2+
# https://github.com/fern-api/protoc-gen-openapi/tree/master/cmd/protoc-gen-openapi
3+
4+
openapi: 3.0.3
5+
info:
6+
title: OneOfService API
7+
description: Service definition
8+
version: 0.0.1
9+
paths:
10+
/v1/payments:
11+
post:
12+
tags:
13+
- OneOfService
14+
operationId: OneOfService_CreatePayment
15+
requestBody:
16+
content:
17+
application/json:
18+
schema:
19+
$ref: '#/components/schemas/OneOfMessage'
20+
required: true
21+
responses:
22+
"200":
23+
description: OK
24+
content:
25+
application/json:
26+
schema:
27+
$ref: '#/components/schemas/OneOfMessage'
28+
default:
29+
description: Default error response
30+
content:
31+
application/json:
32+
schema:
33+
$ref: '#/components/schemas/Status'
34+
x-fern-sdk-group-name: OneOfService
35+
x-fern-sdk-method-name: CreatePayment
36+
components:
37+
schemas:
38+
EmptyOneOf:
39+
type: object
40+
properties: {}
41+
x-fern-type-name: EmptyOneOf
42+
source: tests/oneof/message.proto
43+
GoogleProtobufAny:
44+
type: object
45+
properties:
46+
'@type':
47+
type: string
48+
description: The type of the serialized message.
49+
additionalProperties: true
50+
description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message.
51+
x-fern-encoding:
52+
proto:
53+
type: google.protobuf.Any
54+
OneOfMessage:
55+
type: object
56+
properties:
57+
id:
58+
type: string
59+
description: Identifier for the message
60+
x-fern-encoding:
61+
proto:
62+
type: google.protobuf.StringValue
63+
credit_card:
64+
type: string
65+
x-fern-encoding:
66+
proto:
67+
type: google.protobuf.StringValue
68+
bank_transfer:
69+
type: string
70+
x-fern-encoding:
71+
proto:
72+
type: google.protobuf.StringValue
73+
digital_wallet:
74+
type: string
75+
x-fern-encoding:
76+
proto:
77+
type: google.protobuf.StringValue
78+
authentication_config:
79+
$ref: '#/components/schemas/EmptyOneOf'
80+
description: Message demonstrating oneof field usage
81+
x-fern-type-name: OneOfMessage
82+
source: tests/oneof/message.proto
83+
Status:
84+
type: object
85+
properties:
86+
code:
87+
type: integer
88+
description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
89+
format: int32
90+
message:
91+
type: string
92+
description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
93+
details:
94+
type: array
95+
items:
96+
$ref: '#/components/schemas/GoogleProtobufAny'
97+
description: A list of messages that carry the error details. There is a common set of message types for APIs to use.
98+
description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).'
99+
tags:
100+
- name: OneOfService

cmd/protoc-gen-openapi/generator/generator.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Configuration struct {
4747
DefaultResponse *bool
4848
OutputMode *string
4949
SourceRoot *string
50+
FlattenOneofs *bool
5051
}
5152

5253
const (
@@ -894,10 +895,10 @@ func (g *OpenAPIv3Generator) addSchemasForMessagesToDocumentV3(d *v3.Document, m
894895
var required []string
895896

896897
for _, field := range message.Fields {
897-
// Skip fields that are part of an explicit oneOf.
898+
// Skip fields that are part of an explicit oneOf (unless flattening is enabled).
898899
// Proto3 optional fields create synthetic oneofs that should be
899900
// treated as regular optional fields, not as oneOf variants.
900-
if field.Oneof != nil && !field.Oneof.Desc.IsSynthetic() {
901+
if field.Oneof != nil && !field.Oneof.Desc.IsSynthetic() && !*g.conf.FlattenOneofs {
901902
continue
902903
}
903904

@@ -969,8 +970,11 @@ func (g *OpenAPIv3Generator) addSchemasForMessagesToDocumentV3(d *v3.Document, m
969970
Required: required,
970971
}
971972

972-
// Add oneOf fields to the schema
973-
g.addOneOfFieldsToSchema(d, message.Oneofs, schema, schemaName, filename)
973+
// Add oneOf fields to the schema (unless flattening is enabled,
974+
// in which case they were already added as regular properties).
975+
if !*g.conf.FlattenOneofs {
976+
g.addOneOfFieldsToSchema(d, message.Oneofs, schema, schemaName, filename)
977+
}
974978

975979
// Merge any `Schema` annotations with the current
976980
extSchema := proto.GetExtension(message.Desc.Options(), v3.E_Schema)

cmd/protoc-gen-openapi/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func main() {
3939
DefaultResponse: flags.Bool("default_response", true, `add default response. If "true", automatically adds a default response to operations which use the google.rpc.Status message. Useful if you use envoy or grpc-gateway to transcode as they use this type for their default error responses.`),
4040
OutputMode: flags.String("output_mode", "merged", `output generation mode. By default, a single openapi.yaml is generated at the out folder. Use "source_relative' to generate a separate '[inputfile].openapi.yaml' next to each '[inputfile].proto'.`),
4141
SourceRoot: flags.String("source_root", "", `root directory of the source Protobuf files. This is used to add source information to the openapi.yaml file.`),
42+
FlattenOneofs: flags.Bool("flatten_oneofs", false, `flatten oneof fields as regular properties on the parent message instead of generating a oneOf union`),
4243
}
4344

4445
opts := protogen.Options{

cmd/protoc-gen-openapi/plugin_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,46 @@ func TestOpenAPIStringEnums(t *testing.T) {
270270
}
271271
}
272272

273+
func TestOpenAPIFlattenOneofs(t *testing.T) {
274+
// Set PATH to include the protoc-gen-openapi plugin
275+
os.Setenv("PATH", "../../:"+os.Getenv("PATH"))
276+
277+
for _, tt := range openapiTests {
278+
fixture := path.Join(tt.path, "openapi_flatten_oneofs.yaml")
279+
if _, err := os.Stat(fixture); errors.Is(err, os.ErrNotExist) {
280+
if !GENERATE_FIXTURES {
281+
continue
282+
}
283+
}
284+
t.Run(tt.name, func(t *testing.T) {
285+
err := exec.Command("protoc",
286+
"-I", "../../",
287+
"-I", "../../third_party",
288+
"-I", "examples",
289+
path.Join(tt.path, tt.protofile),
290+
"--plugin=protoc-gen-openapi=./protoc-gen-openapi",
291+
"--openapi_out=naming=proto,flatten_oneofs=true:.").Run()
292+
if err != nil {
293+
t.Fatalf("protoc failed: %+v", err)
294+
}
295+
if GENERATE_FIXTURES {
296+
err := CopyFixture(TEMP_FILE, fixture)
297+
if err != nil {
298+
t.Fatalf("Can't generate fixture: %+v", err)
299+
}
300+
} else {
301+
// Verify that the generated spec matches our expected version.
302+
err = exec.Command("diff", TEMP_FILE, fixture).Run()
303+
if err != nil {
304+
t.Fatalf("Diff failed: %+v", err)
305+
}
306+
}
307+
// if the test succeeded, clean up
308+
os.Remove(TEMP_FILE)
309+
})
310+
}
311+
}
312+
273313
func TestOpenAPIDefaultResponse(t *testing.T) {
274314
// Set PATH to include the protoc-gen-openapi plugin
275315
os.Setenv("PATH", "../../:"+os.Getenv("PATH"))

0 commit comments

Comments
 (0)