refactor to go-restful

This commit is contained in:
Mikaël Cluseau
2019-02-04 13:56:43 +11:00
parent 4d07db7b2c
commit e93b84b60c
390 changed files with 72017 additions and 233 deletions

View File

@ -0,0 +1 @@
examples/examples

View File

@ -0,0 +1,4 @@
language: go
go:
- 1.x

View File

@ -0,0 +1,16 @@
# changes to the go-restful-openapi package
## v1.0.0
- Fix for #19 MapModelTypeNameFunc has incomplete behavior
- prevent array param.Type be overwritten in the else case below (#47)
- Merge paths with existing paths from other webServices (#48)
## v0.11.0
- Register pointer to array/slice of primitives as such rather than as reference to the primitive type definition. (#46)
- Add support for map types using "additional properties" (#44)
## <= v0.10.0
See `git log`.

22
vendor/github.com/emicklei/go-restful-openapi/LICENSE generated vendored Normal file
View File

@ -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.

View File

@ -0,0 +1,26 @@
# go-restful-openapi
[![Build Status](https://travis-ci.org/emicklei/go-restful-openapi.png)](https://travis-ci.org/emicklei/go-restful-openapi)
[![GoDoc](https://godoc.org/github.com/emicklei/go-restful-openapi?status.svg)](https://godoc.org/github.com/emicklei/go-restful-openapi)
[openapi](https://www.openapis.org) extension to the go-restful package, targeting [version 2.0](https://github.com/OAI/OpenAPI-Specification)
## The following Go field tags are translated to OpenAPI equivalents
- description
- minimum
- maximum
- optional ( if set to "true" then it is not listed in `required`)
- unique
- modelDescription
- type (overrides the Go type String())
- enum
- readOnly
See TestThatExtraTagsAreReadIntoModel for examples.
## dependencies
- [go-restful](https://github.com/emicklei/go-restful)
- [go-openapi](https://github.com/go-openapi/spec)
© 2018, ernestmicklei.com. MIT License. Contributions welcome.

View File

@ -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), "")
}
}

View File

@ -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)
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>&lt;Hi!&gt;</b> <br>` -> `&lt;Hi!&gt; `.
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
}

View File

@ -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
}

View File

@ -0,0 +1,493 @@
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 (<theModel>) 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
}

View File

@ -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 ""
}

View File

@ -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)
}

View File

@ -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)
}