diff --git a/vendor/github.com/emicklei/go-restful-openapi/build_path.go b/vendor/github.com/emicklei/go-restful-openapi/build_path.go index 741b49aa0..6d0fa8ccc 100644 --- a/vendor/github.com/emicklei/go-restful-openapi/build_path.go +++ b/vendor/github.com/emicklei/go-restful-openapi/build_path.go @@ -199,7 +199,14 @@ func buildResponse(e restful.ResponseError, cfg Config) (r spec.Response) { } } else { modelName := definitionBuilder{}.keyFrom(st) - r.Schema.Ref = spec.MustCreateRef("#/definitions/" + modelName) + if isPrimitiveType(modelName) { + // If the response is a primitive type, then don't reference any definitions. + // Instead, set the schema's "type" to the model name. + r.Schema.AddType(modelName, "") + } else { + modelName := definitionBuilder{}.keyFrom(st) + r.Schema.Ref = spec.MustCreateRef("#/definitions/" + modelName) + } } } return r diff --git a/vendor/github.com/emicklei/go-restful-openapi/definition_builder.go b/vendor/github.com/emicklei/go-restful-openapi/definition_builder.go index 54e761085..e4a4d26d2 100644 --- a/vendor/github.com/emicklei/go-restful-openapi/definition_builder.go +++ b/vendor/github.com/emicklei/go-restful-openapi/definition_builder.go @@ -196,9 +196,7 @@ func (b definitionBuilder) buildProperty(field reflect.StructField, model *spec. prop.Type = []string{stringt} return jsonName, modelDescription, prop case fieldKind == reflect.Map: - // if it's a map, it's unstructured, and swagger can't handle it - objectType := "object" - prop.Type = []string{objectType} + jsonName, prop := b.buildMapTypeProperty(field, jsonName, modelName) return jsonName, modelDescription, prop } @@ -315,6 +313,38 @@ func (b definitionBuilder) buildArrayTypeProperty(field reflect.StructField, jso 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 @@ -330,7 +360,7 @@ func (b definitionBuilder) buildPointerTypeProperty(field reflect.StructField, j } if isPrimitive { primName := b.jsonSchemaType(elemName) - prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + primName) + prop.Items.Schema.Type = []string{primName} } else { prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemName) } diff --git a/vendor/github.com/emicklei/go-restful-openapi/examples/user-resource.go b/vendor/github.com/emicklei/go-restful-openapi/examples/user-resource.go index 73901be08..b5203acf4 100644 --- a/vendor/github.com/emicklei/go-restful-openapi/examples/user-resource.go +++ b/vendor/github.com/emicklei/go-restful-openapi/examples/user-resource.go @@ -123,9 +123,8 @@ func main() { restful.DefaultContainer.Add(u.WebService()) config := restfulspec.Config{ - WebServices: restful.RegisteredWebServices(), // you control what services are visible - WebServicesURL: "http://localhost:8080", - APIPath: "/apidocs.json", + WebServices: restful.RegisteredWebServices(), // you control what services are visible + APIPath: "/apidocs.json", PostBuildSwaggerObjectHandler: enrichSwaggerObject} restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config)) diff --git a/vendor/github.com/emicklei/go-restful-openapi/property_ext.go b/vendor/github.com/emicklei/go-restful-openapi/property_ext.go index 5aca3b65f..eb91208bc 100644 --- a/vendor/github.com/emicklei/go-restful-openapi/property_ext.go +++ b/vendor/github.com/emicklei/go-restful-openapi/property_ext.go @@ -82,6 +82,16 @@ func setUniqueItems(prop *spec.Schema, field reflect.StructField) { } } +func setReadOnly(prop *spec.Schema, field reflect.StructField) { + tag := field.Tag.Get("readOnly") + switch tag { + case "true": + prop.ReadOnly = true + case "false": + prop.ReadOnly = false + } +} + func setPropertyMetadata(prop *spec.Schema, field reflect.StructField) { setDescription(prop, field) setDefaultValue(prop, field) @@ -90,4 +100,5 @@ func setPropertyMetadata(prop *spec.Schema, field reflect.StructField) { setMaximum(prop, field) setUniqueItems(prop, field) setType(prop, field) + setReadOnly(prop, field) } diff --git a/vendor/github.com/emicklei/go-restful-openapi/spec_resource.go b/vendor/github.com/emicklei/go-restful-openapi/spec_resource.go index 1e62d64f5..b36274061 100644 --- a/vendor/github.com/emicklei/go-restful-openapi/spec_resource.go +++ b/vendor/github.com/emicklei/go-restful-openapi/spec_resource.go @@ -30,6 +30,13 @@ func BuildSwagger(config Config) *spec.Swagger { for _, each := range config.WebServices { for path, item := range buildPaths(each, config).Paths { + existingPathItem, ok := paths.Paths[path] + if ok { + for _, r := range each.Routes() { + _, patterns := sanitizePath(r.Path) + item = buildPathItem(each, r, existingPathItem, patterns, config) + } + } paths.Paths[path] = item } for name, def := range buildDefinitions(each, config) { diff --git a/vendor/github.com/marccarre/go-restful-openapi/LICENSE b/vendor/github.com/marccarre/go-restful-openapi/LICENSE new file mode 100644 index 000000000..aeab5b440 --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2017 Ernest Micklei + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/marccarre/go-restful-openapi/build_definitions.go b/vendor/github.com/marccarre/go-restful-openapi/build_definitions.go new file mode 100644 index 000000000..95452554e --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/build_definitions.go @@ -0,0 +1,32 @@ +package restfulspec + +import ( + "reflect" + + restful "github.com/emicklei/go-restful" + "github.com/go-openapi/spec" +) + +func buildDefinitions(ws *restful.WebService, cfg Config) (definitions spec.Definitions) { + definitions = spec.Definitions{} + for _, each := range ws.Routes() { + addDefinitionsFromRouteTo(each, cfg, definitions) + } + return +} + +func addDefinitionsFromRouteTo(r restful.Route, cfg Config, d spec.Definitions) { + builder := definitionBuilder{Definitions: d, Config: cfg} + if r.ReadSample != nil { + builder.addModel(reflect.TypeOf(r.ReadSample), "") + } + if r.WriteSample != nil { + builder.addModel(reflect.TypeOf(r.WriteSample), "") + } + for _, v := range r.ResponseErrors { + if v.Model == nil { + continue + } + builder.addModel(reflect.TypeOf(v.Model), "") + } +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/build_path.go b/vendor/github.com/marccarre/go-restful-openapi/build_path.go new file mode 100644 index 000000000..6d0fa8ccc --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/build_path.go @@ -0,0 +1,254 @@ +package restfulspec + +import ( + "net/http" + "reflect" + "regexp" + "strconv" + "strings" + + restful "github.com/emicklei/go-restful" + "github.com/go-openapi/spec" +) + +// KeyOpenAPITags is a Metadata key for a restful Route +const KeyOpenAPITags = "openapi.tags" + +func buildPaths(ws *restful.WebService, cfg Config) spec.Paths { + p := spec.Paths{Paths: map[string]spec.PathItem{}} + for _, each := range ws.Routes() { + path, patterns := sanitizePath(each.Path) + existingPathItem, ok := p.Paths[path] + if !ok { + existingPathItem = spec.PathItem{} + } + p.Paths[path] = buildPathItem(ws, each, existingPathItem, patterns, cfg) + } + return p +} + +// sanitizePath removes regex expressions from named path params, +// since openapi only supports setting the pattern as a a property named "pattern". +// Expressions like "/api/v1/{name:[a-z]/" are converted to "/api/v1/{name}/". +// The second return value is a map which contains the mapping from the path parameter +// name to the extracted pattern +func sanitizePath(restfulPath string) (string, map[string]string) { + openapiPath := "" + patterns := map[string]string{} + for _, fragment := range strings.Split(restfulPath, "/") { + if fragment == "" { + continue + } + if strings.HasPrefix(fragment, "{") && strings.Contains(fragment, ":") { + split := strings.Split(fragment, ":") + fragment = split[0][1:] + pattern := split[1][:len(split[1])-1] + patterns[fragment] = pattern + fragment = "{" + fragment + "}" + } + openapiPath += "/" + fragment + } + return openapiPath, patterns +} + +func buildPathItem(ws *restful.WebService, r restful.Route, existingPathItem spec.PathItem, patterns map[string]string, cfg Config) spec.PathItem { + op := buildOperation(ws, r, patterns, cfg) + switch r.Method { + case "GET": + existingPathItem.Get = op + case "POST": + existingPathItem.Post = op + case "PUT": + existingPathItem.Put = op + case "DELETE": + existingPathItem.Delete = op + case "PATCH": + existingPathItem.Patch = op + case "OPTIONS": + existingPathItem.Options = op + case "HEAD": + existingPathItem.Head = op + } + return existingPathItem +} + +func buildOperation(ws *restful.WebService, r restful.Route, patterns map[string]string, cfg Config) *spec.Operation { + o := spec.NewOperation(r.Operation) + o.Description = r.Notes + o.Summary = stripTags(r.Doc) + o.Consumes = r.Consumes + o.Produces = r.Produces + o.Deprecated = r.Deprecated + if r.Metadata != nil { + if tags, ok := r.Metadata[KeyOpenAPITags]; ok { + if tagList, ok := tags.([]string); ok { + o.Tags = tagList + } + } + } + // collect any path parameters + for _, param := range ws.PathParameters() { + o.Parameters = append(o.Parameters, buildParameter(r, param, patterns[param.Data().Name], cfg)) + } + // route specific params + for _, each := range r.ParameterDocs { + o.Parameters = append(o.Parameters, buildParameter(r, each, patterns[each.Data().Name], cfg)) + } + o.Responses = new(spec.Responses) + props := &o.Responses.ResponsesProps + props.StatusCodeResponses = map[int]spec.Response{} + for k, v := range r.ResponseErrors { + r := buildResponse(v, cfg) + props.StatusCodeResponses[k] = r + if 200 == k { // any 2xx code? + o.Responses.Default = &r + } + } + if len(o.Responses.StatusCodeResponses) == 0 { + o.Responses.StatusCodeResponses[200] = spec.Response{ResponseProps: spec.ResponseProps{Description: http.StatusText(http.StatusOK)}} + } + return o +} + +// stringAutoType automatically picks the correct type from an ambiguously typed +// string. Ex. numbers become int, true/false become bool, etc. +func stringAutoType(ambiguous string) interface{} { + if ambiguous == "" { + return nil + } + if parsedInt, err := strconv.ParseInt(ambiguous, 10, 64); err == nil { + return parsedInt + } + if parsedBool, err := strconv.ParseBool(ambiguous); err == nil { + return parsedBool + } + return ambiguous +} + +func buildParameter(r restful.Route, restfulParam *restful.Parameter, pattern string, cfg Config) spec.Parameter { + p := spec.Parameter{} + param := restfulParam.Data() + p.In = asParamType(param.Kind) + if param.AllowMultiple { + p.Type = "array" + p.Items = spec.NewItems() + p.Items.Type = param.DataType + p.CollectionFormat = param.CollectionFormat + } else { + p.Type = param.DataType + } + p.Description = param.Description + p.Name = param.Name + p.Required = param.Required + + if param.Kind == restful.PathParameterKind { + p.Pattern = pattern + } + st := reflect.TypeOf(r.ReadSample) + if param.Kind == restful.BodyParameterKind && r.ReadSample != nil && param.DataType == st.String() { + p.Schema = new(spec.Schema) + p.SimpleSchema = spec.SimpleSchema{} + if st.Kind() == reflect.Array || st.Kind() == reflect.Slice { + dataTypeName := definitionBuilder{}.keyFrom(st.Elem()) + p.Schema.Type = []string{"array"} + p.Schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{}, + } + isPrimitive := isPrimitiveType(dataTypeName) + if isPrimitive { + mapped := jsonSchemaType(dataTypeName) + p.Schema.Items.Schema.Type = []string{mapped} + } else { + p.Schema.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + dataTypeName) + } + } else { + p.Schema.Ref = spec.MustCreateRef("#/definitions/" + param.DataType) + } + + } else { + p.Type = param.DataType + p.Default = stringAutoType(param.DefaultValue) + p.Format = param.DataFormat + } + + return p +} + +func buildResponse(e restful.ResponseError, cfg Config) (r spec.Response) { + r.Description = e.Message + if e.Model != nil { + st := reflect.TypeOf(e.Model) + if st.Kind() == reflect.Ptr { + // For pointer type, use element type as the key; otherwise we'll + // endup with '#/definitions/*Type' which violates openapi spec. + st = st.Elem() + } + r.Schema = new(spec.Schema) + if st.Kind() == reflect.Array || st.Kind() == reflect.Slice { + modelName := definitionBuilder{}.keyFrom(st.Elem()) + r.Schema.Type = []string{"array"} + r.Schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{}, + } + isPrimitive := isPrimitiveType(modelName) + if isPrimitive { + mapped := jsonSchemaType(modelName) + r.Schema.Items.Schema.Type = []string{mapped} + } else { + r.Schema.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + modelName) + } + } else { + modelName := definitionBuilder{}.keyFrom(st) + if isPrimitiveType(modelName) { + // If the response is a primitive type, then don't reference any definitions. + // Instead, set the schema's "type" to the model name. + r.Schema.AddType(modelName, "") + } else { + modelName := definitionBuilder{}.keyFrom(st) + r.Schema.Ref = spec.MustCreateRef("#/definitions/" + modelName) + } + } + } + return r +} + +// stripTags takes a snippet of HTML and returns only the text content. +// For example, `<Hi!>
` -> `<Hi!> `. +func stripTags(html string) string { + re := regexp.MustCompile("<[^>]*>") + return re.ReplaceAllString(html, "") +} + +func 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) +} + +func 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 +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/config.go b/vendor/github.com/marccarre/go-restful-openapi/config.go new file mode 100644 index 000000000..a3fa96f8c --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/config.go @@ -0,0 +1,41 @@ +package restfulspec + +import ( + "reflect" + + restful "github.com/emicklei/go-restful" + "github.com/go-openapi/spec" +) + +// MapSchemaFormatFunc can be used to modify typeName at definition time. +// To use it set the SchemaFormatHandler in the config. +type MapSchemaFormatFunc func(typeName string) string + +// MapModelTypeNameFunc can be used to return the desired typeName for a given +// type. It will return false if the default name should be used. +// To use it set the ModelTypeNameHandler in the config. +type MapModelTypeNameFunc func(t reflect.Type) (string, bool) + +// PostBuildSwaggerObjectFunc can be used to change the creates Swagger Object +// before serving it. To use it set the PostBuildSwaggerObjectHandler in the config. +type PostBuildSwaggerObjectFunc func(s *spec.Swagger) + +// Config holds service api metadata. +type Config struct { + // WebServicesURL is a DEPRECATED field; it never had any effect in this package. + WebServicesURL string + // APIPath is the path where the JSON api is avaiable , e.g. /apidocs.json + APIPath string + // api listing is constructed from this list of restful WebServices. + WebServices []*restful.WebService + // [optional] on default CORS (Cross-Origin-Resource-Sharing) is enabled. + DisableCORS bool + // Top-level API version. Is reflected in the resource listing. + APIVersion string + // [optional] If set, model builder should call this handler to get addition typename-to-swagger-format-field conversion. + SchemaFormatHandler MapSchemaFormatFunc + // [optional] If set, model builder should call this handler to retrieve the name for a given type. + ModelTypeNameHandler MapModelTypeNameFunc + // [optional] If set then call this function with the generated Swagger Object + PostBuildSwaggerObjectHandler PostBuildSwaggerObjectFunc +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/definition_builder.go b/vendor/github.com/marccarre/go-restful-openapi/definition_builder.go new file mode 100644 index 000000000..e4a4d26d2 --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/definition_builder.go @@ -0,0 +1,491 @@ +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 := 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.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(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 = []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 := b.keyFrom(fieldType) + if b.isPrimitiveType(fieldTypeName) { + mapped := b.jsonSchemaType(fieldTypeName) + prop.Type = []string{mapped} + prop.Format = b.jsonSchemaFormat(fieldTypeName) + return jsonName, modelDescription, prop + } + modelType := b.keyFrom(fieldType) + prop.Ref = spec.MustCreateRef("#/definitions/" + modelType) + + if fieldType.Name() == "" { // override type of anonymous structs + 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 + 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 := sub.keyFrom(fieldType) + // 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 = b.keyFrom(fieldType) + 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 := b.keyFrom(fieldType.Elem()) + 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 b.keyFrom(t) +} + +func (b definitionBuilder) keyFrom(st reflect.Type) string { + key := st.String() + if b.Config.ModelTypeNameHandler != nil { + if name, ok := b.Config.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 +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/examples/security/api.go b/vendor/github.com/marccarre/go-restful-openapi/examples/security/api.go new file mode 100644 index 000000000..fdbad1a24 --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/examples/security/api.go @@ -0,0 +1,107 @@ +package main + +import ( + "strings" + + "github.com/sirupsen/logrus" + restful "github.com/emicklei/go-restful" + "github.com/go-openapi/spec" +) + +func enrichSwaggerObject(swo *spec.Swagger) { + swo.Info = &spec.Info{ + InfoProps: spec.InfoProps{ + Title: "Example", + Description: "Resource for doing example things", + Contact: &spec.ContactInfo{ + Name: "dkiser", + Email: "domingo.kiser@gmail.com", + URL: "domingo.space", + }, + License: &spec.License{ + Name: "MIT", + URL: "http://mit.org", + }, + Version: "1.0.0", + }, + } + swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{ + Name: "example", + Description: "Exampling and stuff"}}} + + // setup security definitions + swo.SecurityDefinitions = map[string]*spec.SecurityScheme{ + "jwt": spec.APIKeyAuth("Authorization", "header"), + } + + // map routes to security definitions + enrichSwaggeerObjectSecurity(swo) +} + +func enrichSwaggeerObjectSecurity(swo *spec.Swagger) { + + // loop through all registerd web services + for _, ws := range restful.RegisteredWebServices() { + for _, route := range ws.Routes() { + + // grab route metadata for a SecurityDefinition + secdefn, ok := route.Metadata[SecurityDefinitionKey] + if !ok { + continue + } + + // grab pechelper.OAISecurity from the stored interface{} + var sEntry OAISecurity + switch v := secdefn.(type) { + case *OAISecurity: + sEntry = *v + case OAISecurity: + sEntry = v + default: + // not valid type + logrus.Warningf("skipping Security openapi spec for %s:%s, invalid metadata type %v", route.Method, route.Path, v) + continue + } + + if _, ok := swo.SecurityDefinitions[sEntry.Name]; !ok { + logrus.Warningf("skipping Security openapi spec for %s:%s, '%s' not found in SecurityDefinitions", route.Method, route.Path, sEntry.Name) + continue + } + + // grab path and path item in openapi spec + path, err := swo.Paths.JSONLookup(route.Path) + if err != nil { + logrus.Warning("skipping Security openapi spec for %s:%s, %s", route.Method, route.Path, err.Error()) + continue + } + pItem := path.(*spec.PathItem) + + // Update respective path Option based on method + var pOption *spec.Operation + switch method := strings.ToLower(route.Method); method { + case "get": + pOption = pItem.Get + case "post": + pOption = pItem.Post + case "patch": + pOption = pItem.Patch + case "delete": + pOption = pItem.Delete + case "put": + pOption = pItem.Put + case "head": + pOption = pItem.Head + case "options": + pOption = pItem.Options + default: + // unsupported method + logrus.Warningf("skipping Security openapi spec for %s:%s, unsupported method '%s'", route.Method, route.Path, route.Method) + continue + } + + // update the pOption with security entry + pOption.SecuredWith(sEntry.Name, sEntry.Scopes...) + } + } + +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/examples/security/main.go b/vendor/github.com/marccarre/go-restful-openapi/examples/security/main.go new file mode 100644 index 000000000..55110a251 --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/examples/security/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "encoding/json" + "os" + + restful "github.com/emicklei/go-restful" + restfulspec "github.com/emicklei/go-restful-openapi" +) + +func main() { + config := restfulspec.Config{ + WebServices: restful.RegisteredWebServices(), // you control what services are visible + APIPath: "/apidocs.json", + PostBuildSwaggerObjectHandler: enrichSwaggerObject} + swagger := restfulspec.BuildSwagger(config) + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", "\t") + enc.Encode(swagger) +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/examples/security/service.go b/vendor/github.com/marccarre/go-restful-openapi/examples/security/service.go new file mode 100644 index 000000000..8cdebea6e --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/examples/security/service.go @@ -0,0 +1,33 @@ +package main + +import restful "github.com/emicklei/go-restful" +import restfulspec "github.com/emicklei/go-restful-openapi" + +type ExampleService struct{} +type ExampleReq struct{} +type ExampleResp struct{} +type ResponseMsg struct{} + +func (s ExampleService) WebService() *restful.WebService { + ws := new(restful.WebService) + ws. + Path("/example"). + Consumes(restful.MIME_JSON). + Produces(restful.MIME_JSON) + + tags := []string{"example"} + + ws.Route(ws.POST("/example").To(s.create). + Doc("create example thing"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Metadata(SecurityDefinitionKey, OAISecurity{Name: "jwt"}). + Writes(ExampleResp{}). + Reads(ExampleReq{}). + Returns(200, "OK", &ExampleResp{}). + Returns(404, "NotFound", &ResponseMsg{}). + Returns(500, "InternalServerError", &ResponseMsg{})) + + return ws + +} +func (s ExampleService) create(*restful.Request, *restful.Response) {} diff --git a/vendor/github.com/marccarre/go-restful-openapi/examples/security/spechelper.go b/vendor/github.com/marccarre/go-restful-openapi/examples/security/spechelper.go new file mode 100644 index 000000000..1660bf49d --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/examples/security/spechelper.go @@ -0,0 +1,27 @@ +package main + +import "fmt" + +const ( + SecurityDefinitionKey = "OAPI_SECURITY_DEFINITION" +) + +type OAISecurity struct { + Name string // SecurityDefinition name + Scopes []string // Scopes for oauth2 +} + +func (s *OAISecurity) Valid() error { + switch s.Name { + case "oauth2": + return nil + case "openIdConnect": + return nil + default: + if len(s.Scopes) > 0 { + return fmt.Errorf("oai Security scopes for scheme '%s' should be empty", s.Name) + } + } + + return nil +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/examples/user-resource.go b/vendor/github.com/marccarre/go-restful-openapi/examples/user-resource.go new file mode 100644 index 000000000..b5203acf4 --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/examples/user-resource.go @@ -0,0 +1,176 @@ +package main + +import ( + "log" + "net/http" + + "github.com/emicklei/go-restful" + restfulspec "github.com/emicklei/go-restful-openapi" + "github.com/go-openapi/spec" +) + +type UserResource struct { + // normally one would use DAO (data access object) + users map[string]User +} + +func (u UserResource) WebService() *restful.WebService { + ws := new(restful.WebService) + ws. + Path("/users"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well + + tags := []string{"users"} + + ws.Route(ws.GET("/").To(u.findAllUsers). + // docs + Doc("get all users"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes([]User{}). + Returns(200, "OK", []User{})) + + ws.Route(ws.GET("/{user-id}").To(u.findUser). + // docs + Doc("get a user"). + Param(ws.PathParameter("user-id", "identifier of the user").DataType("integer").DefaultValue("1")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(User{}). // on the response + Returns(200, "OK", User{}). + Returns(404, "Not Found", nil)) + + ws.Route(ws.PUT("/{user-id}").To(u.updateUser). + // docs + Doc("update a user"). + Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(User{})) // from the request + + ws.Route(ws.PUT("").To(u.createUser). + // docs + Doc("create a user"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(User{})) // from the request + + ws.Route(ws.DELETE("/{user-id}").To(u.removeUser). + // docs + Doc("delete a user"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("user-id", "identifier of the user").DataType("string"))) + + return ws +} + +// GET http://localhost:8080/users +// +func (u UserResource) findAllUsers(request *restful.Request, response *restful.Response) { + list := []User{} + for _, each := range u.users { + list = append(list, each) + } + response.WriteEntity(list) +} + +// GET http://localhost:8080/users/1 +// +func (u UserResource) findUser(request *restful.Request, response *restful.Response) { + id := request.PathParameter("user-id") + usr := u.users[id] + if len(usr.ID) == 0 { + response.WriteErrorString(http.StatusNotFound, "User could not be found.") + } else { + response.WriteEntity(usr) + } +} + +// PUT http://localhost:8080/users/1 +// 1Melissa Raspberry +// +func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) { + usr := new(User) + err := request.ReadEntity(&usr) + if err == nil { + u.users[usr.ID] = *usr + response.WriteEntity(usr) + } else { + response.WriteError(http.StatusInternalServerError, err) + } +} + +// PUT http://localhost:8080/users/1 +// 1Melissa +// +func (u *UserResource) createUser(request *restful.Request, response *restful.Response) { + usr := User{ID: request.PathParameter("user-id")} + err := request.ReadEntity(&usr) + if err == nil { + u.users[usr.ID] = usr + response.WriteHeaderAndEntity(http.StatusCreated, usr) + } else { + response.WriteError(http.StatusInternalServerError, err) + } +} + +// DELETE http://localhost:8080/users/1 +// +func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) { + id := request.PathParameter("user-id") + delete(u.users, id) +} + +func main() { + u := UserResource{map[string]User{}} + restful.DefaultContainer.Add(u.WebService()) + + config := restfulspec.Config{ + WebServices: restful.RegisteredWebServices(), // you control what services are visible + APIPath: "/apidocs.json", + PostBuildSwaggerObjectHandler: enrichSwaggerObject} + restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config)) + + // Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API + // You need to download the Swagger HTML5 assets and change the FilePath location in the config below. + // Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json + http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/Users/emicklei/Projects/swagger-ui/dist")))) + + // Optionally, you may need to enable CORS for the UI to work. + cors := restful.CrossOriginResourceSharing{ + AllowedHeaders: []string{"Content-Type", "Accept"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + CookiesAllowed: false, + Container: restful.DefaultContainer} + restful.DefaultContainer.Filter(cors.Filter) + + log.Printf("Get the API using http://localhost:8080/apidocs.json") + log.Printf("Open Swagger UI using http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func enrichSwaggerObject(swo *spec.Swagger) { + swo.Info = &spec.Info{ + InfoProps: spec.InfoProps{ + Title: "UserService", + Description: "Resource for managing Users", + Contact: &spec.ContactInfo{ + Name: "john", + Email: "john@doe.rp", + URL: "http://johndoe.org", + }, + License: &spec.License{ + Name: "MIT", + URL: "http://mit.org", + }, + Version: "1.0.0", + }, + } + swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{ + Name: "users", + Description: "Managing users"}}} +} + +// User is just a sample type +type User struct { + ID string `json:"id" description:"identifier of the user"` + Name string `json:"name" description:"name of the user" default:"john"` + Age int `json:"age" description:"age of the user" default:"21"` +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/lookup.go b/vendor/github.com/marccarre/go-restful-openapi/lookup.go new file mode 100644 index 000000000..27407107a --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/lookup.go @@ -0,0 +1,19 @@ +package restfulspec + +import restful "github.com/emicklei/go-restful" + +func asParamType(kind int) string { + switch { + case kind == restful.PathParameterKind: + return "path" + case kind == restful.QueryParameterKind: + return "query" + case kind == restful.BodyParameterKind: + return "body" + case kind == restful.HeaderParameterKind: + return "header" + case kind == restful.FormParameterKind: + return "formData" + } + return "" +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/property_ext.go b/vendor/github.com/marccarre/go-restful-openapi/property_ext.go new file mode 100644 index 000000000..eb91208bc --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/property_ext.go @@ -0,0 +1,104 @@ +package restfulspec + +import ( + "reflect" + "strconv" + "strings" + + "github.com/go-openapi/spec" +) + +func setDescription(prop *spec.Schema, field reflect.StructField) { + if tag := field.Tag.Get("description"); tag != "" { + prop.Description = tag + } +} + +func setDefaultValue(prop *spec.Schema, field reflect.StructField) { + if tag := field.Tag.Get("default"); tag != "" { + prop.Default = stringAutoType(tag) + } +} + +func setEnumValues(prop *spec.Schema, field reflect.StructField) { + // We use | to separate the enum values. This value is chosen + // since its unlikely to be useful in actual enumeration values. + if tag := field.Tag.Get("enum"); tag != "" { + enums := []interface{}{} + for _, s := range strings.Split(tag, "|") { + enums = append(enums, s) + } + prop.Enum = enums + } +} + +func setMaximum(prop *spec.Schema, field reflect.StructField) { + if tag := field.Tag.Get("maximum"); tag != "" { + value, err := strconv.ParseFloat(tag, 64) + if err == nil { + prop.Maximum = &value + } + } +} + +func setMinimum(prop *spec.Schema, field reflect.StructField) { + if tag := field.Tag.Get("minimum"); tag != "" { + value, err := strconv.ParseFloat(tag, 64) + if err == nil { + prop.Minimum = &value + } + } +} + +func setType(prop *spec.Schema, field reflect.StructField) { + if tag := field.Tag.Get("type"); tag != "" { + // Check if the first two characters of the type tag are + // intended to emulate slice/array behaviour. + // + // If type is intended to be a slice/array then add the + // overriden type to the array item instead of the main property + if len(tag) > 2 && tag[0:2] == "[]" { + pType := "array" + prop.Type = []string{pType} + prop.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{}, + } + iType := tag[2:] + prop.Items.Schema.Type = []string{iType} + return + } + + prop.Type = []string{tag} + } +} + +func setUniqueItems(prop *spec.Schema, field reflect.StructField) { + tag := field.Tag.Get("unique") + switch tag { + case "true": + prop.UniqueItems = true + case "false": + prop.UniqueItems = false + } +} + +func setReadOnly(prop *spec.Schema, field reflect.StructField) { + tag := field.Tag.Get("readOnly") + switch tag { + case "true": + prop.ReadOnly = true + case "false": + prop.ReadOnly = false + } +} + +func setPropertyMetadata(prop *spec.Schema, field reflect.StructField) { + setDescription(prop, field) + setDefaultValue(prop, field) + setEnumValues(prop, field) + setMinimum(prop, field) + setMaximum(prop, field) + setUniqueItems(prop, field) + setType(prop, field) + setReadOnly(prop, field) +} diff --git a/vendor/github.com/marccarre/go-restful-openapi/spec_resource.go b/vendor/github.com/marccarre/go-restful-openapi/spec_resource.go new file mode 100644 index 000000000..b36274061 --- /dev/null +++ b/vendor/github.com/marccarre/go-restful-openapi/spec_resource.go @@ -0,0 +1,76 @@ +package restfulspec + +import ( + restful "github.com/emicklei/go-restful" + "github.com/go-openapi/spec" +) + +// NewOpenAPIService returns a new WebService that provides the API documentation of all services +// conform the OpenAPI documentation specifcation. +func NewOpenAPIService(config Config) *restful.WebService { + + ws := new(restful.WebService) + ws.Path(config.APIPath) + ws.Produces(restful.MIME_JSON) + if config.DisableCORS { + ws.Filter(enableCORS) + } + + swagger := BuildSwagger(config) + resource := specResource{swagger: swagger} + ws.Route(ws.GET("/").To(resource.getSwagger)) + return ws +} + +// BuildSwagger returns a Swagger object for all services' API endpoints. +func BuildSwagger(config Config) *spec.Swagger { + // collect paths and model definitions to build Swagger object. + paths := &spec.Paths{Paths: map[string]spec.PathItem{}} + definitions := spec.Definitions{} + + for _, each := range config.WebServices { + for path, item := range buildPaths(each, config).Paths { + existingPathItem, ok := paths.Paths[path] + if ok { + for _, r := range each.Routes() { + _, patterns := sanitizePath(r.Path) + item = buildPathItem(each, r, existingPathItem, patterns, config) + } + } + paths.Paths[path] = item + } + for name, def := range buildDefinitions(each, config) { + definitions[name] = def + } + } + swagger := &spec.Swagger{ + SwaggerProps: spec.SwaggerProps{ + Swagger: "2.0", + Paths: paths, + Definitions: definitions, + }, + } + if config.PostBuildSwaggerObjectHandler != nil { + config.PostBuildSwaggerObjectHandler(swagger) + } + return swagger +} + +func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { + if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" { + // prevent duplicate header + if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 { + resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin) + } + } + chain.ProcessFilter(req, resp) +} + +// specResource is a REST resource to serve the Open-API spec. +type specResource struct { + swagger *spec.Swagger +} + +func (s specResource) getSwagger(req *restful.Request, resp *restful.Response) { + resp.WriteAsJson(s.swagger) +} diff --git a/vendor/manifest b/vendor/manifest index 9d7b77a12..b47188f09 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -57,6 +57,14 @@ "branch": "master", "notests": true }, + { + "importpath": "github.com/Sirupsen/logrus", + "repository": "https://github.com/Sirupsen/logrus", + "vcs": "git", + "revision": "c108f5553c369120b773da633aca62a8b42b1cf4", + "branch": "master", + "notests": true + }, { "importpath": "github.com/VividCortex/gohistogram", "repository": "https://github.com/VividCortex/gohistogram", @@ -552,7 +560,7 @@ "importpath": "github.com/emicklei/go-restful-openapi", "repository": "https://github.com/emicklei/go-restful-openapi", "vcs": "git", - "revision": "0b23be9885ff0876fdf5dc133e5c9def4a7ab3ec", + "revision": "ca11c31130f54bf8dfaa69dac83d8603b2d4b48e", "branch": "master", "notests": true }, @@ -1157,6 +1165,14 @@ "path": "/jwriter", "notests": true }, + { + "importpath": "github.com/marccarre/go-restful-openapi", + "repository": "https://github.com/marccarre/go-restful-openapi", + "vcs": "git", + "revision": "129557de7d9f2d2ca4a90cd31c379db90a561ad8", + "branch": "HEAD", + "notests": true + }, { "importpath": "github.com/mattn/go-colorable", "repository": "https://github.com/mattn/go-colorable",