Skip to content

Commit 7bb66a1

Browse files
authored
Merge pull request #45 from nikicc/response-with-examples
Add example to fizz.Response & add fizz.ResponseWithExamples
2 parents ffa3de6 + 8aee5a4 commit 7bb66a1

File tree

9 files changed

+178
-22
lines changed

9 files changed

+178
-22
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,18 @@ fizz.ID(id string)
7575
fizz.Deprecated(deprecated bool)
7676

7777
// Add an additional response to the operation.
78-
// model and header may be `nil`.
79-
fizz.Response(statusCode, desc string, model interface{}, headers []*ResponseHeader)
78+
// The example argument will populate a single example in the response schema.
79+
// For populating multiple examples, use fizz.ResponseWithExamples.
80+
// Notice that example and examples fields are mutually exclusive.
81+
// model, header, and example may be `nil`.
82+
fizz.Response(statusCode, desc string, model interface{}, headers []*ResponseHeader, example interface{})
83+
84+
// ResponseWithExamples is a variant of Response that supports providing multiple examples.
85+
// Examples argument will populate multiple examples in the response schema.
86+
// For populating a single example, use fizz.Response.
87+
// Notice that example and examples fields are mutually exclusive.
88+
// model, header, and examples may be `nil`.
89+
fizz.ResponseWithExamples(statusCode, desc string, model interface{}, headers []*ResponseHeader, examples map[string]interface{})
8090

8191
// Add an additional header to the default response.
8292
// Model can be of any type, and may also be `nil`,

examples/market/router.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,25 @@ func routes(grp *fizz.RouterGroup) {
4646
// Add a new fruit to the market.
4747
grp.POST("", []fizz.OperationOption{
4848
fizz.Summary("Add a fruit to the market"),
49-
fizz.Response("400", "Bad request", nil, nil),
49+
fizz.Response("400", "Bad request", nil, nil,
50+
map[string]interface{}{"error": "fruit already exists"},
51+
),
5052
}, tonic.Handler(CreateFruit, 200))
5153

5254
// Remove a fruit from the market,
5355
// probably because it rotted.
5456
grp.DELETE("/:name", []fizz.OperationOption{
5557
fizz.Summary("Remove a fruit from the market"),
56-
fizz.Response("400", "Fruit not found", nil, nil),
58+
fizz.ResponseWithExamples("400", "Bad request", nil, nil, map[string]interface{}{
59+
"fruitNotFound": map[string]interface{}{"error": "fruit not found"},
60+
"invalidApiKey": map[string]interface{}{"error": "invalid api key"},
61+
}),
5762
}, tonic.Handler(DeleteFruit, 204))
5863

5964
// List all available fruits.
6065
grp.GET("", []fizz.OperationOption{
6166
fizz.Summary("List the fruits of the market"),
62-
fizz.Response("400", "Bad request", nil, nil),
67+
fizz.Response("400", "Bad request", nil, nil, nil),
6368
fizz.Header("X-Market-Listing-Size", "Listing size", fizz.Long),
6469
}, tonic.Handler(ListFruits, 200))
6570
}

fizz.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,27 @@ func Deprecated(deprecated bool) func(*openapi.OperationInfo) {
310310
}
311311

312312
// Response adds an additional response to the operation.
313-
func Response(statusCode, desc string, model interface{}, headers []*openapi.ResponseHeader) func(*openapi.OperationInfo) {
313+
func Response(statusCode, desc string, model interface{}, headers []*openapi.ResponseHeader, example interface{}) func(*openapi.OperationInfo) {
314314
return func(o *openapi.OperationInfo) {
315-
o.Responses = append(o.Responses, &openapi.OperationReponse{
315+
o.Responses = append(o.Responses, &openapi.OperationResponse{
316316
Code: statusCode,
317317
Description: desc,
318318
Model: model,
319319
Headers: headers,
320+
Example: example,
321+
})
322+
}
323+
}
324+
325+
// ResponseWithExamples is a variant of Response that accept many examples.
326+
func ResponseWithExamples(statusCode, desc string, model interface{}, headers []*openapi.ResponseHeader, examples map[string]interface{}) func(*openapi.OperationInfo) {
327+
return func(o *openapi.OperationInfo) {
328+
o.Responses = append(o.Responses, &openapi.OperationResponse{
329+
Code: statusCode,
330+
Description: desc,
331+
Model: model,
332+
Headers: headers,
333+
Examples: examples,
320334
})
321335
}
322336
}

fizz_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ func TestSpecHandler(t *testing.T) {
259259
Description: "Rate limit",
260260
Model: Integer,
261261
},
262+
}, nil),
263+
Response("404", "", String, nil, "not-found-example"),
264+
ResponseWithExamples("400", "", String, nil, map[string]interface{}{
265+
"one": "message1",
266+
"two": "message2",
262267
}),
263268
},
264269
tonic.Handler(func(c *gin.Context) error {

openapi/generator.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type,
274274
// Generate the default response from the tonic
275275
// handler return type. If the handler has no output
276276
// type, the response won't have a schema.
277-
if err := g.setOperationResponse(op, out, strconv.Itoa(info.StatusCode), tonic.MediaType(), info.StatusDescription, info.Headers); err != nil {
277+
if err := g.setOperationResponse(op, out, strconv.Itoa(info.StatusCode), tonic.MediaType(), info.StatusDescription, info.Headers, nil, nil); err != nil {
278278
return nil, err
279279
}
280280
// Generate additional responses from the operation
@@ -287,6 +287,8 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type,
287287
tonic.MediaType(),
288288
resp.Description,
289289
resp.Headers,
290+
resp.Example,
291+
resp.Examples,
290292
); err != nil {
291293
return nil, err
292294
}
@@ -345,11 +347,16 @@ func isResponseCodeRange(code string) bool {
345347

346348
// setOperationResponse adds a response to the operation that
347349
// return the type t with the given media type and status code.
348-
func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt, desc string, headers []*ResponseHeader) error {
350+
func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt, desc string, headers []*ResponseHeader, example interface{}, examples map[string]interface{}) error {
349351
if _, ok := op.Responses[code]; ok {
350352
// A response already exists for this code.
351353
return fmt.Errorf("response with code %s already exists", code)
352354
}
355+
if example != nil && examples != nil {
356+
// Cannot set both 'example' and 'examples' values
357+
return fmt.Errorf("'example' and 'examples' are mutually exclusive")
358+
}
359+
353360
// Check that the response code is valid per the spec:
354361
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#patterned-fields-1
355362
if code != "default" {
@@ -371,12 +378,24 @@ func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt
371378
Content: make(map[string]*MediaTypeOrRef),
372379
Headers: make(map[string]*HeaderOrRef),
373380
}
381+
382+
var castedExamples map[string]*ExampleOrRef
383+
if examples != nil {
384+
castedExamples = make(map[string]*ExampleOrRef)
385+
for name, val := range examples {
386+
castedExamples[name] = &ExampleOrRef{Example: &Example{Value: val}}
387+
}
388+
}
389+
374390
// The response may have no content type specified,
375391
// in which case we don't assign a schema.
376392
schema := g.newSchemaFromType(t)
377-
if schema != nil {
393+
394+
if schema != nil || example != nil || castedExamples != nil {
378395
r.Content[mt] = &MediaTypeOrRef{MediaType: &MediaType{
379-
Schema: schema,
396+
Schema: schema,
397+
Example: example,
398+
Examples: castedExamples,
380399
}}
381400
}
382401
// Assign headers.

openapi/generator_test.go

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -364,13 +364,13 @@ func TestAddOperation(t *testing.T) {
364364
Summary: "ABC",
365365
Description: "XYZ",
366366
Deprecated: true,
367-
Responses: []*OperationReponse{
368-
&OperationReponse{
367+
Responses: []*OperationResponse{
368+
&OperationResponse{
369369
Code: "400",
370370
Description: "Bad Request",
371371
Model: CustomError{},
372372
},
373-
&OperationReponse{
373+
&OperationResponse{
374374
Code: "5XX",
375375
Description: "Server Errors",
376376
},
@@ -516,21 +516,75 @@ func TestSetOperationResponseError(t *testing.T) {
516516
op := &Operation{
517517
Responses: make(Responses),
518518
}
519-
err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "200", "application/json", "", nil)
519+
err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "200", "application/json", "", nil, nil, nil)
520520
assert.Nil(t, err)
521521

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

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

531531
// Add out of range response code.
532-
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "777", "", "", nil)
532+
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "777", "", "", nil, nil, nil)
533533
assert.NotNil(t, err)
534+
535+
// Cannot set both example and examples
536+
err = g.setOperationResponse(op, reflect.TypeOf(new(bool)), "404", "", "", nil, "notFoundExample", map[string]interface{}{"badRequest": "message"})
537+
assert.NotNil(t, err)
538+
}
539+
540+
// TestSetOperationResponseExample tests that
541+
// one example is set correctly.
542+
func TestSetOperationResponseExample(t *testing.T) {
543+
g := gen(t)
544+
op := &Operation{
545+
Responses: make(Responses),
546+
}
547+
548+
error1 := map[string]interface{}{"error": "message1"}
549+
550+
err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "400", "application/json", "", nil, error1, nil)
551+
assert.Nil(t, err)
552+
553+
// assert example set correctly
554+
mt := op.Responses["400"].Response.Content["application/json"].MediaType
555+
assert.Equal(t, error1, mt.Example)
556+
557+
// examples should be empty
558+
assert.Nil(t, mt.Examples)
559+
}
560+
561+
// TestSetOperationResponseExamples tests that
562+
// multiple examples are set correctly.
563+
func TestSetOperationResponseExamples(t *testing.T) {
564+
g := gen(t)
565+
op := &Operation{
566+
Responses: make(Responses),
567+
}
568+
569+
error1 := map[string]interface{}{"error": "message1"}
570+
error2 := map[string]interface{}{"error": "message2"}
571+
572+
err := g.setOperationResponse(op, reflect.TypeOf(new(string)), "400", "application/json", "", nil, nil,
573+
map[string]interface{}{
574+
"one": error1,
575+
"two": error2,
576+
},
577+
)
578+
assert.Nil(t, err)
579+
580+
// assert examples set correctly
581+
mt := op.Responses["400"].Response.Content["application/json"].MediaType
582+
assert.Equal(t, 2, len(mt.Examples))
583+
assert.Equal(t, error1, mt.Examples["one"].Example.Value)
584+
assert.Equal(t, error2, mt.Examples["two"].Example.Value)
585+
586+
// example should be empty
587+
assert.Nil(t, mt.Example)
534588
}
535589

536590
// TestSetOperationParamsError tests the various error

openapi/operation.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type OperationInfo struct {
1111
Description string
1212
Deprecated bool
1313
InputModel interface{}
14-
Responses []*OperationReponse
14+
Responses []*OperationResponse
1515
}
1616

1717
// ResponseHeader represents a single header that
@@ -22,13 +22,15 @@ type ResponseHeader struct {
2222
Model interface{}
2323
}
2424

25-
// OperationReponse represents a single response of an
25+
// OperationResponse represents a single response of an
2626
// API operation.
27-
type OperationReponse struct {
27+
type OperationResponse struct {
2828
// The response code can be "default"
2929
// according to OAS3 spec.
3030
Code string
3131
Description string
3232
Model interface{}
3333
Headers []*ResponseHeader
34+
Example interface{}
35+
Examples map[string]interface{}
3436
}

testdata/spec.json

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@
3838
}
3939
}
4040
},
41+
"400": {
42+
"description": "Bad Request",
43+
"content": {
44+
"application/json": {
45+
"schema": {
46+
"type": "string"
47+
},
48+
"examples": {
49+
"one": {
50+
"value": "message1"
51+
},
52+
"two": {
53+
"value": "message2"
54+
}
55+
}
56+
}
57+
}
58+
},
59+
"404": {
60+
"description": "Not Found",
61+
"content": {
62+
"application/json": {
63+
"schema": {
64+
"type": "string"
65+
},
66+
"example": "not-found-example"
67+
}
68+
}
69+
},
4170
"429": {
4271
"description": "Too Many Requests",
4372
"headers": {
@@ -140,4 +169,4 @@
140169
}
141170
}
142171
}
143-
}
172+
}

testdata/spec.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ paths:
2828
description: Unique request ID
2929
schema:
3030
type: string
31+
'400':
32+
description: Bad Request
33+
content:
34+
application/json:
35+
schema:
36+
type: string
37+
examples:
38+
one:
39+
value: message1
40+
two:
41+
value: message2
42+
'404':
43+
description: Not Found
44+
content:
45+
application/json:
46+
schema:
47+
type: string
48+
example: not-found-example
3149
'429':
3250
description: Too Many Requests
3351
headers:

0 commit comments

Comments
 (0)