255 lines
7.4 KiB
Go
255 lines
7.4 KiB
Go
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)
|
|
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 := keyFrom(st.Elem(), cfg)
|
|
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 {
|
|
dataTypeName := keyFrom(st, cfg)
|
|
p.Schema.Ref = spec.MustCreateRef("#/definitions/" + dataTypeName)
|
|
}
|
|
|
|
} else {
|
|
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.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 := keyFrom(st.Elem(), cfg)
|
|
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 := keyFrom(st, cfg)
|
|
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 := keyFrom(st, cfg)
|
|
r.Schema.Ref = spec.MustCreateRef("#/definitions/" + modelName)
|
|
}
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// stripTags takes a snippet of HTML and returns only the text content.
|
|
// For example, `<b><Hi!></b> <br>` -> `<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
|
|
}
|