├── .gitignore ├── go.mod ├── .travis.yml ├── .idea └── .gitignore ├── examples ├── go.mod ├── security │ ├── spechelper.go │ ├── main.go │ ├── service.go │ └── api.go ├── user-resource_test.go ├── openapi.json ├── user-resource.go └── go.sum ├── lookup.go ├── Makefile ├── issue79_test.go ├── LICENSE ├── README.md ├── spec_resource_test.go ├── spec_resource.go ├── .golangci.yml ├── definition_name.go ├── utils_test.go ├── config.go ├── definition_name_test.go ├── CHANGES.md ├── property_ext.go ├── property_ext_test.go ├── go.sum ├── definition_builder.go └── definition_builder_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/examples 2 | .vscode -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emicklei/go-restful-openapi/v2 2 | 3 | require ( 4 | github.com/emicklei/go-restful/v3 v3.11.0 5 | github.com/go-openapi/spec v0.20.9 6 | github.com/json-iterator/go v1.1.12 // indirect 7 | ) 8 | 9 | go 1.13 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13 5 | 6 | before_install: 7 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.33.0 8 | 9 | script: 10 | - go mod vendor 11 | - go mod download 12 | # - make lint-ci 13 | - make test 14 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | go-restful-openapi.iml 10 | misc.xml 11 | modules.xml 12 | vcs.xml 13 | codeStyleConfig.xml 14 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emicklei/go-restful-openapi/v2/examples 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/emicklei/go-restful-openapi/v2 v2.6.1 7 | github.com/emicklei/go-restful/v3 v3.10.1 8 | github.com/go-openapi/spec v0.20.4 9 | ) 10 | 11 | // replace github.com/emicklei/go-restful-openapi/v2 => ../ 12 | -------------------------------------------------------------------------------- /lookup.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import restful "github.com/emicklei/go-restful/v3" 4 | 5 | func asParamType(kind int) string { 6 | switch { 7 | case kind == restful.PathParameterKind: 8 | return "path" 9 | case kind == restful.QueryParameterKind: 10 | return "query" 11 | case kind == restful.BodyParameterKind: 12 | return "body" 13 | case kind == restful.HeaderParameterKind: 14 | return "header" 15 | case kind == restful.FormParameterKind: 16 | return "formData" 17 | } 18 | return "" 19 | } 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm coverage.out 3 | 4 | test: 5 | go test -cover ./... 6 | 7 | coverage: 8 | go test -coverprofile=coverage.out ./... 9 | go tool cover -html=coverage.out 10 | 11 | # for local testing 12 | lint: 13 | docker run --rm -v $(PWD):$(PWD) -w $(PWD) golangci/golangci-lint:v1.33.0 golangci-lint run -v 14 | 15 | # for testing on CI/CD. we specify required linter version in the .travis.yml file 16 | lint-ci: 17 | golangci-lint run 18 | 19 | outdated: 20 | go list -u -m -json all | docker run -i psampaz/go-mod-outdated -update -direct -ci -------------------------------------------------------------------------------- /examples/security/spechelper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const ( 6 | SecurityDefinitionKey = "OAPI_SECURITY_DEFINITION" 7 | ) 8 | 9 | type OAISecurity struct { 10 | Name string // SecurityDefinition name 11 | Scopes []string // Scopes for oauth2 12 | } 13 | 14 | func (s *OAISecurity) Valid() error { 15 | switch s.Name { 16 | case "oauth2": 17 | return nil 18 | case "openIdConnect": 19 | return nil 20 | default: 21 | if len(s.Scopes) > 0 { 22 | return fmt.Errorf("oai Security scopes for scheme '%s' should be empty", s.Name) 23 | } 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /examples/security/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | restfulspec "github.com/emicklei/go-restful-openapi/v2" 7 | restful "github.com/emicklei/go-restful/v3" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | config := restfulspec.Config{ 13 | WebServices: restful.RegisteredWebServices(), // you control what services are visible 14 | APIPath: "/apidocs.json", 15 | PostBuildSwaggerObjectHandler: enrichSwaggerObject} 16 | swagger := restfulspec.BuildSwagger(config) 17 | spec, err := json.MarshalIndent(swagger, "", " ") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | fmt.Println(string(spec)) 22 | } 23 | -------------------------------------------------------------------------------- /issue79_test.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/go-openapi/spec" 8 | ) 9 | 10 | type Parent struct { 11 | FieldA string 12 | } 13 | 14 | func (v Parent) MarshalJSON() ([]byte, error) { return nil, nil } 15 | 16 | type Child struct { 17 | FieldA Parent 18 | FieldB int 19 | } 20 | 21 | func TestParentChildArray(t *testing.T) { 22 | t.Skip() 23 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 24 | db.addModelFrom(Child{}) 25 | s := spec.Schema{ 26 | SchemaProps: spec.SchemaProps{ 27 | Definitions: db.Definitions, 28 | }, 29 | } 30 | data, _ := json.MarshalIndent(s, "", " ") 31 | t.Log(string(data)) 32 | } 33 | -------------------------------------------------------------------------------- /examples/security/service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import restful "github.com/emicklei/go-restful/v3" 4 | import restfulspec "github.com/emicklei/go-restful-openapi/v2" 5 | 6 | type ExampleService struct{} 7 | type ExampleReq struct{} 8 | type ExampleResp struct{} 9 | type ResponseMsg struct{} 10 | 11 | func (s ExampleService) WebService() *restful.WebService { 12 | ws := new(restful.WebService) 13 | ws. 14 | Path("/example"). 15 | Consumes(restful.MIME_JSON). 16 | Produces(restful.MIME_JSON) 17 | 18 | tags := []string{"example"} 19 | 20 | ws.Route(ws.POST("/example").To(s.create). 21 | Doc("create example thing"). 22 | Metadata(restfulspec.KeyOpenAPITags, tags). 23 | Metadata(SecurityDefinitionKey, OAISecurity{Name: "jwt"}). 24 | Writes(ExampleResp{}). 25 | Reads(ExampleReq{}). 26 | Returns(200, "OK", &ExampleResp{}). 27 | Returns(404, "NotFound", &ResponseMsg{}). 28 | Returns(500, "InternalServerError", &ResponseMsg{})) 29 | 30 | return ws 31 | 32 | } 33 | func (s ExampleService) create(*restful.Request, *restful.Response) {} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ernest Micklei 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/user-resource_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "reflect" 7 | "testing" 8 | 9 | restfulspec "github.com/emicklei/go-restful-openapi/v2" 10 | "github.com/emicklei/go-restful/v3" 11 | spec2 "github.com/go-openapi/spec" 12 | ) 13 | 14 | func TestAppleDef(t *testing.T) { 15 | 16 | raw, err := ioutil.ReadFile("./openapi.json") 17 | if err != nil { 18 | t.Error("Loading the openapi specification failed.") 19 | } 20 | ws := UserResource{}.WebService() 21 | expected := asStruct(string(raw)) 22 | 23 | config := restfulspec.Config{ 24 | WebServices: []*restful.WebService{ws}, // you control what services are visible 25 | WebServicesURL: "http://localhost:8080", 26 | APIPath: "/apidocs.json", 27 | PostBuildSwaggerObjectHandler: enrichSwaggerObject} 28 | 29 | actual := restfulspec.BuildSwagger(config) 30 | 31 | if reflect.DeepEqual(expected, asStruct(asJSON(actual))) != true { 32 | t.Errorf("Got:\n%v\nWanted:\n%v", asJSON(actual), asJSON(expected)) 33 | } 34 | } 35 | 36 | func asJSON(v interface{}) string { 37 | data, _ := json.MarshalIndent(v, " ", " ") 38 | return string(data) 39 | } 40 | 41 | func asStruct(raw string) *spec2.Swagger { 42 | expected := &spec2.Swagger{} 43 | json.Unmarshal([]byte(raw), expected) 44 | return expected 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-restful-openapi 2 | 3 | [![Build Status](https://travis-ci.org/emicklei/go-restful-openapi.png)](https://travis-ci.org/emicklei/go-restful-openapi) 4 | [![GoDoc](https://godoc.org/github.com/emicklei/go-restful-openapi?status.svg)](https://godoc.org/github.com/emicklei/go-restful-openapi) 5 | 6 | [openapi](https://www.openapis.org) extension to the go-restful package, targeting [version 2.0](https://github.com/OAI/OpenAPI-Specification) 7 | 8 | ## The following Go field tags are translated to OpenAPI equivalents 9 | - description 10 | - minimum 11 | - maximum 12 | - optional ( if set to "true" then it is not listed in `required`) 13 | - unique 14 | - modelDescription 15 | - type (overrides the Go type String()) 16 | - enum 17 | - readOnly 18 | 19 | See TestThatExtraTagsAreReadIntoModel for examples. 20 | 21 | ## dependencies 22 | 23 | - [go-restful](https://github.com/emicklei/go-restful) 24 | - [go-openapi](https://github.com/go-openapi/spec) 25 | 26 | 27 | ## Go modules 28 | 29 | Versions `v1` of this package require Go module version `v2` of the go-restful package. 30 | To use version `v3` of the go-restful package, you need to import `v2` of this package, such as: 31 | 32 | import ( 33 | restfulspec "github.com/emicklei/go-restful-openapi/v2" 34 | restful "github.com/emicklei/go-restful/v3" 35 | ) 36 | 37 | 38 | © 2017-2020, ernestmicklei.com. MIT License. Contributions welcome. 39 | -------------------------------------------------------------------------------- /spec_resource_test.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | restful "github.com/emicklei/go-restful/v3" 8 | ) 9 | 10 | // nolint:paralleltest 11 | func TestBuildSwagger(t *testing.T) { 12 | path := "/testPath" 13 | 14 | ws1 := new(restful.WebService) 15 | ws1.Path(path) 16 | ws1.Route(ws1.GET("").To(dummy)) 17 | 18 | ws2 := new(restful.WebService) 19 | ws2.Path(path) 20 | ws2.Route(ws2.DELETE("").To(dummy)) 21 | 22 | c := Config{} 23 | c.WebServices = []*restful.WebService{ws1, ws2} 24 | s := BuildSwagger(c) 25 | 26 | if !(s.Paths.Paths[path].Get != nil && s.Paths.Paths[path].Delete != nil) { 27 | t.Errorf("Swagger spec should have methods for GET and DELETE") 28 | } 29 | 30 | } 31 | 32 | // nolint:paralleltest 33 | func TestEnablingCORS(t *testing.T) { 34 | ws := NewOpenAPIService(Config{}) 35 | wc := restful.NewContainer().Add(ws) 36 | recorder := httptest.NewRecorder() 37 | request := httptest.NewRequest("GET", "/", nil) 38 | origin := "somewhere.over.the.rainbow" 39 | request.Header[restful.HEADER_Origin] = []string{origin} 40 | 41 | wc.Dispatch(recorder, request) 42 | 43 | responseHeader := recorder.Result().Header.Get(restful.HEADER_AccessControlAllowOrigin) 44 | if responseHeader != origin { 45 | t.Errorf("The CORS header was set to the wrong value, expected: %s but got: %s", origin, responseHeader) 46 | } 47 | } 48 | 49 | // nolint:paralleltest 50 | func TestDisablingCORS(t *testing.T) { 51 | ws := NewOpenAPIService(Config{DisableCORS: true}) 52 | wc := restful.NewContainer().Add(ws) 53 | recorder := httptest.NewRecorder() 54 | request := httptest.NewRequest("GET", "/", nil) 55 | request.Header[restful.HEADER_Origin] = []string{"somewhere.over.the.rainbow"} 56 | 57 | wc.Dispatch(recorder, request) 58 | 59 | responseHeader := recorder.Result().Header.Get(restful.HEADER_AccessControlAllowOrigin) 60 | if responseHeader != "" { 61 | t.Errorf("The CORS header was set to %s but it was disabled so should not be set", responseHeader) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spec_resource.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | restful "github.com/emicklei/go-restful/v3" 5 | "github.com/go-openapi/spec" 6 | ) 7 | 8 | // NewOpenAPIService returns a new WebService that provides the API documentation of all services 9 | // conform the OpenAPI documentation specifcation. 10 | func NewOpenAPIService(config Config) *restful.WebService { 11 | 12 | ws := new(restful.WebService) 13 | ws.Path(config.APIPath) 14 | ws.Produces(restful.MIME_JSON) 15 | if !config.DisableCORS { 16 | ws.Filter(enableCORS) 17 | } 18 | 19 | swagger := BuildSwagger(config) 20 | resource := specResource{swagger: swagger} 21 | ws.Route(ws.GET("/").To(resource.getSwagger)) 22 | return ws 23 | } 24 | 25 | // BuildSwagger returns a Swagger object for all services' API endpoints. 26 | func BuildSwagger(config Config) *spec.Swagger { 27 | // collect paths and model definitions to build Swagger object. 28 | paths := &spec.Paths{Paths: map[string]spec.PathItem{}} 29 | definitions := spec.Definitions{} 30 | 31 | for _, each := range config.WebServices { 32 | for path, item := range buildPaths(each, config).Paths { 33 | existingPathItem, ok := paths.Paths[path] 34 | if ok { 35 | for _, r := range each.Routes() { 36 | _, patterns := sanitizePath(r.Path) 37 | item = buildPathItem(each, r, existingPathItem, patterns, config) 38 | } 39 | } 40 | paths.Paths[path] = item 41 | } 42 | for name, def := range buildDefinitions(each, config) { 43 | definitions[name] = def 44 | } 45 | } 46 | swagger := &spec.Swagger{ 47 | SwaggerProps: spec.SwaggerProps{ 48 | Host: config.Host, 49 | Schemes: config.Schemes, 50 | Swagger: "2.0", 51 | Paths: paths, 52 | Definitions: definitions, 53 | }, 54 | } 55 | if config.PostBuildSwaggerObjectHandler != nil { 56 | config.PostBuildSwaggerObjectHandler(swagger) 57 | } 58 | return swagger 59 | } 60 | 61 | func enableCORS(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { 62 | if origin := req.HeaderParameter(restful.HEADER_Origin); origin != "" { 63 | // prevent duplicate header 64 | if len(resp.Header().Get(restful.HEADER_AccessControlAllowOrigin)) == 0 { 65 | resp.AddHeader(restful.HEADER_AccessControlAllowOrigin, origin) 66 | } 67 | } 68 | chain.ProcessFilter(req, resp) 69 | } 70 | 71 | // specResource is a REST resource to serve the Open-API spec. 72 | type specResource struct { 73 | swagger *spec.Swagger 74 | } 75 | 76 | func (s specResource) getSwagger(req *restful.Request, resp *restful.Response) { 77 | resp.WriteAsJson(s.swagger) 78 | } 79 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # timeout for analysis, e.g. 30s, 5m, default is 1m 7 | deadline: 5m 8 | 9 | # exit code when at least one issue was found, default is 1 10 | issues-exit-code: 1 11 | 12 | # include test files or not, default is true 13 | tests: true 14 | 15 | # which dirs to skip: they won't be analyzed; 16 | # can use regexp here: generated.*, regexp is applied on full path; 17 | # default value is empty list, but next dirs are always skipped independently 18 | # from this option's value: 19 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 20 | skip-dirs: 21 | 22 | # which files to skip: they will be analyzed, but issues from them 23 | # won't be reported. Default value is empty list, but there is 24 | # no need to include all autogenerated files, we confidently recognize 25 | # autogenerated files. If it's not please let us know. 26 | skip-files: 27 | 28 | # whether to hide "congrats" message if no issues were found, 29 | # default is false (show "congrats" message by default). 30 | # set this option to true to print nothing if no issues were found. 31 | silent: true 32 | 33 | # build-tags: 34 | # - mandrill 35 | # - test 36 | 37 | # output configuration options 38 | output: 39 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" 40 | format: line-number 41 | 42 | # print lines of code with issue, default is true 43 | print-issued-lines: true 44 | 45 | # print linter name in the end of issue text, default is true 46 | print-linter-name: true 47 | 48 | linters: 49 | enable-all: true 50 | disable: 51 | - gochecknoglobals 52 | - gochecknoinits 53 | - lll 54 | - dupl 55 | - wsl 56 | - funlen 57 | - gocognit 58 | - testpackage 59 | - gomnd 60 | - goerr113 61 | - nestif 62 | - interfacer 63 | - godot 64 | - unparam 65 | - nlreturn 66 | - wrapcheck 67 | - exhaustivestruct 68 | - errorlint 69 | # todo enable as much as possible linters below 70 | - paralleltest 71 | - unused 72 | - govet 73 | - gosimple 74 | - exhaustive 75 | - whitespace 76 | - structcheck 77 | - misspell 78 | - golint 79 | - goheader 80 | - stylecheck 81 | - gofumpt 82 | - gofmt 83 | - godox 84 | - gocyclo 85 | - gocritic 86 | - goconst 87 | - goimports 88 | - gci 89 | - errcheck 90 | - deadcode 91 | fast: false 92 | 93 | linters-settings: 94 | govet: 95 | check-shadowing: true 96 | golint: 97 | report-comments: true 98 | 99 | issues: 100 | max-same-issues: 20 101 | exclude: 102 | - ^G104 103 | - ^G501 104 | -------------------------------------------------------------------------------- /definition_name.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import "strings" 4 | 5 | // DefaultNameHandler GoRestfulDefinition -> GoRestfulDefinition (not changed) 6 | func DefaultNameHandler(name string) string { 7 | return name 8 | } 9 | 10 | // LowerSnakeCasedNameHandler GoRestfulDefinition -> go_restful_definition 11 | func LowerSnakeCasedNameHandler(name string) string { 12 | definitionName := make([]byte, 0, len(name)+1) 13 | for i := 0; i < len(name); i++ { 14 | c := name[i] 15 | if isUpper(c) { 16 | if i > 0 { 17 | definitionName = append(definitionName, '_') 18 | } 19 | c += 'a' - 'A' 20 | } 21 | definitionName = append(definitionName, c) 22 | } 23 | 24 | return string(definitionName) 25 | } 26 | 27 | // LowerCamelCasedNameHandler GoRestfulDefinition -> goRestfulDefinition 28 | func LowerCamelCasedNameHandler(name string) string { 29 | definitionName := make([]byte, 0, len(name)+1) 30 | for i := 0; i < len(name); i++ { 31 | c := name[i] 32 | if isUpper(c) && i == 0 { 33 | c += 'a' - 'A' 34 | } 35 | definitionName = append(definitionName, c) 36 | } 37 | 38 | return string(definitionName) 39 | } 40 | 41 | // GoLowerCamelCasedNameHandler HTTPRestfulDefinition -> httpRestfulDefinition 42 | func GoLowerCamelCasedNameHandler(name string) string { 43 | var i = 0 44 | // for continuous Upper letters, check whether is it a common Initialisms 45 | for ; i < len(name) && isUpper(name[i]); i++ { 46 | } 47 | if len(name) != i && i > 1 { 48 | i-- // for continuous Upper letters, the last Upper is should not be check, eg: S for HTTPStatus 49 | } 50 | for ; i > 1; i-- { 51 | if _, ok := commonInitialisms[name[:i]]; ok { 52 | break 53 | } 54 | } 55 | 56 | return strings.ToLower(name[:i]) + name[i:] 57 | } 58 | 59 | // commonInitialisms is a set of common initialisms. (from https://github.com/golang/lint/blob/master/lint.go) 60 | // Only add entries that are highly unlikely to be non-initialisms. 61 | // For instance, "ID" is fine (Freudian code is rare), but "AND" is not. 62 | var commonInitialisms = map[string]bool{ 63 | "ACL": true, 64 | "API": true, 65 | "ASCII": true, 66 | "CPU": true, 67 | "CSS": true, 68 | "DNS": true, 69 | "EOF": true, 70 | "GUID": true, 71 | "HTML": true, 72 | "HTTP": true, 73 | "HTTPS": true, 74 | "ID": true, 75 | "IP": true, 76 | "JSON": true, 77 | "LHS": true, 78 | "QPS": true, 79 | "RAM": true, 80 | "RHS": true, 81 | "RPC": true, 82 | "SLA": true, 83 | "SMTP": true, 84 | "SQL": true, 85 | "SSH": true, 86 | "TCP": true, 87 | "TLS": true, 88 | "TTL": true, 89 | "UDP": true, 90 | "UI": true, 91 | "UID": true, 92 | "UUID": true, 93 | "URI": true, 94 | "URL": true, 95 | "UTF8": true, 96 | "VM": true, 97 | "XML": true, 98 | "XMPP": true, 99 | "XSRF": true, 100 | "XSS": true, 101 | } 102 | 103 | func isUpper(r uint8) bool { 104 | return 'A' <= r && r <= 'Z' 105 | } 106 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | restful "github.com/emicklei/go-restful/v3" 8 | "github.com/go-openapi/spec" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func dummy(i *restful.Request, o *restful.Response) {} 15 | 16 | type Sample struct { 17 | ID string `swagger:"required"` 18 | Root Item `json:"root" description:"root desc"` 19 | Items []Item 20 | } 21 | 22 | type Item struct { 23 | ItemName string `json:"name"` 24 | } 25 | 26 | func asJSON(v interface{}) string { 27 | data, _ := json.MarshalIndent(v, " ", " ") 28 | return string(data) 29 | } 30 | 31 | func compareJSON(t *testing.T, actualJSONAsString string, expectedJSONAsString string) bool { 32 | success := false 33 | var actualMap map[string]interface{} 34 | json.Unmarshal([]byte(actualJSONAsString), &actualMap) 35 | var expectedMap map[string]interface{} 36 | err := json.Unmarshal([]byte(expectedJSONAsString), &expectedMap) 37 | if err != nil { 38 | var actualArray []interface{} 39 | json.Unmarshal([]byte(actualJSONAsString), &actualArray) 40 | var expectedArray []interface{} 41 | err := json.Unmarshal([]byte(expectedJSONAsString), &expectedArray) 42 | success = reflect.DeepEqual(actualArray, expectedArray) 43 | if err != nil { 44 | t.Fatalf("Unparsable expected JSON: %s, actual: %v, expected: %v", err, actualJSONAsString, expectedJSONAsString) 45 | } 46 | } else { 47 | success = reflect.DeepEqual(actualMap, expectedMap) 48 | } 49 | if !success { 50 | t.Log("---- expected -----") 51 | t.Log(withLineNumbers(expectedJSONAsString)) 52 | t.Log("---- actual -----") 53 | t.Log(withLineNumbers(actualJSONAsString)) 54 | t.Log("---- raw -----") 55 | t.Log(actualJSONAsString) 56 | t.Error("there are differences") 57 | return false 58 | } 59 | return true 60 | } 61 | 62 | // nolint:unused 63 | func withLineNumbers(content string) string { 64 | var buffer bytes.Buffer 65 | lines := strings.Split(content, "\n") 66 | for i, each := range lines { 67 | buffer.WriteString(fmt.Sprintf("%d:%s\n", i, each)) 68 | } 69 | return buffer.String() 70 | } 71 | 72 | // mergeStrings returns a new string slice without duplicates. 73 | func mergeStrings(left, right []string) (merged []string) { 74 | include := func(next string) { 75 | for _, dup := range merged { 76 | if next == dup { 77 | return 78 | } 79 | } 80 | merged = append(merged, next) 81 | } 82 | for _, each := range left { 83 | include(each) 84 | } 85 | for _, each := range right { 86 | include(each) 87 | } 88 | return 89 | } 90 | 91 | func definitionsFromStructWithConfig(sample interface{}, config Config) spec.Definitions { 92 | definitions := spec.Definitions{} 93 | builder := definitionBuilder{Definitions: definitions, Config: config} 94 | builder.addModelFrom(sample) 95 | return definitions 96 | } 97 | 98 | func definitionsFromStruct(sample interface{}) spec.Definitions { 99 | return definitionsFromStructWithConfig(sample, Config{}) 100 | } 101 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/emicklei/go-restful/v3" 7 | "github.com/go-openapi/spec" 8 | ) 9 | 10 | // MapSchemaFormatFunc can be used to modify typeName at definition time. 11 | // To use it set the SchemaFormatHandler in the config. 12 | type MapSchemaFormatFunc func(typeName string) string 13 | 14 | // MapModelTypeNameFunc can be used to return the desired typeName for a given 15 | // type. It will return false if the default name should be used. 16 | // To use it set the ModelTypeNameHandler in the config. 17 | type MapModelTypeNameFunc func(t reflect.Type) (string, bool) 18 | 19 | // PostBuildSwaggerObjectFunc can be used to change the creates Swagger Object 20 | // before serving it. To use it set the PostBuildSwaggerObjectHandler in the config. 21 | type PostBuildSwaggerObjectFunc func(s *spec.Swagger) 22 | 23 | // DefinitionNameHandlerFunc generate name by this handler for definition without json tag. 24 | // example: (for more, see file definition_name_test.go) 25 | // field definition_name 26 | // Name `json:"name"` -> name 27 | // Name -> Name 28 | // 29 | // there are some example provided for use 30 | // DefaultNameHandler GoRestfulDefinition -> GoRestfulDefinition (not changed) 31 | // LowerSnakeCasedNameHandler GoRestfulDefinition -> go_restful_definition 32 | // LowerCamelCasedNameHandler GoRestfulDefinition -> goRestfulDefinition 33 | // GoLowerCamelCasedNameHandler HTTPRestfulDefinition -> httpRestfulDefinition 34 | // 35 | type DefinitionNameHandlerFunc func(string) string 36 | 37 | // Config holds service api metadata. 38 | type Config struct { 39 | // [optional] If set then set this field with the generated Swagger Object 40 | Host string 41 | // [optional] If set then set this field with the generated Swagger Object 42 | Schemes []string 43 | // WebServicesURL is a DEPRECATED field; it never had any effect in this package. 44 | WebServicesURL string 45 | // APIPath is the path where the JSON api is available, e.g. /apidocs.json 46 | APIPath string 47 | // api listing is constructed from this list of restful WebServices. 48 | WebServices []*restful.WebService 49 | // [optional] on default CORS (Cross-Origin-Resource-Sharing) is enabled. 50 | DisableCORS bool 51 | // Top-level API version. Is reflected in the resource listing. 52 | APIVersion string 53 | // [optional] If set, model builder should call this handler to get addition typename-to-swagger-format-field conversion. 54 | SchemaFormatHandler MapSchemaFormatFunc 55 | // [optional] If set, model builder should call this handler to retrieve the name for a given type. 56 | ModelTypeNameHandler MapModelTypeNameFunc 57 | // [optional] If set then call this function with the generated Swagger Object 58 | PostBuildSwaggerObjectHandler PostBuildSwaggerObjectFunc 59 | // [optional] If set then call handler's function for to generate name by this handler for definition without json tag, 60 | // you can use you DefinitionNameHandler, also, there are four DefinitionNameHandler provided, see definition_name.go 61 | DefinitionNameHandler DefinitionNameHandlerFunc 62 | } 63 | -------------------------------------------------------------------------------- /definition_name_test.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGoLowerCamelCasedNameHandler_DefaultDefinitionName(t *testing.T) { 8 | testCases := []struct { 9 | name string 10 | input string 11 | excepted string 12 | }{ 13 | {"go normal case", "GoRestfulDefinition", "goRestfulDefinition"}, 14 | {"go ID case", "IDForA", "idForA"}, 15 | {"go HTTP case", "HTTPName", "httpName"}, 16 | {"go HTTPS case", "HTTPSName", "httpsName"}, 17 | {"go HTTP with Started name case", "HTTPStatus", "httpStatus"}, 18 | {"simpleWord", "I", "i"}, 19 | {"simpleLowerWord", "i", "i"}, 20 | } 21 | 22 | for _, testCase := range testCases { 23 | output := GoLowerCamelCasedNameHandler(testCase.input) 24 | if output != testCase.excepted { 25 | t.Errorf("testing %s failed, expected %s, get %s", testCase.name, testCase.excepted, output) 26 | } 27 | } 28 | } 29 | 30 | func TestLowerCamelCasedNameHandler_DefaultDefinitionName(t *testing.T) { 31 | testCases := []struct { 32 | name string 33 | input string 34 | excepted string 35 | }{ 36 | {"go normal case", "GoRestfulDefinition", "goRestfulDefinition"}, 37 | {"go ID case", "IDForA", "iDForA"}, 38 | {"go HTTP case", "HTTPName", "hTTPName"}, 39 | {"go HTTPS case", "HTTPSName", "hTTPSName"}, 40 | {"go HTTP with Started name case", "HTTPStatus", "hTTPStatus"}, 41 | {"simpleWord", "I", "i"}, 42 | {"simpleLowerWord", "i", "i"}, 43 | } 44 | 45 | for _, testCase := range testCases { 46 | output := LowerCamelCasedNameHandler(testCase.input) 47 | if output != testCase.excepted { 48 | t.Errorf("testing %s failed, expected %s, get %s", testCase.name, testCase.excepted, output) 49 | } 50 | } 51 | } 52 | 53 | func TestDefaultNameHandler_DefaultDefinitionName(t *testing.T) { 54 | testCases := []struct { 55 | name string 56 | input string 57 | excepted string 58 | }{ 59 | {"go normal case", "GoRestfulDefinition", "GoRestfulDefinition"}, 60 | {"go ID case", "IDForA", "IDForA"}, 61 | {"go HTTP case", "HTTPName", "HTTPName"}, 62 | {"go HTTPS case", "HTTPSName", "HTTPSName"}, 63 | {"go HTTP with Started name case", "HTTPStatus", "HTTPStatus"}, 64 | {"simpleWord", "I", "I"}, 65 | {"simpleLowerWord", "i", "i"}, 66 | } 67 | 68 | for _, testCase := range testCases { 69 | output := DefaultNameHandler(testCase.input) 70 | if output != testCase.excepted { 71 | t.Errorf("testing %s failed, expected %s, get %s", testCase.name, testCase.excepted, output) 72 | } 73 | } 74 | } 75 | 76 | func TestLowerSnakeCasedNameHandler_DefaultDefinitionName(t *testing.T) { 77 | testCases := []struct { 78 | name string 79 | input string 80 | excepted string 81 | }{ 82 | {"go normal case", "GoRestfulDefinition", "go_restful_definition"}, 83 | {"go ID case", "IDForA", "i_d_for_a"}, 84 | {"simpleWord", "I", "i"}, 85 | {"simpleLowerWord", "i", "i"}, 86 | } 87 | 88 | for _, testCase := range testCases { 89 | output := LowerSnakeCasedNameHandler(testCase.input) 90 | if output != testCase.excepted { 91 | t.Errorf("testing %s failed, expected %s, get %s", testCase.name, testCase.excepted, output) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # changes to the go-restful-openapi package 2 | 3 | # v2+ versions are using the Go module of go-restful v3+ 4 | 5 | ## v2.11.0 6 | 7 | - allow providing example value to a property via "example" tag (#124), thanks to Try Ajitiono 8 | 9 | ## v2.10.2 10 | 11 | - fix handle time.Time field #120 (#121) 12 | 13 | ## v2.10.1 14 | 15 | - Add type SchemaType to use in Return calls allowing for specification of a raw type value and format (#118) 16 | 17 | ## v2.9.2 18 | 19 | - fix unused type because embedded stuct (#113) 20 | 21 | ## v2.9.1 22 | 23 | - fix set array data format (#96) 24 | 25 | ## v2.9.0 26 | 27 | - Add property x-go-name support (#90) 28 | - Add support to set swagger Schemes field (#91) 29 | 30 | ## v2.8.0 31 | 32 | [2022-01-04] 33 | 34 | - refine and fix GoLowerCamelCasedNameHandler bug (#88) 35 | - Add missing fields of response header object (#89) 36 | - support generate field name with config (#86) 37 | 38 | Thanks again to slow-zhang and Sergey Vilgelm 39 | 40 | ## v2.7.0 41 | 42 | [2021-12-08] 43 | 44 | - fix some typos (#85) 45 | - use PossibleValues in favor of AllowedValues (#84) 46 | - PostBuildSwaggerSchema handler for each model (#83) 47 | - Use int64 format for time.Duration type (#82) 48 | 49 | Special thanks to contributions of Sergey Vilgelm 50 | 51 | ## [2021-09-20] v2.6.0 52 | 53 | - feat(parameter): adds additional openapi mappings (#74, robbie@robnrob.com) 54 | 55 | ## [2021-09-20] v2.5.0 56 | 57 | - add support for format tag (#72, askingcat) 58 | 59 | ## [2021-09-18] v2.4.0 60 | 61 | - add support for vendor extensions (#) 62 | 63 | ## [2020-02-10] v2.3.0 64 | - Support for custom attribute "x-nullable" (#70) 65 | 66 | ## v1.4.0 + v2.2.0 67 | - Allow maps as top level types and support maps to slices (#63) 68 | 69 | ## v1.3.0 + v2.1.0 70 | - add json.Number handling (PR #61) 71 | - add type alias support for primitives (PR #61) 72 | 73 | ## v1.2.0 74 | 75 | - handle map[string][]byte (#59) 76 | 77 | ## v1.1.0 (v0.14.1) 78 | 79 | - Add Host field to Config which is copied into Swagger object 80 | - Enable CORS by default as per the documentation (#58) 81 | - add go module 82 | - update dependencies 83 | 84 | ## v0.13.0 85 | 86 | - Do not use 200 as default response, instead use the one explicitly defined. 87 | - support time.Duration 88 | - Fix Parameter 'AllowableValues' to populate swagger definition 89 | 90 | ## v0.12.0 91 | 92 | - add support for time.Duration 93 | - Populate the swagger definition with the parameter's 'AllowableValues' as an enum (#53) 94 | - Fix for #19 MapModelTypeNameFunc has incomplete behavior 95 | - Merge paths with existing paths from other webServices (#48) 96 | - prevent array param.Type be overwritten in the else case below (#47) 97 | 98 | ## v0.11.0 99 | 100 | - Register pointer to array/slice of primitives as such rather than as reference to the primitive type definition. (#46) 101 | - Add support for map types using "additional properties" (#44) 102 | 103 | ## <= v0.10.0 104 | 105 | See `git log`. -------------------------------------------------------------------------------- /examples/security/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/emicklei/go-restful/v3" 8 | "github.com/go-openapi/spec" 9 | ) 10 | 11 | func enrichSwaggerObject(swo *spec.Swagger) { 12 | swo.Info = &spec.Info{ 13 | InfoProps: spec.InfoProps{ 14 | Title: "Example", 15 | Description: "Resource for doing example things", 16 | Contact: &spec.ContactInfo{ 17 | Name: "dkiser", 18 | Email: "domingo.kiser@gmail.com", 19 | URL: "domingo.space", 20 | }, 21 | License: &spec.License{ 22 | Name: "MIT", 23 | URL: "http://mit.org", 24 | }, 25 | Version: "1.0.0", 26 | }, 27 | } 28 | swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{ 29 | Name: "example", 30 | Description: "Exampling and stuff"}}} 31 | 32 | // setup security definitions 33 | swo.SecurityDefinitions = map[string]*spec.SecurityScheme{ 34 | "jwt": spec.APIKeyAuth("Authorization", "header"), 35 | } 36 | 37 | // map routes to security definitions 38 | enrichSwaggerObjectSecurity(swo) 39 | } 40 | 41 | func enrichSwaggerObjectSecurity(swo *spec.Swagger) { 42 | 43 | // loop through all registered web services 44 | for _, ws := range restful.RegisteredWebServices() { 45 | for _, route := range ws.Routes() { 46 | 47 | // grab route metadata for a SecurityDefinition 48 | secdefn, ok := route.Metadata[SecurityDefinitionKey] 49 | if !ok { 50 | continue 51 | } 52 | 53 | // grab pechelper.OAISecurity from the stored interface{} 54 | var sEntry OAISecurity 55 | switch v := secdefn.(type) { 56 | case *OAISecurity: 57 | sEntry = *v 58 | case OAISecurity: 59 | sEntry = v 60 | default: 61 | // not valid type 62 | log.Printf("skipping Security openapi spec for %s:%s, invalid metadata type %v", route.Method, route.Path, v) 63 | continue 64 | } 65 | 66 | if _, ok := swo.SecurityDefinitions[sEntry.Name]; !ok { 67 | log.Printf("skipping Security openapi spec for %s:%s, '%s' not found in SecurityDefinitions", route.Method, route.Path, sEntry.Name) 68 | continue 69 | } 70 | 71 | // grab path and path item in openapi spec 72 | path, err := swo.Paths.JSONLookup(route.Path) 73 | if err != nil { 74 | log.Printf("skipping Security openapi spec for %s:%s, %s", route.Method, route.Path, err.Error()) 75 | continue 76 | } 77 | pItem := path.(*spec.PathItem) 78 | 79 | // Update respective path Option based on method 80 | var pOption *spec.Operation 81 | switch method := strings.ToLower(route.Method); method { 82 | case "get": 83 | pOption = pItem.Get 84 | case "post": 85 | pOption = pItem.Post 86 | case "patch": 87 | pOption = pItem.Patch 88 | case "delete": 89 | pOption = pItem.Delete 90 | case "put": 91 | pOption = pItem.Put 92 | case "head": 93 | pOption = pItem.Head 94 | case "options": 95 | pOption = pItem.Options 96 | default: 97 | // unsupported method 98 | log.Printf("skipping Security openapi spec for %s:%s, unsupported method '%s'", route.Method, route.Path, route.Method) 99 | continue 100 | } 101 | 102 | // update the pOption with security entry 103 | pOption.SecuredWith(sEntry.Name, sEntry.Scopes...) 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /property_ext.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/go-openapi/spec" 9 | ) 10 | 11 | func initPropExtensions(ext *spec.Extensions) { 12 | if *ext == nil { 13 | *ext = make(spec.Extensions, 0) 14 | } 15 | } 16 | 17 | func setDescription(prop *spec.Schema, field reflect.StructField) { 18 | if tag := field.Tag.Get("description"); tag != "" { 19 | prop.Description = tag 20 | } 21 | } 22 | 23 | func setDefaultValue(prop *spec.Schema, field reflect.StructField) { 24 | if tag := field.Tag.Get("default"); tag != "" { 25 | prop.Default = stringAutoType("", tag) 26 | } 27 | } 28 | 29 | func setIsNullableValue(prop *spec.Schema, field reflect.StructField) { 30 | if tag := field.Tag.Get("x-nullable"); tag != "" { 31 | initPropExtensions(&prop.Extensions) 32 | 33 | value, err := strconv.ParseBool(tag) 34 | 35 | prop.Extensions["x-nullable"] = value && err == nil 36 | } 37 | } 38 | 39 | func setGoNameValue(prop *spec.Schema, field reflect.StructField) { 40 | const tagName = "x-go-name" 41 | if tag := field.Tag.Get(tagName); tag != "" { 42 | initPropExtensions(&prop.Extensions) 43 | prop.Extensions[tagName] = tag 44 | } 45 | } 46 | 47 | func setEnumValues(prop *spec.Schema, field reflect.StructField) { 48 | // We use | to separate the enum values. This value is chosen 49 | // since it's unlikely to be useful in actual enumeration values. 50 | if tag := field.Tag.Get("enum"); tag != "" { 51 | enums := []interface{}{} 52 | for _, s := range strings.Split(tag, "|") { 53 | enums = append(enums, s) 54 | } 55 | prop.Enum = enums 56 | } 57 | } 58 | 59 | func setFormat(prop *spec.Schema, field reflect.StructField) { 60 | if tag := field.Tag.Get("format"); tag != "" { 61 | prop.Format = tag 62 | } 63 | 64 | } 65 | 66 | func setMaximum(prop *spec.Schema, field reflect.StructField) { 67 | if tag := field.Tag.Get("maximum"); tag != "" { 68 | value, err := strconv.ParseFloat(tag, 64) 69 | if err == nil { 70 | prop.Maximum = &value 71 | } 72 | } 73 | } 74 | 75 | func setMinimum(prop *spec.Schema, field reflect.StructField) { 76 | if tag := field.Tag.Get("minimum"); tag != "" { 77 | value, err := strconv.ParseFloat(tag, 64) 78 | if err == nil { 79 | prop.Minimum = &value 80 | } 81 | } 82 | } 83 | 84 | func setType(prop *spec.Schema, field reflect.StructField) { 85 | if tag := field.Tag.Get("type"); tag != "" { 86 | // Check if the first two characters of the type tag are 87 | // intended to emulate slice/array behaviour. 88 | // 89 | // If type is intended to be a slice/array then add the 90 | // overridden type to the array item instead of the main property 91 | if len(tag) > 2 && tag[0:2] == "[]" { 92 | pType := "array" 93 | prop.Type = []string{pType} 94 | prop.Items = &spec.SchemaOrArray{ 95 | Schema: &spec.Schema{}, 96 | } 97 | iType := tag[2:] 98 | prop.Items.Schema.Type = []string{iType} 99 | return 100 | } 101 | 102 | prop.Type = []string{tag} 103 | } 104 | } 105 | 106 | func setUniqueItems(prop *spec.Schema, field reflect.StructField) { 107 | tag := field.Tag.Get("unique") 108 | switch tag { 109 | case "true": 110 | prop.UniqueItems = true 111 | case "false": 112 | prop.UniqueItems = false 113 | } 114 | } 115 | 116 | func setReadOnly(prop *spec.Schema, field reflect.StructField) { 117 | tag := field.Tag.Get("readOnly") 118 | switch tag { 119 | case "true": 120 | prop.ReadOnly = true 121 | case "false": 122 | prop.ReadOnly = false 123 | } 124 | } 125 | 126 | func setExample(prop *spec.Schema, field reflect.StructField) { 127 | if exampleTag := field.Tag.Get("example"); exampleTag != "" { 128 | prop.Example = exampleTag 129 | } 130 | } 131 | 132 | func setPropertyMetadata(prop *spec.Schema, field reflect.StructField) { 133 | setDescription(prop, field) 134 | setDefaultValue(prop, field) 135 | setEnumValues(prop, field) 136 | setFormat(prop, field) 137 | setMinimum(prop, field) 138 | setMaximum(prop, field) 139 | setUniqueItems(prop, field) 140 | setType(prop, field) 141 | setReadOnly(prop, field) 142 | setIsNullableValue(prop, field) 143 | setGoNameValue(prop, field) 144 | setExample(prop, field) 145 | } 146 | -------------------------------------------------------------------------------- /property_ext_test.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // nolint:paralleltest 11 | func TestThatExtraTagsAreReadIntoModel(t *testing.T) { 12 | type fakeint int 13 | type fakearray string 14 | type Anything struct { 15 | Name string `description:"name" modelDescription:"a test" readOnly:"false"` 16 | Size int `minimum:"0" maximum:"10"` 17 | Stati string `enum:"off|on" default:"on" modelDescription:"more description"` 18 | ID string `unique:"true"` 19 | FakeInt fakeint `type:"integer"` 20 | FakeArray fakearray `type:"[]string"` 21 | IP net.IP `type:"string"` 22 | Password string 23 | Optional bool `optional:"true"` 24 | Created string `readOnly:"true"` 25 | NullableField string `x-nullable:"true"` 26 | NotNullableField string `x-nullable:"false"` 27 | UUID string `type:"string" format:"UUID"` 28 | XGoName string `x-go-name:"specgoname"` 29 | ByteArray []byte `format:"binary"` 30 | } 31 | d := definitionsFromStruct(Anything{}) 32 | props, _ := d["restfulspec.Anything"] 33 | p1, _ := props.Properties["Name"] 34 | if got, want := p1.Description, "name"; got != want { 35 | t.Errorf("got %v want %v", got, want) 36 | } 37 | if got, want := p1.ReadOnly, false; got != want { 38 | t.Errorf("got %v want %v", got, want) 39 | } 40 | p2, _ := props.Properties["Size"] 41 | if got, want := *p2.Minimum, 0.0; got != want { 42 | t.Errorf("got %v want %v", got, want) 43 | } 44 | if got, want := p2.ReadOnly, false; got != want { 45 | t.Errorf("got %v want %v", got, want) 46 | } 47 | if got, want := *p2.Maximum, 10.0; got != want { 48 | t.Errorf("got %v want %v", got, want) 49 | } 50 | p3, _ := props.Properties["Stati"] 51 | if got, want := p3.Enum[0], "off"; got != want { 52 | t.Errorf("got %v want %v", got, want) 53 | } 54 | if got, want := p3.Enum[1], "on"; got != want { 55 | t.Errorf("got %v want %v", got, want) 56 | } 57 | p4, _ := props.Properties["ID"] 58 | if got, want := p4.UniqueItems, true; got != want { 59 | t.Errorf("got %v want %v", got, want) 60 | } 61 | p5, _ := props.Properties["Password"] 62 | if got, want := p5.Type[0], "string"; got != want { 63 | t.Errorf("got %v want %v", got, want) 64 | } 65 | p6, _ := props.Properties["FakeInt"] 66 | if got, want := p6.Type[0], "integer"; got != want { 67 | t.Errorf("got %v want %v", got, want) 68 | } 69 | p7, _ := props.Properties["FakeArray"] 70 | if got, want := p7.Type[0], "array"; got != want { 71 | t.Errorf("got %v want %v", got, want) 72 | } 73 | p7p, _ := props.Properties["FakeArray"] 74 | if got, want := p7p.Items.Schema.Type[0], "string"; got != want { 75 | t.Errorf("got %v want %v", got, want) 76 | } 77 | p8, _ := props.Properties["IP"] 78 | if got, want := p8.Type[0], "string"; got != want { 79 | t.Errorf("got %v want %v", got, want) 80 | } 81 | p9, _ := props.Properties["Created"] 82 | if got, want := p9.ReadOnly, true; got != want { 83 | t.Errorf("got %v want %v", got, want) 84 | } 85 | if got, want := strings.Contains(fmt.Sprintf("%v", props.Required), "Optional"), false; got != want { 86 | t.Errorf("got %v want %v", got, want) 87 | } 88 | if got, want := props.Description, "a test\nmore description"; got != want { 89 | t.Errorf("got %v want %v", got, want) 90 | } 91 | p10, _ := props.Properties["NullableField"] 92 | if got, want := p10.Extensions["x-nullable"], true; got != want { 93 | t.Errorf("got %v want %v", got, want) 94 | } 95 | p11, _ := props.Properties["NotNullableField"] 96 | if got, want := p11.Extensions["x-nullable"], false; got != want { 97 | t.Errorf("got %v want %v", got, want) 98 | } 99 | p12, _ := props.Properties["UUID"] 100 | if got, want := p12.Type[0], "string"; got != want { 101 | t.Errorf("got %v want %v", got, want) 102 | } 103 | if got, want := p12.Format, "UUID"; got != want { 104 | t.Errorf("got %v want %v", got, want) 105 | } 106 | p13, _ := props.Properties["XGoName"] 107 | if got, want := p13.Extensions["x-go-name"], "specgoname"; got != want { 108 | t.Errorf("got %v want %v", got, want) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 6 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 7 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 8 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 9 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 10 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= 11 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 12 | github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= 13 | github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 14 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 15 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 16 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 17 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 18 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 19 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 20 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 21 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 22 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 25 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 26 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 27 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 28 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 29 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 30 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 31 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 32 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 33 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 34 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 35 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 36 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 42 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 46 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 49 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 52 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /examples/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Resource for managing Users", 5 | "title": "UserService", 6 | "contact": { 7 | "name": "john", 8 | "url": "http://johndoe.org", 9 | "email": "john@doe.rp" 10 | }, 11 | "license": { 12 | "name": "MIT", 13 | "url": "http://mit.org" 14 | }, 15 | "version": "1.0.0" 16 | }, 17 | "paths": { 18 | "/users": { 19 | "get": { 20 | "consumes": [ 21 | "application/xml", 22 | "application/json" 23 | ], 24 | "produces": [ 25 | "application/json", 26 | "application/xml" 27 | ], 28 | "tags": [ 29 | "users" 30 | ], 31 | "summary": "get all users", 32 | "operationId": "findAllUsers", 33 | "responses": { 34 | "200": { 35 | "description": "OK", 36 | "schema": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/main.User" 40 | } 41 | } 42 | }, 43 | "default": { 44 | "description": "OK", 45 | "schema": { 46 | "type": "array", 47 | "items": { 48 | "$ref": "#/definitions/main.User" 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | "put": { 55 | "consumes": [ 56 | "application/xml", 57 | "application/json" 58 | ], 59 | "produces": [ 60 | "application/json", 61 | "application/xml" 62 | ], 63 | "tags": [ 64 | "users" 65 | ], 66 | "summary": "create a user", 67 | "operationId": "createUser", 68 | "parameters": [ 69 | { 70 | "name": "body", 71 | "in": "body", 72 | "required": true, 73 | "schema": { 74 | "$ref": "#/definitions/main.User" 75 | } 76 | } 77 | ], 78 | "responses": { 79 | "200": { 80 | "description": "OK" 81 | } 82 | } 83 | } 84 | }, 85 | "/users/{user-id}": { 86 | "get": { 87 | "consumes": [ 88 | "application/xml", 89 | "application/json" 90 | ], 91 | "produces": [ 92 | "application/json", 93 | "application/xml" 94 | ], 95 | "tags": [ 96 | "users" 97 | ], 98 | "summary": "get a user", 99 | "operationId": "findUser", 100 | "parameters": [ 101 | { 102 | "type": "integer", 103 | "default": 1, 104 | "description": "identifier of the user", 105 | "name": "user-id", 106 | "in": "path", 107 | "required": true 108 | } 109 | ], 110 | "responses": { 111 | "200": { 112 | "description": "OK", 113 | "schema": { 114 | "$ref": "#/definitions/main.User" 115 | } 116 | }, 117 | "404": { 118 | "description": "Not Found" 119 | }, 120 | "default": { 121 | "description": "OK", 122 | "schema": { 123 | "$ref": "#/definitions/main.User" 124 | } 125 | } 126 | } 127 | }, 128 | "put": { 129 | "consumes": [ 130 | "application/xml", 131 | "application/json" 132 | ], 133 | "produces": [ 134 | "application/json", 135 | "application/xml" 136 | ], 137 | "tags": [ 138 | "users" 139 | ], 140 | "summary": "update a user", 141 | "operationId": "updateUser", 142 | "parameters": [ 143 | { 144 | "type": "string", 145 | "description": "identifier of the user", 146 | "name": "user-id", 147 | "in": "path", 148 | "required": true 149 | }, 150 | { 151 | "name": "body", 152 | "in": "body", 153 | "required": true, 154 | "schema": { 155 | "$ref": "#/definitions/main.User" 156 | } 157 | } 158 | ], 159 | "responses": { 160 | "200": { 161 | "description": "OK" 162 | } 163 | } 164 | }, 165 | "delete": { 166 | "consumes": [ 167 | "application/xml", 168 | "application/json" 169 | ], 170 | "produces": [ 171 | "application/json", 172 | "application/xml" 173 | ], 174 | "tags": [ 175 | "users" 176 | ], 177 | "summary": "delete a user", 178 | "operationId": "removeUser", 179 | "parameters": [ 180 | { 181 | "type": "string", 182 | "description": "identifier of the user", 183 | "name": "user-id", 184 | "in": "path", 185 | "required": true 186 | } 187 | ], 188 | "responses": { 189 | "200": { 190 | "description": "OK" 191 | } 192 | } 193 | } 194 | } 195 | }, 196 | "definitions": { 197 | "main.User": { 198 | "required": [ 199 | "id", 200 | "name", 201 | "age" 202 | ], 203 | "properties": { 204 | "age": { 205 | "description": "age of the user", 206 | "type": "integer", 207 | "format": "int32", 208 | "default": 21 209 | }, 210 | "id": { 211 | "description": "identifier of the user", 212 | "type": "string" 213 | }, 214 | "name": { 215 | "description": "name of the user", 216 | "type": "string", 217 | "default": "john" 218 | } 219 | } 220 | } 221 | }, 222 | "tags": [ 223 | { 224 | "description": "Managing users", 225 | "name": "users" 226 | } 227 | ] 228 | } 229 | -------------------------------------------------------------------------------- /examples/user-resource.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | restfulspec "github.com/emicklei/go-restful-openapi/v2" 8 | "github.com/emicklei/go-restful/v3" 9 | "github.com/go-openapi/spec" 10 | ) 11 | 12 | type UserResource struct { 13 | // normally one would use DAO (data access object) 14 | users map[string]User 15 | } 16 | 17 | func (u UserResource) WebService() *restful.WebService { 18 | ws := new(restful.WebService) 19 | ws. 20 | Path("/users"). 21 | Consumes(restful.MIME_XML, restful.MIME_JSON). 22 | Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well 23 | 24 | tags := []string{"users"} 25 | 26 | ws.Route(ws.GET("/").To(u.findAllUsers). 27 | // docs 28 | Doc("get all users"). 29 | Metadata(restfulspec.KeyOpenAPITags, tags). 30 | Writes([]User{}). 31 | Returns(200, "OK", []User{}). 32 | DefaultReturns("OK", []User{})) 33 | 34 | ws.Route(ws.GET("/{user-id}").To(u.findUser). 35 | // docs 36 | Doc("get a user"). 37 | Param(ws.PathParameter("user-id", "identifier of the user").DataType("integer").DefaultValue("1")). 38 | Metadata(restfulspec.KeyOpenAPITags, tags). 39 | Writes(User{}). // on the response 40 | Returns(200, "OK", User{}). 41 | Returns(404, "Not Found", nil). 42 | DefaultReturns("OK", User{})) 43 | 44 | ws.Route(ws.PUT("/{user-id}").To(u.updateUser). 45 | // docs 46 | Doc("update a user"). 47 | Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")). 48 | Metadata(restfulspec.KeyOpenAPITags, tags). 49 | Reads(User{})) // from the request 50 | 51 | ws.Route(ws.PUT("").To(u.createUser). 52 | // docs 53 | Doc("create a user"). 54 | Metadata(restfulspec.KeyOpenAPITags, tags). 55 | Reads(User{})) // from the request 56 | 57 | ws.Route(ws.DELETE("/{user-id}").To(u.removeUser). 58 | // docs 59 | Doc("delete a user"). 60 | Metadata(restfulspec.KeyOpenAPITags, tags). 61 | Param(ws.PathParameter("user-id", "identifier of the user").DataType("string"))) 62 | 63 | return ws 64 | } 65 | 66 | // GET http://localhost:8080/users 67 | // 68 | func (u UserResource) findAllUsers(request *restful.Request, response *restful.Response) { 69 | list := []User{} 70 | for _, each := range u.users { 71 | list = append(list, each) 72 | } 73 | response.WriteEntity(list) 74 | } 75 | 76 | // GET http://localhost:8080/users/1 77 | // 78 | func (u UserResource) findUser(request *restful.Request, response *restful.Response) { 79 | id := request.PathParameter("user-id") 80 | usr := u.users[id] 81 | if len(usr.ID) == 0 { 82 | response.WriteErrorString(http.StatusNotFound, "User could not be found.") 83 | } else { 84 | response.WriteEntity(usr) 85 | } 86 | } 87 | 88 | // PUT http://localhost:8080/users/1 89 | // 1Melissa Raspberry 90 | // 91 | func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) { 92 | usr := new(User) 93 | err := request.ReadEntity(&usr) 94 | if err == nil { 95 | u.users[usr.ID] = *usr 96 | response.WriteEntity(usr) 97 | } else { 98 | response.WriteError(http.StatusInternalServerError, err) 99 | } 100 | } 101 | 102 | // PUT http://localhost:8080/users/1 103 | // 1Melissa 104 | // 105 | func (u *UserResource) createUser(request *restful.Request, response *restful.Response) { 106 | usr := User{ID: request.PathParameter("user-id")} 107 | err := request.ReadEntity(&usr) 108 | if err == nil { 109 | u.users[usr.ID] = usr 110 | response.WriteHeaderAndEntity(http.StatusCreated, usr) 111 | } else { 112 | response.WriteError(http.StatusInternalServerError, err) 113 | } 114 | } 115 | 116 | // DELETE http://localhost:8080/users/1 117 | // 118 | func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) { 119 | id := request.PathParameter("user-id") 120 | delete(u.users, id) 121 | } 122 | 123 | func main() { 124 | u := UserResource{map[string]User{}} 125 | restful.DefaultContainer.Add(u.WebService()) 126 | 127 | config := restfulspec.Config{ 128 | WebServices: restful.RegisteredWebServices(), // you control what services are visible 129 | APIPath: "/apidocs.json", 130 | PostBuildSwaggerObjectHandler: enrichSwaggerObject} 131 | restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config)) 132 | 133 | // Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API 134 | // You need to download the Swagger HTML5 assets and change the FilePath location in the config below. 135 | // Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json 136 | http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/Users/emicklei/Projects/swagger-ui/dist")))) 137 | 138 | // Optionally, you may need to enable CORS for the UI to work. 139 | cors := restful.CrossOriginResourceSharing{ 140 | AllowedHeaders: []string{"Content-Type", "Accept"}, 141 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, 142 | CookiesAllowed: false, 143 | Container: restful.DefaultContainer} 144 | restful.DefaultContainer.Filter(cors.Filter) 145 | 146 | log.Printf("Get the API using http://localhost:8080/apidocs.json") 147 | log.Printf("Open Swagger UI using http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json") 148 | log.Fatal(http.ListenAndServe(":8080", nil)) 149 | } 150 | 151 | func enrichSwaggerObject(swo *spec.Swagger) { 152 | swo.Info = &spec.Info{ 153 | InfoProps: spec.InfoProps{ 154 | Title: "UserService", 155 | Description: "Resource for managing Users", 156 | Contact: &spec.ContactInfo{ 157 | ContactInfoProps: spec.ContactInfoProps{ 158 | Name: "john", 159 | Email: "john@doe.rp", 160 | URL: "http://johndoe.org", 161 | }, 162 | }, 163 | License: &spec.License{ 164 | LicenseProps: spec.LicenseProps{ 165 | Name: "MIT", 166 | URL: "http://mit.org", 167 | }, 168 | }, 169 | Version: "1.0.0", 170 | }, 171 | } 172 | swo.Tags = []spec.Tag{{TagProps: spec.TagProps{ 173 | Name: "users", 174 | Description: "Managing users"}}} 175 | } 176 | 177 | // User is just a sample type 178 | type User struct { 179 | ID string `json:"id" description:"identifier of the user"` 180 | Name string `json:"name" description:"name of the user" default:"john"` 181 | Age int `json:"age" description:"age of the user" default:"21"` 182 | } 183 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 2 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 3 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 4 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful-openapi/v2 v2.6.1 h1:SJ5MRrgtuJ9ZF/i4NuzVrA4uyh61p7H3hdz7By6EN/I= 10 | github.com/emicklei/go-restful-openapi/v2 v2.6.1/go.mod h1:i74MGi/3IdPTeSmWV/Xk5CZk44XL9rsG/sRckPoEmFo= 11 | github.com/emicklei/go-restful/v3 v3.7.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 12 | github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= 13 | github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 14 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 15 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 16 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 17 | github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= 18 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 19 | github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= 20 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 21 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 22 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 23 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 24 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 25 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 26 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 27 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 28 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 35 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 36 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 37 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 39 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 40 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 41 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 42 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 43 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 48 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 49 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758 h1:aEpZnXcAmXkd6AvLb2OPt+EN1Zu/8Ne3pCqPjja5PXY= 51 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 52 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 55 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 56 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 57 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 58 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 62 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 64 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 65 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 68 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 69 | -------------------------------------------------------------------------------- /definition_builder.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/go-openapi/spec" 8 | ) 9 | 10 | type definitionBuilder struct { 11 | Definitions spec.Definitions 12 | Config Config 13 | } 14 | 15 | // Documented is 16 | type Documented interface { 17 | SwaggerDoc() map[string]string 18 | } 19 | 20 | type PostBuildSwaggerSchema interface { 21 | PostBuildSwaggerSchemaHandler(sm *spec.Schema) 22 | } 23 | 24 | // Check if this structure has a method with signature func () SwaggerDoc() map[string]string 25 | // If it exists, retrieve the documentation and overwrite all struct tag descriptions 26 | func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string { 27 | if docable, ok := reflect.New(model).Elem().Interface().(Documented); ok { 28 | return docable.SwaggerDoc() 29 | } 30 | return make(map[string]string) 31 | } 32 | 33 | // addModelFrom creates and adds a Schema to the builder and detects and calls 34 | // the post build hook for customizations 35 | func (b definitionBuilder) addModelFrom(sample interface{}) { 36 | b.addModel(reflect.TypeOf(sample), "") 37 | } 38 | 39 | func (b definitionBuilder) addModel(st reflect.Type, nameOverride string) *spec.Schema { 40 | // Turn pointers into simpler types so further checks are 41 | // correct. 42 | if st.Kind() == reflect.Ptr { 43 | st = st.Elem() 44 | } 45 | if b.isSliceOrArrayType(st.Kind()) { 46 | st = st.Elem() 47 | } 48 | 49 | modelName := keyFrom(st, b.Config) 50 | if nameOverride != "" { 51 | modelName = nameOverride 52 | } 53 | // no models needed for primitive types unless it has alias 54 | if b.isPrimitiveType(modelName, st.Kind()) { 55 | if nameOverride == "" { 56 | return nil 57 | } 58 | } 59 | // golang encoding/json packages says array and slice values encode as 60 | // JSON arrays, except that []byte encodes as a base64-encoded string. 61 | // If we see a []byte here, treat it at as a primitive type (string) 62 | // and deal with it in buildArrayTypeProperty. 63 | if b.isByteArrayType(st) { 64 | return nil 65 | } 66 | // see if we already have visited this model 67 | if _, ok := b.Definitions[modelName]; ok { 68 | return nil 69 | } 70 | sm := spec.Schema{ 71 | SchemaProps: spec.SchemaProps{ 72 | Required: []string{}, 73 | Properties: map[string]spec.Schema{}, 74 | }, 75 | } 76 | 77 | // reference the model before further initializing (enables recursive structs) 78 | b.Definitions[modelName] = sm 79 | 80 | if st.Kind() == reflect.Map { 81 | _, sm = b.buildMapType(st, "value", modelName) 82 | b.Definitions[modelName] = sm 83 | return &sm 84 | } 85 | // check for structure or primitive type 86 | if st.Kind() != reflect.Struct { 87 | return &sm 88 | } 89 | 90 | fullDoc := getDocFromMethodSwaggerDoc2(st) 91 | modelDescriptions := []string{} 92 | 93 | for i := 0; i < st.NumField(); i++ { 94 | field := st.Field(i) 95 | jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName) 96 | if len(modelDescription) > 0 { 97 | modelDescriptions = append(modelDescriptions, modelDescription) 98 | } 99 | 100 | // add if not omitted 101 | if len(jsonName) != 0 { 102 | // update description 103 | if fieldDoc, ok := fullDoc[jsonName]; ok { 104 | prop.Description = fieldDoc 105 | } 106 | // update Required 107 | if b.isPropertyRequired(field) { 108 | sm.Required = append(sm.Required, jsonName) 109 | } 110 | sm.Properties[jsonName] = prop 111 | } 112 | } 113 | 114 | // We always overwrite documentation if SwaggerDoc method exists 115 | // "" is special for documenting the struct itself 116 | if modelDoc, ok := fullDoc[""]; ok { 117 | sm.Description = modelDoc 118 | } else if len(modelDescriptions) != 0 { 119 | sm.Description = strings.Join(modelDescriptions, "\n") 120 | } 121 | // Needed to pass openapi validation. This field exists for json-schema compatibility, 122 | // but it conflicts with the openapi specification. 123 | // See https://github.com/go-openapi/spec/issues/23 for more context 124 | sm.ID = "" 125 | 126 | // Call handler to update sch 127 | if handler, ok := reflect.New(st).Elem().Interface().(PostBuildSwaggerSchema); ok { 128 | handler.PostBuildSwaggerSchemaHandler(&sm) 129 | } 130 | 131 | // update model builder with completed model 132 | b.Definitions[modelName] = sm 133 | 134 | return &sm 135 | } 136 | 137 | func (b definitionBuilder) isPropertyRequired(field reflect.StructField) bool { 138 | required := true 139 | if optionalTag := field.Tag.Get("optional"); optionalTag == "true" { 140 | return false 141 | } 142 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 143 | s := strings.Split(jsonTag, ",") 144 | if len(s) > 1 && s[1] == "omitempty" { 145 | return false 146 | } 147 | } 148 | return required 149 | } 150 | 151 | func (b definitionBuilder) buildProperty(field reflect.StructField, model *spec.Schema, modelName string) (jsonName, modelDescription string, prop spec.Schema) { 152 | jsonName = b.jsonNameOfField(field) 153 | if len(jsonName) == 0 { 154 | // empty name signals skip property 155 | return "", "", prop 156 | } 157 | 158 | if field.Name == "XMLName" && field.Type.String() == "xml.Name" { 159 | // property is metadata for the xml.Name attribute, can be skipped 160 | return "", "", prop 161 | } 162 | 163 | if tag := field.Tag.Get("modelDescription"); tag != "" { 164 | modelDescription = tag 165 | } 166 | 167 | setPropertyMetadata(&prop, field) 168 | if prop.Type != nil { 169 | return jsonName, modelDescription, prop 170 | } 171 | fieldType := field.Type 172 | 173 | // check if type is doing its own marshalling 174 | // marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem() 175 | // if fieldType.Implements(marshalerType) { 176 | // var pType = "string" 177 | // if prop.Type == nil { 178 | // prop.Type = []string{pType} 179 | // } 180 | // if prop.Format == "" { 181 | // prop.Format = b.jsonSchemaFormat(keyFrom(fieldType, b.Config), fieldType.Kind()) 182 | // } 183 | // return jsonName, modelDescription, prop 184 | // } 185 | 186 | // check if annotation says it is a string 187 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 188 | s := strings.Split(jsonTag, ",") 189 | if len(s) > 1 && s[1] == "string" { 190 | stringt := "string" 191 | prop.Type = []string{stringt} 192 | return jsonName, modelDescription, prop 193 | } 194 | } 195 | 196 | fieldKind := fieldType.Kind() 197 | 198 | // check for primitive first 199 | fieldTypeName := keyFrom(fieldType, b.Config) 200 | if b.isPrimitiveType(fieldTypeName, fieldKind) { 201 | mapped := b.jsonSchemaType(fieldTypeName, fieldKind) 202 | prop.Type = []string{mapped} 203 | prop.Format = b.jsonSchemaFormat(fieldTypeName, fieldKind) 204 | return jsonName, modelDescription, prop 205 | } 206 | 207 | // not a primitive 208 | // Since the `prop` variable in each of the cases below is a new reference, we need to re-set the `Example` field. 209 | switch { 210 | case fieldKind == reflect.Struct: 211 | jsonName, prop := b.buildStructTypeProperty(field, jsonName, model) 212 | setExample(&prop, field) 213 | 214 | return jsonName, modelDescription, prop 215 | case b.isSliceOrArrayType(fieldKind): 216 | jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName) 217 | setExample(&prop, field) 218 | 219 | return jsonName, modelDescription, prop 220 | case fieldKind == reflect.Ptr: 221 | jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName) 222 | setExample(&prop, field) 223 | 224 | return jsonName, modelDescription, prop 225 | case fieldKind == reflect.Map: 226 | jsonName, prop := b.buildMapTypeProperty(field, jsonName, modelName) 227 | setExample(&prop, field) 228 | 229 | return jsonName, modelDescription, prop 230 | } 231 | 232 | modelType := keyFrom(fieldType, b.Config) 233 | prop.Ref = spec.MustCreateRef("#/definitions/" + modelType) 234 | 235 | if fieldType.Name() == "" { // override type of anonymous structs 236 | // FIXME: Still need a way to handle anonymous struct model naming. 237 | nestedTypeName := modelName + "." + jsonName 238 | prop.Ref = spec.MustCreateRef("#/definitions/" + nestedTypeName) 239 | b.addModel(fieldType, nestedTypeName) 240 | } 241 | 242 | return jsonName, modelDescription, prop 243 | } 244 | 245 | func hasNamedJSONTag(field reflect.StructField) bool { 246 | parts := strings.Split(field.Tag.Get("json"), ",") 247 | if len(parts) == 0 { 248 | return false 249 | } 250 | for _, s := range parts[1:] { 251 | if s == "inline" { 252 | return false 253 | } 254 | } 255 | return len(parts[0]) > 0 256 | } 257 | 258 | func (b definitionBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *spec.Schema) (nameJson string, prop spec.Schema) { 259 | setPropertyMetadata(&prop, field) 260 | fieldType := field.Type 261 | // check for anonymous 262 | if len(fieldType.Name()) == 0 { 263 | // anonymous 264 | // FIXME: Still need a way to handle anonymous struct model naming. 265 | anonType := model.ID + "." + jsonName 266 | b.addModel(fieldType, anonType) 267 | prop.Ref = spec.MustCreateRef("#/definitions/" + anonType) 268 | return jsonName, prop 269 | } 270 | 271 | if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) { 272 | definitions := make(map[string]spec.Schema, len(b.Definitions)) 273 | for k, v := range b.Definitions { 274 | definitions[k] = v 275 | } 276 | // embedded struct 277 | sub := definitionBuilder{definitions, b.Config} 278 | sub.addModel(fieldType, "") 279 | subKey := keyFrom(fieldType, b.Config) 280 | // merge properties from sub 281 | subModel, _ := sub.Definitions[subKey] 282 | for k, v := range subModel.Properties { 283 | model.Properties[k] = v 284 | // if subModel says this property is required then include it 285 | required := false 286 | for _, each := range subModel.Required { 287 | if k == each { 288 | required = true 289 | break 290 | } 291 | } 292 | if required { 293 | model.Required = append(model.Required, k) 294 | } 295 | } 296 | // add all new referenced models 297 | for key, sub := range sub.Definitions { 298 | if key != subKey { 299 | if _, ok := b.Definitions[key]; !ok { 300 | b.Definitions[key] = sub 301 | } 302 | } 303 | } 304 | // empty name signals skip property 305 | return "", prop 306 | } 307 | // simple struct 308 | b.addModel(fieldType, "") 309 | var pType = keyFrom(fieldType, b.Config) 310 | prop.Ref = spec.MustCreateRef("#/definitions/" + pType) 311 | return jsonName, prop 312 | } 313 | 314 | func (b definitionBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) { 315 | setPropertyMetadata(&prop, field) 316 | fieldType := field.Type 317 | if fieldType.Elem().Kind() == reflect.Uint8 { 318 | stringt := "string" 319 | prop.Type = []string{stringt} 320 | return jsonName, prop 321 | } 322 | prop.Items = &spec.SchemaOrArray{ 323 | Schema: &spec.Schema{}, 324 | } 325 | itemSchema := &prop 326 | itemType := fieldType 327 | isArray := b.isSliceOrArrayType(fieldType.Kind()) 328 | for isArray { 329 | itemType = itemType.Elem() 330 | isArray = b.isSliceOrArrayType(itemType.Kind()) 331 | if itemType.Kind() == reflect.Uint8 { 332 | stringt := "string" 333 | prop.Format = "binary" 334 | itemSchema.Type = []string{stringt} 335 | return jsonName, prop 336 | } 337 | itemSchema.Items = &spec.SchemaOrArray{ 338 | Schema: &spec.Schema{}, 339 | } 340 | itemSchema.Type = []string{"array"} 341 | itemSchema = itemSchema.Items.Schema 342 | } 343 | isPrimitive := b.isPrimitiveType(itemType.Name(), itemType.Kind()) 344 | elemTypeName := b.getElementTypeName(modelName, jsonName, itemType) 345 | 346 | // If enum exists, move the enum definition from the `type: "array"` definition to `items`. 347 | if prop.Enum != nil { 348 | prop.Items.Schema.Enum = prop.Enum 349 | prop.Enum = nil 350 | } 351 | 352 | if isPrimitive { 353 | mapped := b.jsonSchemaType(elemTypeName, itemType.Kind()) 354 | itemSchema.Type = []string{mapped} 355 | itemSchema.Format = b.jsonSchemaFormat(elemTypeName, itemType.Kind()) 356 | } else { 357 | itemSchema.Ref = spec.MustCreateRef("#/definitions/" + elemTypeName) 358 | } 359 | // add|overwrite model for element type 360 | if itemType.Kind() == reflect.Ptr { 361 | itemType = itemType.Elem() 362 | } 363 | if !isPrimitive { 364 | b.addModel(itemType, elemTypeName) 365 | } 366 | return jsonName, prop 367 | } 368 | 369 | func (b definitionBuilder) buildMapTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) { 370 | nameJson, prop = b.buildMapType(field.Type, jsonName, modelName) 371 | setPropertyMetadata(&prop, field) 372 | return nameJson, prop 373 | } 374 | 375 | func (b definitionBuilder) buildMapType(mapType reflect.Type, jsonName, modelName string) (nameJson string, prop spec.Schema) { 376 | var pType = "object" 377 | prop.Type = []string{pType} 378 | 379 | // As long as the element isn't an interface, we should be able to figure out what the 380 | // intended type is and represent it in `AdditionalProperties`. 381 | // See: https://swagger.io/docs/specification/data-models/dictionaries/ 382 | if mapType.Elem().Kind().String() != "interface" { 383 | isSlice := b.isSliceOrArrayType(mapType.Elem().Kind()) 384 | if isSlice && !b.isByteArrayType(mapType.Elem()) { 385 | mapType = mapType.Elem() 386 | } 387 | isPrimitive := b.isPrimitiveType(mapType.Elem().Name(), mapType.Elem().Kind()) 388 | elemTypeName := b.getElementTypeName(modelName, jsonName, mapType.Elem()) 389 | prop.AdditionalProperties = &spec.SchemaOrBool{ 390 | Schema: &spec.Schema{}, 391 | } 392 | // golang encoding/json packages says array and slice values encode as 393 | // JSON arrays, except that []byte encodes as a base64-encoded string. 394 | // If we see a []byte here, treat it at as a string 395 | if b.isByteArrayType(mapType.Elem()) { 396 | prop.AdditionalProperties.Schema.Type = []string{"string"} 397 | } else { 398 | if isSlice { 399 | var item *spec.Schema 400 | if isPrimitive { 401 | mapped := b.jsonSchemaType(elemTypeName, mapType.Kind()) 402 | item = &spec.Schema{} 403 | item.Type = []string{mapped} 404 | item.Format = b.jsonSchemaFormat(elemTypeName, mapType.Kind()) 405 | } else { 406 | item = spec.RefProperty("#/definitions/" + elemTypeName) 407 | } 408 | prop.AdditionalProperties.Schema = spec.ArrayProperty(item) 409 | } else if isPrimitive { 410 | mapped := b.jsonSchemaType(elemTypeName, mapType.Elem().Kind()) 411 | prop.AdditionalProperties.Schema.Type = []string{mapped} 412 | } else { 413 | prop.AdditionalProperties.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemTypeName) 414 | } 415 | // add|overwrite model for element type 416 | if mapType.Elem().Kind() == reflect.Ptr { 417 | mapType = mapType.Elem() 418 | } 419 | if !isPrimitive { 420 | b.addModel(mapType.Elem(), elemTypeName) 421 | } 422 | } 423 | } 424 | return jsonName, prop 425 | } 426 | func (b definitionBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop spec.Schema) { 427 | setPropertyMetadata(&prop, field) 428 | fieldType := field.Type 429 | 430 | // override type of pointer to list-likes 431 | if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array { 432 | var pType = "array" 433 | prop.Type = []string{pType} 434 | isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name(), fieldType.Elem().Elem().Kind()) 435 | elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem()) 436 | prop.Items = &spec.SchemaOrArray{ 437 | Schema: &spec.Schema{}, 438 | } 439 | if isPrimitive { 440 | primName := b.jsonSchemaType(elemName, fieldType.Elem().Elem().Kind()) 441 | prop.Items.Schema.Type = []string{primName} 442 | } else { 443 | prop.Items.Schema.Ref = spec.MustCreateRef("#/definitions/" + elemName) 444 | } 445 | if !isPrimitive { 446 | // add|overwrite model for element type 447 | b.addModel(fieldType.Elem().Elem(), elemName) 448 | } 449 | } else { 450 | // non-array, pointer type 451 | fieldTypeName := keyFrom(fieldType.Elem(), b.Config) 452 | isPrimitive := b.isPrimitiveType(fieldTypeName, fieldType.Elem().Kind()) 453 | var pType = b.jsonSchemaType(fieldTypeName, fieldType.Elem().Kind()) // no star, include pkg path 454 | if isPrimitive { 455 | prop.Type = []string{pType} 456 | prop.Format = b.jsonSchemaFormat(fieldTypeName, fieldType.Elem().Kind()) 457 | return jsonName, prop 458 | } 459 | prop.Ref = spec.MustCreateRef("#/definitions/" + pType) 460 | elemName := "" 461 | if fieldType.Elem().Name() == "" { 462 | elemName = modelName + "." + jsonName 463 | prop.Ref = spec.MustCreateRef("#/definitions/" + elemName) 464 | } 465 | if !isPrimitive { 466 | b.addModel(fieldType.Elem(), elemName) 467 | } 468 | } 469 | return jsonName, prop 470 | } 471 | 472 | func (b definitionBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string { 473 | if t.Kind() == reflect.Ptr { 474 | t = t.Elem() 475 | } 476 | if t.Name() == "" { 477 | return modelName + "." + jsonName 478 | } 479 | return keyFrom(t, b.Config) 480 | } 481 | 482 | func keyFrom(st reflect.Type, cfg Config) string { 483 | key := st.String() 484 | if cfg.ModelTypeNameHandler != nil { 485 | if name, ok := cfg.ModelTypeNameHandler(st); ok { 486 | key = name 487 | } 488 | } 489 | if len(st.Name()) == 0 { // unnamed type 490 | // If it is an array, remove the leading [] 491 | key = strings.TrimPrefix(key, "[]") 492 | // Swagger UI has special meaning for [ 493 | key = strings.Replace(key, "[]", "||", -1) 494 | } 495 | return key 496 | } 497 | 498 | func (b definitionBuilder) isSliceOrArrayType(t reflect.Kind) bool { 499 | return t == reflect.Slice || t == reflect.Array 500 | } 501 | 502 | // Does the type represent a []byte? 503 | func (b definitionBuilder) isByteArrayType(t reflect.Type) bool { 504 | return (t.Kind() == reflect.Slice || t.Kind() == reflect.Array) && 505 | t.Elem().Kind() == reflect.Uint8 506 | } 507 | 508 | // see also https://golang.org/ref/spec#Numeric_types 509 | func (b definitionBuilder) isPrimitiveType(modelName string, modelKind reflect.Kind) bool { 510 | switch modelKind { 511 | case reflect.Bool: 512 | return true 513 | case reflect.Float32, reflect.Float64, 514 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 515 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 516 | return true 517 | case reflect.String: 518 | return true 519 | } 520 | 521 | if len(modelName) == 0 { 522 | return false 523 | } 524 | 525 | return strings.Contains("time.Time time.Duration json.Number", modelName) 526 | } 527 | 528 | // jsonNameOfField returns the name of the field as it should appear in JSON format 529 | // An empty string indicates that this field is not part of the JSON representation 530 | func (b definitionBuilder) jsonNameOfField(field reflect.StructField) string { 531 | if jsonTag := field.Tag.Get("json"); jsonTag != "" { 532 | s := strings.Split(jsonTag, ",") 533 | if s[0] == "-" { 534 | // empty name signals skip property 535 | return "" 536 | } else if s[0] != "" { 537 | return s[0] 538 | } 539 | } 540 | 541 | if b.Config.DefinitionNameHandler == nil { 542 | b.Config.DefinitionNameHandler = DefaultNameHandler 543 | } 544 | return b.Config.DefinitionNameHandler(field.Name) 545 | } 546 | 547 | // see also http://json-schema.org/latest/json-schema-core.html#anchor8 548 | func (b definitionBuilder) jsonSchemaType(modelName string, modelKind reflect.Kind) string { 549 | schemaMap := map[string]string{ 550 | "time.Time": "string", 551 | "time.Duration": "integer", 552 | "json.Number": "number", 553 | } 554 | 555 | if mapped, ok := schemaMap[modelName]; ok { 556 | return mapped 557 | } 558 | 559 | // check if original type is primitive 560 | switch modelKind { 561 | case reflect.Bool: 562 | return "boolean" 563 | case reflect.Float32, reflect.Float64: 564 | return "number" 565 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 566 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 567 | return "integer" 568 | case reflect.String: 569 | return "string" 570 | } 571 | 572 | return modelName // use as is (custom or struct) 573 | } 574 | 575 | func (b definitionBuilder) jsonSchemaFormat(modelName string, modelKind reflect.Kind) string { 576 | if b.Config.SchemaFormatHandler != nil { 577 | if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" { 578 | return mapped 579 | } 580 | } 581 | 582 | schemaMap := map[string]string{ 583 | "time.Time": "date-time", 584 | "*time.Time": "date-time", 585 | "time.Duration": "int64", 586 | "*time.Duration": "int64", 587 | "json.Number": "double", 588 | "*json.Number": "double", 589 | } 590 | 591 | if mapped, ok := schemaMap[modelName]; ok { 592 | return mapped 593 | } 594 | 595 | // check if original type is primitive 596 | switch modelKind { 597 | case reflect.Float32: 598 | return "float" 599 | case reflect.Float64: 600 | return "double" 601 | case reflect.Int: 602 | return "int32" 603 | case reflect.Int8: 604 | return "byte" 605 | case reflect.Int16: 606 | return "integer" 607 | case reflect.Int32: 608 | return "int32" 609 | case reflect.Int64: 610 | return "int64" 611 | case reflect.Uint: 612 | return "integer" 613 | case reflect.Uint8: 614 | return "byte" 615 | case reflect.Uint16: 616 | return "integer" 617 | case reflect.Uint32: 618 | return "integer" 619 | case reflect.Uint64: 620 | return "integer" 621 | } 622 | 623 | return "" // no format 624 | } 625 | -------------------------------------------------------------------------------- /definition_builder_test.go: -------------------------------------------------------------------------------- 1 | package restfulspec 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-openapi/spec" 11 | ) 12 | 13 | type StringAlias string 14 | type IntAlias int 15 | 16 | type Apple struct { 17 | Species string 18 | Volume int `json:"vol"` 19 | Things *[]string 20 | Weight json.Number 21 | StringAlias StringAlias 22 | IntAlias IntAlias 23 | } 24 | 25 | func TestAppleDef(t *testing.T) { 26 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 27 | db.addModelFrom(Apple{}) 28 | 29 | if got, want := len(db.Definitions), 1; got != want { 30 | t.Errorf("got %v want %v", got, want) 31 | } 32 | 33 | schema := db.Definitions["restfulspec.Apple"] 34 | if got, want := len(schema.Required), 6; got != want { 35 | t.Errorf("got %v want %v", got, want) 36 | } 37 | if got, want := schema.Required[0], "Species"; got != want { 38 | t.Errorf("got %v want %v", got, want) 39 | } 40 | if got, want := schema.Required[1], "vol"; got != want { 41 | t.Errorf("got %v want %v", got, want) 42 | } 43 | if got, want := schema.ID, ""; got != want { 44 | t.Errorf("got %v want %v", got, want) 45 | } 46 | if got, want := schema.Properties["Things"].Items.Schema.Type.Contains("string"), true; got != want { 47 | t.Errorf("got %v want %v", got, want) 48 | } 49 | if got, want := schema.Properties["Things"].Items.Schema.Ref.String(), ""; got != want { 50 | t.Errorf("got %v want %v", got, want) 51 | } 52 | if got, want := schema.Properties["Weight"].Type.Contains("number"), true; got != want { 53 | t.Errorf("got %v want %v", got, want) 54 | } 55 | if got, want := schema.Properties["StringAlias"].Type.Contains("string"), true; got != want { 56 | t.Errorf("got %v want %v", got, want) 57 | } 58 | if got, want := schema.Properties["IntAlias"].Type.Contains("integer"), true; got != want { 59 | t.Errorf("got %v want %v", got, want) 60 | } 61 | } 62 | 63 | type MyDictionaryResponse struct { 64 | Dictionary1 map[string]DictionaryValue `json:"dictionary1"` 65 | Dictionary2 map[string]interface{} `json:"dictionary2"` 66 | Dictionary3 map[string][]byte `json:"dictionary3"` 67 | Dictionary4 map[string]string `json:"dictionary4"` 68 | Dictionary5 map[string][]DictionaryValue `json:"dictionary5"` 69 | Dictionary6 map[string][]string `json:"dictionary6"` 70 | } 71 | type DictionaryValue struct { 72 | Key1 string `json:"key1"` 73 | Key2 string `json:"key2"` 74 | } 75 | 76 | func TestDictionarySupport(t *testing.T) { 77 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 78 | db.addModelFrom(MyDictionaryResponse{}) 79 | 80 | // Make sure that only the types that we want were created. 81 | if got, want := len(db.Definitions), 2; got != want { 82 | t.Errorf("got %v want %v", got, want) 83 | } 84 | 85 | schema, schemaFound := db.Definitions["restfulspec.MyDictionaryResponse"] 86 | if !schemaFound { 87 | t.Errorf("could not find schema") 88 | } else { 89 | if got, want := len(schema.Required), 6; got != want { 90 | t.Errorf("got %v want %v", got, want) 91 | } else { 92 | if got, want := schema.Required[0], "dictionary1"; got != want { 93 | t.Errorf("got %v want %v", got, want) 94 | } 95 | if got, want := schema.Required[1], "dictionary2"; got != want { 96 | t.Errorf("got %v want %v", got, want) 97 | } 98 | if got, want := schema.Required[2], "dictionary3"; got != want { 99 | t.Errorf("got %v want %v", got, want) 100 | } 101 | if got, want := schema.Required[3], "dictionary4"; got != want { 102 | t.Errorf("got %v want %v", got, want) 103 | } 104 | if got, want := schema.Required[4], "dictionary5"; got != want { 105 | t.Errorf("got %v want %v", got, want) 106 | } 107 | if got, want := schema.Required[5], "dictionary6"; got != want { 108 | t.Errorf("got %v want %v", got, want) 109 | } 110 | } 111 | if got, want := len(schema.Properties), 6; got != want { 112 | t.Errorf("got %v want %v", got, want) 113 | } else { 114 | if property, found := schema.Properties["dictionary1"]; !found { 115 | t.Errorf("could not find property") 116 | } else { 117 | if got, want := property.AdditionalProperties.Schema.SchemaProps.Ref.String(), "#/definitions/restfulspec.DictionaryValue"; got != want { 118 | t.Errorf("got %v want %v", got, want) 119 | } 120 | } 121 | if property, found := schema.Properties["dictionary2"]; !found { 122 | t.Errorf("could not find property") 123 | } else { 124 | if property.AdditionalProperties != nil { 125 | t.Errorf("unexpected additional properties") 126 | } 127 | } 128 | if property, found := schema.Properties["dictionary3"]; !found { 129 | t.Errorf("could not find property") 130 | } else { 131 | if got, want := property.AdditionalProperties.Schema.Type[0], "string"; got != want { 132 | t.Errorf("got %v want %v", got, want) 133 | } 134 | } 135 | if property, found := schema.Properties["dictionary4"]; !found { 136 | t.Errorf("could not find property") 137 | } else { 138 | if got, want := property.AdditionalProperties.Schema.Type[0], "string"; got != want { 139 | t.Errorf("got %v want %v", got, want) 140 | } 141 | } 142 | if property, found := schema.Properties["dictionary5"]; !found { 143 | t.Errorf("could not find property") 144 | } else { 145 | if got, want := len(property.AdditionalProperties.Schema.Type), 1; got != want { 146 | t.Errorf("got %v want %v", got, want) 147 | } else { 148 | if got, want := property.AdditionalProperties.Schema.Type[0], "array"; got != want { 149 | t.Errorf("got %v want %v", got, want) 150 | } 151 | if property.AdditionalProperties.Schema.Items == nil { 152 | t.Errorf("Items not set") 153 | } else { 154 | if got, want := property.AdditionalProperties.Schema.Items.Schema.Ref.String(), "#/definitions/restfulspec.DictionaryValue"; got != want { 155 | t.Errorf("got %v want %v", got, want) 156 | } 157 | } 158 | } 159 | } 160 | if property, found := schema.Properties["dictionary6"]; !found { 161 | t.Errorf("could not find property") 162 | } else { 163 | if got, want := len(property.AdditionalProperties.Schema.Type), 1; got != want { 164 | t.Errorf("got %v want %v", got, want) 165 | } else { 166 | if got, want := property.AdditionalProperties.Schema.Type[0], "array"; got != want { 167 | t.Errorf("got %v want %v", got, want) 168 | } 169 | if property.AdditionalProperties.Schema.Items == nil { 170 | t.Errorf("Items not set") 171 | } else { 172 | if got, want := property.AdditionalProperties.Schema.Items.Schema.Type[0], "string"; got != want { 173 | t.Errorf("got %v want %v", got, want) 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | schema, schemaFound = db.Definitions["restfulspec.DictionaryValue"] 182 | if !schemaFound { 183 | t.Errorf("could not find schema") 184 | } else { 185 | if got, want := len(schema.Required), 2; got != want { 186 | t.Errorf("got %v want %v", got, want) 187 | } else { 188 | if got, want := schema.Required[0], "key1"; got != want { 189 | t.Errorf("got %v want %v", got, want) 190 | } 191 | if got, want := schema.Required[1], "key2"; got != want { 192 | t.Errorf("got %v want %v", got, want) 193 | } 194 | } 195 | } 196 | } 197 | 198 | type MyRecursiveDictionaryResponse struct { 199 | Dictionary1 map[string]RecursiveDictionaryValue `json:"dictionary1"` 200 | } 201 | type RecursiveDictionaryValue struct { 202 | Key1 string `json:"key1"` 203 | Key2 map[string]RecursiveDictionaryValue `json:"key2"` 204 | } 205 | 206 | func TestRecursiveDictionarySupport(t *testing.T) { 207 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 208 | db.addModelFrom(MyRecursiveDictionaryResponse{}) 209 | 210 | // Make sure that only the types that we want were created. 211 | if got, want := len(db.Definitions), 2; got != want { 212 | t.Errorf("got %v want %v", got, want) 213 | } 214 | 215 | schema, schemaFound := db.Definitions["restfulspec.MyRecursiveDictionaryResponse"] 216 | if !schemaFound { 217 | t.Errorf("could not find schema") 218 | } else { 219 | if got, want := len(schema.Required), 1; got != want { 220 | t.Errorf("got %v want %v", got, want) 221 | } else { 222 | if got, want := schema.Required[0], "dictionary1"; got != want { 223 | t.Errorf("got %v want %v", got, want) 224 | } 225 | } 226 | if got, want := len(schema.Properties), 1; got != want { 227 | t.Errorf("got %v want %v", got, want) 228 | } else { 229 | if property, found := schema.Properties["dictionary1"]; !found { 230 | t.Errorf("could not find property") 231 | } else { 232 | if got, want := property.AdditionalProperties.Schema.SchemaProps.Ref.String(), "#/definitions/restfulspec.RecursiveDictionaryValue"; got != want { 233 | t.Errorf("got %v want %v", got, want) 234 | } 235 | } 236 | } 237 | } 238 | 239 | schema, schemaFound = db.Definitions["restfulspec.RecursiveDictionaryValue"] 240 | if !schemaFound { 241 | t.Errorf("could not find schema") 242 | } else { 243 | if got, want := len(schema.Required), 2; got != want { 244 | t.Errorf("got %v want %v", got, want) 245 | } else { 246 | if got, want := schema.Required[0], "key1"; got != want { 247 | t.Errorf("got %v want %v", got, want) 248 | } 249 | if got, want := schema.Required[1], "key2"; got != want { 250 | t.Errorf("got %v want %v", got, want) 251 | } 252 | } 253 | if got, want := len(schema.Properties), 2; got != want { 254 | t.Errorf("got %v want %v", got, want) 255 | } else { 256 | if property, found := schema.Properties["key1"]; !found { 257 | t.Errorf("could not find property") 258 | } else { 259 | if property.AdditionalProperties != nil { 260 | t.Errorf("unexpected additional properties") 261 | } 262 | } 263 | if property, found := schema.Properties["key2"]; !found { 264 | t.Errorf("could not find property") 265 | } else { 266 | if got, want := property.AdditionalProperties.Schema.SchemaProps.Ref.String(), "#/definitions/restfulspec.RecursiveDictionaryValue"; got != want { 267 | t.Errorf("got %v want %v", got, want) 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | func TestReturningStringToStringDictionary(t *testing.T) { 275 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 276 | db.addModelFrom(map[string]string{}) 277 | 278 | if got, want := len(db.Definitions), 1; got != want { 279 | t.Errorf("got %v want %v", got, want) 280 | } 281 | 282 | schema, schemaFound := db.Definitions["map[string]string"] 283 | if !schemaFound { 284 | t.Errorf("could not find schema") 285 | } else { 286 | if schema.AdditionalProperties == nil { 287 | t.Errorf("AdditionalProperties not set") 288 | } else { 289 | if got, want := schema.AdditionalProperties.Schema.Type[0], "string"; got != want { 290 | t.Errorf("got %v want %v", got, want) 291 | } 292 | } 293 | } 294 | } 295 | 296 | func TestReturningStringToSliceObjectDictionary(t *testing.T) { 297 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 298 | db.addModelFrom(map[string][]DictionaryValue{}) 299 | 300 | if got, want := len(db.Definitions), 2; got != want { 301 | t.Errorf("got %v want %v", got, want) 302 | } 303 | schema, schemaFound := db.Definitions["map[string]||restfulspec.DictionaryValue"] 304 | if !schemaFound { 305 | t.Errorf("could not find schema") 306 | } else { 307 | if schema.AdditionalProperties == nil { 308 | t.Errorf("AdditionalProperties not set") 309 | } else { 310 | if got, want := len(schema.AdditionalProperties.Schema.Type), 1; got != want { 311 | t.Errorf("got %v want %v", got, want) 312 | } else { 313 | if got, want := schema.AdditionalProperties.Schema.Type[0], "array"; got != want { 314 | t.Errorf("got %v want %v", got, want) 315 | } 316 | if schema.AdditionalProperties.Schema.Items == nil { 317 | t.Errorf("Items not set") 318 | } else { 319 | if got, want := schema.AdditionalProperties.Schema.Items.Schema.Ref.String(), "#/definitions/restfulspec.DictionaryValue"; got != want { 320 | t.Errorf("got %v want %v", got, want) 321 | } 322 | } 323 | } 324 | } 325 | } 326 | 327 | schema, schemaFound = db.Definitions["restfulspec.DictionaryValue"] 328 | if !schemaFound { 329 | t.Errorf("could not find schema") 330 | } else { 331 | if got, want := len(schema.Required), 2; got != want { 332 | t.Errorf("got %v want %v", got, want) 333 | } else { 334 | if got, want := schema.Required[0], "key1"; got != want { 335 | t.Errorf("got %v want %v", got, want) 336 | } 337 | if got, want := schema.Required[1], "key2"; got != want { 338 | t.Errorf("got %v want %v", got, want) 339 | } 340 | } 341 | } 342 | } 343 | 344 | func TestAddSliceOfPrimitiveCreatesNoType(t *testing.T) { 345 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 346 | db.addModelFrom([]string{}) 347 | 348 | if got, want := len(db.Definitions), 0; got != want { 349 | t.Errorf("got %v want %v", got, want) 350 | } 351 | } 352 | 353 | type StructForSlice struct { 354 | Value string 355 | } 356 | 357 | func TestAddSliceOfStructCreatesTypeForStruct(t *testing.T) { 358 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 359 | db.addModelFrom([]StructForSlice{}) 360 | 361 | if got, want := len(db.Definitions), 1; got != want { 362 | t.Errorf("got %v want %v", got, want) 363 | } 364 | schema, schemaFound := db.Definitions["restfulspec.StructForSlice"] 365 | if !schemaFound { 366 | t.Errorf("could not find schema") 367 | } else { 368 | if got, want := len(schema.Required), 1; got != want { 369 | t.Errorf("got %v want %v", got, want) 370 | } else { 371 | if got, want := schema.Required[0], "Value"; got != want { 372 | t.Errorf("got %v want %v", got, want) 373 | } 374 | } 375 | if got, want := len(schema.Properties), 1; got != want { 376 | t.Errorf("got %v want %v", got, want) 377 | } else { 378 | if property, found := schema.Properties["Value"]; !found { 379 | t.Errorf("could not find property") 380 | } else { 381 | if property.AdditionalProperties != nil { 382 | t.Errorf("unexpected additional properties") 383 | } 384 | } 385 | } 386 | } 387 | } 388 | 389 | type ( 390 | X struct { 391 | yy []Y 392 | } 393 | Y struct { 394 | X 395 | } 396 | ) 397 | 398 | func TestPotentialStackOverflow(t *testing.T) { 399 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 400 | db.addModelFrom(X{}) 401 | 402 | if got, want := len(db.Definitions), 2; got != want { 403 | t.Errorf("got %v want %v", got, want) 404 | } 405 | 406 | schema := db.Definitions["restfulspec.X"] 407 | if got, want := len(schema.Required), 1; got != want { 408 | t.Errorf("got %v want %v", got, want) 409 | } 410 | if got, want := schema.Required[0], "yy"; got != want { 411 | t.Errorf("got %v want %v", got, want) 412 | } 413 | if got, want := schema.ID, ""; got != want { 414 | t.Errorf("got %v want %v", got, want) 415 | } 416 | } 417 | 418 | type Foo struct { 419 | b *Bar 420 | Embed 421 | } 422 | 423 | // nolint:unused 424 | type Bar struct { 425 | Foo `json:"foo"` 426 | B struct { 427 | Foo 428 | f Foo 429 | b []*Bar 430 | } 431 | } 432 | 433 | type Embed struct { 434 | C string `json:"c"` 435 | } 436 | 437 | func TestRecursiveFieldStructure(t *testing.T) { 438 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 439 | // should not panic 440 | db.addModelFrom(Foo{}) 441 | } 442 | 443 | type email struct { 444 | Attachments [][]byte `json:"attachments,omitempty" optional:"true"` 445 | } 446 | 447 | // Definition Builder fails with [][]byte #77 448 | func TestDoubleByteArray(t *testing.T) { 449 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 450 | db.addModelFrom(email{}) 451 | scParent, ok := db.Definitions["restfulspec.email"] 452 | if !ok { 453 | t.Fail() 454 | } 455 | sc, ok := scParent.Properties["attachments"] 456 | if !ok { 457 | t.Fail() 458 | } 459 | if got, want := sc.Type[0], "array"; got != want { 460 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 461 | } 462 | } 463 | 464 | type matrix struct { 465 | Cells [][]string 466 | } 467 | 468 | func TestDoubleStringArray(t *testing.T) { 469 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 470 | db.addModelFrom(matrix{}) 471 | scParent, ok := db.Definitions["restfulspec.matrix"] 472 | if !ok { 473 | t.Log(db.Definitions) 474 | t.Fail() 475 | } 476 | sc, ok := scParent.Properties["Cells"] 477 | if !ok { 478 | t.Log(db.Definitions) 479 | t.Fail() 480 | } 481 | if got, want := sc.Type[0], "array"; got != want { 482 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 483 | } 484 | tp := sc.Items.Schema.Type 485 | if got, want := fmt.Sprint(tp), "[array]"; got != want { 486 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 487 | } 488 | } 489 | 490 | type Cell struct { 491 | Value string 492 | } 493 | 494 | type DoubleStructArray struct { 495 | Cells [][]Cell 496 | } 497 | 498 | func TestDoubleStructArray(t *testing.T) { 499 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 500 | db.addModelFrom(DoubleStructArray{}) 501 | 502 | schema, ok := db.Definitions["restfulspec.DoubleStructArray"] 503 | if !ok { 504 | t.Logf("definitions: %v", db.Definitions) 505 | t.Fail() 506 | } 507 | //t.Logf("%+v", schema) 508 | if want, got := schema.Properties["Cells"].Type[0], "array"; got != want { 509 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 510 | } 511 | if want, got := schema.Properties["Cells"].Items.Schema.Type[0], "array"; got != want { 512 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 513 | } 514 | if want, got := schema.Properties["Cells"].Items.Schema.Items.Schema.Ref.String(), "#/definitions/restfulspec.Cell"; got != want { 515 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 516 | } 517 | } 518 | 519 | type childPostBuildSwaggerSchema struct { 520 | Value string 521 | } 522 | 523 | func (m childPostBuildSwaggerSchema) PostBuildSwaggerSchemaHandler(sm *spec.Schema) { 524 | sm.Description = "child's description" 525 | } 526 | 527 | type parentPostBuildSwaggerSchema struct { 528 | Node childPostBuildSwaggerSchema 529 | } 530 | 531 | func (m parentPostBuildSwaggerSchema) PostBuildSwaggerSchemaHandler(sm *spec.Schema) { 532 | sm.Description = "parent's description" 533 | } 534 | 535 | func TestPostBuildSwaggerSchema(t *testing.T) { 536 | var obj interface{} = parentPostBuildSwaggerSchema{} 537 | if _, ok := obj.(PostBuildSwaggerSchema); !ok { 538 | t.Fatalf("object does not implement PostBuildSwaggerSchema interface") 539 | } 540 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 541 | db.addModelFrom(obj) 542 | sc, ok := db.Definitions["restfulspec.parentPostBuildSwaggerSchema"] 543 | if !ok { 544 | t.Logf("definitions: %#v", db.Definitions) 545 | t.Fail() 546 | } 547 | // t.Logf("sc: %#v", sc) 548 | if got, want := sc.Description, "parent's description"; got != want { 549 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 550 | } 551 | 552 | sc, ok = db.Definitions["restfulspec.childPostBuildSwaggerSchema"] 553 | if !ok { 554 | t.Logf("definitions: %#v", db.Definitions) 555 | t.Fail() 556 | } 557 | //t.Logf("sc: %#v", sc) 558 | if got, want := sc.Description, "child's description"; got != want { 559 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 560 | } 561 | } 562 | 563 | func TestEmbedStruct(t *testing.T) { 564 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 565 | db.addModelFrom(Foo{}) 566 | if got, exists := db.Definitions["restfulspec.Embed"]; exists { 567 | t.Errorf("got %v want nil", got) 568 | } 569 | } 570 | 571 | type timeHolder struct { 572 | when time.Time 573 | } 574 | 575 | func TestTimeField(t *testing.T) { 576 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 577 | db.addModelFrom(timeHolder{}) 578 | sc := db.Definitions["restfulspec.timeHolder"] 579 | pr := sc.Properties["when"] 580 | if got, want := pr.Format, "date-time"; got != want { 581 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 582 | } 583 | } 584 | 585 | type arrayofEnumsHolder struct { 586 | Titles []string `json:"titles" enum:"EMPLOYEE|MANAGER"` 587 | } 588 | 589 | func TestArrayOfEnumsField(t *testing.T) { 590 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 591 | db.addModelFrom(arrayofEnumsHolder{}) 592 | sc := db.Definitions["restfulspec.arrayofEnumsHolder"] 593 | pr := sc.Properties["titles"] 594 | 595 | if got, want := pr.Items.Schema.Enum, []interface{}{"EMPLOYEE", "MANAGER"}; !reflect.DeepEqual(got, want) { 596 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 597 | } 598 | } 599 | 600 | type URLField string 601 | type EmailField string 602 | 603 | type customSchemaFormatHolder struct { 604 | URL URLField `json:"url"` 605 | Email EmailField `json:"email"` 606 | } 607 | 608 | func TestCustomSchemaFormatHandler(t *testing.T) { 609 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{ 610 | SchemaFormatHandler: func(modelName string) string { 611 | switch modelName { 612 | case "restfulspec.URLField": 613 | return "uri" 614 | case "restfulspec.EmailField": 615 | return "email" 616 | } 617 | 618 | return "" 619 | }, 620 | }} 621 | db.addModelFrom(customSchemaFormatHolder{}) 622 | sc := db.Definitions["restfulspec.customSchemaFormatHolder"] 623 | 624 | testCases := []struct { 625 | Field string 626 | Expected string 627 | }{ 628 | {Field: "url", Expected: "uri"}, 629 | {Field: "email", Expected: "email"}, 630 | } 631 | 632 | for _, testCase := range testCases { 633 | pr := sc.Properties[testCase.Field] 634 | 635 | if got, want := pr.Format, testCase.Expected; got != want { 636 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 637 | } 638 | } 639 | } 640 | 641 | type exampleTagHolder struct { 642 | URL string `json:"url" example:"https://example.com"` 643 | URLs []string `json:"urls" example:"[\"https://example.com\"]"` 644 | Email string `json:"email" example:"test@example.com"` 645 | Status string `json:"status"` 646 | Description simpleChildExampleTagHolder `json:"description" example:"Test description"` 647 | Companies []childExampleTagHolder `json:"companies"` 648 | } 649 | 650 | type childExampleTagHolder struct { 651 | Name string `json:"name" example:"Company A"` 652 | } 653 | 654 | type simpleChildExampleTagHolder string 655 | 656 | func TestCustomTagHandler(t *testing.T) { 657 | db := definitionBuilder{Definitions: spec.Definitions{}, Config: Config{}} 658 | db.addModelFrom(exampleTagHolder{}) 659 | sc := db.Definitions["restfulspec.exampleTagHolder"] 660 | 661 | pr := sc.Properties["url"] 662 | 663 | testCases := []struct { 664 | Field string 665 | Expected string 666 | }{ 667 | {Field: "url", Expected: "https://example.com"}, 668 | {Field: "urls", Expected: "[\"https://example.com\"]"}, 669 | {Field: "email", Expected: "test@example.com"}, 670 | {Field: "description", Expected: "Test description"}, 671 | } 672 | 673 | for _, testCase := range testCases { 674 | pr = sc.Properties[testCase.Field] 675 | 676 | if got, want := pr.Example, testCase.Expected; got != want { 677 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 678 | } 679 | } 680 | 681 | // Test fields without examples and `companies` child structs. 682 | nilTestCases := []string{ 683 | "status", 684 | "companies", 685 | } 686 | 687 | for _, testCase := range nilTestCases { 688 | pr = sc.Properties[testCase] 689 | 690 | if got := pr.Example; got != nil { 691 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, nil, nil) 692 | } 693 | } 694 | 695 | sc = db.Definitions["restfulspec.childExampleTagHolder"] 696 | pr = sc.Properties["name"] 697 | 698 | if got, want := pr.Example, "Company A"; got != want { 699 | t.Errorf("got [%v:%T] want [%v:%T]", got, got, want, want) 700 | } 701 | } 702 | --------------------------------------------------------------------------------