Merge pull request #2 from davidebianchi/handle-request-response

This commit is contained in:
Davide Bianchi
2020-10-18 00:32:55 +02:00
committed by GitHub
17 changed files with 1768 additions and 210 deletions

View File

@@ -4,3 +4,17 @@ Initial test&try to generate a swagger dynamically.
It uses [gorilla-mux](https://github.com/gorilla/mux) and [kin-openapi](https://github.com/getkin/kin-openapi)
to automatically generate and serve a swagger file.
To convert struct to schemas, we use [this library](https://github.com/alecthomas/jsonschema).
The struct should contains the appropriate struct tags to be inserted in json schema.
### FAQ
1. How to add format `binary`?
Formats `date-time`, `email`, `hostname`, `ipv4`, `ipv6`, `uri` could be added with tag `jsonschema`. Others format could be added with tag `jsonschema_extra`. Not all the formats are supported (see discovered unsupported formats [here](#discovered-unsupported-schema-features)).
#### Discovered unsupported schema features
*Formats*:
* `uuid` is unsupported by kin-openapi

4
go.mod
View File

@@ -3,7 +3,9 @@ module github.com/davidebianchi/gorilla-swagger
go 1.15
require (
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
github.com/getkin/kin-openapi v0.22.1
github.com/gorilla/mux v1.8.0
github.com/stretchr/testify v1.5.1
github.com/iancoleman/orderedmap v0.1.0 // indirect
github.com/stretchr/testify v1.6.1
)

45
go.sum
View File

@@ -1,49 +1,30 @@
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 h1:T3+cD5fYvuH36h7EZq+TDpm+d8a6FSD4pQsbmuGGQ8o=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.22.1 h1:ODA1olTp175o//NfHko/uCAAhwUSfm5P4+K52XvTg4w=
github.com/getkin/kin-openapi v0.22.1/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc=
github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/iancoleman/orderedmap v0.1.0 h1:2orAxZBJsvimgEBmMWfXaFlzSG2fbQil5qzP3F6cCkg=
github.com/iancoleman/orderedmap v0.1.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

88
integration_test.go Normal file
View File

@@ -0,0 +1,88 @@
package swagger
import (
"context"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/gorilla/mux"
"github.com/stretchr/testify/require"
)
const (
swaggerOpenapiTitle = "test swagger title"
swaggerOpenapiVersion = "test swagger version"
)
func TestIntegration(t *testing.T) {
t.Run("router works correctly", func(t *testing.T) {
muxRouter := setupSwagger(t)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/hello", nil)
muxRouter.ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
body := readBody(t, w.Result().Body)
require.Equal(t, "OK", body)
t.Run("and generate swagger", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil)
muxRouter.ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
body := readBody(t, w.Result().Body)
require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body)
})
})
}
func readBody(t *testing.T, requestBody io.ReadCloser) string {
t.Helper()
body, err := ioutil.ReadAll(requestBody)
require.NoError(t, err)
return string(body)
}
func setupSwagger(t *testing.T) *mux.Router {
t.Helper()
context := context.Background()
muxRouter := mux.NewRouter()
router, err := New(muxRouter, Options{
Context: context,
Openapi: &openapi3.Swagger{
Info: &openapi3.Info{
Title: swaggerOpenapiTitle,
Version: swaggerOpenapiVersion,
},
},
})
require.NoError(t, err)
handler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`OK`))
}
operation := Operation{}
_, err = router.AddRawRoute(http.MethodGet, "/hello", handler, operation)
require.NoError(t, err)
err = router.GenerateAndExposeSwagger()
require.NoError(t, err)
return muxRouter
}

147
main.go
View File

@@ -7,7 +7,6 @@ import (
"net/http"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gorilla/mux"
)
@@ -21,93 +20,24 @@ var (
const (
// JSONDocumentationPath is the path of the swagger documentation in json format.
JSONDocumentationPath = "/documentation/json"
defaultOpenapiVersion = "3.0.0"
)
// Router handle the gorilla mux router and the swagger schema
type Router struct {
router *mux.Router
SwaggerSchema *openapi3.Swagger
enableRequestValidation bool
context context.Context
swaggerRouter *openapi3filter.Router
router *mux.Router
swaggerSchema *openapi3.Swagger
context context.Context
}
// Handler is the http type handler
type Handler func(w http.ResponseWriter, req *http.Request)
// GenerateAndExposeSwagger creates a /documentation/json route on router and
// expose the generated swagger
func (r Router) GenerateAndExposeSwagger() error {
if err := r.SwaggerSchema.Validate(r.context); err != nil {
return fmt.Errorf("%w: %s", ErrValidatingSwagger, err)
}
jsonSwagger, err := r.SwaggerSchema.MarshalJSON()
if err != nil {
return fmt.Errorf("%w: %s", ErrGenerateSwagger, err)
}
r.router.HandleFunc(JSONDocumentationPath, func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("content-type", "application/json")
w.Write(jsonSwagger)
})
// TODO: add yaml endpoint
err = r.swaggerRouter.AddSwagger(r.SwaggerSchema)
if err != nil {
return err
}
return nil
}
// AddRoute add route to router with specific method, path and handler. Add the
// router also to the swagger schema, after validating it
func (r Router) AddRoute(method string, path string, handler Handler, operation Operation) (*mux.Route, error) {
if operation.Operation != nil {
err := operation.Validate(r.context)
if err != nil {
return nil, err
}
} else {
operation.Operation = openapi3.NewOperation()
operation.Responses = openapi3.NewResponses()
}
r.SwaggerSchema.AddOperation(path, method, operation.Operation)
if operation.Operation != nil && r.enableRequestValidation {
return r.router.HandleFunc(path, func(h http.ResponseWriter, req *http.Request) {
err := validateRequest(r, req)
if err != nil {
// TODO: add response for validation response
return
}
handler(h, req)
}).Methods(method), nil
}
return r.router.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
// Handle, when content-type is json, the request/response marshalling? Maybe with a specific option.
handler(w, req)
}).Methods(method), nil
}
func (r Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.router.ServeHTTP(w, req)
}
// RouterOptions to be passed to create the new router and swagger
type RouterOptions struct {
Context context.Context
EnableRequestValidation bool
Openapi *openapi3.Swagger
// Options to be passed to create the new router and swagger
type Options struct {
Context context.Context
Openapi *openapi3.Swagger
}
// New generate new router with swagger. Default to OpenAPI 3.0.0
func New(router *mux.Router, options RouterOptions) (*Router, error) {
func New(router *mux.Router, options Options) (*Router, error) {
swagger, err := generateNewValidSwagger(options.Openapi)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrValidatingSwagger, err)
@@ -119,53 +49,25 @@ func New(router *mux.Router, options RouterOptions) (*Router, error) {
}
return &Router{
router: router,
enableRequestValidation: options.EnableRequestValidation,
SwaggerSchema: swagger,
context: ctx,
swaggerRouter: openapi3filter.NewRouter(),
router: router,
swaggerSchema: swagger,
context: ctx,
}, nil
}
// Operation type
type Operation struct {
*openapi3.Operation
// TODO: handle request and response
}
func validateRequest(r Router, req *http.Request) error {
// Find route
route, pathParams, err := r.swaggerRouter.FindRoute(req.Method, req.URL)
if err != nil {
return err
}
// Validate request
requestValidationInput := &openapi3filter.RequestValidationInput{
Request: req,
PathParams: pathParams,
Route: route,
// TODO: add query params
}
return openapi3filter.ValidateRequest(req.Context(), requestValidationInput)
}
func generateNewValidSwagger(swagger *openapi3.Swagger) (*openapi3.Swagger, error) {
if swagger == nil {
swagger = &openapi3.Swagger{
OpenAPI: defaultOpenapiVersion,
}
return nil, fmt.Errorf("swagger is required")
}
if swagger.OpenAPI == "" {
swagger.OpenAPI = defaultOpenapiVersion
}
if swagger.Paths == nil {
swagger.Paths = openapi3.Paths{}
}
if swagger.Info == nil {
return nil, fmt.Errorf("swagger info must not be empty")
return nil, fmt.Errorf("swagger info is required")
}
if swagger.Info.Title == "" {
return nil, fmt.Errorf("swagger info title is required")
@@ -176,3 +78,24 @@ func generateNewValidSwagger(swagger *openapi3.Swagger) (*openapi3.Swagger, erro
return swagger, nil
}
// GenerateAndExposeSwagger creates a /documentation/json route on router and
// expose the generated swagger
func (r Router) GenerateAndExposeSwagger() error {
if err := r.swaggerSchema.Validate(r.context); err != nil {
return fmt.Errorf("%w: %s", ErrValidatingSwagger, err)
}
jsonSwagger, err := r.swaggerSchema.MarshalJSON()
if err != nil {
return fmt.Errorf("%w: %s", ErrGenerateSwagger, err)
}
r.router.HandleFunc(JSONDocumentationPath, func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonSwagger)
})
// TODO: add yaml endpoint
return nil
}

View File

@@ -2,10 +2,11 @@ package swagger
import (
"context"
"io"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/getkin/kin-openapi/openapi3"
@@ -13,76 +14,167 @@ import (
"github.com/stretchr/testify/require"
)
const (
swaggerOpenapiTitle = "test swagger title"
swaggerOpenapiVersion = "test swagger version"
)
func TestNewRouter(t *testing.T) {
mRouter := mux.NewRouter()
func TestIntegration(t *testing.T) {
t.Run("router works correctly", func(t *testing.T) {
router := setupSwagger(t)
info := &openapi3.Info{
Title: "my title",
Version: "my version",
}
openapi := &openapi3.Swagger{
Info: info,
Paths: openapi3.Paths{},
}
t.Run("not ok - invalid Openapi option", func(t *testing.T) {
r, err := New(mRouter, Options{})
require.Nil(t, r)
require.EqualError(t, err, fmt.Sprintf("%s: swagger is required", ErrValidatingSwagger))
})
t.Run("ok - with default context", func(t *testing.T) {
r, err := New(mRouter, Options{
Openapi: openapi,
})
require.NoError(t, err)
require.Equal(t, &Router{
context: context.Background(),
router: mRouter,
swaggerSchema: openapi,
}, r)
})
t.Run("ok - with custom context", func(t *testing.T) {
type key struct{}
ctx := context.WithValue(context.Background(), key{}, "value")
r, err := New(mRouter, Options{
Openapi: openapi,
Context: ctx,
})
require.NoError(t, err)
require.Equal(t, &Router{
context: ctx,
router: mRouter,
swaggerSchema: openapi,
}, r)
})
}
func TestGenerateValidSwagger(t *testing.T) {
t.Run("not ok - empty swagger info", func(t *testing.T) {
swagger := &openapi3.Swagger{}
swagger, err := generateNewValidSwagger(swagger)
require.Nil(t, swagger)
require.EqualError(t, err, "swagger info is required")
})
t.Run("not ok - empty info title", func(t *testing.T) {
swagger := &openapi3.Swagger{
Info: &openapi3.Info{},
}
swagger, err := generateNewValidSwagger(swagger)
require.Nil(t, swagger)
require.EqualError(t, err, "swagger info title is required")
})
t.Run("not ok - empty info version", func(t *testing.T) {
swagger := &openapi3.Swagger{
Info: &openapi3.Info{
Title: "title",
},
}
swagger, err := generateNewValidSwagger(swagger)
require.Nil(t, swagger)
require.EqualError(t, err, "swagger info version is required")
})
t.Run("ok - custom swagger", func(t *testing.T) {
swagger := &openapi3.Swagger{
Info: &openapi3.Info{},
}
swagger, err := generateNewValidSwagger(swagger)
require.Nil(t, swagger)
require.EqualError(t, err, "swagger info title is required")
})
t.Run("not ok - swagger is required", func(t *testing.T) {
swagger, err := generateNewValidSwagger(nil)
require.Nil(t, swagger)
require.EqualError(t, err, "swagger is required")
})
t.Run("ok", func(t *testing.T) {
info := &openapi3.Info{
Title: "my title",
Version: "my version",
}
swagger := &openapi3.Swagger{
Info: info,
}
swagger, err := generateNewValidSwagger(swagger)
require.NoError(t, err)
require.Equal(t, &openapi3.Swagger{
OpenAPI: defaultOpenapiVersion,
Info: info,
Paths: openapi3.Paths{},
}, swagger)
})
}
func TestGenerateAndExposeSwagger(t *testing.T) {
t.Run("fails swagger validation", func(t *testing.T) {
mRouter := mux.NewRouter()
router, err := New(mRouter, Options{
Openapi: &openapi3.Swagger{
Info: &openapi3.Info{
Title: "title",
Version: "version",
},
Components: openapi3.Components{
Schemas: map[string]*openapi3.SchemaRef{
"&%": {},
},
},
},
})
require.NoError(t, err)
err = router.GenerateAndExposeSwagger()
require.Error(t, err)
require.True(t, strings.HasPrefix(err.Error(), fmt.Sprintf("%s:", ErrValidatingSwagger)))
})
t.Run("correctly expose json documentation from loaded swagger file", func(t *testing.T) {
mRouter := mux.NewRouter()
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("testdata/users_employees.json")
require.NoError(t, err)
router, err := New(mRouter, Options{
Openapi: swagger,
})
err = router.GenerateAndExposeSwagger()
require.NoError(t, err)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/hello", nil)
router.ServeHTTP(w, r)
req := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil)
mRouter.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
require.True(t, strings.Contains(w.Result().Header.Get("content-type"), "application/json"))
body := readBody(t, w.Result().Body)
require.Equal(t, "OK", body)
t.Run("and generate swagger", func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil)
router.ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
body := readBody(t, w.Result().Body)
require.Equal(t, "{\"components\":{},\"info\":{\"title\":\"test swagger title\",\"version\":\"test swagger version\"},\"openapi\":\"3.0.0\",\"paths\":{\"/hello\":{\"get\":{\"responses\":{\"default\":{\"description\":\"\"}}}}}}", body)
})
actual, err := ioutil.ReadFile("testdata/users_employees.json")
require.NoError(t, err)
require.JSONEq(t, string(actual), body)
})
}
func readBody (t *testing.T, requestBody io.ReadCloser) string {
t.Helper()
body, err := ioutil.ReadAll(requestBody)
require.NoError(t, err)
return string(body)
}
func setupSwagger(t *testing.T) *Router {
t.Helper()
context := context.Background()
r := mux.NewRouter()
router, err := New(r, RouterOptions{
Context: context,
Openapi: &openapi3.Swagger{
Info: &openapi3.Info{
Title: swaggerOpenapiTitle,
Version: swaggerOpenapiVersion,
},
},
})
require.NoError(t, err)
handler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`OK`))
}
operation := Operation{}
_, err = router.AddRoute(http.MethodGet, "/hello", handler, operation)
require.NoError(t, err)
err = router.GenerateAndExposeSwagger()
require.NoError(t, err)
return router
}

240
route.go Normal file
View File

@@ -0,0 +1,240 @@
package swagger
import (
"errors"
"fmt"
"net/http"
"sort"
"github.com/alecthomas/jsonschema"
"github.com/getkin/kin-openapi/openapi3"
"github.com/gorilla/mux"
)
var (
// ErrResponses is thrown if error occurs generating responses schemas.
ErrResponses = errors.New("errors generating responses schema")
// ErrRequestBody is thrown if error occurs generating responses schemas.
ErrRequestBody = errors.New("errors generating request body schema")
// ErrPathParams is thrown if error occurs generating path params schemas.
ErrPathParams = errors.New("errors generating path parameters schema")
// ErrQuerystring is thrown if error occurs generating querystring params schemas.
ErrQuerystring = errors.New("errors generating querystring schema")
)
// Operation type
type Operation struct {
*openapi3.Operation
}
// Handler is the http type handler
type Handler func(w http.ResponseWriter, req *http.Request)
// AddRawRoute add route to router with specific method, path and handler. Add the
// router also to the swagger schema, after validating it
func (r Router) AddRawRoute(method string, path string, handler Handler, operation Operation) (*mux.Route, error) {
if operation.Operation != nil {
err := operation.Validate(r.context)
if err != nil {
return nil, err
}
} else {
operation.Operation = openapi3.NewOperation()
operation.Responses = openapi3.NewResponses()
}
r.swaggerSchema.AddOperation(path, method, operation.Operation)
return r.router.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
// Handle, when content-type is json, the request/response marshalling? Maybe with a specific option.
handler(w, req)
}).Methods(method), nil
}
// SchemaValue is the struct containing the schema information.
type SchemaValue struct {
Content interface{}
Description string
// ContentType is to be used only with RequestBody. Valid ContentType
// are application/json or multipart/form-data.
ContentType string
AllowAdditionalProperties bool
}
// Schema of the route.
type Schema struct {
PathParams map[string]SchemaValue
Querystring map[string]SchemaValue
Headers map[string]SchemaValue
Cookies map[string]SchemaValue
RequestBody *SchemaValue
Responses map[int]SchemaValue
}
const (
pathParamsType = "path"
queryParamType = "query"
headerParamType = "header"
cookieParamType = "cookie"
)
// AddRoute add a route with json schema inferted by passed schema.
func (r Router) AddRoute(method string, path string, handler Handler, schema Schema) (*mux.Route, error) {
operation := openapi3.NewOperation()
operation.Responses = make(openapi3.Responses)
err := r.resolveRequestBodySchema(schema.RequestBody, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrRequestBody, err)
}
err = r.resolveResponsesSchema(schema.Responses, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrResponses, err)
}
err = r.resolveParameterSchema(pathParamsType, schema.PathParams, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
err = r.resolveParameterSchema(queryParamType, schema.Querystring, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
err = r.resolveParameterSchema(headerParamType, schema.Headers, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
err = r.resolveParameterSchema(cookieParamType, schema.Cookies, operation)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrPathParams, err)
}
return r.AddRawRoute(method, path, handler, Operation{operation})
}
func (r Router) getSchemaFromInterface(v interface{}, allowAdditionalProperties bool) (*openapi3.Schema, error) {
if v == nil {
return &openapi3.Schema{}, nil
}
reflector := &jsonschema.Reflector{
DoNotReference: true,
AllowAdditionalProperties: allowAdditionalProperties,
}
jsonSchema := reflector.Reflect(v)
jsonschema.Version = ""
// Empty definitions. Definitions are not valid in openapi3, which use components.
// In the future, we could add an option to fill the components in openapi spec.
jsonSchema.Definitions = nil
data, err := jsonSchema.MarshalJSON()
if err != nil {
return nil, err
}
schema := openapi3.NewSchema()
err = schema.UnmarshalJSON(data)
if err != nil {
return nil, err
}
return schema, nil
}
func (r Router) resolveRequestBodySchema(bodySchema *SchemaValue, operation *openapi3.Operation) error {
if bodySchema == nil {
return nil
}
requestBodySchema, err := r.getSchemaFromInterface(bodySchema.Content, bodySchema.AllowAdditionalProperties)
if err != nil {
return err
}
requestBody := openapi3.NewRequestBody()
switch bodySchema.ContentType {
case "multipart/form-data":
requestBody = requestBody.WithFormDataSchema(requestBodySchema)
case "application/json", "":
requestBody = requestBody.WithJSONSchema(requestBodySchema)
default:
return fmt.Errorf("invalid content-type in request body")
}
if bodySchema.Description != "" {
requestBody.WithDescription(bodySchema.Description)
}
operation.RequestBody = &openapi3.RequestBodyRef{
Value: requestBody,
}
return nil
}
func (r Router) resolveResponsesSchema(responses map[int]SchemaValue, operation *openapi3.Operation) error {
if responses == nil {
operation.Responses = openapi3.NewResponses()
}
for statusCode, v := range responses {
response := openapi3.NewResponse()
responseSchema, err := r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties)
if err != nil {
return err
}
response = response.WithDescription(v.Description)
response = response.WithJSONSchema(responseSchema)
operation.AddResponse(statusCode, response)
}
return nil
}
func (r Router) resolveParameterSchema(paramType string, paramConfig map[string]SchemaValue, operation *openapi3.Operation) error {
var keys = make([]string, 0, len(paramConfig))
for k := range paramConfig {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
v := paramConfig[key]
var param *openapi3.Parameter
switch paramType {
case pathParamsType:
param = openapi3.NewPathParameter(key)
case queryParamType:
param = openapi3.NewQueryParameter(key)
case headerParamType:
param = openapi3.NewHeaderParameter(key)
case cookieParamType:
param = openapi3.NewCookieParameter(key)
default:
return fmt.Errorf("invalid param type")
}
schema := openapi3.NewSchema()
if v.Content != nil {
var err error
schema, err = r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties)
if err != nil {
return err
}
}
param = param.WithSchema(schema)
if v.Description != "" {
param = param.WithDescription(v.Description)
}
operation.AddParameter(param)
}
return nil
}

723
route_test.go Normal file
View File

@@ -0,0 +1,723 @@
package swagger
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/gorilla/mux"
"github.com/stretchr/testify/require"
)
func TestAddRoute(t *testing.T) {
type User struct {
Name string `json:"name" jsonschema:"title=The user name,required" jsonschema_extras:"example=Jane"`
PhoneNumber int `json:"phone" jsonschema:"title=mobile number of user"`
Groups []string `json:"groups,omitempty" jsonschema:"title=groups of the user,default=users"`
Address string `json:"address" jsonschema:"title=user address"`
}
type Users []User
type errorResponse struct {
Message string `json:"message"`
}
type Employees struct {
OrganizationName string `json:"organization_name"`
Users Users `json:"users" jsonschema:"selected users"`
}
type FormData struct {
ID string `json:"id,omitempty"`
Address struct {
Street string `json:"street,omitempty"`
City string `json:"city,omitempty"`
} `json:"address,omitempty"`
ProfileImage string `json:"profileImage,omitempty" jsonschema_extras:"format=binary"`
}
okHandler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
tests := []struct {
name string
routes func(t *testing.T, router *Router)
fixturesPath string
testPath string
testMethod string
}{
{
name: "no routes",
routes: func(t *testing.T, router *Router) {},
fixturesPath: "testdata/empty.json",
},
{
name: "empty route schema",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodPost, "/", okHandler, Schema{})
require.NoError(t, err)
},
testPath: "/",
testMethod: http.MethodPost,
fixturesPath: "testdata/empty-route-schema.json",
},
{
name: "multiple real routes",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodPost, "/users", okHandler, Schema{
RequestBody: &SchemaValue{
Content: User{},
},
Responses: map[int]SchemaValue{
201: {
Content: "",
},
401: {
Content: &errorResponse{},
Description: "invalid request",
},
},
})
require.NoError(t, err)
_, err = router.AddRoute(http.MethodGet, "/users", okHandler, Schema{
Responses: map[int]SchemaValue{
200: {
Content: &Users{},
},
},
})
require.NoError(t, err)
_, err = router.AddRoute(http.MethodGet, "/employees", okHandler, Schema{
Responses: map[int]SchemaValue{
200: {
Content: &Employees{},
},
},
})
require.NoError(t, err)
},
testPath: "/users",
fixturesPath: "testdata/users_employees.json",
},
{
name: "multipart request body",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodPost, "/files", okHandler, Schema{
RequestBody: &SchemaValue{
Content: &FormData{},
Description: "upload file",
ContentType: "multipart/form-data",
AllowAdditionalProperties: true,
},
Responses: map[int]SchemaValue{
200: {Content: ""},
},
})
require.NoError(t, err)
},
testPath: "/files",
testMethod: http.MethodPost,
fixturesPath: "testdata/multipart-requestbody.json",
},
{
name: "schema with params",
routes: func(t *testing.T, router *Router) {
var number = 0
_, err := router.AddRoute(http.MethodGet, "/users/{userId}", okHandler, Schema{
PathParams: map[string]SchemaValue{
"userId": {
Content: number,
Description: "userId is a number above 0",
},
},
})
require.NoError(t, err)
_, err = router.AddRoute(http.MethodGet, "/cars/{carId}/drivers/{driverId}", okHandler, Schema{
PathParams: map[string]SchemaValue{
"carId": {
Content: "",
},
"driverId": {
Content: "",
},
},
})
require.NoError(t, err)
},
testPath: "/users/12",
fixturesPath: "testdata/params.json",
},
{
name: "schema with querystring",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{
Querystring: map[string]SchemaValue{
"projectId": {
Content: "",
Description: "projectId is the project id",
},
},
})
require.NoError(t, err)
},
testPath: "/projects",
fixturesPath: "testdata/query.json",
},
{
name: "schema with headers",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{
Headers: map[string]SchemaValue{
"foo": {
Content: "",
Description: "foo description",
},
"bar": {
Content: "",
},
},
})
require.NoError(t, err)
},
testPath: "/projects",
fixturesPath: "testdata/headers.json",
},
{
name: "schema with cookies",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{
Cookies: map[string]SchemaValue{
"debug": {
Content: 0,
Description: "boolean. Set 0 to disable and 1 to enable",
},
"csrftoken": {
Content: "",
},
},
})
require.NoError(t, err)
},
testPath: "/projects",
fixturesPath: "testdata/cookies.json",
},
{
name: "schema defined without content",
routes: func(t *testing.T, router *Router) {
_, err := router.AddRoute(http.MethodPost, "/{id}", okHandler, Schema{
RequestBody: &SchemaValue{
Description: "request body without schema",
},
Responses: map[int]SchemaValue{
204: {},
},
PathParams: map[string]SchemaValue{
"id": {},
},
Querystring: map[string]SchemaValue{
"q": {},
},
Headers: map[string]SchemaValue{
"key": {},
},
Cookies: map[string]SchemaValue{
"cookie1": {},
},
})
require.NoError(t, err)
},
testPath: "/foobar",
testMethod: http.MethodPost,
fixturesPath: "testdata/schema-no-content.json",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
context := context.Background()
r := mux.NewRouter()
router, err := New(r, Options{
Context: context,
Openapi: getBaseSwagger(t),
})
require.NoError(t, err)
require.NotNil(t, router)
// Add routes to test
test.routes(t, router)
err = router.GenerateAndExposeSwagger()
require.NoError(t, err)
if test.testPath != "" {
if test.testMethod == "" {
test.testMethod = http.MethodGet
}
w := httptest.NewRecorder()
req := httptest.NewRequest(test.testMethod, test.testPath, nil)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
body := readBody(t, w.Result().Body)
require.Equal(t, "OK", body)
}
t.Run("and generate swagger documentation in json", func(t *testing.T) {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Result().StatusCode)
body := readBody(t, w.Result().Body)
actual, err := ioutil.ReadFile(test.fixturesPath)
require.NoError(t, err)
require.JSONEq(t, string(actual), body, "actual json data: ", string(actual))
})
})
}
t.Run("", func(t *testing.T) {
})
}
func TestResolveRequestBodySchema(t *testing.T) {
type TestStruct struct {
ID string `json:"id,omitempty"`
}
tests := []struct {
name string
bodySchema *SchemaValue
expectedErr error
expectedJSON string
}{
{
name: "empty body schema",
expectedErr: nil,
expectedJSON: `{"responses": null}`,
},
{
name: "schema multipart",
expectedErr: nil,
bodySchema: &SchemaValue{
Content: &TestStruct{},
ContentType: "multipart/form-data",
},
expectedJSON: `{
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type":"object",
"additionalProperties":false,
"properties": {
"id": {"type":"string"}
}
}
}
}
},
"responses": null
}`,
},
{
name: "content-type application/json",
expectedErr: nil,
bodySchema: &SchemaValue{
Content: &TestStruct{},
ContentType: "application/json",
},
expectedJSON: `{
"requestBody": {
"content": {
"application/json": {
"schema": {
"type":"object",
"additionalProperties":false,
"properties": {
"id": {"type":"string"}
}
}
}
}
},
"responses": null
}`,
},
{
name: "no content-type - default to json",
expectedErr: nil,
bodySchema: &SchemaValue{
Content: &TestStruct{},
},
expectedJSON: `{
"requestBody": {
"content": {
"application/json": {
"schema": {
"type":"object",
"additionalProperties":false,
"properties": {
"id": {"type":"string"}
}
}
}
}
},
"responses": null
}`,
},
{
name: "with description",
expectedErr: nil,
bodySchema: &SchemaValue{
Content: &TestStruct{},
Description: "my custom description",
},
expectedJSON: `{
"requestBody": {
"description": "my custom description",
"content": {
"application/json": {
"schema": {
"type":"object",
"additionalProperties":false,
"properties": {
"id": {"type":"string"}
}
}
}
}
},
"responses": null
}`,
},
// FIXME: this test case exhibits a wrong behavior. It should be supported.
{
name: "content type text/plain",
expectedErr: fmt.Errorf("invalid content-type in request body"),
bodySchema: &SchemaValue{
Content: &TestStruct{},
ContentType: "text/plain",
},
},
// FIXME: this test case exhibits a wrong behavior. It should be supported.
{
name: "generic content type - it represent all types",
expectedErr: fmt.Errorf("invalid content-type in request body"),
bodySchema: &SchemaValue{
Content: &TestStruct{},
ContentType: "*/*",
},
},
}
mux := mux.NewRouter()
router, err := New(mux, Options{
Openapi: getBaseSwagger(t),
})
require.NoError(t, err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
operation := openapi3.NewOperation()
err := router.resolveRequestBodySchema(test.bodySchema, operation)
if err == nil {
data, _ := operation.MarshalJSON()
jsonData := string(data)
require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: ", jsonData)
require.NoError(t, err)
}
require.Equal(t, test.expectedErr, err)
})
}
}
func TestResolveResponsesSchema(t *testing.T) {
type TestStruct struct {
Message string `json:"message,omitempty"`
}
tests := []struct {
name string
responsesSchema map[int]SchemaValue
expectedErr error
expectedJSON string
}{
{
name: "empty responses schema",
expectedErr: nil,
expectedJSON: `{"responses": {"default":{"description":""}}}`,
},
{
name: "with 1 status code",
responsesSchema: map[int]SchemaValue{
200: {
Content: &TestStruct{},
},
},
expectedErr: nil,
expectedJSON: `{
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"additionalProperties": false
}
}
}
}
}
}`,
},
{
name: "with more status codes",
responsesSchema: map[int]SchemaValue{
200: {
Content: &TestStruct{},
},
400: {
Content: "",
},
},
expectedErr: nil,
expectedJSON: `{
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"additionalProperties": false
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}`,
},
{
name: "with custom description",
responsesSchema: map[int]SchemaValue{
400: {
Content: "",
Description: "a description",
},
},
expectedErr: nil,
expectedJSON: `{
"responses": {
"400": {
"description": "a description",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}`,
},
}
mux := mux.NewRouter()
router, err := New(mux, Options{
Openapi: getBaseSwagger(t),
})
require.NoError(t, err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
operation := openapi3.NewOperation()
operation.Responses = make(openapi3.Responses)
err := router.resolveResponsesSchema(test.responsesSchema, operation)
if err == nil {
data, _ := operation.MarshalJSON()
jsonData := string(data)
require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: ", jsonData)
require.NoError(t, err)
}
require.Equal(t, test.expectedErr, err)
})
}
}
func TestResolveParametersSchema(t *testing.T) {
type TestStruct struct {
Message string `json:"message,omitempty"`
}
tests := []struct {
name string
paramsSchema map[string]SchemaValue
paramType string
expectedErr error
expectedJSON string
}{
{
name: "empty responses schema",
paramType: pathParamsType,
expectedJSON: `{"responses": null}`,
},
{
name: "path param",
paramType: pathParamsType,
paramsSchema: map[string]SchemaValue{
"foo": {
Content: "",
},
},
expectedJSON: `{
"parameters": [{
"in": "path",
"name": "foo",
"required": true,
"schema": {
"type": "string"
}
}],
"responses": null
}`,
},
{
name: "query param",
paramType: queryParamType,
paramsSchema: map[string]SchemaValue{
"foo": {
Content: "",
},
},
expectedJSON: `{
"parameters": [{
"in": "query",
"name": "foo",
"schema": {
"type": "string"
}
}],
"responses": null
}`,
},
{
name: "cookie param",
paramType: cookieParamType,
paramsSchema: map[string]SchemaValue{
"foo": {
Content: "",
},
},
expectedJSON: `{
"parameters": [{
"in": "cookie",
"name": "foo",
"schema": {
"type": "string"
}
}],
"responses": null
}`,
},
{
name: "header param",
paramType: headerParamType,
paramsSchema: map[string]SchemaValue{
"foo": {
Content: "",
},
},
expectedJSON: `{
"parameters": [{
"in": "header",
"name": "foo",
"schema": {
"type": "string"
}
}],
"responses": null
}`,
},
{
name: "wrong param type",
paramType: "wrong",
paramsSchema: map[string]SchemaValue{
"foo": {
Content: "",
},
},
expectedErr: fmt.Errorf("invalid param type"),
},
}
mux := mux.NewRouter()
router, err := New(mux, Options{
Openapi: getBaseSwagger(t),
})
require.NoError(t, err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
operation := openapi3.NewOperation()
err := router.resolveParameterSchema(test.paramType, test.paramsSchema, operation)
if err == nil {
data, _ := operation.MarshalJSON()
jsonData := string(data)
require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: ", jsonData)
require.NoError(t, err)
}
require.Equal(t, test.expectedErr, err)
})
}
}
func getBaseSwagger(t *testing.T) *openapi3.Swagger {
t.Helper()
return &openapi3.Swagger{
Info: &openapi3.Info{
Title: swaggerOpenapiTitle,
Version: swaggerOpenapiVersion,
},
}
}

36
testdata/cookies.json vendored Normal file
View File

@@ -0,0 +1,36 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/projects": {
"get": {
"parameters": [
{
"in": "cookie",
"name": "csrftoken",
"schema": {
"type": "string"
}
},
{
"description": "boolean. Set 0 to disable and 1 to enable",
"in": "cookie",
"name": "debug",
"schema": {
"type": "integer"
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
}
}
}

19
testdata/empty-route-schema.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/": {
"post": {
"responses": {
"default": {
"description": ""
}
}
}
}
}
}

9
testdata/empty.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {}
}

36
testdata/headers.json vendored Normal file
View File

@@ -0,0 +1,36 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/projects": {
"get": {
"parameters": [
{
"in": "header",
"name": "bar",
"schema": {
"type": "string"
}
},
{
"description": "foo description",
"in": "header",
"name": "foo",
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
}
}
}

58
testdata/multipart-requestbody.json vendored Normal file
View File

@@ -0,0 +1,58 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/files": {
"post": {
"requestBody": {
"description": "upload file",
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"additionalProperties": true,
"properties": {
"id": {
"type": "string"
},
"address": {
"additionalProperties": true,
"type": "object",
"properties": {
"street": {
"type": "string"
},
"city": {
"type": "string"
}
}
},
"profileImage": {
"type": "string",
"format": "binary"
}
}
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}

57
testdata/params.json vendored Normal file
View File

@@ -0,0 +1,57 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/cars/{carId}/drivers/{driverId}": {
"get": {
"parameters": [
{
"in": "path",
"name": "carId",
"required": true,
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "driverId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
},
"/users/{userId}": {
"get": {
"parameters": [
{
"description": "userId is a number above 0",
"in": "path",
"name": "userId",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
}
}
}

29
testdata/query.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/projects": {
"get": {
"parameters": [
{
"description": "projectId is the project id",
"in": "query",
"name": "projectId",
"schema": {
"type": "string"
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
}
}
}

55
testdata/schema-no-content.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/{id}": {
"post": {
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {}
},
{
"in": "query",
"name": "q",
"schema": {}
},
{
"in": "header",
"name": "key",
"schema": {}
},
{
"in": "cookie",
"name": "cookie1",
"schema": {}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "request body without schema"
},
"responses": {
"204": {
"content": {
"application/json": {
"schema": {}
}
},
"description": ""
}
}
}
}
}
}

196
testdata/users_employees.json vendored Normal file
View File

@@ -0,0 +1,196 @@
{
"components": {},
"info": {
"title": "test swagger title",
"version": "test swagger version"
},
"openapi": "3.0.0",
"paths": {
"/employees": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"organization_name": {
"type": "string"
},
"users": {
"items": {
"additionalProperties": false,
"properties": {
"address": {
"title": "user address",
"type": "string"
},
"groups": {
"default": [
"users"
],
"items": {
"type": "string"
},
"title": "groups of the user",
"type": "array"
},
"name": {
"example": "Jane",
"title": "The user name",
"type": "string"
},
"phone": {
"title": "mobile number of user",
"type": "integer"
}
},
"required": [
"name",
"phone",
"address"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"organization_name",
"users"
],
"type": "object"
}
}
},
"description": ""
}
}
}
},
"/users": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"additionalProperties": false,
"properties": {
"address": {
"title": "user address",
"type": "string"
},
"groups": {
"default": [
"users"
],
"items": {
"type": "string"
},
"title": "groups of the user",
"type": "array"
},
"name": {
"example": "Jane",
"title": "The user name",
"type": "string"
},
"phone": {
"title": "mobile number of user",
"type": "integer"
}
},
"required": [
"name",
"phone",
"address"
],
"type": "object"
},
"type": "array"
}
}
},
"description": ""
}
}
},
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"address": {
"title": "user address",
"type": "string"
},
"groups": {
"default": [
"users"
],
"items": {
"type": "string"
},
"title": "groups of the user",
"type": "array"
},
"name": {
"example": "Jane",
"title": "The user name",
"type": "string"
},
"phone": {
"title": "mobile number of user",
"type": "integer"
}
},
"required": [
"name",
"phone",
"address"
],
"type": "object"
}
}
}
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": ""
},
"401": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
}
},
"description": "invalid request"
}
}
}
}
}
}