Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement a bunch of validation keywords #1

Merged
merged 9 commits into from
Jul 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ _testmain.go
*.exe
*.test
*.prof

# Ignore IntellIJ Gogland project files
.idea
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ func main(){
* `required:"true"` - field will be marked as required
* `description:"description"` - description will be added

On string fields:

* `minLength:"5"` - Set the minimum length of the value
* `maxLength:"5"` - Set the maximum length of the value
* `enum:"apple|banana|pear"` - Limit the available values to a defined set, separated by vertical bars
* `const:"I need to be there"` - Require the field to have a specific value.

On numeric types (strings and floats)

* `min:"-4.141592"` - Set a minimum value
* `max:"123456789"` - Set a maximum value
* `exclusiveMin:"0"` - Values must be strictly greater than this value
* `exclusiveMax:"11"` - Values must be strictly smaller than this value
* `const:"42"` - Property must have exactly this value.

### Expected behaviour

If struct field is pointer to the primitive type, then schema will allow this typa and null.
Expand Down
136 changes: 128 additions & 8 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
package generator

import (
"strings"
"reflect"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
)

//func main() {
Expand All @@ -17,6 +18,8 @@ import (

const DEFAULT_SCHEMA = "http://json-schema.org/schema#"

var rTypeInt64, rTypeFloat64 = reflect.TypeOf(int64(0)), reflect.TypeOf(float64(0))

type Document struct {
Schema string `json:"$schema,omitempty"`
property
Expand All @@ -27,7 +30,7 @@ func Generate(v interface{}) string {
}

// Reads the variable structure into the JSON-Schema Document
func (d *Document) Read(variable interface{}) *Document{
func (d *Document) Read(variable interface{}) *Document {
d.setDefaultSchema()

value := reflect.ValueOf(variable)
Expand All @@ -41,7 +44,6 @@ func (d *Document) setDefaultSchema() {
}
}


// String return the JSON encoding of the Document as a string
func (d *Document) String() string {
json, _ := json.MarshalIndent(d, "", " ")
Expand All @@ -57,7 +59,28 @@ type property struct {
AdditionalProperties bool `json:"additionalProperties,omitempty"`
Description string `json:"description,omitempty"`
AnyOf []*property `json:"anyOf,omitempty"`

// validation keywords:
// For any number-valued fields, we're making them pointers, because
// we want empty values to be omitted, but for numbers, 0 is seen as empty.

// numbers validators
MultipleOf *float64 `json:"multipleOf,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
// string validators
MaxLength *int64 `json:"maxLength,omitempty"`
MinLength *int64 `json:"minLength,omitempty"`
Pattern string `json:"pattern,omitempty"`
// Enum is defined for arbitrary types, but I'm currently just implementing it for strings.
Enum []string `json:"enum,omitempty"`

// Implemented for strings and numbers
Const interface{} `json:"const,omitempty"`
}

func (p *property) read(t reflect.Type) {
jsType, format, kind := getTypeFromMapping(t)
if jsType != "" {
Expand All @@ -81,8 +104,8 @@ func (p *property) read(t reflect.Type) {
// say we have *int
if kind == reflect.Ptr && isPrimitive(t.Elem().Kind()) {
p.AnyOf = []*property{
{Type:p.Type},
{Type:"null"},
{Type: p.Type},
{Type: "null"},
}
p.Type = ""
}
Expand Down Expand Up @@ -131,6 +154,7 @@ func (p *property) readFromStruct(t reflect.Type) {
p.Properties[name] = &property{}
p.Properties[name].read(field.Type)
p.Properties[name].Description = field.Tag.Get("description")
p.Properties[name].addValidatorsFromTags(&field.Tag)

if opts.Contains("omitempty") || !required {
continue
Expand All @@ -139,6 +163,102 @@ func (p *property) readFromStruct(t reflect.Type) {
}
}

func (p *property) addValidatorsFromTags(tag *reflect.StructTag) {
switch p.Type {
case "string":
p.addStringValidators(tag)
case "number", "integer":
p.addNumberValidators(tag)
}
}

// Some helper functions for not having to create temp variables all over the place
func int64ptr(i interface{}) *int64 {
v := reflect.ValueOf(i)
if !v.Type().ConvertibleTo(rTypeInt64) {
return nil
}
j := v.Convert(rTypeInt64).Interface().(int64)
return &j
}

func float64ptr(i interface{}) *float64 {
v := reflect.ValueOf(i)
if !v.Type().ConvertibleTo(rTypeFloat64) {
return nil
}
j := v.Convert(rTypeFloat64).Interface().(float64)
return &j
}

func (p *property) addStringValidators(tag *reflect.StructTag) {
// min length
mls := tag.Get("minLength")
ml, err := strconv.ParseInt(mls, 10, 64)
if err == nil {
p.MinLength = int64ptr(ml)
}
// max length
mls = tag.Get("maxLength")
ml, err = strconv.ParseInt(mls, 10, 64)
if err == nil {
p.MaxLength = int64ptr(ml)
}
// pattern
pat := tag.Get("pattern")
if pat != "" {
p.Pattern = pat
}
// enum
en := tag.Get("enum")
if en != "" {
p.Enum = strings.Split(en, "|")
}
//const
c := tag.Get("const")
if c != "" {
p.Const = c
}
}

func (p *property) addNumberValidators(tag *reflect.StructTag) {
m, err := strconv.ParseFloat(tag.Get("multipleOf"), 64)
if err == nil {
p.MultipleOf = float64ptr(m)
}
m, err = strconv.ParseFloat(tag.Get("min"), 64)
if err == nil {
p.Minimum = float64ptr(m)
}
m, err = strconv.ParseFloat(tag.Get("max"), 64)
if err == nil {
p.Maximum = float64ptr(m)
}
m, err = strconv.ParseFloat(tag.Get("exclusiveMin"), 64)
if err == nil {
p.ExclusiveMinimum = float64ptr(m)
}
m, err = strconv.ParseFloat(tag.Get("exclusiveMax"), 64)
if err == nil {
p.ExclusiveMaximum = float64ptr(m)
}
c, err := parseType(tag.Get("const"), p.Type)
if err == nil {
p.Const = c
}
}

func parseType(str, ty string) (interface{}, error) {
var v interface{}
var err error
if ty == "number" {
v, err = strconv.ParseFloat(str, 64)
} else {
v, err = strconv.ParseInt(str, 10, 64)
}
return v, err
}

var formatMapping = map[string][]string{
"time.Time": []string{"string", "date-time"},
}
Expand Down Expand Up @@ -192,7 +312,7 @@ type structTag string

func parseTag(tag string) (string, structTag) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], structTag(tag[idx + 1:])
return tag[:idx], structTag(tag[idx+1:])
}
return tag, structTag("")
}
Expand All @@ -207,7 +327,7 @@ func (o structTag) Contains(optionName string) bool {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i + 1:]
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
Expand Down
64 changes: 50 additions & 14 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"
"time"

_ "github.com/tonnerre/golang-pretty"
. "gopkg.in/check.v1"
)

Expand Down Expand Up @@ -33,7 +34,7 @@ type ExampleJSONBasic struct {
Float32 float32 `json:",omitempty"`
Float64 float64
Interface interface{} `required:"true"`
Timestamp time.Time `json:",omitempty"`
Timestamp time.Time `json:",omitempty"`
}

func (self *propertySuite) TestLoad(c *C) {
Expand Down Expand Up @@ -69,7 +70,13 @@ func (self *propertySuite) TestLoad(c *C) {
}

type ExampleJSONBasicWithTag struct {
Bool bool `json:"test"`
Bool bool `json:"test"`
String string `json:"string" description:"blah" minLength:"3" maxLength:"10" pattern:"m{3,10}"`
Const string `json:"const" const:"blah"`
Float float32 `json:"float" min:"1.5" max:"42"`
Int int64 `json:"int" exclusiveMin:"-10" exclusiveMax:"0"`
AnswerToLife int `json:"answer" const:"42"`
Fruit string `json:"fruit" enum:"apple|banana|pear"`
}

func (self *propertySuite) TestLoadWithTag(c *C) {
Expand All @@ -79,9 +86,38 @@ func (self *propertySuite) TestLoadWithTag(c *C) {
c.Assert(*j, DeepEquals, Document{
Schema: "http://json-schema.org/schema#",
property: property{
Type: "object",
Type: "object",
Properties: map[string]*property{
"test": &property{Type: "boolean"},
"string": &property{
Type: "string",
MinLength: int64ptr(3),
MaxLength: int64ptr(10),
Pattern: "m{3,10}",
Description: "blah",
},
"const": &property{
Type: "string",
Const: "blah",
},
"float": &property{
Type: "number",
Minimum: float64ptr(1.5),
Maximum: float64ptr(42),
},
"int": &property{
Type: "integer",
ExclusiveMinimum: float64ptr(-10),
ExclusiveMaximum: float64ptr(0),
},
"answer": &property{
Type: "integer",
Const: int64(42),
},
"fruit": &property{
Type: "string",
Enum: []string{"apple", "banana", "pear"},
},
},
},
})
Expand Down Expand Up @@ -117,8 +153,8 @@ func (self *propertySuite) TestLoadSliceAndContains(c *C) {

type ExampleJSONNestedStruct struct {
Struct struct {
Foo string `required:"true"`
}
Foo string `required:"true"`
}
}

func (self *propertySuite) TestLoadNested(c *C) {
Expand Down Expand Up @@ -190,9 +226,9 @@ func (self *propertySuite) TestString(c *C) {
j.Read(true)

expected := "{\n" +
" \"$schema\": \"http://json-schema.org/schema#\",\n" +
" \"type\": \"boolean\"\n" +
"}"
" \"$schema\": \"http://json-schema.org/schema#\",\n" +
" \"type\": \"boolean\"\n" +
"}"

c.Assert(j.String(), Equals, expected)
}
Expand All @@ -202,16 +238,16 @@ func (self *propertySuite) TestMarshal(c *C) {
j.Read(10)

expected := "{\n" +
" \"$schema\": \"http://json-schema.org/schema#\",\n" +
" \"type\": \"integer\"\n" +
"}"
" \"$schema\": \"http://json-schema.org/schema#\",\n" +
" \"type\": \"integer\"\n" +
"}"

json := j.String()
c.Assert(string(json), Equals, expected)
}

type ExampleJSONNestedSliceStruct struct {
Struct []ItemStruct
Struct []ItemStruct
Struct2 []*ItemStruct
}
type ItemStruct struct {
Expand All @@ -227,7 +263,7 @@ func (self *propertySuite) TestLoadNestedSlice(c *C) {
property: property{
Type: "object",
Properties: map[string]*property{
"Struct":&property{
"Struct": &property{
Type: "array",
Items: &property{
Type: "object",
Expand All @@ -237,7 +273,7 @@ func (self *propertySuite) TestLoadNestedSlice(c *C) {
Required: []string{"Foo"},
},
},
"Struct2":&property{
"Struct2": &property{
Type: "array",
Items: &property{
Type: "object",
Expand Down