Bump github.com/emicklei/go-restful-openapi to v2.9.2-0.20230507070325-d6acc08e570c (#5669)

This commit is contained in:
hongming
2023-05-09 10:13:45 +08:00
committed by GitHub
parent 16c2dbc693
commit 673fdde52c
160 changed files with 709 additions and 4635 deletions

View File

@@ -0,0 +1,2 @@
examples/examples
.vscode

View File

@@ -0,0 +1,103 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 5m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
# whether to hide "congrats" message if no issues were found,
# default is false (show "congrats" message by default).
# set this option to true to print nothing if no issues were found.
silent: true
# build-tags:
# - mandrill
# - test
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
linters:
enable-all: true
disable:
- gochecknoglobals
- gochecknoinits
- lll
- dupl
- wsl
- funlen
- gocognit
- testpackage
- gomnd
- goerr113
- nestif
- interfacer
- godot
- unparam
- nlreturn
- wrapcheck
- exhaustivestruct
- errorlint
# todo enable as much as possible linters below
- paralleltest
- unused
- govet
- gosimple
- exhaustive
- whitespace
- structcheck
- misspell
- golint
- goheader
- stylecheck
- gofumpt
- gofmt
- godox
- gocyclo
- gocritic
- goconst
- goimports
- gci
- errcheck
- deadcode
fast: false
linters-settings:
govet:
check-shadowing: true
golint:
report-comments: true
issues:
max-same-issues: 20
exclude:
- ^G104
- ^G501

View File

@@ -0,0 +1,13 @@
language: go
go:
- 1.13
before_install:
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.33.0
script:
- go mod vendor
- go mod download
# - make lint-ci
- make test

View File

@@ -0,0 +1,90 @@
# changes to the go-restful-openapi package
# v2+ versions are using the Go module of go-restful v3+
## v2.9.1
- fix set array data format (#96)
## v2.9.0
- Add property x-go-name support (#90)
- Add support to set swagger Schemes field (#91)
## v2.8.0
[2022-01-04]
- refine and fix GoLowerCamelCasedNameHandler bug (#88)
- Add missing fields of response header object (#89)
- support generate field name with config (#86)
Thanks again to slow-zhang and Sergey Vilgelm
## v2.7.0
[2021-12-08]
- fix some typos (#85)
- use PossibleValues in favor of AllowedValues (#84)
- PostBuildSwaggerSchema handler for each model (#83)
- Use int64 format for time.Duration type (#82)
Special thanks to contributions of Sergey Vilgelm <sergey@vilgelm.com>
## [2021-09-20] v2.6.0
- feat(parameter): adds additional openapi mappings (#74, robbie@robnrob.com)
## [2021-09-20] v2.5.0
- add support for format tag (#72, askingcat)
## [2021-09-18] v2.4.0
- add support for vendor extensions (#)
## [2020-02-10] v2.3.0
- Support for custom attribute "x-nullable" (#70)
## v1.4.0 + v2.2.0
- Allow maps as top level types and support maps to slices (#63)
## v1.3.0 + v2.1.0
- add json.Number handling (PR #61)
- add type alias support for primitives (PR #61)
## v1.2.0
- handle map[string][]byte (#59)
## v1.1.0 (v0.14.1)
- Add Host field to Config which is copied into Swagger object
- Enable CORS by default as per the documentation (#58)
- add go module
- update dependencies
## v0.13.0
- Do not use 200 as default response, instead use the one explicitly defined.
- support time.Duration
- Fix Parameter 'AllowableValues' to populate swagger definition
## v0.12.0
- add support for time.Duration
- Populate the swagger definition with the parameter's 'AllowableValues' as an enum (#53)
- Fix for #19 MapModelTypeNameFunc has incomplete behavior
- Merge paths with existing paths from other webServices (#48)
- prevent array param.Type be overwritten in the else case below (#47)
## 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`.

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,20 @@
clean:
rm coverage.out
test:
go test -cover -race -count=1 ./...
coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# for local testing
lint:
docker run --rm -v $(PWD):$(PWD) -w $(PWD) golangci/golangci-lint:v1.33.0 golangci-lint run -v
# for testing on CI/CD. we specify required linter version in the .travis.yml file
lint-ci:
golangci-lint run
outdated:
go list -u -m -json all | docker run -i psampaz/go-mod-outdated -update -direct -ci

View File

@@ -0,0 +1,38 @@
# 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)
## Go modules
Versions `v1` of this package require Go module version `v2` of the go-restful package.
To use version `v3` of the go-restful package, you need to import `v2` of this package, such as:
import (
restfulspec "github.com/emicklei/go-restful-openapi/v2"
restful "github.com/emicklei/go-restful/v3"
)
© 2017-2020, ernestmicklei.com. MIT License. Contributions welcome.

View File

@@ -0,0 +1,32 @@
package restfulspec
import (
"reflect"
restful "github.com/emicklei/go-restful/v3"
"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,380 @@
package restfulspec
import (
"net/http"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"github.com/go-openapi/spec"
"github.com/emicklei/go-restful/v3"
)
const (
// KeyOpenAPITags is a Metadata key for a restful Route
KeyOpenAPITags = "openapi.tags"
// ExtensionPrefix is the only prefix accepted for VendorExtensible extension keys
ExtensionPrefix = "x-"
arrayType = "array"
definitionRoot = "#/definitions/"
)
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 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, ":")
// skip google custom method like `resource/{resource-id}:customVerb`
if !strings.Contains(split[0], "}") {
fragment = split[0][1:]
pattern := split[1][:len(split[1])-1]
if pattern == "*" { // special case
pattern = ".*"
}
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 http.MethodGet:
existingPathItem.Get = op
case http.MethodPost:
existingPathItem.Post = op
case http.MethodPut:
existingPathItem.Put = op
case http.MethodDelete:
existingPathItem.Delete = op
case http.MethodPatch:
existingPathItem.Patch = op
case http.MethodOptions:
existingPathItem.Options = op
case http.MethodHead:
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
}
}
}
extractVendorExtensions(&o.VendorExtensible, r.ExtensionProperties)
// collect any path parameters
for _, param := range ws.PathParameters() {
p := buildParameter(r, param, patterns[param.Data().Name], cfg)
o.Parameters = append(o.Parameters, p)
}
// route specific params
for _, param := range r.ParameterDocs {
p := buildParameter(r, param, patterns[param.Data().Name], cfg)
o.Parameters = append(o.Parameters, p)
}
o.Responses = new(spec.Responses)
props := &o.Responses.ResponsesProps
props.StatusCodeResponses = make(map[int]spec.Response, len(r.ResponseErrors))
for k, v := range r.ResponseErrors {
r := buildResponse(v, cfg)
props.StatusCodeResponses[k] = r
}
if r.DefaultResponse != nil {
rsp := buildResponse(*r.DefaultResponse, cfg)
o.Responses.Default = &rsp
}
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 extractVendorExtensions(extensible *spec.VendorExtensible, extensions restful.ExtensionProperties) {
if len(extensions.Extensions) > 0 {
for key := range extensions.Extensions {
if strings.HasPrefix(key, ExtensionPrefix) {
extensible.AddExtension(key, extensions.Extensions[key])
}
}
}
}
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 {
// If the param is an array apply the validations to the items in it
p.Type = arrayType
p.Items = spec.NewItems()
p.Items.Type = param.DataType
p.Items.Pattern = param.Pattern
p.Items.MinLength = param.MinLength
p.Items.MaxLength = param.MaxLength
p.CollectionFormat = param.CollectionFormat
p.MinItems = param.MinItems
p.MaxItems = param.MaxItems
p.UniqueItems = param.UniqueItems
} else {
// Otherwise, for non-arrays, apply the validations directly to the param
p.Type = param.DataType
p.MinLength = param.MinLength
p.MaxLength = param.MaxLength
p.Minimum = param.Minimum
p.Maximum = param.Maximum
}
// Prefer PossibleValues over deprecated AllowableValues
if numPossible := len(param.PossibleValues); numPossible > 0 {
// init Enum to our known size and populate it
p.Enum = make([]interface{}, 0, numPossible)
for _, value := range param.PossibleValues {
p.Enum = append(p.Enum, value)
}
} else {
if numAllowable := len(param.AllowableValues); numAllowable > 0 {
// If allowable values are defined, set the enum array to the sorted values
allowableSortedKeys := make([]string, 0, numAllowable)
for k := range param.AllowableValues {
allowableSortedKeys = append(allowableSortedKeys, k)
}
// sort away
sort.Strings(allowableSortedKeys)
// init Enum to our known size and populate it
p.Enum = make([]interface{}, 0, numAllowable)
for _, key := range allowableSortedKeys {
p.Enum = append(p.Enum, param.AllowableValues[key])
}
}
}
p.Description = param.Description
p.Name = param.Name
p.Required = param.Required
p.AllowEmptyValue = param.AllowEmptyValue
if param.Kind == restful.PathParameterKind {
p.Pattern = pattern
} else if !param.AllowMultiple {
p.Pattern = param.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{arrayType}
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(definitionRoot + dataTypeName)
}
} else {
dataTypeName := keyFrom(st, cfg)
p.Schema.Ref = spec.MustCreateRef(definitionRoot + dataTypeName)
}
} else {
if param.AllowMultiple {
p.Type = arrayType
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
}
extractVendorExtensions(&p.VendorExtensible, param.ExtensionProperties)
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{arrayType}
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(definitionRoot + 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(definitionRoot + modelName)
}
}
}
if len(e.Headers) > 0 {
r.Headers = make(map[string]spec.Header, len(e.Headers))
for k, v := range e.Headers {
r.Headers[k] = buildHeader(v)
}
}
extractVendorExtensions(&r.VendorExtensible, e.ExtensionProperties)
return r
}
// buildHeader builds a specification header structure from restful.Header
func buildHeader(header restful.Header) spec.Header {
responseHeader := spec.Header{}
responseHeader.Type = header.Type
responseHeader.Description = header.Description
responseHeader.Format = header.Format
responseHeader.Default = header.Default
// If type is "array" items field is required
if header.Type == arrayType {
responseHeader.CollectionFormat = header.CollectionFormat
responseHeader.Items = buildHeadersItems(header.Items)
}
return responseHeader
}
// buildHeadersItems builds
func buildHeadersItems(items *restful.Items) *spec.Items {
responseItems := spec.NewItems()
responseItems.Format = items.Format
responseItems.Type = items.Type
responseItems.Default = items.Default
responseItems.CollectionFormat = items.CollectionFormat
if items.Items != nil {
responseItems.Items = buildHeadersItems(items.Items)
}
return responseItems
}
// 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 time.Duration", 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",
"time.Duration": "integer",
}
mapped, ok := schemaMap[modelName]
if !ok {
return modelName // use as is (custom or struct)
}
return mapped
}

View File

@@ -0,0 +1,62 @@
package restfulspec
import (
"reflect"
"github.com/emicklei/go-restful/v3"
"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)
// DefinitionNameHandlerFunc generate name by this handler for definition without json tag.
// example: (for more, see file definition_name_test.go)
// field definition_name
// Name `json:"name"` -> name
// Name -> Name
//
// there are some example provided for use
// DefaultNameHandler GoRestfulDefinition -> GoRestfulDefinition (not changed)
// LowerSnakeCasedNameHandler GoRestfulDefinition -> go_restful_definition
// LowerCamelCasedNameHandler GoRestfulDefinition -> goRestfulDefinition
// GoLowerCamelCasedNameHandler HTTPRestfulDefinition -> httpRestfulDefinition
//
type DefinitionNameHandlerFunc func(string) string
// Config holds service api metadata.
type Config struct {
// [optional] If set then set this field with the generated Swagger Object
Host string
// [optional] If set then set this field with the generated Swagger Object
Schemes []string
// WebServicesURL is a DEPRECATED field; it never had any effect in this package.
WebServicesURL string
// APIPath is the path where the JSON api is available, 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
// [optional] If set then call handler's function for to generate name by this handler for definition without json tag,
// you can use you DefinitionNameHandler, also, there are four DefinitionNameHandler provided, see definition_name.go
DefinitionNameHandler DefinitionNameHandlerFunc
}

View File

@@ -0,0 +1,595 @@
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
}
type PostBuildSwaggerSchema interface {
PostBuildSwaggerSchemaHandler(sm *spec.Schema)
}
// 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.
isArray := false
if st.Kind() == reflect.Ptr {
st = st.Elem()
}
if b.isSliceOrArrayType(st.Kind()) {
isArray = true
st = st.Elem()
}
modelName := keyFrom(st, b.Config)
if nameOverride != "" {
modelName = nameOverride
}
// no models needed for primitive types unless it has alias
if b.isPrimitiveType(modelName, st.Kind()) {
if nameOverride == "" {
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 b.isByteArrayType(st) {
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{},
},
}
// fixes issue 77 but feels like a workaround.
if b.isPrimitiveType(modelName, st.Kind()) {
if isArray {
sm.Type = []string{"array"}
sm.Items = &spec.SchemaOrArray{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{Type: []string{jsonSchemaType(st.Kind().String())}}}}
}
}
// reference the model before further initializing (enables recursive structs)
b.Definitions[modelName] = sm
if st.Kind() == reflect.Map {
_, sm = b.buildMapType(st, "value", modelName)
b.Definitions[modelName] = sm
return &sm
}
// check for structure or primitive type
if st.Kind() != reflect.Struct {
return &sm
}
fullDoc := getDocFromMethodSwaggerDoc2(st)
modelDescriptions := []string{}
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
if len(modelDescription) > 0 {
modelDescriptions = append(modelDescriptions, modelDescription)
}
// add if not omitted
if len(jsonName) != 0 {
// update description
if fieldDoc, ok := fullDoc[jsonName]; ok {
prop.Description = fieldDoc
}
// update Required
if b.isPropertyRequired(field) {
sm.Required = append(sm.Required, jsonName)
}
sm.Properties[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 = ""
// Call handler to update sch
if handler, ok := reflect.New(st).Elem().Interface().(PostBuildSwaggerSchema); ok {
handler.PostBuildSwaggerSchemaHandler(&sm)
}
// 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), fieldType.Kind())
}
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 b.isSliceOrArrayType(fieldKind):
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.Map:
jsonName, prop := b.buildMapTypeProperty(field, jsonName, modelName)
return jsonName, modelDescription, prop
}
fieldTypeName := keyFrom(fieldType, b.Config)
if b.isPrimitiveType(fieldTypeName, fieldKind) {
mapped := b.jsonSchemaType(fieldTypeName, fieldKind)
prop.Type = []string{mapped}
prop.Format = b.jsonSchemaFormat(fieldTypeName, fieldKind)
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{b.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(), fieldType.Elem().Kind())
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
prop.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{},
}
if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName, fieldType.Elem().Kind())
prop.Items.Schema.Type = []string{mapped}
prop.Items.Schema.Format = b.jsonSchemaFormat(elemTypeName, fieldType.Elem().Kind())
} 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) {
nameJson, prop = b.buildMapType(field.Type, jsonName, modelName)
setPropertyMetadata(&prop, field)
return nameJson, prop
}
func (b definitionBuilder) buildMapType(mapType reflect.Type, jsonName, modelName string) (nameJson string, prop spec.Schema) {
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 mapType.Elem().Kind().String() != "interface" {
isSlice := b.isSliceOrArrayType(mapType.Elem().Kind())
if isSlice && !b.isByteArrayType(mapType.Elem()) {
mapType = mapType.Elem()
}
isPrimitive := b.isPrimitiveType(mapType.Elem().Name(), mapType.Elem().Kind())
elemTypeName := b.getElementTypeName(modelName, jsonName, mapType.Elem())
prop.AdditionalProperties = &spec.SchemaOrBool{
Schema: &spec.Schema{},
}
// 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 string
if b.isByteArrayType(mapType.Elem()) {
prop.AdditionalProperties.Schema.Type = []string{"string"}
} else {
if isSlice {
var item *spec.Schema
if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName, mapType.Kind())
item = &spec.Schema{}
item.Type = []string{mapped}
item.Format = b.jsonSchemaFormat(elemTypeName, mapType.Kind())
} else {
item = spec.RefProperty("#/definitions/" + elemTypeName)
}
prop.AdditionalProperties.Schema = spec.ArrayProperty(item)
} else if isPrimitive {
mapped := b.jsonSchemaType(elemTypeName, mapType.Elem().Kind())
prop.AdditionalProperties.Schema.Type = []string{mapped}
} else {
prop.AdditionalProperties.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemTypeName)
}
// add|overwrite model for element type
if mapType.Elem().Kind() == reflect.Ptr {
mapType = mapType.Elem()
}
if !isPrimitive {
b.addModel(mapType.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(), fieldType.Elem().Elem().Kind())
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
prop.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{},
}
if isPrimitive {
primName := b.jsonSchemaType(elemName, fieldType.Elem().Elem().Kind())
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)
isPrimitive := b.isPrimitiveType(fieldTypeName, fieldType.Elem().Kind())
var pType = b.jsonSchemaType(fieldTypeName, fieldType.Elem().Kind()) // no star, include pkg path
if isPrimitive {
prop.Type = []string{pType}
prop.Format = b.jsonSchemaFormat(fieldTypeName, fieldType.Elem().Kind())
return jsonName, prop
}
prop.Ref = spec.MustCreateRef("#/definitions/" + pType)
elemName := ""
if fieldType.Elem().Name() == "" {
elemName = modelName + "." + jsonName
prop.Ref = spec.MustCreateRef("#/definitions/" + elemName)
}
if !isPrimitive {
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
}
func (b definitionBuilder) isSliceOrArrayType(t reflect.Kind) bool {
return t == reflect.Slice || t == reflect.Array
}
// Does the type represent a []byte?
func (b definitionBuilder) isByteArrayType(t reflect.Type) bool {
return (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) &&
t.Elem().Kind() == reflect.Uint8
}
// see also https://golang.org/ref/spec#Numeric_types
func (b definitionBuilder) isPrimitiveType(modelName string, modelKind reflect.Kind) bool {
switch modelKind {
case reflect.Bool:
return true
case reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
case reflect.String:
return true
}
if len(modelName) == 0 {
return false
}
return strings.Contains("time.Time time.Duration json.Number", 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]
}
}
if b.Config.DefinitionNameHandler == nil {
b.Config.DefinitionNameHandler = DefaultNameHandler
}
return b.Config.DefinitionNameHandler(field.Name)
}
// see also http://json-schema.org/latest/json-schema-core.html#anchor8
func (b definitionBuilder) jsonSchemaType(modelName string, modelKind reflect.Kind) string {
schemaMap := map[string]string{
"time.Time": "string",
"time.Duration": "integer",
"json.Number": "number",
}
if mapped, ok := schemaMap[modelName]; ok {
return mapped
}
// check if original type is primitive
switch modelKind {
case reflect.Bool:
return "boolean"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.String:
return "string"
}
return modelName // use as is (custom or struct)
}
func (b definitionBuilder) jsonSchemaFormat(modelName string, modelKind reflect.Kind) string {
if b.Config.SchemaFormatHandler != nil {
if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
return mapped
}
}
schemaMap := map[string]string{
"time.Time": "date-time",
"*time.Time": "date-time",
"time.Duration": "int64",
"*time.Duration": "int64",
"json.Number": "double",
"*json.Number": "double",
}
if mapped, ok := schemaMap[modelName]; ok {
return mapped
}
// check if original type is primitive
switch modelKind {
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
case reflect.Int:
return "int32"
case reflect.Int8:
return "byte"
case reflect.Int16:
return "integer"
case reflect.Int32:
return "int32"
case reflect.Int64:
return "int64"
case reflect.Uint:
return "integer"
case reflect.Uint8:
return "byte"
case reflect.Uint16:
return "integer"
case reflect.Uint32:
return "integer"
case reflect.Uint64:
return "integer"
}
return "" // no format
}

View File

@@ -0,0 +1,105 @@
package restfulspec
import "strings"
// DefaultNameHandler GoRestfulDefinition -> GoRestfulDefinition (not changed)
func DefaultNameHandler(name string) string {
return name
}
// LowerSnakeCasedNameHandler GoRestfulDefinition -> go_restful_definition
func LowerSnakeCasedNameHandler(name string) string {
definitionName := make([]byte, 0, len(name)+1)
for i := 0; i < len(name); i++ {
c := name[i]
if isUpper(c) {
if i > 0 {
definitionName = append(definitionName, '_')
}
c += 'a' - 'A'
}
definitionName = append(definitionName, c)
}
return string(definitionName)
}
// LowerCamelCasedNameHandler GoRestfulDefinition -> goRestfulDefinition
func LowerCamelCasedNameHandler(name string) string {
definitionName := make([]byte, 0, len(name)+1)
for i := 0; i < len(name); i++ {
c := name[i]
if isUpper(c) && i == 0 {
c += 'a' - 'A'
}
definitionName = append(definitionName, c)
}
return string(definitionName)
}
// GoLowerCamelCasedNameHandler HTTPRestfulDefinition -> httpRestfulDefinition
func GoLowerCamelCasedNameHandler(name string) string {
var i = 0
// for continuous Upper letters, check whether is it a common Initialisms
for ; i < len(name) && isUpper(name[i]); i++ {
}
if len(name) != i && i > 1 {
i-- // for continuous Upper letters, the last Upper is should not be check, eg: S for HTTPStatus
}
for ; i > 1; i-- {
if _, ok := commonInitialisms[name[:i]]; ok {
break
}
}
return strings.ToLower(name[:i]) + name[i:]
}
// commonInitialisms is a set of common initialisms. (from https://github.com/golang/lint/blob/master/lint.go)
// Only add entries that are highly unlikely to be non-initialisms.
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
var commonInitialisms = map[string]bool{
"ACL": true,
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTP": true,
"HTTPS": true,
"ID": true,
"IP": true,
"JSON": true,
"LHS": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SQL": true,
"SSH": true,
"TCP": true,
"TLS": true,
"TTL": true,
"UDP": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XMPP": true,
"XSRF": true,
"XSS": true,
}
func isUpper(r uint8) bool {
return 'A' <= r && r <= 'Z'
}

View File

@@ -0,0 +1,19 @@
package restfulspec
import restful "github.com/emicklei/go-restful/v3"
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,138 @@
package restfulspec
import (
"reflect"
"strconv"
"strings"
"github.com/go-openapi/spec"
)
func initPropExtensions(ext *spec.Extensions) {
if *ext == nil {
*ext = make(spec.Extensions, 0)
}
}
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 setIsNullableValue(prop *spec.Schema, field reflect.StructField) {
if tag := field.Tag.Get("x-nullable"); tag != "" {
initPropExtensions(&prop.Extensions)
value, err := strconv.ParseBool(tag)
prop.Extensions["x-nullable"] = value && err == nil
}
}
func setGoNameValue(prop *spec.Schema, field reflect.StructField) {
const tagName = "x-go-name"
if tag := field.Tag.Get(tagName); tag != "" {
initPropExtensions(&prop.Extensions)
prop.Extensions[tagName] = tag
}
}
func setEnumValues(prop *spec.Schema, field reflect.StructField) {
// We use | to separate the enum values. This value is chosen
// since it's 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 setFormat(prop *spec.Schema, field reflect.StructField) {
if tag := field.Tag.Get("format"); tag != "" {
prop.Format = tag
}
}
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
// overridden 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)
setFormat(prop, field)
setMinimum(prop, field)
setMaximum(prop, field)
setUniqueItems(prop, field)
setType(prop, field)
setReadOnly(prop, field)
setIsNullableValue(prop, field)
setGoNameValue(prop, field)
}

View File

@@ -0,0 +1,78 @@
package restfulspec
import (
restful "github.com/emicklei/go-restful/v3"
"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{
Host: config.Host,
Schemes: config.Schemes,
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)
}