package restfulspec import ( "encoding/json" "reflect" "strings" "github.com/go-openapi/spec" ) type definitionBuilder struct { Definitions spec.Definitions Config Config } // Documented is type Documented interface { SwaggerDoc() map[string]string } // Check if this structure has a method with signature func () SwaggerDoc() map[string]string // If it exists, retrieve the documentation and overwrite all struct tag descriptions func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string { if docable, ok := reflect.New(model).Elem().Interface().(Documented); ok { return docable.SwaggerDoc() } return make(map[string]string) } // addModelFrom creates and adds a Schema to the builder and detects and calls // the post build hook for customizations func (b definitionBuilder) addModelFrom(sample interface{}) { b.addModel(reflect.TypeOf(sample), "") } func (b definitionBuilder) addModel(st reflect.Type, nameOverride string) *spec.Schema { // Turn pointers into simpler types so further checks are // correct. if st.Kind() == reflect.Ptr { st = st.Elem() } modelName := keyFrom(st, b.Config) 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.Definitions[modelName]; ok { return nil } sm := spec.Schema{ SchemaProps: spec.SchemaProps{ Required: []string{}, Properties: map[string]spec.Schema{}, }, } // reference the model before further initializing (enables recursive structs) b.Definitions[modelName] = sm // check for slice or array if st.Kind() == reflect.Slice || st.Kind() == reflect.Array { st = st.Elem() } // 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[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") } // Needed to pass openapi validation. This field exists for json-schema compatibility, // but it conflicts with the openapi specification. // See https://github.com/go-openapi/spec/issues/23 for more context sm.ID = "" // update model builder with completed model b.Definitions[modelName] = sm return &sm } func (b definitionBuilder) isPropertyRequired(field reflect.StructField) bool { required := true if optionalTag := field.Tag.Get("optional"); optionalTag == "true" { return false } if jsonTag := field.Tag.Get("json"); jsonTag != "" { s := strings.Split(jsonTag, ",") if len(s) > 1 && s[1] == "omitempty" { return false } } return required } func (b definitionBuilder) buildProperty(field reflect.StructField, model *spec.Schema, modelName string) (jsonName, modelDescription string, prop spec.Schema) { 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 } setPropertyMetadata(&prop, 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 = []string{pType} } if prop.Format == "" { prop.Format = b.jsonSchemaFormat(keyFrom(fieldType, b.Config)) } 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 = []string{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 = []string{stringt} return jsonName, modelDescription, prop case fieldKind == reflect.Map: jsonName, prop := b.buildMapTypeProperty(field, jsonName, modelName) return jsonName, modelDescription, prop } fieldTypeName := keyFrom(fieldType, b.Config) if b.isPrimitiveType(fieldTypeName) { mapped := b.jsonSchemaType(fieldTypeName) prop.Type = []string{mapped} prop.Format = b.jsonSchemaFormat(fieldTypeName) return jsonName, modelDescription, prop } modelType := keyFrom(fieldType, b.Config) prop.Ref = spec.MustCreateRef("#/definitions/" + modelType) if fieldType.Name() == "" { // override type of anonymous structs // FIXME: Still need a way to handle anonymous struct model naming. nestedTypeName := modelName + "." + jsonName prop.Ref = spec.MustCreateRef("#/definitions/" + 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 definitionBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *spec.Schema) (nameJson string, prop spec.Schema) { setPropertyMetadata(&prop, field) fieldType := field.Type // check for anonymous if len(fieldType.Name()) == 0 { // anonymous // FIXME: Still need a way to handle anonymous struct model naming. anonType := model.ID + "." + jsonName b.addModel(fieldType, anonType) prop.Ref = spec.MustCreateRef("#/definitions/" + anonType) return jsonName, prop } if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) { // embedded struct sub := definitionBuilder{make(spec.Definitions), b.Config} sub.addModel(fieldType, "") subKey := keyFrom(fieldType, b.Config) // merge properties from sub subModel, _ := sub.Definitions[subKey] for k, v := range subModel.Properties { model.Properties[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 for key, sub := range sub.Definitions { if key != subKey { if _, ok := b.Definitions[key]; !ok { b.Definitions[key] = sub } } } // empty name signals skip property return "", prop } // simple struct b.addModel(fieldType, "") var pType = keyFrom(fieldType, b.Config) prop.Ref = spec.MustCreateRef("#/definitions/" + pType) return jsonName, prop } func (b definitionBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) { setPropertyMetadata(&prop, field) fieldType := field.Type if fieldType.Elem().Kind() == reflect.Uint8 { stringt := "string" prop.Type = []string{stringt} return jsonName, prop } var pType = "array" prop.Type = []string{pType} isPrimitive := b.isPrimitiveType(fieldType.Elem().Name()) elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem()) prop.Items = &spec.SchemaOrArray{ Schema: &spec.Schema{}, } if isPrimitive { mapped := b.jsonSchemaType(elemTypeName) prop.Items.Schema.Type = []string{mapped} } else { prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + 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 definitionBuilder) buildMapTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) { setPropertyMetadata(&prop, field) fieldType := field.Type var pType = "object" prop.Type = []string{pType} // As long as the element isn't an interface, we should be able to figure out what the // intended type is and represent it in `AdditionalProperties`. // See: https://swagger.io/docs/specification/data-models/dictionaries/ if fieldType.Elem().Kind().String() != "interface" { isPrimitive := b.isPrimitiveType(fieldType.Elem().Name()) elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem()) prop.AdditionalProperties = &spec.SchemaOrBool{ Schema: &spec.Schema{}, } if isPrimitive { mapped := b.jsonSchemaType(elemTypeName) prop.AdditionalProperties.Schema.Type = []string{mapped} } else { prop.AdditionalProperties.Schema.Ref = spec.MustCreateRef("#/definitions/" + 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 definitionBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) { setPropertyMetadata(&prop, field) 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 = []string{pType} isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name()) elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem()) prop.Items = &spec.SchemaOrArray{ Schema: &spec.Schema{}, } if isPrimitive { primName := b.jsonSchemaType(elemName) prop.Items.Schema.Type = []string{primName} } else { prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemName) } if !isPrimitive { // add|overwrite model for element type b.addModel(fieldType.Elem().Elem(), elemName) } } else { // non-array, pointer type fieldTypeName := keyFrom(fieldType.Elem(), b.Config) var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path if b.isPrimitiveType(fieldTypeName) { prop.Type = []string{pType} prop.Format = b.jsonSchemaFormat(fieldTypeName) return jsonName, prop } prop.Ref = spec.MustCreateRef("#/definitions/" + pType) elemName := "" if fieldType.Elem().Name() == "" { elemName = modelName + "." + jsonName prop.Ref = spec.MustCreateRef("#/definitions/" + elemName) } b.addModel(fieldType.Elem(), elemName) } return jsonName, prop } func (b definitionBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string { if t.Kind() == reflect.Ptr { t = t.Elem() } if t.Name() == "" { return modelName + "." + jsonName } return keyFrom(t, b.Config) } func keyFrom(st reflect.Type, cfg Config) string { key := st.String() if cfg.ModelTypeNameHandler != nil { if name, ok := cfg.ModelTypeNameHandler(st); ok { key = name } } if len(st.Name()) == 0 { // unnamed type // If it is an array, remove the leading [] key = strings.TrimPrefix(key, "[]") // Swagger UI has special meaning for [ key = strings.Replace(key, "[]", "||", -1) } return key } // see also https://golang.org/ref/spec#Numeric_types func (b definitionBuilder) 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 definitionBuilder) 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 definitionBuilder) 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 definitionBuilder) jsonSchemaFormat(modelName string) string { if 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 }