d56bf090ce
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
467 lines
13 KiB
Go
467 lines
13 KiB
Go
package swagger
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// ModelBuildable is used for extending Structs that need more control over
|
|
// how the Model appears in the Swagger api declaration.
|
|
type ModelBuildable interface {
|
|
PostBuildModel(m *Model) *Model
|
|
}
|
|
|
|
type modelBuilder struct {
|
|
Models *ModelList
|
|
Config *Config
|
|
}
|
|
|
|
type documentable interface {
|
|
SwaggerDoc() map[string]string
|
|
}
|
|
|
|
// Check if this structure has a method with signature func (<theModel>) SwaggerDoc() map[string]string
|
|
// If it exists, retrive the documentation and overwrite all struct tag descriptions
|
|
func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string {
|
|
if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok {
|
|
return docable.SwaggerDoc()
|
|
}
|
|
return make(map[string]string)
|
|
}
|
|
|
|
// addModelFrom creates and adds a Model to the builder and detects and calls
|
|
// the post build hook for customizations
|
|
func (b modelBuilder) addModelFrom(sample interface{}) {
|
|
if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil {
|
|
// allow customizations
|
|
if buildable, ok := sample.(ModelBuildable); ok {
|
|
modelOrNil = buildable.PostBuildModel(modelOrNil)
|
|
b.Models.Put(modelOrNil.Id, *modelOrNil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
|
|
// Turn pointers into simpler types so further checks are
|
|
// correct.
|
|
if st.Kind() == reflect.Ptr {
|
|
st = st.Elem()
|
|
}
|
|
|
|
modelName := b.keyFrom(st)
|
|
if nameOverride != "" {
|
|
modelName = nameOverride
|
|
}
|
|
// no models needed for primitive types
|
|
if b.isPrimitiveType(modelName) {
|
|
return nil
|
|
}
|
|
// golang encoding/json packages says array and slice values encode as
|
|
// JSON arrays, except that []byte encodes as a base64-encoded string.
|
|
// If we see a []byte here, treat it at as a primitive type (string)
|
|
// and deal with it in buildArrayTypeProperty.
|
|
if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) &&
|
|
st.Elem().Kind() == reflect.Uint8 {
|
|
return nil
|
|
}
|
|
// see if we already have visited this model
|
|
if _, ok := b.Models.At(modelName); ok {
|
|
return nil
|
|
}
|
|
sm := Model{
|
|
Id: modelName,
|
|
Required: []string{},
|
|
Properties: ModelPropertyList{}}
|
|
|
|
// reference the model before further initializing (enables recursive structs)
|
|
b.Models.Put(modelName, sm)
|
|
|
|
// check for slice or array
|
|
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
|
|
b.addModel(st.Elem(), "")
|
|
return &sm
|
|
}
|
|
// check for structure or primitive type
|
|
if st.Kind() != reflect.Struct {
|
|
return &sm
|
|
}
|
|
|
|
fullDoc := getDocFromMethodSwaggerDoc2(st)
|
|
modelDescriptions := []string{}
|
|
|
|
for i := 0; i < st.NumField(); i++ {
|
|
field := st.Field(i)
|
|
jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
|
|
if len(modelDescription) > 0 {
|
|
modelDescriptions = append(modelDescriptions, modelDescription)
|
|
}
|
|
|
|
// add if not omitted
|
|
if len(jsonName) != 0 {
|
|
// update description
|
|
if fieldDoc, ok := fullDoc[jsonName]; ok {
|
|
prop.Description = fieldDoc
|
|
}
|
|
// update Required
|
|
if b.isPropertyRequired(field) {
|
|
sm.Required = append(sm.Required, jsonName)
|
|
}
|
|
sm.Properties.Put(jsonName, prop)
|
|
}
|
|
}
|
|
|
|
// We always overwrite documentation if SwaggerDoc method exists
|
|
// "" is special for documenting the struct itself
|
|
if modelDoc, ok := fullDoc[""]; ok {
|
|
sm.Description = modelDoc
|
|
} else if len(modelDescriptions) != 0 {
|
|
sm.Description = strings.Join(modelDescriptions, "\n")
|
|
}
|
|
|
|
// update model builder with completed model
|
|
b.Models.Put(modelName, sm)
|
|
|
|
return &sm
|
|
}
|
|
|
|
func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
|
|
required := true
|
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
|
s := strings.Split(jsonTag, ",")
|
|
if len(s) > 1 && s[1] == "omitempty" {
|
|
return false
|
|
}
|
|
}
|
|
return required
|
|
}
|
|
|
|
func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) {
|
|
jsonName = b.jsonNameOfField(field)
|
|
if len(jsonName) == 0 {
|
|
// empty name signals skip property
|
|
return "", "", prop
|
|
}
|
|
|
|
if field.Name == "XMLName" && field.Type.String() == "xml.Name" {
|
|
// property is metadata for the xml.Name attribute, can be skipped
|
|
return "", "", prop
|
|
}
|
|
|
|
if tag := field.Tag.Get("modelDescription"); tag != "" {
|
|
modelDescription = tag
|
|
}
|
|
|
|
prop.setPropertyMetadata(field)
|
|
if prop.Type != nil {
|
|
return jsonName, modelDescription, prop
|
|
}
|
|
fieldType := field.Type
|
|
|
|
// check if type is doing its own marshalling
|
|
marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
|
|
if fieldType.Implements(marshalerType) {
|
|
var pType = "string"
|
|
if prop.Type == nil {
|
|
prop.Type = &pType
|
|
}
|
|
if prop.Format == "" {
|
|
prop.Format = b.jsonSchemaFormat(b.keyFrom(fieldType))
|
|
}
|
|
return jsonName, modelDescription, prop
|
|
}
|
|
|
|
// check if annotation says it is a string
|
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
|
s := strings.Split(jsonTag, ",")
|
|
if len(s) > 1 && s[1] == "string" {
|
|
stringt := "string"
|
|
prop.Type = &stringt
|
|
return jsonName, modelDescription, prop
|
|
}
|
|
}
|
|
|
|
fieldKind := fieldType.Kind()
|
|
switch {
|
|
case fieldKind == reflect.Struct:
|
|
jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
|
|
return jsonName, modelDescription, prop
|
|
case fieldKind == reflect.Slice || fieldKind == reflect.Array:
|
|
jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
|
|
return jsonName, modelDescription, prop
|
|
case fieldKind == reflect.Ptr:
|
|
jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
|
|
return jsonName, modelDescription, prop
|
|
case fieldKind == reflect.String:
|
|
stringt := "string"
|
|
prop.Type = &stringt
|
|
return jsonName, modelDescription, prop
|
|
case fieldKind == reflect.Map:
|
|
// if it's a map, it's unstructured, and swagger 1.2 can't handle it
|
|
objectType := "object"
|
|
prop.Type = &objectType
|
|
return jsonName, modelDescription, prop
|
|
}
|
|
|
|
fieldTypeName := b.keyFrom(fieldType)
|
|
if b.isPrimitiveType(fieldTypeName) {
|
|
mapped := b.jsonSchemaType(fieldTypeName)
|
|
prop.Type = &mapped
|
|
prop.Format = b.jsonSchemaFormat(fieldTypeName)
|
|
return jsonName, modelDescription, prop
|
|
}
|
|
modelType := b.keyFrom(fieldType)
|
|
prop.Ref = &modelType
|
|
|
|
if fieldType.Name() == "" { // override type of anonymous structs
|
|
nestedTypeName := modelName + "." + jsonName
|
|
prop.Ref = &nestedTypeName
|
|
b.addModel(fieldType, nestedTypeName)
|
|
}
|
|
return jsonName, modelDescription, prop
|
|
}
|
|
|
|
func hasNamedJSONTag(field reflect.StructField) bool {
|
|
parts := strings.Split(field.Tag.Get("json"), ",")
|
|
if len(parts) == 0 {
|
|
return false
|
|
}
|
|
for _, s := range parts[1:] {
|
|
if s == "inline" {
|
|
return false
|
|
}
|
|
}
|
|
return len(parts[0]) > 0
|
|
}
|
|
|
|
func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
|
|
prop.setPropertyMetadata(field)
|
|
// Check for type override in tag
|
|
if prop.Type != nil {
|
|
return jsonName, prop
|
|
}
|
|
fieldType := field.Type
|
|
// check for anonymous
|
|
if len(fieldType.Name()) == 0 {
|
|
// anonymous
|
|
anonType := model.Id + "." + jsonName
|
|
b.addModel(fieldType, anonType)
|
|
prop.Ref = &anonType
|
|
return jsonName, prop
|
|
}
|
|
|
|
if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) {
|
|
// embedded struct
|
|
sub := modelBuilder{new(ModelList), b.Config}
|
|
sub.addModel(fieldType, "")
|
|
subKey := sub.keyFrom(fieldType)
|
|
// merge properties from sub
|
|
subModel, _ := sub.Models.At(subKey)
|
|
subModel.Properties.Do(func(k string, v ModelProperty) {
|
|
model.Properties.Put(k, v)
|
|
// if subModel says this property is required then include it
|
|
required := false
|
|
for _, each := range subModel.Required {
|
|
if k == each {
|
|
required = true
|
|
break
|
|
}
|
|
}
|
|
if required {
|
|
model.Required = append(model.Required, k)
|
|
}
|
|
})
|
|
// add all new referenced models
|
|
sub.Models.Do(func(key string, sub Model) {
|
|
if key != subKey {
|
|
if _, ok := b.Models.At(key); !ok {
|
|
b.Models.Put(key, sub)
|
|
}
|
|
}
|
|
})
|
|
// empty name signals skip property
|
|
return "", prop
|
|
}
|
|
// simple struct
|
|
b.addModel(fieldType, "")
|
|
var pType = b.keyFrom(fieldType)
|
|
prop.Ref = &pType
|
|
return jsonName, prop
|
|
}
|
|
|
|
func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
|
|
// check for type override in tags
|
|
prop.setPropertyMetadata(field)
|
|
if prop.Type != nil {
|
|
return jsonName, prop
|
|
}
|
|
fieldType := field.Type
|
|
if fieldType.Elem().Kind() == reflect.Uint8 {
|
|
stringt := "string"
|
|
prop.Type = &stringt
|
|
return jsonName, prop
|
|
}
|
|
var pType = "array"
|
|
prop.Type = &pType
|
|
isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
|
|
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
|
|
prop.Items = new(Item)
|
|
if isPrimitive {
|
|
mapped := b.jsonSchemaType(elemTypeName)
|
|
prop.Items.Type = &mapped
|
|
} else {
|
|
prop.Items.Ref = &elemTypeName
|
|
}
|
|
// add|overwrite model for element type
|
|
if fieldType.Elem().Kind() == reflect.Ptr {
|
|
fieldType = fieldType.Elem()
|
|
}
|
|
if !isPrimitive {
|
|
b.addModel(fieldType.Elem(), elemTypeName)
|
|
}
|
|
return jsonName, prop
|
|
}
|
|
|
|
func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
|
|
prop.setPropertyMetadata(field)
|
|
// Check for type override in tags
|
|
if prop.Type != nil {
|
|
return jsonName, prop
|
|
}
|
|
fieldType := field.Type
|
|
|
|
// override type of pointer to list-likes
|
|
if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
|
|
var pType = "array"
|
|
prop.Type = &pType
|
|
isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name())
|
|
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
|
|
if isPrimitive {
|
|
primName := b.jsonSchemaType(elemName)
|
|
prop.Items = &Item{Ref: &primName}
|
|
} else {
|
|
prop.Items = &Item{Ref: &elemName}
|
|
}
|
|
if !isPrimitive {
|
|
// add|overwrite model for element type
|
|
b.addModel(fieldType.Elem().Elem(), elemName)
|
|
}
|
|
} else {
|
|
// non-array, pointer type
|
|
fieldTypeName := b.keyFrom(fieldType.Elem())
|
|
var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path
|
|
if b.isPrimitiveType(fieldTypeName) {
|
|
prop.Type = &pType
|
|
prop.Format = b.jsonSchemaFormat(fieldTypeName)
|
|
return jsonName, prop
|
|
}
|
|
prop.Ref = &pType
|
|
elemName := ""
|
|
if fieldType.Elem().Name() == "" {
|
|
elemName = modelName + "." + jsonName
|
|
prop.Ref = &elemName
|
|
}
|
|
b.addModel(fieldType.Elem(), elemName)
|
|
}
|
|
return jsonName, prop
|
|
}
|
|
|
|
func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Name() == "" {
|
|
return modelName + "." + jsonName
|
|
}
|
|
return b.keyFrom(t)
|
|
}
|
|
|
|
func (b modelBuilder) keyFrom(st reflect.Type) string {
|
|
key := st.String()
|
|
if b.Config != nil && b.Config.ModelTypeNameHandler != nil {
|
|
if name, ok := b.Config.ModelTypeNameHandler(st); ok {
|
|
key = name
|
|
}
|
|
}
|
|
if len(st.Name()) == 0 { // unnamed type
|
|
// Swagger UI has special meaning for [
|
|
key = strings.Replace(key, "[]", "||", -1)
|
|
}
|
|
return key
|
|
}
|
|
|
|
// see also https://golang.org/ref/spec#Numeric_types
|
|
func (b modelBuilder) isPrimitiveType(modelName string) bool {
|
|
if len(modelName) == 0 {
|
|
return false
|
|
}
|
|
return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
|
|
}
|
|
|
|
// jsonNameOfField returns the name of the field as it should appear in JSON format
|
|
// An empty string indicates that this field is not part of the JSON representation
|
|
func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
|
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
|
s := strings.Split(jsonTag, ",")
|
|
if s[0] == "-" {
|
|
// empty name signals skip property
|
|
return ""
|
|
} else if s[0] != "" {
|
|
return s[0]
|
|
}
|
|
}
|
|
return field.Name
|
|
}
|
|
|
|
// see also http://json-schema.org/latest/json-schema-core.html#anchor8
|
|
func (b modelBuilder) jsonSchemaType(modelName string) string {
|
|
schemaMap := map[string]string{
|
|
"uint": "integer",
|
|
"uint8": "integer",
|
|
"uint16": "integer",
|
|
"uint32": "integer",
|
|
"uint64": "integer",
|
|
|
|
"int": "integer",
|
|
"int8": "integer",
|
|
"int16": "integer",
|
|
"int32": "integer",
|
|
"int64": "integer",
|
|
|
|
"byte": "integer",
|
|
"float64": "number",
|
|
"float32": "number",
|
|
"bool": "boolean",
|
|
"time.Time": "string",
|
|
}
|
|
mapped, ok := schemaMap[modelName]
|
|
if !ok {
|
|
return modelName // use as is (custom or struct)
|
|
}
|
|
return mapped
|
|
}
|
|
|
|
func (b modelBuilder) jsonSchemaFormat(modelName string) string {
|
|
if b.Config != nil && b.Config.SchemaFormatHandler != nil {
|
|
if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
|
|
return mapped
|
|
}
|
|
}
|
|
schemaMap := map[string]string{
|
|
"int": "int32",
|
|
"int32": "int32",
|
|
"int64": "int64",
|
|
"byte": "byte",
|
|
"uint": "integer",
|
|
"uint8": "byte",
|
|
"float64": "double",
|
|
"float32": "float",
|
|
"time.Time": "date-time",
|
|
"*time.Time": "date-time",
|
|
}
|
|
mapped, ok := schemaMap[modelName]
|
|
if !ok {
|
|
return "" // no format
|
|
}
|
|
return mapped
|
|
}
|