blob: d40786f251fa111e58ee2a1da1ebd7cd29888f56 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001package swagger
2
3import (
4 "encoding/json"
5 "reflect"
6 "strings"
7)
8
9// ModelBuildable is used for extending Structs that need more control over
10// how the Model appears in the Swagger api declaration.
11type ModelBuildable interface {
12 PostBuildModel(m *Model) *Model
13}
14
15type modelBuilder struct {
16 Models *ModelList
17 Config *Config
18}
19
20type documentable interface {
21 SwaggerDoc() map[string]string
22}
23
24// Check if this structure has a method with signature func (<theModel>) SwaggerDoc() map[string]string
25// If it exists, retrive the documentation and overwrite all struct tag descriptions
26func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string {
27 if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok {
28 return docable.SwaggerDoc()
29 }
30 return make(map[string]string)
31}
32
33// addModelFrom creates and adds a Model to the builder and detects and calls
34// the post build hook for customizations
35func (b modelBuilder) addModelFrom(sample interface{}) {
36 if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil {
37 // allow customizations
38 if buildable, ok := sample.(ModelBuildable); ok {
39 modelOrNil = buildable.PostBuildModel(modelOrNil)
40 b.Models.Put(modelOrNil.Id, *modelOrNil)
41 }
42 }
43}
44
45func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
46 // Turn pointers into simpler types so further checks are
47 // correct.
48 if st.Kind() == reflect.Ptr {
49 st = st.Elem()
50 }
51
52 modelName := b.keyFrom(st)
53 if nameOverride != "" {
54 modelName = nameOverride
55 }
56 // no models needed for primitive types
57 if b.isPrimitiveType(modelName) {
58 return nil
59 }
60 // golang encoding/json packages says array and slice values encode as
61 // JSON arrays, except that []byte encodes as a base64-encoded string.
62 // If we see a []byte here, treat it at as a primitive type (string)
63 // and deal with it in buildArrayTypeProperty.
64 if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) &&
65 st.Elem().Kind() == reflect.Uint8 {
66 return nil
67 }
68 // see if we already have visited this model
69 if _, ok := b.Models.At(modelName); ok {
70 return nil
71 }
72 sm := Model{
73 Id: modelName,
74 Required: []string{},
75 Properties: ModelPropertyList{}}
76
77 // reference the model before further initializing (enables recursive structs)
78 b.Models.Put(modelName, sm)
79
80 // check for slice or array
81 if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
82 b.addModel(st.Elem(), "")
83 return &sm
84 }
85 // check for structure or primitive type
86 if st.Kind() != reflect.Struct {
87 return &sm
88 }
89
90 fullDoc := getDocFromMethodSwaggerDoc2(st)
91 modelDescriptions := []string{}
92
93 for i := 0; i < st.NumField(); i++ {
94 field := st.Field(i)
95 jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
96 if len(modelDescription) > 0 {
97 modelDescriptions = append(modelDescriptions, modelDescription)
98 }
99
100 // add if not omitted
101 if len(jsonName) != 0 {
102 // update description
103 if fieldDoc, ok := fullDoc[jsonName]; ok {
104 prop.Description = fieldDoc
105 }
106 // update Required
107 if b.isPropertyRequired(field) {
108 sm.Required = append(sm.Required, jsonName)
109 }
110 sm.Properties.Put(jsonName, prop)
111 }
112 }
113
114 // We always overwrite documentation if SwaggerDoc method exists
115 // "" is special for documenting the struct itself
116 if modelDoc, ok := fullDoc[""]; ok {
117 sm.Description = modelDoc
118 } else if len(modelDescriptions) != 0 {
119 sm.Description = strings.Join(modelDescriptions, "\n")
120 }
121
122 // update model builder with completed model
123 b.Models.Put(modelName, sm)
124
125 return &sm
126}
127
128func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
129 required := true
130 if jsonTag := field.Tag.Get("json"); jsonTag != "" {
131 s := strings.Split(jsonTag, ",")
132 if len(s) > 1 && s[1] == "omitempty" {
133 return false
134 }
135 }
136 return required
137}
138
139func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) {
140 jsonName = b.jsonNameOfField(field)
141 if len(jsonName) == 0 {
142 // empty name signals skip property
143 return "", "", prop
144 }
145
146 if field.Name == "XMLName" && field.Type.String() == "xml.Name" {
147 // property is metadata for the xml.Name attribute, can be skipped
148 return "", "", prop
149 }
150
151 if tag := field.Tag.Get("modelDescription"); tag != "" {
152 modelDescription = tag
153 }
154
155 prop.setPropertyMetadata(field)
156 if prop.Type != nil {
157 return jsonName, modelDescription, prop
158 }
159 fieldType := field.Type
160
161 // check if type is doing its own marshalling
162 marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
163 if fieldType.Implements(marshalerType) {
164 var pType = "string"
165 if prop.Type == nil {
166 prop.Type = &pType
167 }
168 if prop.Format == "" {
169 prop.Format = b.jsonSchemaFormat(b.keyFrom(fieldType))
170 }
171 return jsonName, modelDescription, prop
172 }
173
174 // check if annotation says it is a string
175 if jsonTag := field.Tag.Get("json"); jsonTag != "" {
176 s := strings.Split(jsonTag, ",")
177 if len(s) > 1 && s[1] == "string" {
178 stringt := "string"
179 prop.Type = &stringt
180 return jsonName, modelDescription, prop
181 }
182 }
183
184 fieldKind := fieldType.Kind()
185 switch {
186 case fieldKind == reflect.Struct:
187 jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
188 return jsonName, modelDescription, prop
189 case fieldKind == reflect.Slice || fieldKind == reflect.Array:
190 jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
191 return jsonName, modelDescription, prop
192 case fieldKind == reflect.Ptr:
193 jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
194 return jsonName, modelDescription, prop
195 case fieldKind == reflect.String:
196 stringt := "string"
197 prop.Type = &stringt
198 return jsonName, modelDescription, prop
199 case fieldKind == reflect.Map:
200 // if it's a map, it's unstructured, and swagger 1.2 can't handle it
201 objectType := "object"
202 prop.Type = &objectType
203 return jsonName, modelDescription, prop
204 }
205
206 fieldTypeName := b.keyFrom(fieldType)
207 if b.isPrimitiveType(fieldTypeName) {
208 mapped := b.jsonSchemaType(fieldTypeName)
209 prop.Type = &mapped
210 prop.Format = b.jsonSchemaFormat(fieldTypeName)
211 return jsonName, modelDescription, prop
212 }
213 modelType := b.keyFrom(fieldType)
214 prop.Ref = &modelType
215
216 if fieldType.Name() == "" { // override type of anonymous structs
217 nestedTypeName := modelName + "." + jsonName
218 prop.Ref = &nestedTypeName
219 b.addModel(fieldType, nestedTypeName)
220 }
221 return jsonName, modelDescription, prop
222}
223
224func hasNamedJSONTag(field reflect.StructField) bool {
225 parts := strings.Split(field.Tag.Get("json"), ",")
226 if len(parts) == 0 {
227 return false
228 }
229 for _, s := range parts[1:] {
230 if s == "inline" {
231 return false
232 }
233 }
234 return len(parts[0]) > 0
235}
236
237func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
238 prop.setPropertyMetadata(field)
239 // Check for type override in tag
240 if prop.Type != nil {
241 return jsonName, prop
242 }
243 fieldType := field.Type
244 // check for anonymous
245 if len(fieldType.Name()) == 0 {
246 // anonymous
247 anonType := model.Id + "." + jsonName
248 b.addModel(fieldType, anonType)
249 prop.Ref = &anonType
250 return jsonName, prop
251 }
252
253 if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) {
254 // embedded struct
255 sub := modelBuilder{new(ModelList), b.Config}
256 sub.addModel(fieldType, "")
257 subKey := sub.keyFrom(fieldType)
258 // merge properties from sub
259 subModel, _ := sub.Models.At(subKey)
260 subModel.Properties.Do(func(k string, v ModelProperty) {
261 model.Properties.Put(k, v)
262 // if subModel says this property is required then include it
263 required := false
264 for _, each := range subModel.Required {
265 if k == each {
266 required = true
267 break
268 }
269 }
270 if required {
271 model.Required = append(model.Required, k)
272 }
273 })
274 // add all new referenced models
275 sub.Models.Do(func(key string, sub Model) {
276 if key != subKey {
277 if _, ok := b.Models.At(key); !ok {
278 b.Models.Put(key, sub)
279 }
280 }
281 })
282 // empty name signals skip property
283 return "", prop
284 }
285 // simple struct
286 b.addModel(fieldType, "")
287 var pType = b.keyFrom(fieldType)
288 prop.Ref = &pType
289 return jsonName, prop
290}
291
292func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
293 // check for type override in tags
294 prop.setPropertyMetadata(field)
295 if prop.Type != nil {
296 return jsonName, prop
297 }
298 fieldType := field.Type
299 if fieldType.Elem().Kind() == reflect.Uint8 {
300 stringt := "string"
301 prop.Type = &stringt
302 return jsonName, prop
303 }
304 var pType = "array"
305 prop.Type = &pType
306 isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
307 elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
308 prop.Items = new(Item)
309 if isPrimitive {
310 mapped := b.jsonSchemaType(elemTypeName)
311 prop.Items.Type = &mapped
312 } else {
313 prop.Items.Ref = &elemTypeName
314 }
315 // add|overwrite model for element type
316 if fieldType.Elem().Kind() == reflect.Ptr {
317 fieldType = fieldType.Elem()
318 }
319 if !isPrimitive {
320 b.addModel(fieldType.Elem(), elemTypeName)
321 }
322 return jsonName, prop
323}
324
325func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
326 prop.setPropertyMetadata(field)
327 // Check for type override in tags
328 if prop.Type != nil {
329 return jsonName, prop
330 }
331 fieldType := field.Type
332
333 // override type of pointer to list-likes
334 if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
335 var pType = "array"
336 prop.Type = &pType
337 isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name())
338 elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
339 if isPrimitive {
340 primName := b.jsonSchemaType(elemName)
341 prop.Items = &Item{Ref: &primName}
342 } else {
343 prop.Items = &Item{Ref: &elemName}
344 }
345 if !isPrimitive {
346 // add|overwrite model for element type
347 b.addModel(fieldType.Elem().Elem(), elemName)
348 }
349 } else {
350 // non-array, pointer type
351 fieldTypeName := b.keyFrom(fieldType.Elem())
352 var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path
353 if b.isPrimitiveType(fieldTypeName) {
354 prop.Type = &pType
355 prop.Format = b.jsonSchemaFormat(fieldTypeName)
356 return jsonName, prop
357 }
358 prop.Ref = &pType
359 elemName := ""
360 if fieldType.Elem().Name() == "" {
361 elemName = modelName + "." + jsonName
362 prop.Ref = &elemName
363 }
364 b.addModel(fieldType.Elem(), elemName)
365 }
366 return jsonName, prop
367}
368
369func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string {
370 if t.Kind() == reflect.Ptr {
371 t = t.Elem()
372 }
373 if t.Name() == "" {
374 return modelName + "." + jsonName
375 }
376 return b.keyFrom(t)
377}
378
379func (b modelBuilder) keyFrom(st reflect.Type) string {
380 key := st.String()
381 if b.Config != nil && b.Config.ModelTypeNameHandler != nil {
382 if name, ok := b.Config.ModelTypeNameHandler(st); ok {
383 key = name
384 }
385 }
386 if len(st.Name()) == 0 { // unnamed type
387 // Swagger UI has special meaning for [
388 key = strings.Replace(key, "[]", "||", -1)
389 }
390 return key
391}
392
393// see also https://golang.org/ref/spec#Numeric_types
394func (b modelBuilder) isPrimitiveType(modelName string) bool {
395 if len(modelName) == 0 {
396 return false
397 }
398 return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
399}
400
401// jsonNameOfField returns the name of the field as it should appear in JSON format
402// An empty string indicates that this field is not part of the JSON representation
403func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
404 if jsonTag := field.Tag.Get("json"); jsonTag != "" {
405 s := strings.Split(jsonTag, ",")
406 if s[0] == "-" {
407 // empty name signals skip property
408 return ""
409 } else if s[0] != "" {
410 return s[0]
411 }
412 }
413 return field.Name
414}
415
416// see also http://json-schema.org/latest/json-schema-core.html#anchor8
417func (b modelBuilder) jsonSchemaType(modelName string) string {
418 schemaMap := map[string]string{
419 "uint": "integer",
420 "uint8": "integer",
421 "uint16": "integer",
422 "uint32": "integer",
423 "uint64": "integer",
424
425 "int": "integer",
426 "int8": "integer",
427 "int16": "integer",
428 "int32": "integer",
429 "int64": "integer",
430
431 "byte": "integer",
432 "float64": "number",
433 "float32": "number",
434 "bool": "boolean",
435 "time.Time": "string",
436 }
437 mapped, ok := schemaMap[modelName]
438 if !ok {
439 return modelName // use as is (custom or struct)
440 }
441 return mapped
442}
443
444func (b modelBuilder) jsonSchemaFormat(modelName string) string {
445 if b.Config != nil && b.Config.SchemaFormatHandler != nil {
446 if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
447 return mapped
448 }
449 }
450 schemaMap := map[string]string{
451 "int": "int32",
452 "int32": "int32",
453 "int64": "int64",
454 "byte": "byte",
455 "uint": "integer",
456 "uint8": "byte",
457 "float64": "double",
458 "float32": "float",
459 "time.Time": "date-time",
460 "*time.Time": "date-time",
461 }
462 mapped, ok := schemaMap[modelName]
463 if !ok {
464 return "" // no format
465 }
466 return mapped
467}