Skip to content

Commit

Permalink
Merge pull request #45 from nikicc/response-with-examples
Browse files Browse the repository at this point in the history
Add example to fizz.Response & add fizz.ResponseWithExamples
  • Loading branch information
wI2L committed Nov 24, 2020
2 parents ffa3de6 + 8aee5a4 commit 7bb66a1
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 22 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,18 @@ fizz.ID(id string)
fizz.Deprecated(deprecated bool)

// Add an additional response to the operation.
// model and header may be `nil`.
fizz.Response(statusCode, desc string, model interface{}, headers []*ResponseHeader)
// The example argument will populate a single example in the response schema.
// For populating multiple examples, use fizz.ResponseWithExamples.
// Notice that example and examples fields are mutually exclusive.
// model, header, and example may be `nil`.
fizz.Response(statusCode, desc string, model interface{}, headers []*ResponseHeader, example interface{})

// ResponseWithExamples is a variant of Response that supports providing multiple examples.
// Examples argument will populate multiple examples in the response schema.
// For populating a single example, use fizz.Response.
// Notice that example and examples fields are mutually exclusive.
// model, header, and examples may be `nil`.
fizz.ResponseWithExamples(statusCode, desc string, model interface{}, headers []*ResponseHeader, examples map[string]interface{})

// Add an additional header to the default response.
// Model can be of any type, and may also be `nil`,
Expand Down
11 changes: 8 additions & 3 deletions examples/market/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,25 @@ func routes(grp *fizz.RouterGroup) {
// Add a new fruit to the market.
grp.POST("", []fizz.OperationOption{
fizz.Summary("Add a fruit to the market"),
fizz.Response("400", "Bad request", nil, nil),
fizz.Response("400", "Bad request", nil, nil,
map[string]interface{}{"error": "fruit already exists"},
),
}, tonic.Handler(CreateFruit, 200))

// Remove a fruit from the market,
// probably because it rotted.
grp.DELETE("/:name", []fizz.OperationOption{
fizz.Summary("Remove a fruit from the market"),
fizz.Response("400", "Fruit not found", nil, nil),
fizz.ResponseWithExamples("400", "Bad request", nil, nil, map[string]interface{}{
"fruitNotFound": map[string]interface{}{"error": "fruit not found"},
"invalidApiKey": map[string]interface{}{"error": "invalid api key"},
}),
}, tonic.Handler(DeleteFruit, 204))

// List all available fruits.
grp.GET("", []fizz.OperationOption{
fizz.Summary("List the fruits of the market"),
fizz.Response("400", "Bad request", nil, nil),
fizz.Response("400", "Bad request", nil, nil, nil),
fizz.Header("X-Market-Listing-Size", "Listing size", fizz.Long),
}, tonic.Handler(ListFruits, 200))
}
18 changes: 16 additions & 2 deletions fizz.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,27 @@ func Deprecated(deprecated bool) func(*openapi.OperationInfo) {
}

// Response adds an additional response to the operation.
func Response(statusCode, desc string, model interface{}, headers []*openapi.ResponseHeader) func(*openapi.OperationInfo) {
func Response(statusCode, desc string, model interface{}, headers []*openapi.ResponseHeader, example interface{}) func(*openapi.OperationInfo) {
return func(o *openapi.OperationInfo) {
o.Responses = append(o.Responses, &openapi.OperationReponse{
o.Responses = append(o.Responses, &openapi.OperationResponse{
Code: statusCode,
Description: desc,
Model: model,
Headers: headers,
Example: example,
})
}
}

// ResponseWithExamples is a variant of Response that accept many examples.
func ResponseWithExamples(statusCode, desc string, model interface{}, headers []*openapi.ResponseHeader, examples map[string]interface{}) func(*openapi.OperationInfo) {
return func(o *openapi.OperationInfo) {
o.Responses = append(o.Responses, &openapi.OperationResponse{
Code: statusCode,
Description: desc,
Model: model,
Headers: headers,
Examples: examples,
})
}
}
Expand Down
5 changes: 5 additions & 0 deletions fizz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ func TestSpecHandler(t *testing.T) {
Description: "Rate limit",
Model: Integer,
},
}, nil),
Response("404", "", String, nil, "not-found-example"),
ResponseWithExamples("400", "", String, nil, map[string]interface{}{
"one": "message1",
"two": "message2",
}),
},
tonic.Handler(func(c *gin.Context) error {
Expand Down
27 changes: 23 additions & 4 deletions openapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type,
// Generate the default response from the tonic
// handler return type. If the handler has no output
// type, the response won't have a schema.
if err := g.setOperationResponse(op, out, strconv.Itoa(info.StatusCode), tonic.MediaType(), info.StatusDescription, info.Headers); err != nil {
if err := g.setOperationResponse(op, out, strconv.Itoa(info.StatusCode), tonic.MediaType(), info.StatusDescription, info.Headers, nil, nil); err != nil {
return nil, err
}
// Generate additional responses from the operation
Expand All @@ -287,6 +287,8 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type,
tonic.MediaType(),
resp.Description,
resp.Headers,
resp.Example,
resp.Examples,
); err != nil {
return nil, err
}
Expand Down Expand Up @@ -345,11 +347,16 @@ func isResponseCodeRange(code string) bool {

// setOperationResponse adds a response to the operation that
// return the type t with the given media type and status code.
func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt, desc string, headers []*ResponseHeader) error {
func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt, desc string, headers []*ResponseHeader, example interface{}, examples map[string]interface{}) error {
if _, ok := op.Responses[code]; ok {
// A response already exists for this code.
return fmt.Errorf("response with code %s already exists", code)
}
if example != nil && examples != nil {
// Cannot set both 'example' and 'examples' values
return fmt.Errorf("'example' and 'examples' are mutually exclusive")
}

// Check that the response code is valid per the spec:
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#patterned-fields-1
if code != "default" {
Expand All @@ -371,12 +378,24 @@ func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt
Content: make(map[string]*MediaTypeOrRef),
Headers: make(map[string]*HeaderOrRef),
}

var castedExamples map[string]*ExampleOrRef
if examples != nil {
castedExamples = make(map[string]*ExampleOrRef)
for name, val := range examples {
castedExamples[name] = &ExampleOrRef{Example: &Example{Value: val}}
}
}

// The response may have no content type specified,
// in which case we don't assign a schema.
schema := g.newSchemaFromType(t)
if schema != nil {

if schema != nil || example != nil || castedExamples != nil {
r.Content[mt] = &MediaTypeOrRef{MediaType: &MediaType{
Schema: schema,
Schema: schema,
Example: example,
Examples: castedExamples,
}}
}
// Assign headers.
Expand Down
68 changes: 61 additions & 7 deletions openapi/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,13 +364,13 @@ func TestAddOperation(t *testing.T) {
Summary: "ABC",
Description: "XYZ",
Deprecated: true,
Responses: []*OperationReponse{
&OperationReponse{
Responses: []*OperationResponse{
&OperationResponse{
Code: "400",
Description: "Bad Request",
Model: CustomError{},
},
&OperationReponse{
&OperationResponse{
Code: "5XX",
Description: "Server Errors",
},
Expand Down Expand Up @@ -516,21 +516,75 @@ func TestSetOperationResponseError(t *testing.T) {
op := &Operation{
Responses: make(Responses),
}
err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "200", "application/json", "", nil)
err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "200", "application/json", "", nil, nil, nil)
assert.Nil(t, err)

// Add another response with same code.
err = g.setOperationResponse(op, reflect.TypeOf(new(int)), "200", "application/xml", "", nil)
err = g.setOperationResponse(op, reflect.TypeOf(new(int)), "200", "application/xml", "", nil, nil, nil)
assert.NotNil(t, err)

// Add invalid response code that cannot
// be converted to an integer.
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "two-hundred", "", "", nil)
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "two-hundred", "", "", nil, nil, nil)
assert.NotNil(t, err)

// Add out of range response code.
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "777", "", "", nil)
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "777", "", "", nil, nil, nil)
assert.NotNil(t, err)

// Cannot set both example and examples
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "404", "", "", nil, "notFoundExample", map[string]interface{}{"badRequest": "message"})
assert.NotNil(t, err)
}

// TestSetOperationResponseExample tests that
// one example is set correctly.
func TestSetOperationResponseExample(t *testing.T) {
g := gen(t)
op := &Operation{
Responses: make(Responses),
}

error1 := map[string]interface{}{"error": "message1"}

err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "400", "application/json", "", nil, error1, nil)
assert.Nil(t, err)

// assert example set correctly
mt := op.Responses["400"].Response.Content["application/json"].MediaType
assert.Equal(t, error1, mt.Example)

// examples should be empty
assert.Nil(t, mt.Examples)
}

// TestSetOperationResponseExamples tests that
// multiple examples are set correctly.
func TestSetOperationResponseExamples(t *testing.T) {
g := gen(t)
op := &Operation{
Responses: make(Responses),
}

error1 := map[string]interface{}{"error": "message1"}
error2 := map[string]interface{}{"error": "message2"}

err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "400", "application/json", "", nil, nil,
map[string]interface{}{
"one": error1,
"two": error2,
},
)
assert.Nil(t, err)

// assert examples set correctly
mt := op.Responses["400"].Response.Content["application/json"].MediaType
assert.Equal(t, 2, len(mt.Examples))
assert.Equal(t, error1, mt.Examples["one"].Example.Value)
assert.Equal(t, error2, mt.Examples["two"].Example.Value)

// example should be empty
assert.Nil(t, mt.Example)
}

// TestSetOperationParamsError tests the various error
Expand Down
8 changes: 5 additions & 3 deletions openapi/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type OperationInfo struct {
Description string
Deprecated bool
InputModel interface{}
Responses []*OperationReponse
Responses []*OperationResponse
}

// ResponseHeader represents a single header that
Expand All @@ -22,13 +22,15 @@ type ResponseHeader struct {
Model interface{}
}

// OperationReponse represents a single response of an
// OperationResponse represents a single response of an
// API operation.
type OperationReponse struct {
type OperationResponse struct {
// The response code can be "default"
// according to OAS3 spec.
Code string
Description string
Model interface{}
Headers []*ResponseHeader
Example interface{}
Examples map[string]interface{}
}
31 changes: 30 additions & 1 deletion testdata/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"type": "string"
},
"examples": {
"one": {
"value": "message1"
},
"two": {
"value": "message2"
}
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"type": "string"
},
"example": "not-found-example"
}
}
},
"429": {
"description": "Too Many Requests",
"headers": {
Expand Down Expand Up @@ -140,4 +169,4 @@
}
}
}
}
}
18 changes: 18 additions & 0 deletions testdata/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ paths:
description: Unique request ID
schema:
type: string
'400':
description: Bad Request
content:
application/json:
schema:
type: string
examples:
one:
value: message1
two:
value: message2
'404':
description: Not Found
content:
application/json:
schema:
type: string
example: not-found-example
'429':
description: Too Many Requests
headers:
Expand Down

0 comments on commit 7bb66a1

Please sign in to comment.