diff --git a/route.go b/route.go index 4bfd1c2..bdf789e 100644 --- a/route.go +++ b/route.go @@ -50,25 +50,38 @@ func (r Router) AddRawRoute(method string, path string, handler Handler, operati }).Methods(method), nil } -// SchemaValue is the struct containing the schema information. -type SchemaValue struct { - Content interface{} - Description string +// Content is the type of a content. +// The key of the map define the content-type. +type Content map[string]Schema - // ContentType is to be used only with RequestBody. Valid ContentType - // are application/json or multipart/form-data. - ContentType string +// Schema contains the value and if properties allow additional properties. +type Schema struct { + Value interface{} AllowAdditionalProperties bool } -// Schema of the route. -type Schema struct { - PathParams map[string]SchemaValue - Querystring map[string]SchemaValue - Headers map[string]SchemaValue - Cookies map[string]SchemaValue - RequestBody *SchemaValue - Responses map[int]SchemaValue +// ParameterValue is the struct containing the schema or the content information. +// If content is specified, it takes precedence. +type ParameterValue map[string]struct { + Content Content + Schema *Schema + Description string +} + +// ContentValue is the struct containing the content information. +type ContentValue struct { + Content Content + Description string +} + +// Definitions of the route. +type Definitions struct { + PathParams ParameterValue + Querystring ParameterValue + Headers ParameterValue + Cookies ParameterValue + RequestBody *ContentValue + Responses map[int]ContentValue } const ( @@ -79,7 +92,7 @@ const ( ) // 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) { +func (r Router) AddRoute(method string, path string, handler Handler, schema Definitions) (*mux.Route, error) { operation := openapi3.NewOperation() operation.Responses = make(openapi3.Responses) @@ -146,24 +159,17 @@ func (r Router) getSchemaFromInterface(v interface{}, allowAdditionalProperties return schema, nil } -func (r Router) resolveRequestBodySchema(bodySchema *SchemaValue, operation *openapi3.Operation) error { +func (r Router) resolveRequestBodySchema(bodySchema *ContentValue, operation *openapi3.Operation) error { if bodySchema == nil { return nil } - requestBodySchema, err := r.getSchemaFromInterface(bodySchema.Content, bodySchema.AllowAdditionalProperties) + requestBody := openapi3.NewRequestBody() + + content, err := r.addContentToOASSchema(bodySchema.Content) 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") - } + requestBody = requestBody.WithContent(content) if bodySchema.Description != "" { requestBody.WithDescription(bodySchema.Description) @@ -175,20 +181,19 @@ func (r Router) resolveRequestBodySchema(bodySchema *SchemaValue, operation *ope return nil } -func (r Router) resolveResponsesSchema(responses map[int]SchemaValue, operation *openapi3.Operation) error { +func (r Router) resolveResponsesSchema(responses map[int]ContentValue, 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) + content, err := r.addContentToOASSchema(v.Content) if err != nil { return err } + response = response.WithContent(content) response = response.WithDescription(v.Description) - response = response.WithJSONSchema(responseSchema) operation.AddResponse(statusCode, response) } @@ -196,7 +201,7 @@ func (r Router) resolveResponsesSchema(responses map[int]SchemaValue, operation return nil } -func (r Router) resolveParameterSchema(paramType string, paramConfig map[string]SchemaValue, operation *openapi3.Operation) error { +func (r Router) resolveParameterSchema(paramType string, paramConfig ParameterValue, operation *openapi3.Operation) error { var keys = make([]string, 0, len(paramConfig)) for k := range paramConfig { keys = append(keys, k) @@ -219,18 +224,26 @@ func (r Router) resolveParameterSchema(paramType string, paramConfig map[string] return fmt.Errorf("invalid param type") } - schema := openapi3.NewSchema() + if v.Description != "" { + param = param.WithDescription(v.Description) + } + if v.Content != nil { - var err error - schema, err = r.getSchemaFromInterface(v.Content, v.AllowAdditionalProperties) + content, err := r.addContentToOASSchema(v.Content) if err != nil { return err } - } - param = param.WithSchema(schema) - - if v.Description != "" { - param = param.WithDescription(v.Description) + param.Content = content + } else { + schema := openapi3.NewSchema() + if v.Schema != nil { + var err error + schema, err = r.getSchemaFromInterface(v.Schema.Value, v.Schema.AllowAdditionalProperties) + if err != nil { + return err + } + } + param.WithSchema(schema) } operation.AddParameter(param) @@ -238,3 +251,16 @@ func (r Router) resolveParameterSchema(paramType string, paramConfig map[string] return nil } + +func (r Router) addContentToOASSchema(content Content) (openapi3.Content, error) { + oasContent := openapi3.NewContent() + for k, v := range content { + var err error + schema, err := r.getSchemaFromInterface(v.Value, v.AllowAdditionalProperties) + if err != nil { + return nil, err + } + oasContent[k] = openapi3.NewMediaType().WithSchema(schema) + } + return oasContent, nil +} diff --git a/route_test.go b/route_test.go index 7b4a031..0d7c726 100644 --- a/route_test.go +++ b/route_test.go @@ -58,7 +58,7 @@ func TestAddRoute(t *testing.T) { { name: "empty route schema", routes: func(t *testing.T, router *Router) { - _, err := router.AddRoute(http.MethodPost, "/", okHandler, Schema{}) + _, err := router.AddRoute(http.MethodPost, "/", okHandler, Definitions{}) require.NoError(t, err) }, testPath: "/", @@ -68,35 +68,45 @@ func TestAddRoute(t *testing.T) { { name: "multiple real routes", routes: func(t *testing.T, router *Router) { - _, err := router.AddRoute(http.MethodPost, "/users", okHandler, Schema{ - RequestBody: &SchemaValue{ - Content: User{}, + _, err := router.AddRoute(http.MethodPost, "/users", okHandler, Definitions{ + RequestBody: &ContentValue{ + Content: Content{ + "application/json": {Value: User{}}, + }, }, - Responses: map[int]SchemaValue{ + Responses: map[int]ContentValue{ 201: { - Content: "", + Content: Content{ + "text/html": {Value: ""}, + }, }, 401: { - Content: &errorResponse{}, + Content: Content{ + "application/json": {Value: &errorResponse{}}, + }, Description: "invalid request", }, }, }) require.NoError(t, err) - _, err = router.AddRoute(http.MethodGet, "/users", okHandler, Schema{ - Responses: map[int]SchemaValue{ + _, err = router.AddRoute(http.MethodGet, "/users", okHandler, Definitions{ + Responses: map[int]ContentValue{ 200: { - Content: &Users{}, + Content: Content{ + "application/json": {Value: &Users{}}, + }, }, }, }) require.NoError(t, err) - _, err = router.AddRoute(http.MethodGet, "/employees", okHandler, Schema{ - Responses: map[int]SchemaValue{ + _, err = router.AddRoute(http.MethodGet, "/employees", okHandler, Definitions{ + Responses: map[int]ContentValue{ 200: { - Content: &Employees{}, + Content: Content{ + "application/json": {Value: &Employees{}}, + }, }, }, }) @@ -108,15 +118,24 @@ func TestAddRoute(t *testing.T) { { 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, + _, err := router.AddRoute(http.MethodPost, "/files", okHandler, Definitions{ + RequestBody: &ContentValue{ + Content: Content{ + "multipart/form-data": { + Value: &FormData{}, + AllowAdditionalProperties: true, + }, + }, + Description: "upload file", }, - Responses: map[int]SchemaValue{ - 200: {Content: ""}, + Responses: map[int]ContentValue{ + 200: { + Content: Content{ + "application/json": { + Value: "", + }, + }, + }, }, }) require.NoError(t, err) @@ -129,23 +148,23 @@ func TestAddRoute(t *testing.T) { 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{ + _, err := router.AddRoute(http.MethodGet, "/users/{userId}", okHandler, Definitions{ + PathParams: ParameterValue{ "userId": { - Content: number, + Schema: &Schema{Value: 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{ + _, err = router.AddRoute(http.MethodGet, "/cars/{carId}/drivers/{driverId}", okHandler, Definitions{ + PathParams: ParameterValue{ "carId": { - Content: "", + Schema: &Schema{Value: ""}, }, "driverId": { - Content: "", + Schema: &Schema{Value: ""}, }, }, }) @@ -157,10 +176,10 @@ func TestAddRoute(t *testing.T) { { name: "schema with querystring", routes: func(t *testing.T, router *Router) { - _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{ - Querystring: map[string]SchemaValue{ + _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Definitions{ + Querystring: ParameterValue{ "projectId": { - Content: "", + Schema: &Schema{Value: ""}, Description: "projectId is the project id", }, }, @@ -173,14 +192,14 @@ func TestAddRoute(t *testing.T) { { name: "schema with headers", routes: func(t *testing.T, router *Router) { - _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{ - Headers: map[string]SchemaValue{ + _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Definitions{ + Headers: ParameterValue{ "foo": { - Content: "", + Schema: &Schema{Value: ""}, Description: "foo description", }, "bar": { - Content: "", + Schema: &Schema{Value: ""}, }, }, }) @@ -192,14 +211,14 @@ func TestAddRoute(t *testing.T) { { name: "schema with cookies", routes: func(t *testing.T, router *Router) { - _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Schema{ - Cookies: map[string]SchemaValue{ + _, err := router.AddRoute(http.MethodGet, "/projects", okHandler, Definitions{ + Cookies: ParameterValue{ "debug": { - Content: 0, + Schema: &Schema{Value: 0}, Description: "boolean. Set 0 to disable and 1 to enable", }, "csrftoken": { - Content: "", + Schema: &Schema{Value: ""}, }, }, }) @@ -209,25 +228,25 @@ func TestAddRoute(t *testing.T) { fixturesPath: "testdata/cookies.json", }, { - name: "schema defined without content", + name: "schema defined without value", routes: func(t *testing.T, router *Router) { - _, err := router.AddRoute(http.MethodPost, "/{id}", okHandler, Schema{ - RequestBody: &SchemaValue{ + _, err := router.AddRoute(http.MethodPost, "/{id}", okHandler, Definitions{ + RequestBody: &ContentValue{ Description: "request body without schema", }, - Responses: map[int]SchemaValue{ + Responses: map[int]ContentValue{ 204: {}, }, - PathParams: map[string]SchemaValue{ + PathParams: ParameterValue{ "id": {}, }, - Querystring: map[string]SchemaValue{ + Querystring: ParameterValue{ "q": {}, }, - Headers: map[string]SchemaValue{ + Headers: ParameterValue{ "key": {}, }, - Cookies: map[string]SchemaValue{ + Cookies: ParameterValue{ "cookie1": {}, }, }) @@ -283,7 +302,7 @@ func TestAddRoute(t *testing.T) { 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)) + require.JSONEq(t, string(actual), body, "actual json data: %s", string(actual)) }) }) } @@ -299,7 +318,7 @@ func TestResolveRequestBodySchema(t *testing.T) { } tests := []struct { name string - bodySchema *SchemaValue + bodySchema *ContentValue expectedErr error expectedJSON string }{ @@ -311,9 +330,12 @@ func TestResolveRequestBodySchema(t *testing.T) { { name: "schema multipart", expectedErr: nil, - bodySchema: &SchemaValue{ - Content: &TestStruct{}, - ContentType: "multipart/form-data", + bodySchema: &ContentValue{ + Content: Content{ + "multipart/form-data": { + Value: &TestStruct{}, + }, + }, }, expectedJSON: `{ "requestBody": { @@ -335,32 +357,10 @@ func TestResolveRequestBodySchema(t *testing.T) { { 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"} - } - } - } - } + bodySchema: &ContentValue{ + Content: Content{ + "application/json": {Value: &TestStruct{}}, }, - "responses": null - }`, - }, - { - name: "no content-type - default to json", - expectedErr: nil, - bodySchema: &SchemaValue{ - Content: &TestStruct{}, }, expectedJSON: `{ "requestBody": { @@ -382,8 +382,12 @@ func TestResolveRequestBodySchema(t *testing.T) { { name: "with description", expectedErr: nil, - bodySchema: &SchemaValue{ - Content: &TestStruct{}, + bodySchema: &ContentValue{ + Content: Content{ + "application/json": { + Value: &TestStruct{}, + }, + }, Description: "my custom description", }, expectedJSON: `{ @@ -404,23 +408,52 @@ func TestResolveRequestBodySchema(t *testing.T) { "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", + name: "content type text/plain", + bodySchema: &ContentValue{ + Content: Content{ + "text/plain": {Value: ""}, + }, }, + expectedJSON: `{ + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type":"string" + } + } + } + }, + "responses": null + }`, }, - // 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: "*/*", + name: "generic content type - it represent all types", + bodySchema: &ContentValue{ + Content: Content{ + "*/*": { + Value: &TestStruct{}, + AllowAdditionalProperties: true, + }, + }, }, + expectedJSON: `{ + "requestBody": { + "content": { + "*/*": { + "schema": { + "type":"object", + "properties": { + "id": {"type": "string"} + }, + "additionalProperties": true + } + } + } + }, + "responses": null + }`, }, } @@ -439,7 +472,7 @@ func TestResolveRequestBodySchema(t *testing.T) { if err == nil { data, _ := operation.MarshalJSON() jsonData := string(data) - require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: ", jsonData) + require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: %s", jsonData) require.NoError(t, err) } require.Equal(t, test.expectedErr, err) @@ -453,7 +486,7 @@ func TestResolveResponsesSchema(t *testing.T) { } tests := []struct { name string - responsesSchema map[int]SchemaValue + responsesSchema map[int]ContentValue expectedErr error expectedJSON string }{ @@ -464,9 +497,11 @@ func TestResolveResponsesSchema(t *testing.T) { }, { name: "with 1 status code", - responsesSchema: map[int]SchemaValue{ + responsesSchema: map[int]ContentValue{ 200: { - Content: &TestStruct{}, + Content: Content{ + "application/json": {Value: &TestStruct{}}, + }, }, }, expectedErr: nil, @@ -493,12 +528,16 @@ func TestResolveResponsesSchema(t *testing.T) { }, { name: "with more status codes", - responsesSchema: map[int]SchemaValue{ + responsesSchema: map[int]ContentValue{ 200: { - Content: &TestStruct{}, + Content: Content{ + "application/json": {Value: &TestStruct{}}, + }, }, 400: { - Content: "", + Content: Content{ + "application/json": {Value: ""}, + }, }, }, expectedErr: nil, @@ -535,9 +574,11 @@ func TestResolveResponsesSchema(t *testing.T) { }, { name: "with custom description", - responsesSchema: map[int]SchemaValue{ + responsesSchema: map[int]ContentValue{ 400: { - Content: "", + Content: Content{ + "application/json": {Value: ""}, + }, Description: "a description", }, }, @@ -575,7 +616,7 @@ func TestResolveResponsesSchema(t *testing.T) { if err == nil { data, _ := operation.MarshalJSON() jsonData := string(data) - require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: ", jsonData) + require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: %s", jsonData) require.NoError(t, err) } require.Equal(t, test.expectedErr, err) @@ -589,7 +630,7 @@ func TestResolveParametersSchema(t *testing.T) { } tests := []struct { name string - paramsSchema map[string]SchemaValue + paramsSchema ParameterValue paramType string expectedErr error expectedJSON string @@ -602,9 +643,11 @@ func TestResolveParametersSchema(t *testing.T) { { name: "path param", paramType: pathParamsType, - paramsSchema: map[string]SchemaValue{ + paramsSchema: ParameterValue{ "foo": { - Content: "", + Schema: &Schema{ + Value: "", + }, }, }, expectedJSON: `{ @@ -622,9 +665,11 @@ func TestResolveParametersSchema(t *testing.T) { { name: "query param", paramType: queryParamType, - paramsSchema: map[string]SchemaValue{ + paramsSchema: ParameterValue{ "foo": { - Content: "", + Schema: &Schema{ + Value: "", + }, }, }, expectedJSON: `{ @@ -641,9 +686,11 @@ func TestResolveParametersSchema(t *testing.T) { { name: "cookie param", paramType: cookieParamType, - paramsSchema: map[string]SchemaValue{ + paramsSchema: ParameterValue{ "foo": { - Content: "", + Schema: &Schema{ + Value: "", + }, }, }, expectedJSON: `{ @@ -660,9 +707,11 @@ func TestResolveParametersSchema(t *testing.T) { { name: "header param", paramType: headerParamType, - paramsSchema: map[string]SchemaValue{ + paramsSchema: ParameterValue{ "foo": { - Content: "", + Schema: &Schema{ + Value: "", + }, }, }, expectedJSON: `{ @@ -679,13 +728,46 @@ func TestResolveParametersSchema(t *testing.T) { { name: "wrong param type", paramType: "wrong", - paramsSchema: map[string]SchemaValue{ + paramsSchema: ParameterValue{ "foo": { - Content: "", + Schema: &Schema{ + Value: "", + }, }, }, expectedErr: fmt.Errorf("invalid param type"), }, + { + name: "content param", + paramType: "query", + paramsSchema: ParameterValue{ + "foo": { + Content: Content{ + "application/json": { + Value: &TestStruct{}, + }, + }, + }, + }, + expectedJSON: `{ + "parameters": [{ + "in": "query", + "name": "foo", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "additionalProperties": false + } + } + } + }], + "responses": null + }`, + }, } mux := mux.NewRouter() @@ -703,7 +785,7 @@ func TestResolveParametersSchema(t *testing.T) { if err == nil { data, _ := operation.MarshalJSON() jsonData := string(data) - require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: ", jsonData) + require.JSONEq(t, test.expectedJSON, jsonData, "actual json data: %s", jsonData) require.NoError(t, err) } require.Equal(t, test.expectedErr, err) diff --git a/testdata/schema-no-content.json b/testdata/schema-no-content.json index 9835a68..1008ef8 100644 --- a/testdata/schema-no-content.json +++ b/testdata/schema-no-content.json @@ -32,20 +32,10 @@ } ], "requestBody": { - "content": { - "application/json": { - "schema": {} - } - }, "description": "request body without schema" }, "responses": { "204": { - "content": { - "application/json": { - "schema": {} - } - }, "description": "" } } diff --git a/testdata/users_employees.json b/testdata/users_employees.json index 61e58ee..364a236 100644 --- a/testdata/users_employees.json +++ b/testdata/users_employees.json @@ -162,7 +162,7 @@ "responses": { "201": { "content": { - "application/json": { + "text/html": { "schema": { "type": "string" }