From 4c34a09f78b31b0e5e1db29d1197bbb1856c814a Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Wed, 14 Oct 2020 01:05:35 +0200 Subject: [PATCH 01/11] handle request body and responses and generates a correct openapi3 swagger --- README.md | 3 + go.mod | 3 +- go.sum | 10 +++ jsonschema2openapi.go | 64 +++++++++++++++++++ jsonschema2openapi_test.go | 62 ++++++++++++++++++ main.go | 123 +++++------------------------------- main_test.go | 10 +-- router.go | 125 +++++++++++++++++++++++++++++++++++++ router_test.go | 100 +++++++++++++++++++++++++++++ testdata/users.json | 1 + validation.go | 19 ++++++ 11 files changed, 408 insertions(+), 112 deletions(-) create mode 100644 jsonschema2openapi.go create mode 100644 jsonschema2openapi_test.go create mode 100644 router.go create mode 100644 router_test.go create mode 100644 testdata/users.json create mode 100644 validation.go diff --git a/README.md b/README.md index 0ab2b73..167fa4a 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,6 @@ Initial test&try to generate a swagger dynamically. It uses [gorilla-mux](https://github.com/gorilla/mux) and [kin-openapi](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. diff --git a/go.mod b/go.mod index b3be1f1..1bfcc4e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ 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/stretchr/testify v1.6.1 ) diff --git a/go.sum b/go.sum index baa0a2c..e51d617 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN 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= @@ -20,6 +22,8 @@ github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tF 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/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= +github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= 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= @@ -28,10 +32,14 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= @@ -47,3 +55,5 @@ 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= diff --git a/jsonschema2openapi.go b/jsonschema2openapi.go new file mode 100644 index 0000000..d481578 --- /dev/null +++ b/jsonschema2openapi.go @@ -0,0 +1,64 @@ +package swagger + +import ( + "github.com/alecthomas/jsonschema" + "github.com/getkin/kin-openapi/openapi3" +) + +func (r Router) getSchemaFromInterface(v interface{}, components *openapi3.Components) (*openapi3.Schema, *openapi3.Components, error) { + if v == nil { + return &openapi3.Schema{}, components, nil + } + + reflector := &jsonschema.Reflector{ + DoNotReference: true, + } + + jsonschema.Version = "" + jsonSchema := reflector.Reflect(v) + // TODO: integrate to have components filled by option + err := definitionsToComponents(components, jsonSchema) + if err != nil { + return nil, nil, err + } + + // jsonSchema = cleanJSONSchemaVersion(jsonSchema) + data, err := jsonSchema.MarshalJSON() + if err != nil { + return nil, nil, err + } + + schema := openapi3.NewSchema() + err = schema.UnmarshalJSON(data) + if err != nil { + return nil, nil, err + } + + return schema, components, nil +} + +func definitionsToComponents(components *openapi3.Components, schema *jsonschema.Schema) error { + if components == nil { + schema.Definitions = nil + return nil + } + + if components.Schemas == nil { + components.Schemas = map[string]*openapi3.SchemaRef{} + } + // Rename definitions to components + for k, definition := range schema.Definitions { + + marshalledDefinition, err := definition.MarshalJSON() + if err != nil { + return err + } + + components.Schemas[k] = &openapi3.SchemaRef{} + err = components.Schemas[k].UnmarshalJSON(marshalledDefinition) + if err != nil { + return err + } + } + return nil +} diff --git a/jsonschema2openapi_test.go b/jsonschema2openapi_test.go new file mode 100644 index 0000000..c74c341 --- /dev/null +++ b/jsonschema2openapi_test.go @@ -0,0 +1,62 @@ +package swagger + +import ( + "encoding/json" + "testing" + + "github.com/alecthomas/jsonschema" + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestConvertJSONSchema2OpenAPI(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 NestedDefinitions struct { + Name string `json:"name" jsonschema:"title=The user name,required" jsonschema_extras:"example=Jane"` + Users Users `json:"users,omitempty" jsonschema:"title=List of game users"` + } + reflector := &jsonschema.Reflector{ + DoNotReference: true, + } + + t.Run("add definitions to components", func(t *testing.T) { + jsonSchema := reflector.Reflect(&[]User{}) + jsonSchema.Version = "" + + components := &openapi3.Components{} + + definitionsToComponents(components, jsonSchema) + + output, err := json.Marshal(&components.Schemas) + require.NoError(t, err) + + expected, err := json.Marshal(jsonSchema.Definitions) + require.NoError(t, err) + + require.JSONEq(t, string(output), string(expected)) + }) + + t.Run("add nested definitions to components", func(t *testing.T) { + jsonSchema := reflector.Reflect(&NestedDefinitions{}) + jsonSchema.Version = "" + + components := &openapi3.Components{} + + definitionsToComponents(components, jsonSchema) + + output, err := json.Marshal(&components.Schemas) + require.NoError(t, err) + + expected, err := json.Marshal(jsonSchema.Definitions) + require.NoError(t, err) + + require.JSONEq(t, string(output), string(expected)) + }) +} diff --git a/main.go b/main.go index e50a417..6b78528 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,8 @@ import ( "context" "errors" "fmt" - "net/http" "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/openapi3filter" "github.com/gorilla/mux" ) @@ -27,87 +25,25 @@ const ( // 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 + requiredFromJSONSchemaTags bool } -// Handler is the http type handler -type Handler func(w http.ResponseWriter, req *http.Request) +// Options to be passed to create the new router and swagger +type Options struct { + Context context.Context + Openapi *openapi3.Swagger -// 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 + // RequiredFromJSONSchemaTags will generate a schema that requires any key + // tagged with `jsonschema:required`, overriding the + // default of requiring any key *not* tagged with `json:,omitempty`. + RequiredFromJSONSchemaTags bool } // 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,38 +55,13 @@ 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, + requiredFromJSONSchemaTags: options.RequiredFromJSONSchemaTags, }, 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{ diff --git a/main_test.go b/main_test.go index 915c003..e4f4015 100644 --- a/main_test.go +++ b/main_test.go @@ -14,7 +14,7 @@ import ( ) const ( - swaggerOpenapiTitle = "test swagger title" + swaggerOpenapiTitle = "test swagger title" swaggerOpenapiVersion = "test swagger version" ) @@ -46,7 +46,7 @@ func TestIntegration(t *testing.T) { }) } -func readBody (t *testing.T, requestBody io.ReadCloser) string { +func readBody(t *testing.T, requestBody io.ReadCloser) string { t.Helper() body, err := ioutil.ReadAll(requestBody) @@ -61,11 +61,11 @@ func setupSwagger(t *testing.T) *Router { context := context.Background() r := mux.NewRouter() - router, err := New(r, RouterOptions{ + router, err := New(r, Options{ Context: context, Openapi: &openapi3.Swagger{ Info: &openapi3.Info{ - Title: swaggerOpenapiTitle, + Title: swaggerOpenapiTitle, Version: swaggerOpenapiVersion, }, }, @@ -78,7 +78,7 @@ func setupSwagger(t *testing.T) *Router { } operation := Operation{} - _, err = router.AddRoute(http.MethodGet, "/hello", handler, operation) + _, err = router.AddRawRoute(http.MethodGet, "/hello", handler, operation) require.NoError(t, err) err = router.GenerateAndExposeSwagger() diff --git a/router.go b/router.go new file mode 100644 index 0000000..d128243 --- /dev/null +++ b/router.go @@ -0,0 +1,125 @@ +package swagger + +import ( + "errors" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/gorilla/mux" +) + +var ( + ErrResponses = errors.New("errors generating responses") + ErrRequestBody = errors.New("errors generating request body") +) + +// Operation type +type Operation struct { + *openapi3.Operation + // TODO: handle request and response +} + +// 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 + + // TODO: enable validation + // err = r.swaggerRouter.AddSwagger(r.SwaggerSchema) + // if err != nil { + // return err + // } + + return nil +} + +// 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 { + // fmt.Printf("FFFFFFFFF %s \n", err) + // 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 +} + +type Response struct { + Value interface{} + Description string +} + +type Schema struct { + // Parameters interface{} + // Querystring interface{} + RequestBody interface{} + Responses map[int]Response +} + +func (r Router) AddRoute(method string, path string, handler Handler, schema Schema) (*mux.Route, error) { + operation := openapi3.NewOperation() + operation.Responses = make(openapi3.Responses) + + if schema.RequestBody != nil { + requestBody := openapi3.NewRequestBody() + + requestBodySchema, _, err := r.getSchemaFromInterface(schema.RequestBody, nil) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrResponses, err) + } + requestBody = requestBody.WithJSONSchema(requestBodySchema) + + operation.RequestBody = &openapi3.RequestBodyRef{ + Value: requestBody, + } + } + if schema.Responses != nil { + for statusCode, v := range schema.Responses { + response := openapi3.NewResponse() + + responseSchema, _, err := r.getSchemaFromInterface(v.Value, nil) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrRequestBody, err) + } + + response = response.WithDescription(v.Description) + response = response.WithJSONSchema(responseSchema) + + operation.AddResponse(statusCode, response) + } + } + + return r.AddRawRoute(method, path, handler, Operation{operation}) +} + +func (r Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + r.router.ServeHTTP(w, req) +} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..6e68e39 --- /dev/null +++ b/router_test.go @@ -0,0 +1,100 @@ +package swagger + +import ( + "context" + "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) { + okHandler := func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`OK`)) + } + + t.Run("router works correctly - simple request body", func(t *testing.T) { + context := context.Background() + r := mux.NewRouter() + swagger := openapi3.Swagger{ + Info: &openapi3.Info{ + Title: swaggerOpenapiTitle, + Version: swaggerOpenapiVersion, + }, + } + + router, err := New(r, Options{ + Context: context, + Openapi: &swagger, + }) + require.NoError(t, err) + require.NotNil(t, router) + + 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"` + } + + _, err = router.AddRoute(http.MethodPost, "/users", okHandler, Schema{ + RequestBody: &User{}, + Responses: map[int]Response{ + 201: { + Value: "", + }, + 401: { + Value: &errorResponse{}, + Description: "invalid request", + }, + }, + }) + require.NoError(t, err) + + _, err = router.AddRoute(http.MethodGet, "/users", okHandler, Schema{ + Responses: map[int]Response{ + 200: { + Value: &Users{}, + }, + }, + }) + require.NoError(t, err) + + err = router.GenerateAndExposeSwagger() + require.NoError(t, err) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/users", nil) + + router.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", 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) + actual, err := ioutil.ReadFile("testdata/users.json") + require.NoError(t, err) + // require.JSONEq(t, string(actual), body) + require.Equal(t, string(actual), body) + }) + }) +} diff --git a/testdata/users.json b/testdata/users.json new file mode 100644 index 0000000..29cc3c6 --- /dev/null +++ b/testdata/users.json @@ -0,0 +1 @@ +{"components":{},"info":{"title":"test swagger title","version":"test swagger version"},"openapi":"3.0.0","paths":{"/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"}}}}}} \ No newline at end of file diff --git a/validation.go b/validation.go new file mode 100644 index 0000000..2667f5d --- /dev/null +++ b/validation.go @@ -0,0 +1,19 @@ +package swagger + +// 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) +// } From ec6788e607d7a558e5097055b549397e6fc6b67a Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Wed, 14 Oct 2020 23:11:19 +0200 Subject: [PATCH 02/11] refactor: remove unused code and add function doc --- jsonschema2openapi.go | 64 ----------- jsonschema2openapi_test.go | 62 ----------- router.go | 57 ++++++++-- router_test.go | 19 +++- testdata/users.json | 1 - testdata/users_employees.json | 196 ++++++++++++++++++++++++++++++++++ validation.go | 19 ---- 7 files changed, 258 insertions(+), 160 deletions(-) delete mode 100644 jsonschema2openapi.go delete mode 100644 jsonschema2openapi_test.go delete mode 100644 testdata/users.json create mode 100644 testdata/users_employees.json delete mode 100644 validation.go diff --git a/jsonschema2openapi.go b/jsonschema2openapi.go deleted file mode 100644 index d481578..0000000 --- a/jsonschema2openapi.go +++ /dev/null @@ -1,64 +0,0 @@ -package swagger - -import ( - "github.com/alecthomas/jsonschema" - "github.com/getkin/kin-openapi/openapi3" -) - -func (r Router) getSchemaFromInterface(v interface{}, components *openapi3.Components) (*openapi3.Schema, *openapi3.Components, error) { - if v == nil { - return &openapi3.Schema{}, components, nil - } - - reflector := &jsonschema.Reflector{ - DoNotReference: true, - } - - jsonschema.Version = "" - jsonSchema := reflector.Reflect(v) - // TODO: integrate to have components filled by option - err := definitionsToComponents(components, jsonSchema) - if err != nil { - return nil, nil, err - } - - // jsonSchema = cleanJSONSchemaVersion(jsonSchema) - data, err := jsonSchema.MarshalJSON() - if err != nil { - return nil, nil, err - } - - schema := openapi3.NewSchema() - err = schema.UnmarshalJSON(data) - if err != nil { - return nil, nil, err - } - - return schema, components, nil -} - -func definitionsToComponents(components *openapi3.Components, schema *jsonschema.Schema) error { - if components == nil { - schema.Definitions = nil - return nil - } - - if components.Schemas == nil { - components.Schemas = map[string]*openapi3.SchemaRef{} - } - // Rename definitions to components - for k, definition := range schema.Definitions { - - marshalledDefinition, err := definition.MarshalJSON() - if err != nil { - return err - } - - components.Schemas[k] = &openapi3.SchemaRef{} - err = components.Schemas[k].UnmarshalJSON(marshalledDefinition) - if err != nil { - return err - } - } - return nil -} diff --git a/jsonschema2openapi_test.go b/jsonschema2openapi_test.go deleted file mode 100644 index c74c341..0000000 --- a/jsonschema2openapi_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package swagger - -import ( - "encoding/json" - "testing" - - "github.com/alecthomas/jsonschema" - "github.com/getkin/kin-openapi/openapi3" - "github.com/stretchr/testify/require" -) - -func TestConvertJSONSchema2OpenAPI(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 NestedDefinitions struct { - Name string `json:"name" jsonschema:"title=The user name,required" jsonschema_extras:"example=Jane"` - Users Users `json:"users,omitempty" jsonschema:"title=List of game users"` - } - reflector := &jsonschema.Reflector{ - DoNotReference: true, - } - - t.Run("add definitions to components", func(t *testing.T) { - jsonSchema := reflector.Reflect(&[]User{}) - jsonSchema.Version = "" - - components := &openapi3.Components{} - - definitionsToComponents(components, jsonSchema) - - output, err := json.Marshal(&components.Schemas) - require.NoError(t, err) - - expected, err := json.Marshal(jsonSchema.Definitions) - require.NoError(t, err) - - require.JSONEq(t, string(output), string(expected)) - }) - - t.Run("add nested definitions to components", func(t *testing.T) { - jsonSchema := reflector.Reflect(&NestedDefinitions{}) - jsonSchema.Version = "" - - components := &openapi3.Components{} - - definitionsToComponents(components, jsonSchema) - - output, err := json.Marshal(&components.Schemas) - require.NoError(t, err) - - expected, err := json.Marshal(jsonSchema.Definitions) - require.NoError(t, err) - - require.JSONEq(t, string(output), string(expected)) - }) -} diff --git a/router.go b/router.go index d128243..8405ced 100644 --- a/router.go +++ b/router.go @@ -5,12 +5,15 @@ import ( "fmt" "net/http" + "github.com/alecthomas/jsonschema" "github.com/getkin/kin-openapi/openapi3" "github.com/gorilla/mux" ) var ( - ErrResponses = errors.New("errors generating responses") + // ErrResponses is thrown if error occurs generating responses schemas. + ErrResponses = errors.New("errors generating responses") + // ErrRequestBody is thrown if error occurs generating responses schemas. ErrRequestBody = errors.New("errors generating request body") ) @@ -26,9 +29,9 @@ 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) - // } + if err := r.SwaggerSchema.Validate(r.context); err != nil { + return fmt.Errorf("%w: %s", ErrValidatingSwagger, err) + } jsonSwagger, err := r.SwaggerSchema.MarshalJSON() if err != nil { @@ -55,11 +58,10 @@ func (r Router) GenerateAndExposeSwagger() error { // 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 { - // fmt.Printf("FFFFFFFFF %s \n", err) - // return nil, err - // } + err := operation.Validate(r.context) + if err != nil { + return nil, err + } } else { operation.Operation = openapi3.NewOperation() operation.Responses = openapi3.NewResponses() @@ -72,11 +74,13 @@ func (r Router) AddRawRoute(method string, path string, handler Handler, operati }).Methods(method), nil } +// Response is the struct containing a single route response. type Response struct { Value interface{} Description string } +// Schema of the route. type Schema struct { // Parameters interface{} // Querystring interface{} @@ -84,6 +88,7 @@ type Schema struct { Responses map[int]Response } +// 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) @@ -93,7 +98,7 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch requestBodySchema, _, err := r.getSchemaFromInterface(schema.RequestBody, nil) if err != nil { - return nil, fmt.Errorf("%w: %s", ErrResponses, err) + return nil, fmt.Errorf("%w: %s", ErrRequestBody, err) } requestBody = requestBody.WithJSONSchema(requestBodySchema) @@ -107,7 +112,7 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch responseSchema, _, err := r.getSchemaFromInterface(v.Value, nil) if err != nil { - return nil, fmt.Errorf("%w: %s", ErrRequestBody, err) + return nil, fmt.Errorf("%w: %s", ErrResponses, err) } response = response.WithDescription(v.Description) @@ -123,3 +128,33 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch func (r Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.router.ServeHTTP(w, req) } + +func (r Router) getSchemaFromInterface(v interface{}, components *openapi3.Components) (*openapi3.Schema, *openapi3.Components, error) { + if v == nil { + return &openapi3.Schema{}, components, nil + } + + reflector := &jsonschema.Reflector{ + DoNotReference: true, + } + + 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 + + // jsonSchema = cleanJSONSchemaVersion(jsonSchema) + data, err := jsonSchema.MarshalJSON() + if err != nil { + return nil, nil, err + } + + schema := openapi3.NewSchema() + err = schema.UnmarshalJSON(data) + if err != nil { + return nil, nil, err + } + + return schema, components, nil +} diff --git a/router_test.go b/router_test.go index 6e68e39..2f2b29d 100644 --- a/router_test.go +++ b/router_test.go @@ -46,6 +46,11 @@ func TestAddRoute(t *testing.T) { Message string `json:"message"` } + type Employees struct { + OrganizationName string `json:"organization_name"` + Users Users `json:"users" jsonschema:"selected users"` + } + _, err = router.AddRoute(http.MethodPost, "/users", okHandler, Schema{ RequestBody: &User{}, Responses: map[int]Response{ @@ -69,6 +74,15 @@ func TestAddRoute(t *testing.T) { }) require.NoError(t, err) + _, err = router.AddRoute(http.MethodGet, "/employees", okHandler, Schema{ + Responses: map[int]Response{ + 200: { + Value: &Employees{}, + }, + }, + }) + require.NoError(t, err) + err = router.GenerateAndExposeSwagger() require.NoError(t, err) @@ -91,10 +105,9 @@ func TestAddRoute(t *testing.T) { require.Equal(t, http.StatusOK, w.Result().StatusCode) body := readBody(t, w.Result().Body) - actual, err := ioutil.ReadFile("testdata/users.json") + actual, err := ioutil.ReadFile("testdata/users_employees.json") require.NoError(t, err) - // require.JSONEq(t, string(actual), body) - require.Equal(t, string(actual), body) + require.JSONEq(t, string(actual), body) }) }) } diff --git a/testdata/users.json b/testdata/users.json deleted file mode 100644 index 29cc3c6..0000000 --- a/testdata/users.json +++ /dev/null @@ -1 +0,0 @@ -{"components":{},"info":{"title":"test swagger title","version":"test swagger version"},"openapi":"3.0.0","paths":{"/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"}}}}}} \ No newline at end of file diff --git a/testdata/users_employees.json b/testdata/users_employees.json new file mode 100644 index 0000000..61e58ee --- /dev/null +++ b/testdata/users_employees.json @@ -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" + } + } + } + } + } +} diff --git a/validation.go b/validation.go deleted file mode 100644 index 2667f5d..0000000 --- a/validation.go +++ /dev/null @@ -1,19 +0,0 @@ -package swagger - -// 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) -// } From fd0b235774241f5db50cd91f86ef899df40c7bd2 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Wed, 14 Oct 2020 23:14:42 +0200 Subject: [PATCH 03/11] feat: remove ServeHTTP method. Use the external mux router method instead --- main_test.go | 14 +++++++------- router.go | 4 ---- router_test.go | 6 +++--- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/main_test.go b/main_test.go index e4f4015..ff3b76f 100644 --- a/main_test.go +++ b/main_test.go @@ -20,12 +20,12 @@ const ( func TestIntegration(t *testing.T) { t.Run("router works correctly", func(t *testing.T) { - router := setupSwagger(t) + muxRouter := setupSwagger(t) w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/hello", nil) - router.ServeHTTP(w, r) + muxRouter.ServeHTTP(w, r) require.Equal(t, http.StatusOK, w.Result().StatusCode) @@ -36,7 +36,7 @@ func TestIntegration(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil) - router.ServeHTTP(w, r) + muxRouter.ServeHTTP(w, r) require.Equal(t, http.StatusOK, w.Result().StatusCode) @@ -55,13 +55,13 @@ func readBody(t *testing.T, requestBody io.ReadCloser) string { return string(body) } -func setupSwagger(t *testing.T) *Router { +func setupSwagger(t *testing.T) *mux.Router { t.Helper() context := context.Background() - r := mux.NewRouter() + muxRouter := mux.NewRouter() - router, err := New(r, Options{ + router, err := New(muxRouter, Options{ Context: context, Openapi: &openapi3.Swagger{ Info: &openapi3.Info{ @@ -84,5 +84,5 @@ func setupSwagger(t *testing.T) *Router { err = router.GenerateAndExposeSwagger() require.NoError(t, err) - return router + return muxRouter } diff --git a/router.go b/router.go index 8405ced..99fe0b3 100644 --- a/router.go +++ b/router.go @@ -125,10 +125,6 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch return r.AddRawRoute(method, path, handler, Operation{operation}) } -func (r Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - r.router.ServeHTTP(w, req) -} - func (r Router) getSchemaFromInterface(v interface{}, components *openapi3.Components) (*openapi3.Schema, *openapi3.Components, error) { if v == nil { return &openapi3.Schema{}, components, nil diff --git a/router_test.go b/router_test.go index 2f2b29d..ad3b946 100644 --- a/router_test.go +++ b/router_test.go @@ -89,7 +89,7 @@ func TestAddRoute(t *testing.T) { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/users", nil) - router.ServeHTTP(w, req) + r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Result().StatusCode) @@ -98,9 +98,9 @@ func TestAddRoute(t *testing.T) { t.Run("and generate swagger", func(t *testing.T) { w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil) + req := httptest.NewRequest(http.MethodGet, JSONDocumentationPath, nil) - router.ServeHTTP(w, r) + r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Result().StatusCode) From 5e7af61ad866de3b656bbb73f60788c5f1a76ae6 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Wed, 14 Oct 2020 23:17:06 +0200 Subject: [PATCH 04/11] feat: remove unused option --- main.go | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index 6b78528..4724692 100644 --- a/main.go +++ b/main.go @@ -19,27 +19,20 @@ 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 - context context.Context - requiredFromJSONSchemaTags bool + router *mux.Router + SwaggerSchema *openapi3.Swagger + context context.Context } // Options to be passed to create the new router and swagger type Options struct { Context context.Context Openapi *openapi3.Swagger - - // RequiredFromJSONSchemaTags will generate a schema that requires any key - // tagged with `jsonschema:required`, overriding the - // default of requiring any key *not* tagged with `json:,omitempty`. - RequiredFromJSONSchemaTags bool } // New generate new router with swagger. Default to OpenAPI 3.0.0 @@ -55,10 +48,9 @@ func New(router *mux.Router, options Options) (*Router, error) { } return &Router{ - router: router, - SwaggerSchema: swagger, - context: ctx, - requiredFromJSONSchemaTags: options.RequiredFromJSONSchemaTags, + router: router, + SwaggerSchema: swagger, + context: ctx, }, nil } From abf3da2a6fed6b943ab2692881e528eb57f9c9f2 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Wed, 14 Oct 2020 23:54:19 +0200 Subject: [PATCH 05/11] test: complete New function test coverage --- integration_test.go | 88 ++++++++++++++++++++++ main.go | 12 ++- main_test.go | 180 +++++++++++++++++++++++++++----------------- router.go | 6 +- 4 files changed, 205 insertions(+), 81 deletions(-) create mode 100644 integration_test.go diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..ff3b76f --- /dev/null +++ b/integration_test.go @@ -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 +} diff --git a/main.go b/main.go index 4724692..e34172c 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,7 @@ const ( // Router handle the gorilla mux router and the swagger schema type Router struct { router *mux.Router - SwaggerSchema *openapi3.Swagger + swaggerSchema *openapi3.Swagger context context.Context } @@ -49,26 +49,24 @@ func New(router *mux.Router, options Options) (*Router, error) { return &Router{ router: router, - SwaggerSchema: swagger, + swaggerSchema: swagger, context: ctx, }, nil } 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") diff --git a/main_test.go b/main_test.go index ff3b76f..587a8a2 100644 --- a/main_test.go +++ b/main_test.go @@ -2,10 +2,7 @@ package swagger import ( "context" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" + "fmt" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -13,76 +10,117 @@ 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) { - 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`)) + info := &openapi3.Info{ + Title: "my title", + Version: "my version", + } + openapi := &openapi3.Swagger{ + Info: info, + Paths: openapi3.Paths{}, } - operation := Operation{} - _, err = router.AddRawRoute(http.MethodGet, "/hello", handler, operation) - require.NoError(t, err) + t.Run("not ok - invalid Openapi option", func(t *testing.T) { + r, err := New(mRouter, Options{}) - err = router.GenerateAndExposeSwagger() - require.NoError(t, err) + require.Nil(t, r) + require.EqualError(t, err, fmt.Sprintf("%s: swagger is required", ErrValidatingSwagger)) + }) - return muxRouter + 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) + }) } diff --git a/router.go b/router.go index 99fe0b3..83faba1 100644 --- a/router.go +++ b/router.go @@ -29,11 +29,11 @@ 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 { + if err := r.swaggerSchema.Validate(r.context); err != nil { return fmt.Errorf("%w: %s", ErrValidatingSwagger, err) } - jsonSwagger, err := r.SwaggerSchema.MarshalJSON() + jsonSwagger, err := r.swaggerSchema.MarshalJSON() if err != nil { return fmt.Errorf("%w: %s", ErrGenerateSwagger, err) } @@ -66,7 +66,7 @@ func (r Router) AddRawRoute(method string, path string, handler Handler, operati operation.Operation = openapi3.NewOperation() operation.Responses = openapi3.NewResponses() } - r.SwaggerSchema.AddOperation(path, method, operation.Operation) + 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. From e31c44beef69dc189bbb508a8dbf055b7197dc94 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Thu, 15 Oct 2020 00:39:53 +0200 Subject: [PATCH 06/11] test: add test to swagger generation. Fix returned content-type --- router.go | 9 +-------- router_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/router.go b/router.go index 83faba1..73018ea 100644 --- a/router.go +++ b/router.go @@ -37,20 +37,13 @@ func (r Router) GenerateAndExposeSwagger() error { 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.Header().Set("content-type", "application/json") w.Write(jsonSwagger) }) // TODO: add yaml endpoint - // TODO: enable validation - // err = r.swaggerRouter.AddSwagger(r.SwaggerSchema) - // if err != nil { - // return err - // } - return nil } diff --git a/router_test.go b/router_test.go index ad3b946..fda0a6a 100644 --- a/router_test.go +++ b/router_test.go @@ -2,9 +2,11 @@ package swagger import ( "context" + "fmt" "io/ioutil" "net/http" "net/http/httptest" + "strings" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -111,3 +113,53 @@ func TestAddRoute(t *testing.T) { }) }) } + +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() + 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) + actual, err := ioutil.ReadFile("testdata/users_employees.json") + require.NoError(t, err) + require.JSONEq(t, string(actual), body) + }) +} From 8f8a7dbf7db183348725875ec75962e630e10599 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Fri, 16 Oct 2020 09:46:27 +0200 Subject: [PATCH 07/11] feat: handle params and query in swagger --- README.md | 11 ++ router.go | 172 +++++++++++++---- router_test.go | 275 ++++++++++++++++++++-------- testdata/empty-route-schema.json | 19 ++ testdata/empty.json | 9 + testdata/multipart-requestbody.json | 58 ++++++ testdata/params.json | 57 ++++++ testdata/query.json | 29 +++ 8 files changed, 513 insertions(+), 117 deletions(-) create mode 100644 testdata/empty-route-schema.json create mode 100644 testdata/empty.json create mode 100644 testdata/multipart-requestbody.json create mode 100644 testdata/params.json create mode 100644 testdata/query.json diff --git a/README.md b/README.md index 167fa4a..0bef3ff 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,14 @@ 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 diff --git a/router.go b/router.go index 73018ea..47f8ad4 100644 --- a/router.go +++ b/router.go @@ -12,9 +12,13 @@ import ( var ( // ErrResponses is thrown if error occurs generating responses schemas. - ErrResponses = errors.New("errors generating responses") + ErrResponses = errors.New("errors generating responses schema") // ErrRequestBody is thrown if error occurs generating responses schemas. - ErrRequestBody = errors.New("errors generating request body") + 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 @@ -67,18 +71,25 @@ func (r Router) AddRawRoute(method string, path string, handler Handler, operati }).Methods(method), nil } -// Response is the struct containing a single route response. -type Response struct { - Value interface{} +// 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 { - // Parameters interface{} - // Querystring interface{} - RequestBody interface{} - Responses map[int]Response + PathParams map[string]SchemaValue + QueryParams map[string]SchemaValue + HeaderParams map[string]SchemaValue + CookieParams map[string]SchemaValue + RequestBody *SchemaValue + Responses map[int]SchemaValue } // AddRoute add a route with json schema inferted by passed schema. @@ -86,45 +97,37 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch operation := openapi3.NewOperation() operation.Responses = make(openapi3.Responses) - if schema.RequestBody != nil { - requestBody := openapi3.NewRequestBody() - - requestBodySchema, _, err := r.getSchemaFromInterface(schema.RequestBody, nil) - if err != nil { - return nil, fmt.Errorf("%w: %s", ErrRequestBody, err) - } - requestBody = requestBody.WithJSONSchema(requestBodySchema) - - operation.RequestBody = &openapi3.RequestBodyRef{ - Value: requestBody, - } + err := r.resolveRequestBodySchema(schema.RequestBody, operation) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrRequestBody, err) } - if schema.Responses != nil { - for statusCode, v := range schema.Responses { - response := openapi3.NewResponse() - responseSchema, _, err := r.getSchemaFromInterface(v.Value, nil) - if err != nil { - return nil, fmt.Errorf("%w: %s", ErrResponses, err) - } + err = r.resolveResponsesSchema(schema.Responses, operation) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrResponses, err) + } - response = response.WithDescription(v.Description) - response = response.WithJSONSchema(responseSchema) + err = r.resolvePathParamsSchema(schema.PathParams, operation) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrPathParams, err) + } - operation.AddResponse(statusCode, response) - } + err = r.resolveQuerySchema(schema.QueryParams, operation) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrQuerystring, err) } return r.AddRawRoute(method, path, handler, Operation{operation}) } -func (r Router) getSchemaFromInterface(v interface{}, components *openapi3.Components) (*openapi3.Schema, *openapi3.Components, error) { +func (r Router) getSchemaFromInterface(v interface{}, allowAdditionalProperties bool) (*openapi3.Schema, error) { if v == nil { - return &openapi3.Schema{}, components, nil + return &openapi3.Schema{}, nil } reflector := &jsonschema.Reflector{ - DoNotReference: true, + DoNotReference: true, + AllowAdditionalProperties: allowAdditionalProperties, } jsonSchema := reflector.Reflect(v) @@ -133,17 +136,108 @@ func (r Router) getSchemaFromInterface(v interface{}, components *openapi3.Compo // In the future, we could add an option to fill the components in openapi spec. jsonSchema.Definitions = nil - // jsonSchema = cleanJSONSchemaVersion(jsonSchema) data, err := jsonSchema.MarshalJSON() if err != nil { - return nil, nil, err + return nil, err } schema := openapi3.NewSchema() err = schema.UnmarshalJSON(data) if err != nil { - return nil, nil, err + return nil, err } - return schema, components, nil + 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) resolvePathParamsSchema(pathParams map[string]SchemaValue, operation *openapi3.Operation) error { + for k, v := range pathParams { + parameter := openapi3.NewPathParameter(k) + + if v.Content != nil { + schema, err := r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) + if err != nil { + return err + } + parameter = parameter.WithSchema(schema) + } + if v.Description != "" { + parameter = parameter.WithDescription(v.Description) + } + + operation.AddParameter(parameter) + } + + return nil +} + +func (r Router) resolveQuerySchema(qsParams map[string]SchemaValue, operation *openapi3.Operation) error { + for k, v := range qsParams { + queryParams := openapi3.NewQueryParameter(k) + + if v.Content != nil { + schema, err := r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) + if err != nil { + return err + } + queryParams = queryParams.WithSchema(schema) + } + if v.Description != "" { + queryParams = queryParams.WithDescription(v.Description) + } + + operation.AddParameter(queryParams) + } + + return nil } diff --git a/router_test.go b/router_test.go index fda0a6a..20bdf51 100644 --- a/router_test.go +++ b/router_test.go @@ -17,101 +17,220 @@ import ( func TestAddRoute(t *testing.T) { okHandler := func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`OK`)) + w.Write([]byte("OK")) } - t.Run("router works correctly - simple request body", func(t *testing.T) { - context := context.Background() - r := mux.NewRouter() - swagger := openapi3.Swagger{ + 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"` + } + + getBaseSwagger := func() *openapi3.Swagger { + return &openapi3.Swagger{ Info: &openapi3.Info{ Title: swaggerOpenapiTitle, Version: swaggerOpenapiVersion, }, } + } - router, err := New(r, Options{ - Context: context, - Openapi: &swagger, - }) - require.NoError(t, err) - require.NotNil(t, router) - - 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"` - } - - _, err = router.AddRoute(http.MethodPost, "/users", okHandler, Schema{ - RequestBody: &User{}, - Responses: map[int]Response{ - 201: { - Value: "", - }, - 401: { - Value: &errorResponse{}, - Description: "invalid request", - }, + 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) }, - }) - 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]Response{ - 200: { - Value: &Users{}, - }, + _, 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) }, - }) - require.NoError(t, err) - - _, err = router.AddRoute(http.MethodGet, "/employees", okHandler, Schema{ - Responses: map[int]Response{ - 200: { - Value: &Employees{}, - }, + 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) }, - }) - 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.GenerateAndExposeSwagger() - 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{ + QueryParams: map[string]SchemaValue{ + "projectId": { + Content: "", + Description: "projectId is the project id", + }, + }, + }) + require.NoError(t, err) + }, + testPath: "/projects", + fixturesPath: "testdata/query.json", + }, + } - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, "/users", nil) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + context := context.Background() + r := mux.NewRouter() - 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", 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("testdata/users_employees.json") + router, err := New(r, Options{ + Context: context, + Openapi: getBaseSwagger(), + }) require.NoError(t, err) - require.JSONEq(t, string(actual), body) + 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) + t.Log("actual json schema", body) + require.JSONEq(t, string(actual), body) + }) }) - }) + } } func TestGenerateAndExposeSwagger(t *testing.T) { diff --git a/testdata/empty-route-schema.json b/testdata/empty-route-schema.json new file mode 100644 index 0000000..2ad2a57 --- /dev/null +++ b/testdata/empty-route-schema.json @@ -0,0 +1,19 @@ +{ + "components": {}, + "info": { + "title": "test swagger title", + "version": "test swagger version" + }, + "openapi": "3.0.0", + "paths": { + "/": { + "post": { + "responses": { + "default": { + "description": "" + } + } + } + } + } +} diff --git a/testdata/empty.json b/testdata/empty.json new file mode 100644 index 0000000..6b479ba --- /dev/null +++ b/testdata/empty.json @@ -0,0 +1,9 @@ +{ + "components": {}, + "info": { + "title": "test swagger title", + "version": "test swagger version" + }, + "openapi": "3.0.0", + "paths": {} +} diff --git a/testdata/multipart-requestbody.json b/testdata/multipart-requestbody.json new file mode 100644 index 0000000..b1524d0 --- /dev/null +++ b/testdata/multipart-requestbody.json @@ -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" + } + } + } + } + } + } + } + } +} diff --git a/testdata/params.json b/testdata/params.json new file mode 100644 index 0000000..5b0efe4 --- /dev/null +++ b/testdata/params.json @@ -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": "" + } + } + } + } + } +} diff --git a/testdata/query.json b/testdata/query.json new file mode 100644 index 0000000..223167c --- /dev/null +++ b/testdata/query.json @@ -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": "" + } + } + } + } + } +} From 169ca8e4a34ea863f1005e0648f027726762d675 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Sat, 17 Oct 2020 00:13:02 +0200 Subject: [PATCH 08/11] feat: handle all oas3 parameters --- router.go | 78 +++++++++++++++++++++++-------------------- router_test.go | 40 +++++++++++++++++++++- testdata/cookies.json | 36 ++++++++++++++++++++ testdata/headers.json | 36 ++++++++++++++++++++ 4 files changed, 153 insertions(+), 37 deletions(-) create mode 100644 testdata/cookies.json create mode 100644 testdata/headers.json diff --git a/router.go b/router.go index 47f8ad4..df89913 100644 --- a/router.go +++ b/router.go @@ -84,14 +84,21 @@ type SchemaValue struct { // Schema of the route. type Schema struct { - PathParams map[string]SchemaValue - QueryParams map[string]SchemaValue - HeaderParams map[string]SchemaValue - CookieParams map[string]SchemaValue - RequestBody *SchemaValue - Responses map[int]SchemaValue + 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" + headersParamType = "headers" + 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() @@ -107,14 +114,24 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch return nil, fmt.Errorf("%w: %s", ErrResponses, err) } - err = r.resolvePathParamsSchema(schema.PathParams, operation) + err = r.resolveParameterSchema(pathParamsType, schema.PathParams, operation) if err != nil { return nil, fmt.Errorf("%w: %s", ErrPathParams, err) } - err = r.resolveQuerySchema(schema.QueryParams, operation) + err = r.resolveParameterSchema(queryParamType, schema.Querystring, operation) if err != nil { - return nil, fmt.Errorf("%w: %s", ErrQuerystring, err) + return nil, fmt.Errorf("%w: %s", ErrPathParams, err) + } + + err = r.resolveParameterSchema(headersParamType, 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}) @@ -200,43 +217,32 @@ func (r Router) resolveResponsesSchema(responses map[int]SchemaValue, operation return nil } -func (r Router) resolvePathParamsSchema(pathParams map[string]SchemaValue, operation *openapi3.Operation) error { - for k, v := range pathParams { - parameter := openapi3.NewPathParameter(k) +func (r Router) resolveParameterSchema(paramType string, paramConfig map[string]SchemaValue, operation *openapi3.Operation) error { + for k, v := range paramConfig { + var param *openapi3.Parameter + switch paramType { + case "path": + param = openapi3.NewPathParameter(k) + case "query": + param = openapi3.NewQueryParameter(k) + case "headers": + param = openapi3.NewHeaderParameter(k) + case "cookie": + param = openapi3.NewCookieParameter(k) + } if v.Content != nil { schema, err := r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) if err != nil { return err } - parameter = parameter.WithSchema(schema) + param = param.WithSchema(schema) } if v.Description != "" { - parameter = parameter.WithDescription(v.Description) + param = param.WithDescription(v.Description) } - operation.AddParameter(parameter) - } - - return nil -} - -func (r Router) resolveQuerySchema(qsParams map[string]SchemaValue, operation *openapi3.Operation) error { - for k, v := range qsParams { - queryParams := openapi3.NewQueryParameter(k) - - if v.Content != nil { - schema, err := r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) - if err != nil { - return err - } - queryParams = queryParams.WithSchema(schema) - } - if v.Description != "" { - queryParams = queryParams.WithDescription(v.Description) - } - - operation.AddParameter(queryParams) + operation.AddParameter(param) } return nil diff --git a/router_test.go b/router_test.go index 20bdf51..a51ce69 100644 --- a/router_test.go +++ b/router_test.go @@ -168,7 +168,7 @@ func TestAddRoute(t *testing.T) { name: "schema with querystring", routes: func(t *testing.T, router *Router) { _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{ - QueryParams: map[string]SchemaValue{ + Querystring: map[string]SchemaValue{ "projectId": { Content: "", Description: "projectId is the project id", @@ -180,6 +180,44 @@ func TestAddRoute(t *testing.T) { 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", + }, } for _, test := range tests { diff --git a/testdata/cookies.json b/testdata/cookies.json new file mode 100644 index 0000000..b9843f0 --- /dev/null +++ b/testdata/cookies.json @@ -0,0 +1,36 @@ +{ + "components": {}, + "info": { + "title": "test swagger title", + "version": "test swagger version" + }, + "openapi": "3.0.0", + "paths": { + "/projects": { + "get": { + "parameters": [ + { + "description": "boolean. Set 0 to disable and 1 to enable", + "in": "cookie", + "name": "debug", + "schema": { + "type": "integer" + } + }, + { + "in": "cookie", + "name": "csrftoken", + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + } + } +} diff --git a/testdata/headers.json b/testdata/headers.json new file mode 100644 index 0000000..bb37342 --- /dev/null +++ b/testdata/headers.json @@ -0,0 +1,36 @@ +{ + "components": {}, + "info": { + "title": "test swagger title", + "version": "test swagger version" + }, + "openapi": "3.0.0", + "paths": { + "/projects": { + "get": { + "parameters": [ + { + "description": "foo description", + "in": "header", + "name": "foo", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "bar", + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + } + } +} From 8dff4afe096fbef9363058bbc7c08bb185047614 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Sat, 17 Oct 2020 00:50:15 +0200 Subject: [PATCH 09/11] feat: sort params in creation. In this manner, the params order is the ever same --- go.mod | 1 + go.sum | 2 ++ router.go | 18 +++++++++++++----- testdata/cookies.json | 14 +++++++------- testdata/headers.json | 6 +++--- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 1bfcc4e..39086db 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,6 @@ 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/iancoleman/orderedmap v0.1.0 // indirect github.com/stretchr/testify v1.6.1 ) diff --git a/go.sum b/go.sum index e51d617..a05fd2a 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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/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= diff --git a/router.go b/router.go index df89913..9db0b85 100644 --- a/router.go +++ b/router.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "sort" "github.com/alecthomas/jsonschema" "github.com/getkin/kin-openapi/openapi3" @@ -218,17 +219,24 @@ func (r Router) resolveResponsesSchema(responses map[int]SchemaValue, operation } func (r Router) resolveParameterSchema(paramType string, paramConfig map[string]SchemaValue, operation *openapi3.Operation) error { - for k, v := range paramConfig { + 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 "path": - param = openapi3.NewPathParameter(k) + param = openapi3.NewPathParameter(key) case "query": - param = openapi3.NewQueryParameter(k) + param = openapi3.NewQueryParameter(key) case "headers": - param = openapi3.NewHeaderParameter(k) + param = openapi3.NewHeaderParameter(key) case "cookie": - param = openapi3.NewCookieParameter(k) + param = openapi3.NewCookieParameter(key) } if v.Content != nil { diff --git a/testdata/cookies.json b/testdata/cookies.json index b9843f0..47bb859 100644 --- a/testdata/cookies.json +++ b/testdata/cookies.json @@ -9,6 +9,13 @@ "/projects": { "get": { "parameters": [ + { + "in": "cookie", + "name": "csrftoken", + "schema": { + "type": "string" + } + }, { "description": "boolean. Set 0 to disable and 1 to enable", "in": "cookie", @@ -16,13 +23,6 @@ "schema": { "type": "integer" } - }, - { - "in": "cookie", - "name": "csrftoken", - "schema": { - "type": "string" - } } ], "responses": { diff --git a/testdata/headers.json b/testdata/headers.json index bb37342..7904e24 100644 --- a/testdata/headers.json +++ b/testdata/headers.json @@ -10,16 +10,16 @@ "get": { "parameters": [ { - "description": "foo description", "in": "header", - "name": "foo", + "name": "bar", "schema": { "type": "string" } }, { + "description": "foo description", "in": "header", - "name": "bar", + "name": "foo", "schema": { "type": "string" } From 24755b84e9a4f40f8b330135f899087110f5998b Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Sun, 18 Oct 2020 00:30:34 +0200 Subject: [PATCH 10/11] feat: fixed and add test to route --- main.go | 22 + main_test.go | 54 +++ router.go => route.go | 49 +-- route_test.go | 723 ++++++++++++++++++++++++++++++++ router_test.go | 322 -------------- testdata/schema-no-content.json | 55 +++ 6 files changed, 870 insertions(+), 355 deletions(-) rename router.go => route.go (85%) create mode 100644 route_test.go delete mode 100644 router_test.go create mode 100644 testdata/schema-no-content.json diff --git a/main.go b/main.go index e34172c..053bf08 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "github.com/getkin/kin-openapi/openapi3" "github.com/gorilla/mux" @@ -77,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 +} diff --git a/main_test.go b/main_test.go index 587a8a2..aaa25aa 100644 --- a/main_test.go +++ b/main_test.go @@ -3,6 +3,10 @@ package swagger import ( "context" "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -124,3 +128,53 @@ func TestGenerateValidSwagger(t *testing.T) { }, 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() + 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) + actual, err := ioutil.ReadFile("testdata/users_employees.json") + require.NoError(t, err) + require.JSONEq(t, string(actual), body) + }) +} diff --git a/router.go b/route.go similarity index 85% rename from router.go rename to route.go index 9db0b85..4bfd1c2 100644 --- a/router.go +++ b/route.go @@ -25,33 +25,11 @@ var ( // Operation type type Operation struct { *openapi3.Operation - // TODO: handle request and response } // 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.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(jsonSwagger) - }) - // TODO: add yaml endpoint - - return nil -} - // 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) { @@ -94,10 +72,10 @@ type Schema struct { } const ( - pathParamsType = "path" - queryParamType = "query" - headersParamType = "headers" - cookieParamType = "cookie" + pathParamsType = "path" + queryParamType = "query" + headerParamType = "header" + cookieParamType = "cookie" ) // AddRoute add a route with json schema inferted by passed schema. @@ -125,7 +103,7 @@ func (r Router) AddRoute(method string, path string, handler Handler, schema Sch return nil, fmt.Errorf("%w: %s", ErrPathParams, err) } - err = r.resolveParameterSchema(headersParamType, schema.Headers, operation) + err = r.resolveParameterSchema(headerParamType, schema.Headers, operation) if err != nil { return nil, fmt.Errorf("%w: %s", ErrPathParams, err) } @@ -229,23 +207,28 @@ func (r Router) resolveParameterSchema(paramType string, paramConfig map[string] v := paramConfig[key] var param *openapi3.Parameter switch paramType { - case "path": + case pathParamsType: param = openapi3.NewPathParameter(key) - case "query": + case queryParamType: param = openapi3.NewQueryParameter(key) - case "headers": + case headerParamType: param = openapi3.NewHeaderParameter(key) - case "cookie": + case cookieParamType: param = openapi3.NewCookieParameter(key) + default: + return fmt.Errorf("invalid param type") } + schema := openapi3.NewSchema() if v.Content != nil { - schema, err := r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) + var err error + schema, err = r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) if err != nil { return err } - param = param.WithSchema(schema) } + param = param.WithSchema(schema) + if v.Description != "" { param = param.WithDescription(v.Description) } diff --git a/route_test.go b/route_test.go new file mode 100644 index 0000000..7b4a031 --- /dev/null +++ b/route_test.go @@ -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, + }, + } +} diff --git a/router_test.go b/router_test.go deleted file mode 100644 index a51ce69..0000000 --- a/router_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package swagger - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/getkin/kin-openapi/openapi3" - "github.com/gorilla/mux" - "github.com/stretchr/testify/require" -) - -func TestAddRoute(t *testing.T) { - okHandler := func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - } - - 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"` - } - - getBaseSwagger := func() *openapi3.Swagger { - return &openapi3.Swagger{ - Info: &openapi3.Info{ - Title: swaggerOpenapiTitle, - Version: swaggerOpenapiVersion, - }, - } - } - - 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", - }, - } - - 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(), - }) - 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) - t.Log("actual json schema", body) - require.JSONEq(t, string(actual), body) - }) - }) - } -} - -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() - 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) - actual, err := ioutil.ReadFile("testdata/users_employees.json") - require.NoError(t, err) - require.JSONEq(t, string(actual), body) - }) -} diff --git a/testdata/schema-no-content.json b/testdata/schema-no-content.json new file mode 100644 index 0000000..9835a68 --- /dev/null +++ b/testdata/schema-no-content.json @@ -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": "" + } + } + } + } + } +} From 185f2d113b546aa6aa8f925a5e97cb38bc7141b3 Mon Sep 17 00:00:00 2001 From: Davide Bianchi Date: Sun, 18 Oct 2020 00:31:42 +0200 Subject: [PATCH 11/11] run go mod tidy --- go.sum | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/go.sum b/go.sum index a05fd2a..983a16d 100644 --- a/go.sum +++ b/go.sum @@ -1,60 +1,29 @@ -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/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/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/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= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -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= +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=