Skip to content

Commit

Permalink
feat: support for providing examples to custom types (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikicc committed Feb 11, 2022
1 parent ad0868d commit eb08933
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 9 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ You can use additional tags. Some will be interpreted by *tonic*, others will be
| `description` | Add a description of the field in the spec. |
| `deprecated` | Indicates if the field is deprecated. Accepted values are `1`, `t`, `T`, `TRUE`, `true`, `True`, `0`, `f`, `F`, `FALSE`. Invalid value are considered to be false. |
| `enum` | A coma separated list of acceptable values for the parameter. |
| `example` | An example value to be used in OpenAPI specification. |
| `example` | An example value to be used in OpenAPI specification. See [section below](#Providing-Examples-for-Custom-Types) for the demonstration on how to provide example for custom types. |
| `format` | Override the format of the field in the specification. Read the [documentation](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypeFormat) for more informations. |
| `validate` | Field validation rules. Read the [documentation](https://godoc.org/gopkg.in/go-playground/validator.v8) for more informations. |
| `explode` | Specifies whether arrays should generate separate parameters for each array item or object property (limited to query parameters with *form* style). Accepted values are `1`, `t`, `T`, `TRUE`, `true`, `True`, `0`, `f`, `F`, `FALSE`. Invalid value are considered to be false. |
Expand Down Expand Up @@ -344,6 +344,16 @@ Note that, according to the doc, the inherent version of the address is a semant

To help you write markdown descriptions in Go, a simple builder is available in the sub-package `markdown`. This is quite handy to avoid conflicts with backticks that are both used in Go for litteral multi-lines strings and code blocks in markdown.

#### Providing Examples for Custom Types
To be able to provide examples for custom types, they must implement the `json.Marshaler` and/or `yaml.Marshaler` and the following interface:
```go
type Exampler interface {
ParseExample(v string) (interface{}, error)
}
```

If the custom type implements the interface, Fizz will pass the value from the `example` tag to the `ParseExample` method and use the return value as the example in the OpenAPI specification.

## Known limitations

- Since *OpenAPI* is based on the *JSON Schema* specification itself, objects (Go maps) with keys that are not of type `string` are not supported and will be ignored during the generation of the specification.
Expand Down
46 changes: 38 additions & 8 deletions fizz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fizz

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -113,9 +114,35 @@ func TestHandler(t *testing.T) {
wg.Wait()
}

// customTime shows the date & time without timezone information
type customTime time.Time

func (c customTime) String() string {
return time.Time(c).Format("2006-01-02T15:04:05")
}

func (c customTime) MarshalJSON() ([]byte, error) {
// add quotes for JSON representation
ts := fmt.Sprintf("\"%s\"", c.String())
return []byte(ts), nil
}

func (c customTime) MarshalYAML() (interface{}, error) {
return c.String(), nil
}

func (c customTime) ParseExample(v string) (interface{}, error) {
t1, err := time.Parse(time.RFC3339, v)
if err != nil {
return nil, err
}
return customTime(t1), nil
}

type T struct {
X string `json:"x" description:"This is X"`
Y int `json:"y" description:"This is Y"`
X string `json:"x" yaml:"x" description:"This is X"`
Y int `json:"y" yaml:"y" description:"This is Y"`
Z customTime `json:"z" yaml:"z" example:"2022-02-07T18:00:00+09:00" description:"This is Z"`
}
type In struct {
A int `path:"a" description:"This is A"`
Expand All @@ -128,12 +155,15 @@ type In struct {
func TestTonicHandler(t *testing.T) {
fizz := New()

t1, err := time.Parse(time.RFC3339, "2022-02-07T18:00:00+09:00")
assert.Nil(t, err)

fizz.GET("/foo/:a", nil, tonic.Handler(func(c *gin.Context, params *In) (*T, error) {
assert.Equal(t, 0, params.A)
assert.Equal(t, "foobar", params.B)
assert.Equal(t, "foobaz", params.C)

return &T{X: "foo", Y: 1}, nil
return &T{X: "foo", Y: 1, Z: customTime(t1)}, nil
}, 200))

// Create a router group to test that tonic handlers works with router groups.
Expand All @@ -144,7 +174,7 @@ func TestTonicHandler(t *testing.T) {
assert.Equal(t, "group-foobar", params.B)
assert.Equal(t, "group-foobaz", params.C)

return &T{X: "group-foo", Y: 2}, nil
return &T{X: "group-foo", Y: 2, Z: customTime(t1)}, nil
}, 200))

srv := httptest.NewServer(fizz)
Expand All @@ -168,7 +198,7 @@ func TestTonicHandler(t *testing.T) {
"X-Test-C": []string{"foobaz"},
},
expectStatus: 200,
expectBody: `{"x":"foo","y":1}`,
expectBody: `{"x":"foo","y":1,"z":"2022-02-07T18:00:00"}`,
},
{
url: "/test/bar/42?b=group-foobar",
Expand All @@ -177,7 +207,7 @@ func TestTonicHandler(t *testing.T) {
"X-Test-C": []string{"group-foobaz"},
},
expectStatus: 200,
expectBody: `{"x":"group-foo","y":2}`,
expectBody: `{"x":"group-foo","y":2,"z":"2022-02-07T18:00:00"}`,
},
{
url: "/bar/42?b=group-foobar",
Expand Down Expand Up @@ -275,8 +305,8 @@ func TestSpecHandler(t *testing.T) {
WithoutSecurity(),
XInternal(),
},
tonic.Handler(func(c *gin.Context) error {
return nil
tonic.Handler(func(c *gin.Context) (*T, error) {
return &T{}, nil
}, 200),
)

Expand Down
8 changes: 8 additions & 0 deletions openapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,12 @@ func fieldNameFromTag(sf reflect.StructField, tagName string) string {

/// parseExampleValue is used to transform the string representation of the example value to the correct type.
func parseExampleValue(t reflect.Type, value string) (interface{}, error) {
// If the type implements Exampler use the ParseExample method to create the example
i, ok := reflect.New(t).Interface().(Exampler)
if ok {
return i.ParseExample(value)
}

switch t.Kind() {
case reflect.Bool:
return strconv.ParseBool(value)
Expand Down Expand Up @@ -1276,6 +1282,8 @@ func parseExampleValue(t reflect.Type, value string) (interface{}, error) {
return strconv.ParseFloat(value, t.Bits())
case reflect.Ptr:
return parseExampleValue(t.Elem(), value)
case reflect.Struct:
return nil, fmt.Errorf("type %s does not implement Exampler", t.String())
default:
return nil, fmt.Errorf("unsuported type: %s", t.String())
}
Expand Down
46 changes: 46 additions & 0 deletions openapi/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package openapi

import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"reflect"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -733,6 +735,26 @@ func TestSetServers(t *testing.T) {
assert.Equal(t, servers, g.API().Servers)
}

type customUnit float64

func (c customUnit) ParseExample(v string) (interface{}, error) {
s, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil, err
}
return fmt.Sprintf("%.2f USD", s), nil
}

type customTime time.Time

func (c customTime) ParseExample(v string) (interface{}, error) {
t1, err := time.Parse(time.RFC3339, v)
if err != nil {
return nil, err
}
return customTime(t1), nil
}

// TestGenerator_parseExampleValue tests the parsing of example values.
func TestGenerator_parseExampleValue(t *testing.T) {
var testCases = []struct {
Expand Down Expand Up @@ -873,6 +895,30 @@ func TestGenerator_parseExampleValue(t *testing.T) {
"true",
true,
},
{
"mapping to customUnit",
reflect.TypeOf(customUnit(1)),
"15",
"15.00 USD",
},
{
"mapping pointer to customUnit",
reflect.PtrTo(reflect.TypeOf(customUnit(1))),
"20.00000",
"20.00 USD",
},
{
"mapping to customTime",
reflect.TypeOf(customTime{}),
"2022-02-07T18:00:00+09:00",
customTime(time.Date(2022, time.February, 7, 18, 0, 0, 0, time.FixedZone("", 9*3600))),
},
{
"mapping pointer to customTime",
reflect.PtrTo(reflect.TypeOf(customTime{})),
"2022-02-07T18:00:00+09:00",
customTime(time.Date(2022, time.February, 7, 18, 0, 0, 0, time.FixedZone("", 9*3600))),
},
}

for _, tc := range testCases {
Expand Down
6 changes: 6 additions & 0 deletions openapi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ type DataType interface {
Format() string
}

// Exampler is the interface implemented by custom types
// that can parse example values.
type Exampler interface {
ParseExample(v string) (interface{}, error)
}

// InternalDataType represents an internal type.
type InternalDataType int

Expand Down
29 changes: 29 additions & 0 deletions testdata/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
"type": "string"
}
}
},
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/FizzT"
}
}
}
},
"400": {
Expand Down Expand Up @@ -185,6 +192,28 @@
},
"components": {
"schemas": {
"FizzCustomTime":{
"type":"object",
"description":"This is Z",
"example": "2022-02-07T18:00:00"
},
"FizzT":{
"type":"object",
"properties":{
"x":{
"type":"string",
"description":"This is X"
},
"y":{
"type":"integer",
"description":"This is Y",
"format":"int32"
},
"z":{
"$ref":"#/components/schemas/FizzCustomTime"
}
}
},
"PostTestInput": {
"type": "object",
"properties": {
Expand Down
20 changes: 20 additions & 0 deletions testdata/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ paths:
description: Unique request ID
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/FizzT'
'400':
description: Bad Request
content:
Expand Down Expand Up @@ -116,6 +120,22 @@ paths:
description: Created
components:
schemas:
FizzCustomTime:
type: object
description: This is Z
example: 2022-02-07T18:00:00
FizzT:
type: object
properties:
x:
type: string
description: This is X
"y":
type: integer
description: This is Y
format: int32
z:
$ref: '#/components/schemas/FizzCustomTime'
PostTestInput:
type: object
properties:
Expand Down

0 comments on commit eb08933

Please sign in to comment.