Skip to content

Commit

Permalink
feat: add route.RequestContentType
Browse files Browse the repository at this point in the history
Provides the ability for the user to explicitly
set what content types and request body can be.
Maintains the defaullt of `application/json` and `application/xml`
  • Loading branch information
dylanhitt committed Aug 29, 2024
1 parent 57abbc3 commit e514438
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 16 deletions.
2 changes: 2 additions & 0 deletions examples/petstore/controllers/pets.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func (rs PetsRessources) Routes(s *fuego.Server) {
fuego.Get(petsGroup, "/{id}", rs.getPets)
fuego.Get(petsGroup, "/by-name/{name...}", rs.getPetByName)
fuego.Put(petsGroup, "/{id}", rs.putPets)
fuego.Put(petsGroup, "/{id}/json", rs.putPets).
RequestContentType("application/json")
fuego.Delete(petsGroup, "/{id}", rs.deletePets)
}

Expand Down
79 changes: 79 additions & 0 deletions examples/petstore/lib/testdata/doc/openapi.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,85 @@
"pets"
]
}
},
"/pets/{id}/json": {
"put": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsRessources.putPets`\n\n---\n\n",
"operationId": "PUT_/pets/:id/json",
"parameters": [
{
"description": "header description",
"in": "header",
"name": "X-Header",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PetsUpdate"
}
}
},
"description": "Request body for models.PetsUpdate",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Bad Request _(validation or deserialization error)_"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPError"
}
}
},
"description": "Internal Server Error _(panics)_"
},
"default": {
"description": ""
}
},
"summary": "put pets",
"tags": [
"pets"
]
}
}
},
"servers": [
Expand Down
38 changes: 22 additions & 16 deletions openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,23 +169,21 @@ func RegisterOpenAPIOperation[T, B any](s *Server, route Route[T, B]) (*openapi3
route.Param(param.Type, param.Name, param.Description, param.OpenAPIParamOption)
}

// Request body
bodyTag := schemaTagFromType(s, *new(B))
if bodyTag.name != "unknown-interface" {
content := openapi3.NewContentWithSchemaRef(&bodyTag.SchemaRef, []string{"application/json", "application/xml"})
requestBody := openapi3.NewRequestBody().
WithRequired(true).
WithDescription("Request body for " + reflect.TypeOf(*new(B)).String()).
WithContent(content)

s.OpenApiSpec.Components.RequestBodies[bodyTag.name] = &openapi3.RequestBodyRef{
Value: requestBody,
}
// Request Body
if route.Operation.RequestBody == nil {
bodyTag := schemaTagFromType(s, *new(B))

if bodyTag.name != "unknown-interface" {
requestBody := newRequestBody[B](bodyTag, []string{"application/json", "application/xml"})
s.OpenApiSpec.Components.RequestBodies[bodyTag.name] = &openapi3.RequestBodyRef{
Value: requestBody,
}

// add request body to operation
route.Operation.RequestBody = &openapi3.RequestBodyRef{
Ref: "#/components/requestBodies/" + bodyTag.name,
Value: requestBody,
// add request body to operation
route.Operation.RequestBody = &openapi3.RequestBodyRef{
Ref: "#/components/requestBodies/" + bodyTag.name,
Value: requestBody,
}
}
}

Expand Down Expand Up @@ -216,6 +214,14 @@ func RegisterOpenAPIOperation[T, B any](s *Server, route Route[T, B]) (*openapi3
return route.Operation, nil
}

func newRequestBody[RequestBody any](tag schemaTag, consumes []string) *openapi3.RequestBody {
content := openapi3.NewContentWithSchemaRef(&tag.SchemaRef, consumes)
return openapi3.NewRequestBody().
WithRequired(true).
WithDescription("Request body for " + reflect.TypeOf(*new(RequestBody)).String()).
WithContent(content)
}

// schemaTag is a struct that holds the name of the struct and the associated openapi3.SchemaRef
type schemaTag struct {
openapi3.SchemaRef
Expand Down
17 changes: 17 additions & 0 deletions openapi_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ func (r Route[ResponseBody, RequestBody]) Tags(tags ...string) Route[ResponseBod
return r
}

// Replace the available request Content-Types for the route.
// By default, the request Content-Types are `application/json` and `application/xml`
func (r Route[ResponseBody, RequestBody]) RequestContentType(consumes ...string) Route[ResponseBody, RequestBody] {
bodyTag := schemaTagFromType(r.mainRouter, *new(RequestBody))

if bodyTag.name != "unknown-interface" {
requestBody := newRequestBody[RequestBody](bodyTag, consumes)

// set just Value as we do not want to reference
// a global requestBody
r.Operation.RequestBody = &openapi3.RequestBodyRef{
Value: requestBody,
}
}
return r
}

// AddTags adds tags to the route.
func (r Route[ResponseBody, RequestBody]) AddTags(tags ...string) Route[ResponseBody, RequestBody] {
r.Operation.Tags = append(r.Operation.Tags, tags...)
Expand Down
29 changes: 29 additions & 0 deletions openapi_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,35 @@ func TestHeaderParams(t *testing.T) {
require.Equal(t, "my description", route.Operation.Parameters.GetByInAndName("header", "my-header").Description)
}

func TestRequestContentType(t *testing.T) {
t.Run("base", func(t *testing.T) {
s := NewServer()
route := Post(s, "/test", testControllerWithBody).
RequestContentType("application/json")

content := route.Operation.RequestBody.Value.Content
require.NotNil(t, content.Get("application/json"))
require.Nil(t, content.Get("application/xml"))
require.Equal(t, "#/components/schemas/TestRequestBody", content.Get("application/json").Schema.Ref)
_, ok := s.OpenApiSpec.Components.RequestBodies["TestRequestBody"]
require.True(t, ok)
})

t.Run("variadic", func(t *testing.T) {
s := NewServer()
route := Post(s, "/test", testControllerWithBody).
RequestContentType("application/json", "my/content-type")

content := route.Operation.RequestBody.Value.Content
require.NotNil(t, content.Get("application/json"))
require.NotNil(t, content.Get("my/content-type"))
require.Nil(t, content.Get("application/xml"))
require.Equal(t, "#/components/schemas/TestRequestBody", content.Get("application/json").Schema.Ref)
_, ok := s.OpenApiSpec.Components.RequestBodies["TestRequestBody"]
require.True(t, ok)
})
}

func TestCustomError(t *testing.T) {
type MyError struct {
Message string
Expand Down
17 changes: 17 additions & 0 deletions serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ func testControllerReturningPtrToString(c *ContextNoBody) (*string, error) {
return &s, nil
}

type TestRequestBody struct {
A string
B int
}

type TestResponseBody struct {
TestRequestBody
}

func testControllerWithBody(c *ContextWithBody[TestRequestBody]) (*TestResponseBody, error) {
body, err := c.Body()
if err != nil {
return nil, err
}
return &TestResponseBody{TestRequestBody: body}, nil
}

func TestHttpHandler(t *testing.T) {
s := NewServer()

Expand Down

0 comments on commit e514438

Please sign in to comment.