diff --git a/.gitignore b/.gitignore index 1da79b2..96011e7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ # tests coverage.txt + +# GoLand +.idea diff --git a/README.md b/README.md index 1ef8616..b154963 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,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. - `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. diff --git a/examples/market/market.go b/examples/market/market.go index 4557725..dbb51ce 100644 --- a/examples/market/market.go +++ b/examples/market/market.go @@ -9,9 +9,9 @@ import ( // Fruit represents a sweet, fresh fruit. type Fruit struct { - Name string `json:"name" validate:"required"` + Name string `json:"name" validate:"required" example:"banana"` Origin string `json:"origin" validate:"required" description:"Country of origin of the fruit" enum:"ecuador,france,senegal,china,spain"` - Price float64 `json:"price" validate:"required" description:"Price in euros"` + Price float64 `json:"price" validate:"required" description:"Price in euros" example:"5.13"` AddedAt time.Time `json:"-" binding:"-" description:"Date of addition of the fruit to the market"` } diff --git a/openapi/generator.go b/openapi/generator.go index 38fae4c..0bcc6ec 100644 --- a/openapi/generator.go +++ b/openapi/generator.go @@ -758,6 +758,22 @@ func (g *Generator) newSchemaFromStructField(sf reflect.StructField, required bo if t, ok := sf.Tag.Lookup(formatTag); ok { schema.Format = t } + + // Set example value from tag to schema + if e := strings.TrimSpace(sf.Tag.Get("example")); e != "" { + if parsed, err := parseExampleValue(sf.Type, e); err != nil { + g.error(&FieldError{ + Message: fmt.Sprintf("could not parse the example value %q of field %q: %s", e, fname, err), + Name: fname, + Type: sf.Type, + TypeName: g.typeName(sf.Type), + Parent: parent, + }) + } else { + schema.Example = parsed + } + } + return sor } @@ -1161,3 +1177,21 @@ func fieldNameFromTag(sf reflect.StructField, tagName string) string { } return name } + +/// 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) { + switch t.Kind() { + case reflect.Bool: + return strconv.ParseBool(value) + case reflect.String: + return value, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.ParseInt(value, 10, t.Bits()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.ParseUint(value, 10, t.Bits()) + case reflect.Float32, reflect.Float64: + return strconv.ParseFloat(value, t.Bits()) + default: + return nil, fmt.Errorf("unsuported type: %s", t.String()) + } +} diff --git a/openapi/generator_test.go b/openapi/generator_test.go index ba69334..1dbfd6c 100644 --- a/openapi/generator_test.go +++ b/openapi/generator_test.go @@ -226,6 +226,40 @@ func TestNewSchemaFromStructErrors(t *testing.T) { assert.Nil(t, sor) } +// TestNewSchemaFromStructFieldExampleValues tests the +// case of setting example values. +func TestNewSchemaFromStructFieldExampleValues(t *testing.T) { + g := gen(t) + + type T struct { + A string `example:"value"` + B int `example:"1"` + C float64 `example:"0.1"` + D bool `example:"true"` + } + typ := reflect.TypeOf(T{}) + + // Field A contains string example. + sor := g.newSchemaFromStructField(typ.Field(0), false, "A", typ) + assert.NotNil(t, sor) + assert.Equal(t, "value", sor.Example) + + // Field B contains int example. + sor = g.newSchemaFromStructField(typ.Field(1), false, "B", typ) + assert.NotNil(t, sor) + assert.Equal(t, int64(1), sor.Example) + + // Field C contains float example. + sor = g.newSchemaFromStructField(typ.Field(2), false, "C", typ) + assert.NotNil(t, sor) + assert.Equal(t, 0.1, sor.Example) + + // Field D contains boolean example. + sor = g.newSchemaFromStructField(typ.Field(3), false, "D", typ) + assert.NotNil(t, sor) + assert.Equal(t, true, sor.Example) +} + // TestNewSchemaFromStructFieldErrors tests the errors // case of generation of a schema from a struct field. func TestNewSchemaFromStructFieldErrors(t *testing.T) { @@ -235,6 +269,7 @@ func TestNewSchemaFromStructFieldErrors(t *testing.T) { A string `validate:"required" default:"foobar"` B int `default:"foobaz"` C int `enum:"a,1,c"` + D bool `example:"not-a-bool-value"` } typ := reflect.TypeOf(T{}) @@ -260,6 +295,17 @@ func TestNewSchemaFromStructFieldErrors(t *testing.T) { assert.Len(t, g.Errors(), 4) assert.NotEmpty(t, g.Errors()[2].Error()) assert.NotEmpty(t, g.Errors()[3].Error()) + + // Field D has example value that cannot be parsed to bool. + sor = g.newSchemaFromStructField(typ.Field(3), false, "D", typ) + assert.NotNil(t, sor) + assert.Len(t, g.Errors(), 5) + assert.NotEmpty(t, g.Errors()[4].Error()) + // check that Name & Type of the error are set correctly + fe, ok := g.Errors()[4].(*FieldError) + assert.True(t, ok) + assert.Equal(t, "D", fe.Name) + assert.Equal(t, reflect.Bool, fe.Type.Kind()) } func diffJSON(a, b []byte) (bool, error) { @@ -576,6 +622,58 @@ func TestSetServers(t *testing.T) { assert.Equal(t, servers, g.API().Servers) } +// TestGenerator_parseExampleValue tests the parsing of example values. +func TestGenerator_parseExampleValue(t *testing.T) { + var testCases = []struct { + testName string + typ reflect.Type + inputValue string + outputValue interface{} + }{ + { + "mapping to string", + reflect.TypeOf("value"), + "value", + "value", + }, { + "mapping to int", + reflect.TypeOf(1), + "1", + int64(1), + }, { + "mapping to uint8", + reflect.TypeOf(uint8(1)), + "1", + uint64(1), + }, { + "mapping to number", + reflect.TypeOf(1.23), + "1.23", + 1.23, + }, { + "mapping to boolean", + reflect.TypeOf(true), + "true", + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + returned, err := parseExampleValue(tc.typ, tc.inputValue) + assert.Nil(t, err) + assert.Equal(t, tc.outputValue, returned) + }) + } +} + +// TestGenerator_parseExampleValueError tests that +// parseExampleValue raises error on unsupported type. +func TestGenerator_parseExampleValueError(t *testing.T) { + _, err := parseExampleValue(reflect.TypeOf(map[string]string{}), "whatever") + assert.Error(t, err, "parseExampleValue does not support type") +} + func gen(t *testing.T) *Generator { g, err := NewGenerator(genConfig) if err != nil { diff --git a/openapi/spec.go b/openapi/spec.go index 522b2f4..23892c4 100644 --- a/openapi/spec.go +++ b/openapi/spec.go @@ -156,6 +156,7 @@ type Schema struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` // The following properties are taken directly from the // JSON Schema definition and follow the same specifications