Skip to content

Commit 1f23646

Browse files
feat(protoc-gen-openapiv3): implemented basic component generation
1 parent e4404e0 commit 1f23646

File tree

3 files changed

+281
-161
lines changed

3 files changed

+281
-161
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,3 @@ vendor
1414
# Generated travis files
1515
.travis.yml
1616

17-
protoc-gen-openapiv3

protoc-gen-openapiv3/internal/genopenapiv3/generator.go

Lines changed: 134 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package genopenapiv3
33
import (
44
"fmt"
55
"path/filepath"
6+
"strings"
7+
"sync"
68

79
"github.com/getkin/kin-openapi/openapi3"
810
"github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor"
@@ -14,71 +16,6 @@ import (
1416
"google.golang.org/protobuf/types/pluginpb"
1517
)
1618

17-
var wktSchemas = map[string]*openapi3.Schema{
18-
".google.protobuf.FieldMask": {
19-
Type: &openapi3.Types{"string"},
20-
},
21-
".google.protobuf.Timestamp": {
22-
Type: &openapi3.Types{"string"},
23-
Format: "date-time",
24-
},
25-
".google.protobuf.Duration": {
26-
Type: &openapi3.Types{"string"},
27-
},
28-
".google.protobuf.StringValue": {
29-
Type: &openapi3.Types{"string"},
30-
},
31-
".google.protobuf.BytesValue": {
32-
Type: &openapi3.Types{"string"},
33-
Format: "byte",
34-
},
35-
".google.protobuf.Int32Value": {
36-
Type: &openapi3.Types{"integer"},
37-
Format: "int32",
38-
},
39-
".google.protobuf.UInt32Value": {
40-
Type: &openapi3.Types{"integer"},
41-
Format: "int64",
42-
},
43-
".google.protobuf.Int64Value": {
44-
Type: &openapi3.Types{"string"},
45-
Format: "int64",
46-
},
47-
".google.protobuf.UInt64Value": {
48-
Type: &openapi3.Types{"string"},
49-
Format: "uint64",
50-
},
51-
".google.protobuf.FloatValue": {
52-
Type: &openapi3.Types{"number"},
53-
Format: "float",
54-
},
55-
".google.protobuf.DoubleValue": {
56-
Type: &openapi3.Types{"number"},
57-
Format: "double",
58-
},
59-
".google.protobuf.BoolValue": {
60-
Type: &openapi3.Types{"boolean"},
61-
},
62-
".google.protobuf.Empty": {
63-
Type: &openapi3.Types{"object"},
64-
},
65-
".google.protobuf.Struct": {
66-
Type: &openapi3.Types{"object"},
67-
},
68-
".google.protobuf.Value": {},
69-
".google.protobuf.ListValue": {
70-
Type: &openapi3.Types{"array"},
71-
Items: &openapi3.SchemaRef{
72-
Value: &openapi3.Schema{
73-
Type: &openapi3.Types{"object"},
74-
},
75-
},
76-
},
77-
".google.protobuf.NullValue": {
78-
Type: &openapi3.Types{"string"},
79-
},
80-
}
81-
8219
type generator struct {
8320
reg *descriptor.Registry
8421
format Format
@@ -104,6 +41,9 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
10441
}
10542

10643
components := openapi3.NewComponents()
44+
components.Schemas = make(openapi3.Schemas)
45+
doc.Components = &components
46+
10747
for _, msg := range t.Messages {
10848
g.addMessageToSchemes(msg, components.Schemas)
10949
}
@@ -130,14 +70,91 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*descriptor.Response
13070
}
13171

13272
func (g *generator) addMessageToSchemes(msg *descriptor.Message, schemas openapi3.Schemas) {
133-
if scheme, ok := wktSchemas[msg.FQMN()]; ok {
134-
schemas[msg.FQMN()] = &openapi3.SchemaRef{
73+
msgName := g.getMessageName(msg.FQMN())
74+
if scheme, ok := wktSchemas[msgName]; ok {
75+
schemas[msgName] = &openapi3.SchemaRef{
13576
Value: scheme,
13677
}
13778
return
13879
}
13980

140-
// TODO: Implement schema generation for custom messages
81+
schema := &openapi3.Schema{
82+
Type: &openapi3.Types{openapi3.TypeObject},
83+
}
84+
85+
properties := make(openapi3.Schemas)
86+
87+
for _, field := range msg.Fields {
88+
properties[field.GetName()] = g.generateFieldDoc(field, schemas)
89+
}
90+
91+
schema.Properties = properties
92+
93+
schemas[msgName] = &openapi3.SchemaRef{
94+
Value: schema,
95+
}
96+
}
97+
98+
func (g *generator) generateFieldDoc(field *descriptor.Field, schemas openapi3.Schemas) *openapi3.SchemaRef {
99+
fd := field.FieldDescriptorProto
100+
location := ""
101+
if ix := strings.LastIndex(field.Message.FQMN(), "."); ix > 0 {
102+
location = field.Message.FQMN()[0:ix]
103+
}
104+
105+
if m, err := g.reg.LookupMsg(location, field.GetTypeName()); err == nil {
106+
if opt := m.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry {
107+
// Generate Map<k, v> schema
108+
return &openapi3.SchemaRef{
109+
Value: &openapi3.Schema{
110+
AdditionalProperties: openapi3.AdditionalProperties{
111+
Schema: g.generateFieldTypeSchema(m.GetField()[1], schemas),
112+
},
113+
},
114+
}
115+
}
116+
}
117+
118+
if field.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
119+
return &openapi3.SchemaRef{
120+
Value: &openapi3.Schema{
121+
Type: &openapi3.Types{openapi3.TypeArray},
122+
Items: g.generateFieldTypeSchema(fd, schemas),
123+
},
124+
}
125+
}
126+
127+
return g.generateFieldTypeSchema(fd, schemas)
128+
}
129+
130+
func (g *generator) generateFieldTypeSchema(fd *descriptorpb.FieldDescriptorProto, schemas openapi3.Schemas) *openapi3.SchemaRef {
131+
if schema, ok := primitiveTypeSchemas[fd.GetType()]; ok {
132+
return &openapi3.SchemaRef{
133+
Value: schema,
134+
}
135+
}
136+
137+
switch ft := fd.GetType(); ft {
138+
case descriptorpb.FieldDescriptorProto_TYPE_ENUM, descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, descriptorpb.FieldDescriptorProto_TYPE_GROUP:
139+
openAPIRef, ok := g.fullyQualifiedNameToOpenAPIName(fd.GetTypeName())
140+
if !ok {
141+
panic(fmt.Sprintf("can't resolve OpenAPI ref from typename %q", fd.GetTypeName()))
142+
}
143+
144+
return &openapi3.SchemaRef{
145+
Ref: "#/definitions/" + openAPIRef,
146+
}
147+
default:
148+
return &openapi3.SchemaRef{
149+
Value: &openapi3.Schema{Type: &openapi3.Types{ft.String()}, Format: "UNKNOWN"},
150+
}
151+
}
152+
153+
}
154+
155+
func (g *generator) getMessageName(fqmn string) string {
156+
// TODO: have different naming stratgies
157+
return fqmn[1:]
141158
}
142159

143160
func (g *generator) generateServiceDoc(svc *descriptor.Service) {
@@ -238,97 +255,54 @@ func extractOperationOptionFromMethodDescriptor(meth *descriptorpb.MethodDescrip
238255
return opts, nil
239256
}
240257

241-
func primitiveTypeSchema(t descriptorpb.FieldDescriptorProto_Type) (*openapi3.Schema, bool) {
242-
switch t {
243-
case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE:
244-
return &openapi3.Schema{
245-
Type: &openapi3.Types{"number"},
246-
Format: "double",
247-
}, true
248-
case descriptorpb.FieldDescriptorProto_TYPE_FLOAT:
249-
return &openapi3.Schema{
250-
Type: &openapi3.Types{"number"},
251-
Format: "float",
252-
}, true
253-
case descriptorpb.FieldDescriptorProto_TYPE_INT64:
254-
// 64bit integer types are marshaled as string in the default JSONPb marshaler.
255-
// This maintains compatibility with JSON's limited number precision.
256-
return &openapi3.Schema{
257-
Type: &openapi3.Types{"string"},
258-
Format: "int64",
259-
}, true
260-
case descriptorpb.FieldDescriptorProto_TYPE_UINT64:
261-
// 64bit integer types are marshaled as string in the default JSONPb marshaler.
262-
// TODO(yugui) Add an option to declare 64bit integers as int64.
263-
//
264-
// NOTE: uint64 is not a standard format in OpenAPI spec.
265-
// So we cannot expect that uint64 is commonly supported by OpenAPI processors.
266-
return &openapi3.Schema{
267-
Type: &openapi3.Types{"string"},
268-
Format: "uint64",
269-
}, true
270-
case descriptorpb.FieldDescriptorProto_TYPE_INT32:
271-
return &openapi3.Schema{
272-
Type: &openapi3.Types{"integer"},
273-
Format: "int32",
274-
}, true
275-
case descriptorpb.FieldDescriptorProto_TYPE_FIXED64:
276-
// 64bit types marshaled as string for JSON compatibility
277-
return &openapi3.Schema{
278-
Type: &openapi3.Types{"string"},
279-
Format: "uint64",
280-
}, true
281-
case descriptorpb.FieldDescriptorProto_TYPE_FIXED32:
282-
// Fixed 32-bit unsigned integer
283-
return &openapi3.Schema{
284-
Type: &openapi3.Types{"integer"},
285-
Format: "int32",
286-
}, true
287-
case descriptorpb.FieldDescriptorProto_TYPE_BOOL:
288-
// NOTE: In OpenAPI v3 specification, format should be empty on boolean type
289-
return &openapi3.Schema{
290-
Type: &openapi3.Types{"boolean"},
291-
}, true
292-
case descriptorpb.FieldDescriptorProto_TYPE_STRING:
293-
// NOTE: In OpenAPI v3 specification, format can be empty on string type
294-
return &openapi3.Schema{
295-
Type: &openapi3.Types{"string"},
296-
}, true
297-
case descriptorpb.FieldDescriptorProto_TYPE_BYTES:
298-
// Base64 encoded string representation
299-
return &openapi3.Schema{
300-
Type: &openapi3.Types{"string"},
301-
Format: "byte",
302-
}, true
303-
case descriptorpb.FieldDescriptorProto_TYPE_UINT32:
304-
// 32-bit unsigned integer
305-
return &openapi3.Schema{
306-
Type: &openapi3.Types{"integer"},
307-
Format: "int32",
308-
}, true
309-
case descriptorpb.FieldDescriptorProto_TYPE_SFIXED32:
310-
return &openapi3.Schema{
311-
Type: &openapi3.Types{"integer"},
312-
Format: "int32",
313-
}, true
314-
case descriptorpb.FieldDescriptorProto_TYPE_SFIXED64:
315-
// 64bit types marshaled as string for JSON compatibility
316-
return &openapi3.Schema{
317-
Type: &openapi3.Types{"string"},
318-
Format: "int64",
319-
}, true
320-
case descriptorpb.FieldDescriptorProto_TYPE_SINT32:
321-
return &openapi3.Schema{
322-
Type: &openapi3.Types{"integer"},
323-
Format: "int32",
324-
}, true
325-
case descriptorpb.FieldDescriptorProto_TYPE_SINT64:
326-
// 64bit types marshaled as string for JSON compatibility
327-
return &openapi3.Schema{
328-
Type: &openapi3.Types{"string"},
329-
Format: "int64",
330-
}, true
331-
default:
332-
return nil, false
258+
// Take in a FQMN or FQEN and return a OpenAPI safe version of the FQMN and
259+
// a boolean indicating if FQMN was properly resolved.
260+
func (g *generator) fullyQualifiedNameToOpenAPIName(fqn string) (string, bool) {
261+
registriesSeenMutex.Lock()
262+
defer registriesSeenMutex.Unlock()
263+
if mapping, present := registriesSeen[g.reg]; present {
264+
ret, ok := mapping[fqn]
265+
return ret, ok
266+
}
267+
268+
mapping := g.resolveFullyQualifiedNameToOpenAPINames(append(g.reg.GetAllFQMNs(), append(g.reg.GetAllFQENs(), g.reg.GetAllFQMethNs()...)...), g.reg.GetOpenAPINamingStrategy())
269+
registriesSeen[g.reg] = mapping
270+
ret, ok := mapping[fqn]
271+
272+
return ret, ok
273+
}
274+
275+
// Lookup message type by location.name and return an openapiv2-safe version
276+
// of its FQMN.
277+
func (g *generator) lookupMsgAndOpenAPIName(location, name string) (*descriptor.Message, string, error) {
278+
msg, err := g.reg.LookupMsg(location, name)
279+
if err != nil {
280+
return nil, "", err
281+
}
282+
swgName, ok := g.fullyQualifiedNameToOpenAPIName(msg.FQMN())
283+
if !ok {
284+
return nil, "", fmt.Errorf("can't map OpenAPI name from FQMN %q", msg.FQMN())
285+
}
286+
return msg, swgName, nil
287+
}
288+
289+
// registriesSeen is used to memoise calls to resolveFullyQualifiedNameToOpenAPINames so
290+
// we don't repeat it unnecessarily, since it can take some time.
291+
var (
292+
registriesSeen = map[*descriptor.Registry]map[string]string{}
293+
registriesSeenMutex sync.Mutex
294+
)
295+
296+
// Take the names of every proto message and generate a unique reference for each, according to the given strategy.
297+
func (g *generator) resolveFullyQualifiedNameToOpenAPINames(messages []string, _ string) map[string]string {
298+
strategyFn := func(messages []string) map[string]string {
299+
res := make(map[string]string)
300+
for _, msg := range messages {
301+
res[msg] = g.getMessageName(msg)
302+
}
303+
304+
return res
333305
}
306+
307+
return strategyFn(messages)
334308
}

0 commit comments

Comments
 (0)