@@ -3,6 +3,8 @@ package genopenapiv3
33import (
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-
8219type 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
13272func (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
143160func (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