From 9a9544841d30768fa4c58ded2edc115df383c21f Mon Sep 17 00:00:00 2001 From: Arvydas Sidorenko Date: Sat, 31 Aug 2019 20:48:36 +0200 Subject: [PATCH 1/4] feat(*): implement higher level json types --- keywords.go | 74 ++++-------- keywords_arrays.go | 32 ++--- keywords_booleans.go | 8 +- keywords_conditionals.go | 6 +- keywords_format.go | 38 +++--- keywords_numeric.go | 87 ++++++++++---- keywords_objects.go | 66 +++++------ keywords_strings.go | 16 +-- schema.go | 4 +- schema_test.go | 30 ++--- types.go | 248 +++++++++++++++++++++++++++++++++++++++ val_error.go | 4 +- validate.go | 6 +- validate_test.go | 6 +- 14 files changed, 440 insertions(+), 185 deletions(-) create mode 100644 types.go diff --git a/keywords.go b/keywords.go index f8467ad..162bab0 100644 --- a/keywords.go +++ b/keywords.go @@ -5,47 +5,8 @@ import ( "fmt" "reflect" "strconv" - "strings" ) -// primitiveTypes is a map of strings to check types against -var primitiveTypes = map[string]bool{ - "null": true, - "boolean": true, - "object": true, - "array": true, - "number": true, - "string": true, - "integer": true, -} - -// DataType gives the primitive json type of a standard json-decoded value, plus the special case -// "integer" for when numbers are whole -func DataType(data interface{}) string { - if data == nil { - return "null" - } - - switch reflect.TypeOf(data).Kind() { - case reflect.Bool: - return "boolean" - case reflect.Float64: - number := reflect.ValueOf(data).Float() - if float64(int(number)) == number { - return "integer" - } - return "number" - case reflect.String: - return "string" - case reflect.Array, reflect.Slice: - return "array" - case reflect.Map, reflect.Struct: - return "object" - default: - return "unknown" - } -} - // Type specifies one of the six json primitive types. // The value of this keyword MUST be either a string or an array. // If it is an array, elements of the array MUST be strings and MUST be unique. @@ -55,7 +16,7 @@ func DataType(data interface{}) string { type Type struct { BaseValidator strVal bool // set to true if Type decoded from a string, false if an array - vals []string + vals Ts } // NewType creates a new Type Validator @@ -68,14 +29,13 @@ func (t Type) String() string { if len(t.vals) == 0 { return "unknown" } - return strings.Join(t.vals, ",") + return t.vals.String() } // Validate checks to see if input data satisfies the type constraint -func (t Type) Validate(propPath string, data interface{}, errs *[]ValError) { - jt := DataType(data) +func (t Type) Validate(propPath string, data Val, errs *[]ValError) { for _, typestr := range t.vals { - if jt == typestr || jt == "integer" && typestr == "number" { + if data.Type() == typestr || data.Type() == Integer && typestr == Number { return } } @@ -86,7 +46,7 @@ func (t Type) Validate(propPath string, data interface{}, errs *[]ValError) { str := "" for _, ts := range t.vals { - str += ts + "," + str += string(ts) + "," } t.AddError(errs, propPath, data, fmt.Sprintf(`type should be one of: %s`, str[:len(str)-1])) @@ -108,9 +68,9 @@ func (t Type) JSONProp(name string) interface{} { func (t *Type) UnmarshalJSON(data []byte) error { var single string if err := json.Unmarshal(data, &single); err == nil { - *t = Type{strVal: true, vals: []string{single}} + *t = Type{strVal: true, vals: Ts{T(single)}} } else { - var set []string + var set Ts if err := json.Unmarshal(data, &set); err == nil { *t = Type{vals: set} } else { @@ -119,7 +79,7 @@ func (t *Type) UnmarshalJSON(data []byte) error { } for _, pr := range t.vals { - if !primitiveTypes[pr] { + if !mapT[pr] { return fmt.Errorf(`"%s" is not a valid type`, pr) } } @@ -160,7 +120,7 @@ func (e Enum) Path() string { } // Validate implements the Validator interface for Enum -func (e Enum) Validate(propPath string, data interface{}, errs *[]ValError) { +func (e Enum) Validate(propPath string, data Val, errs *[]ValError) { for _, v := range e { test := &[]ValError{} v.Validate(propPath, data, test) @@ -209,14 +169,26 @@ func (c Const) Path() string { } // Validate implements the validate interface for Const -func (c Const) Validate(propPath string, data interface{}, errs *[]ValError) { +func (c Const) Validate(propPath string, data Val, errs *[]ValError) { var con interface{} if err := json.Unmarshal(c, &con); err != nil { AddError(errs, propPath, data, err.Error()) return } - if !reflect.DeepEqual(con, data) { + // Normalize the input to match the constant type. + var val interface{} + b, err := json.Marshal(data.Raw()) + if err != nil { + AddError(errs, propPath, data, err.Error()) + return + } + if err := json.Unmarshal(b, &val); err != nil { + AddError(errs, propPath, data, err.Error()) + return + } + + if !reflect.DeepEqual(con, val) { AddError(errs, propPath, data, fmt.Sprintf(`must equal %s`, InvalidValueString(con))) } } diff --git a/keywords_arrays.go b/keywords_arrays.go index a372624..8f5b4ef 100644 --- a/keywords_arrays.go +++ b/keywords_arrays.go @@ -30,23 +30,23 @@ func NewItems() Validator { } // Validate implements the Validator interface for Items -func (it Items) Validate(propPath string, data interface{}, errs *[]ValError) { +func (it Items) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, fmt.Sprintf("invalid property path: %s", err.Error())) } - if arr, ok := data.([]interface{}); ok { + if arr, ok := data.(ArrayVal); ok { if it.single { for i, elem := range arr { d, _ := jp.Descendant(strconv.Itoa(i)) - it.Schemas[0].Validate(d.String(), elem, errs) + it.Schemas[0].Validate(d.String(), dataToVal(elem), errs) } } else { for i, vs := range it.Schemas { if i < len(arr) { d, _ := jp.Descendant(strconv.Itoa(i)) - vs.Validate(d.String(), arr[i], errs) + vs.Validate(d.String(), dataToVal(arr[i]), errs) } } } @@ -114,20 +114,20 @@ func NewAdditionalItems() Validator { } // Validate implements the Validator interface for AdditionalItems -func (a *AdditionalItems) Validate(propPath string, data interface{}, errs *[]ValError) { +func (a *AdditionalItems) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, fmt.Sprintf("invalid property path: %s", err.Error())) } if a.startIndex >= 0 { - if arr, ok := data.([]interface{}); ok { + if arr, ok := data.(ArrayVal); ok { for i, elem := range arr { if i < a.startIndex { continue } d, _ := jp.Descendant(strconv.Itoa(i)) - a.Schema.Validate(d.String(), elem, errs) + a.Schema.Validate(d.String(), dataToVal(elem), errs) } } } @@ -168,8 +168,8 @@ func NewMaxItems() Validator { } // Validate implements the Validator interface for MaxItems -func (m MaxItems) Validate(propPath string, data interface{}, errs *[]ValError) { - if arr, ok := data.([]interface{}); ok { +func (m MaxItems) Validate(propPath string, data Val, errs *[]ValError) { + if arr, ok := data.(ArrayVal); ok { if len(arr) > int(m) { AddError(errs, propPath, data, fmt.Sprintf("array length %d exceeds %d max", len(arr), m)) return @@ -188,8 +188,8 @@ func NewMinItems() Validator { } // Validate implements the Validator interface for MinItems -func (m MinItems) Validate(propPath string, data interface{}, errs *[]ValError) { - if arr, ok := data.([]interface{}); ok { +func (m MinItems) Validate(propPath string, data Val, errs *[]ValError) { + if arr, ok := data.(ArrayVal); ok { if len(arr) < int(m) { AddError(errs, propPath, data, fmt.Sprintf("array length %d below %d minimum items", len(arr), m)) return @@ -209,8 +209,8 @@ func NewUniqueItems() Validator { } // Validate implements the Validator interface for UniqueItems -func (u *UniqueItems) Validate(propPath string, data interface{}, errs *[]ValError) { - if arr, ok := data.([]interface{}); ok { +func (u *UniqueItems) Validate(propPath string, data Val, errs *[]ValError) { + if arr, ok := data.(ArrayVal); ok { found := []interface{}{} for _, elem := range arr { for _, f := range found { @@ -234,12 +234,12 @@ func NewContains() Validator { } // Validate implements the Validator interface for Contains -func (c *Contains) Validate(propPath string, data interface{}, errs *[]ValError) { +func (c *Contains) Validate(propPath string, data Val, errs *[]ValError) { v := Schema(*c) - if arr, ok := data.([]interface{}); ok { + if arr, ok := data.(ArrayVal); ok { for _, elem := range arr { test := &[]ValError{} - v.Validate(propPath, elem, test) + v.Validate(propPath, dataToVal(elem), test) if len(*test) == 0 { return } diff --git a/keywords_booleans.go b/keywords_booleans.go index 2713d21..6f35a36 100644 --- a/keywords_booleans.go +++ b/keywords_booleans.go @@ -15,7 +15,7 @@ func NewAllOf() Validator { } // Validate implements the validator interface for AllOf -func (a AllOf) Validate(propPath string, data interface{}, errs *[]ValError) { +func (a AllOf) Validate(propPath string, data Val, errs *[]ValError) { for _, sch := range a { sch.Validate(propPath, data, errs) } @@ -53,7 +53,7 @@ func NewAnyOf() Validator { } // Validate implements the validator interface for AnyOf -func (a AnyOf) Validate(propPath string, data interface{}, errs *[]ValError) { +func (a AnyOf) Validate(propPath string, data Val, errs *[]ValError) { for _, sch := range a { test := &[]ValError{} sch.Validate(propPath, data, test) @@ -95,7 +95,7 @@ func NewOneOf() Validator { } // Validate implements the validator interface for OneOf -func (o OneOf) Validate(propPath string, data interface{}, errs *[]ValError) { +func (o OneOf) Validate(propPath string, data Val, errs *[]ValError) { matched := false for _, sch := range o { test := &[]ValError{} @@ -145,7 +145,7 @@ func NewNot() Validator { } // Validate implements the validator interface for Not -func (n *Not) Validate(propPath string, data interface{}, errs *[]ValError) { +func (n *Not) Validate(propPath string, data Val, errs *[]ValError) { sch := Schema(*n) test := &[]ValError{} sch.Validate(propPath, data, test) diff --git a/keywords_conditionals.go b/keywords_conditionals.go index 042ff20..5a368ce 100644 --- a/keywords_conditionals.go +++ b/keywords_conditionals.go @@ -20,7 +20,7 @@ func NewIf() Validator { } // Validate implements the Validator interface for If -func (i *If) Validate(propPath string, data interface{}, errs *[]ValError) { +func (i *If) Validate(propPath string, data Val, errs *[]ValError) { test := &[]ValError{} i.Schema.Validate(propPath, data, test) if len(*test) == 0 { @@ -76,7 +76,7 @@ func NewThen() Validator { } // Validate implements the Validator interface for Then -func (t *Then) Validate(propPath string, data interface{}, errs *[]ValError) {} +func (t *Then) Validate(propPath string, data Val, errs *[]ValError) {} // JSONProp implements JSON property name indexing for Then func (t Then) JSONProp(name string) interface{} { @@ -114,7 +114,7 @@ func NewElse() Validator { } // Validate implements the Validator interface for Else -func (e *Else) Validate(propPath string, data interface{}, err *[]ValError) {} +func (e *Else) Validate(propPath string, data Val, err *[]ValError) {} // JSONProp implements JSON property name indexing for Else func (e Else) JSONProp(name string) interface{} { diff --git a/keywords_format.go b/keywords_format.go index 795f0b5..2204bfb 100644 --- a/keywords_format.go +++ b/keywords_format.go @@ -58,44 +58,44 @@ func NewFormat() Validator { } // Validate validates input against a keyword -func (f Format) Validate(propPath string, data interface{}, errs *[]ValError) { +func (f Format) Validate(propPath string, data Val, errs *[]ValError) { var err error - if str, ok := data.(string); ok { + if str, ok := data.(StringVal); ok { switch f { case "date-time": - err = isValidDateTime(str) + err = isValidDateTime(string(str)) case "date": - err = isValidDate(str) + err = isValidDate(string(str)) case "email": - err = isValidEmail(str) + err = isValidEmail(string(str)) case "hostname": - err = isValidHostname(str) + err = isValidHostname(string(str)) case "idn-email": - err = isValidIDNEmail(str) + err = isValidIDNEmail(string(str)) case "idn-hostname": - err = isValidIDNHostname(str) + err = isValidIDNHostname(string(str)) case "ipv4": - err = isValidIPv4(str) + err = isValidIPv4(string(str)) case "ipv6": - err = isValidIPv6(str) + err = isValidIPv6(string(str)) case "iri-reference": - err = isValidIriRef(str) + err = isValidIriRef(string(str)) case "iri": - err = isValidIri(str) + err = isValidIri(string(str)) case "json-pointer": - err = isValidJSONPointer(str) + err = isValidJSONPointer(string(str)) case "regex": - err = isValidRegex(str) + err = isValidRegex(string(str)) case "relative-json-pointer": - err = isValidRelJSONPointer(str) + err = isValidRelJSONPointer(string(str)) case "time": - err = isValidTime(str) + err = isValidTime(string(str)) case "uri-reference": - err = isValidURIRef(str) + err = isValidURIRef(string(str)) case "uri-template": - err = isValidURITemplate(str) + err = isValidURITemplate(string(str)) case "uri": - err = isValidURI(str) + err = isValidURI(string(str)) default: err = nil } diff --git a/keywords_numeric.go b/keywords_numeric.go index de08d3c..254fcc3 100644 --- a/keywords_numeric.go +++ b/keywords_numeric.go @@ -15,12 +15,19 @@ func NewMultipleOf() Validator { } // Validate implements the Validator interface for MultipleOf -func (m MultipleOf) Validate(propPath string, data interface{}, errs *[]ValError) { - if num, ok := data.(float64); ok { - div := num / float64(m) - if float64(int(div)) != div { - AddError(errs, propPath, data, fmt.Sprintf("must be a multiple of %f", m)) - } +func (m MultipleOf) Validate(propPath string, data Val, errs *[]ValError) { + var num float64 + if v, ok := data.(IntVal); ok { + num = float64(v.Value()) + } else if v, ok := data.(NumberVal); ok { + num = v.Value() + } else { + return + } + + div := num / float64(m) + if float64(int(div)) != div { + AddError(errs, propPath, data, fmt.Sprintf("must be a multiple of %f", m)) } } @@ -35,11 +42,18 @@ func NewMaximum() Validator { } // Validate implements the Validator interface for Maximum -func (m Maximum) Validate(propPath string, data interface{}, errs *[]ValError) { - if num, ok := data.(float64); ok { - if num > float64(m) { - AddError(errs, propPath, data, fmt.Sprintf("must be less than or equal to %f", m)) - } +func (m Maximum) Validate(propPath string, data Val, errs *[]ValError) { + var num float64 + if v, ok := data.(IntVal); ok { + num = float64(v.Value()) + } else if v, ok := data.(NumberVal); ok { + num = v.Value() + } else { + return + } + + if num > float64(m) { + AddError(errs, propPath, data, fmt.Sprintf("must be less than or equal to %f", m)) } } @@ -54,11 +68,18 @@ func NewExclusiveMaximum() Validator { } // Validate implements the Validator interface for ExclusiveMaximum -func (m ExclusiveMaximum) Validate(propPath string, data interface{}, errs *[]ValError) { - if num, ok := data.(float64); ok { - if num >= float64(m) { - AddError(errs, propPath, data, fmt.Sprintf("must be less than %f", m)) - } +func (m ExclusiveMaximum) Validate(propPath string, data Val, errs *[]ValError) { + var num float64 + if v, ok := data.(IntVal); ok { + num = float64(v.Value()) + } else if v, ok := data.(NumberVal); ok { + num = v.Value() + } else { + return + } + + if num >= float64(m) { + AddError(errs, propPath, data, fmt.Sprintf("must be less than %f", m)) } } @@ -72,11 +93,18 @@ func NewMinimum() Validator { } // Validate implements the Validator interface for Minimum -func (m Minimum) Validate(propPath string, data interface{}, errs *[]ValError) { - if num, ok := data.(float64); ok { - if num < float64(m) { - AddError(errs, propPath, data, fmt.Sprintf("must be greater than or equal to %f", m)) - } +func (m Minimum) Validate(propPath string, data Val, errs *[]ValError) { + var num float64 + if v, ok := data.(IntVal); ok { + num = float64(v.Value()) + } else if v, ok := data.(NumberVal); ok { + num = v.Value() + } else { + return + } + + if num < float64(m) { + AddError(errs, propPath, data, fmt.Sprintf("must be greater than or equal to %f", m)) } } @@ -90,10 +118,17 @@ func NewExclusiveMinimum() Validator { } // Validate implements the Validator interface for ExclusiveMinimum -func (m ExclusiveMinimum) Validate(propPath string, data interface{}, errs *[]ValError) { - if num, ok := data.(float64); ok { - if num <= float64(m) { - AddError(errs, propPath, data, fmt.Sprintf("must be greater than %f", m)) - } +func (m ExclusiveMinimum) Validate(propPath string, data Val, errs *[]ValError) { + var num float64 + if v, ok := data.(IntVal); ok { + num = float64(v.Value()) + } else if v, ok := data.(NumberVal); ok { + num = v.Value() + } else { + return + } + + if num <= float64(m) { + AddError(errs, propPath, data, fmt.Sprintf("must be greater than %f", m)) } } diff --git a/keywords_objects.go b/keywords_objects.go index b560b8b..d990b2d 100644 --- a/keywords_objects.go +++ b/keywords_objects.go @@ -18,10 +18,10 @@ func NewMaxProperties() Validator { } // Validate implements the validator interface for MaxProperties -func (m MaxProperties) Validate(propPath string, data interface{}, errs *[]ValError) { - if obj, ok := data.(map[string]interface{}); ok { - if len(obj) > int(m) { - AddError(errs, propPath, data, fmt.Sprintf("%d object Properties exceed %d maximum", len(obj), m)) +func (m MaxProperties) Validate(propPath string, data Val, errs *[]ValError) { + if obj, ok := data.(*ObjectVal); ok { + if len(obj.Map()) > int(m) { + AddError(errs, propPath, data, fmt.Sprintf("%d object Properties exceed %d maximum", len(obj.Map()), m)) } } } @@ -37,10 +37,10 @@ func NewMinProperties() Validator { } // Validate implements the validator interface for minProperties -func (m minProperties) Validate(propPath string, data interface{}, errs *[]ValError) { - if obj, ok := data.(map[string]interface{}); ok { - if len(obj) < int(m) { - AddError(errs, propPath, data, fmt.Sprintf("%d object Properties below %d minimum", len(obj), m)) +func (m minProperties) Validate(propPath string, data Val, errs *[]ValError) { + if obj, ok := data.(*ObjectVal); ok { + if len(obj.Map()) < int(m) { + AddError(errs, propPath, data, fmt.Sprintf("%d object Properties below %d minimum", len(obj.Map()), m)) } } } @@ -56,10 +56,10 @@ func NewRequired() Validator { } // Validate implements the validator interface for Required -func (r Required) Validate(propPath string, data interface{}, errs *[]ValError) { - if obj, ok := data.(map[string]interface{}); ok { +func (r Required) Validate(propPath string, data Val, errs *[]ValError) { + if obj, ok := data.(*ObjectVal); ok { for _, key := range r { - if val, ok := obj[key]; val == nil && !ok { + if val, ok := obj.Map()[key]; val == nil && !ok { AddError(errs, propPath, data, fmt.Sprintf(`"%s" value is required`, key)) } } @@ -92,18 +92,18 @@ func NewProperties() Validator { } // Validate implements the validator interface for Properties -func (p Properties) Validate(propPath string, data interface{}, errs *[]ValError) { +func (p Properties) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, "invalid property path") return } - if obj, ok := data.(map[string]interface{}); ok { - for key, val := range obj { + if obj, ok := data.(*ObjectVal); ok { + for key, val := range obj.Map() { if p[key] != nil { d, _ := jp.Descendant(key) - p[key].Validate(d.String(), val, errs) + p[key].Validate(d.String(), dataToVal(val), errs) } } } @@ -146,19 +146,19 @@ type patternSchema struct { } // Validate implements the validator interface for PatternProperties -func (p PatternProperties) Validate(propPath string, data interface{}, errs *[]ValError) { +func (p PatternProperties) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, "invalid property path") return } - if obj, ok := data.(map[string]interface{}); ok { - for key, val := range obj { + if obj, ok := data.(*ObjectVal); ok { + for key, val := range obj.Map() { for _, ptn := range p { if ptn.re.Match([]byte(key)) { d, _ := jp.Descendant(key) - ptn.schema.Validate(d.String(), val, errs) + ptn.schema.Validate(d.String(), dataToVal(val), errs) } } } @@ -237,16 +237,16 @@ func NewAdditionalProperties() Validator { } // Validate implements the validator interface for AdditionalProperties -func (ap AdditionalProperties) Validate(propPath string, data interface{}, errs *[]ValError) { +func (ap AdditionalProperties) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, "invalid property path") return } - if obj, ok := data.(map[string]interface{}); ok { + if obj, ok := data.(*ObjectVal); ok { KEYS: - for key, val := range obj { + for key, val := range obj.Map() { if ap.Properties != nil { for propKey := range *ap.Properties { if propKey == key { @@ -263,7 +263,7 @@ func (ap AdditionalProperties) Validate(propPath string, data interface{}, errs } // c := len(*errs) d, _ := jp.Descendant(key) - ap.Schema.Validate(d.String(), val, errs) + ap.Schema.Validate(d.String(), dataToVal(val), errs) // if len(*errs) > c { // // fmt.Sprintf("object key %s AdditionalProperties error: %s", key, err.Error()) // return @@ -320,16 +320,16 @@ func NewDependencies() Validator { } // Validate implements the validator interface for Dependencies -func (d Dependencies) Validate(propPath string, data interface{}, errs *[]ValError) { +func (d Dependencies) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, "invalid property path") return } - if obj, ok := data.(map[string]interface{}); ok { + if obj, ok := data.(*ObjectVal); ok { for key, val := range d { - if obj[key] != nil { + if obj.Map()[key] != nil { d, _ := jp.Descendant(key) val.Validate(d.String(), obj, errs) } @@ -361,13 +361,13 @@ type Dependency struct { } // Validate implements the validator interface for Dependency -func (d Dependency) Validate(propPath string, data interface{}, errs *[]ValError) { - if obj, ok := data.(map[string]interface{}); ok { +func (d Dependency) Validate(propPath string, data Val, errs *[]ValError) { + if obj, ok := data.(*ObjectVal); ok { if d.schema != nil { d.schema.Validate(propPath, data, errs) } else if len(d.props) > 0 { for _, k := range d.props { - if obj[k] == nil { + if obj.Map()[k] == nil { AddError(errs, propPath, data, fmt.Sprintf("Dependency property %s is Required", k)) } } @@ -411,7 +411,7 @@ func NewPropertyNames() Validator { } // Validate implements the validator interface for PropertyNames -func (p PropertyNames) Validate(propPath string, data interface{}, errs *[]ValError) { +func (p PropertyNames) Validate(propPath string, data Val, errs *[]ValError) { jp, err := jsonpointer.Parse(propPath) if err != nil { AddError(errs, propPath, nil, "invalid property path") @@ -419,11 +419,11 @@ func (p PropertyNames) Validate(propPath string, data interface{}, errs *[]ValEr } sch := Schema(p) - if obj, ok := data.(map[string]interface{}); ok { - for key := range obj { + if obj, ok := data.(*ObjectVal); ok { + for key := range obj.Map() { // TODO - adjust error message & prop path d, _ := jp.Descendant(key) - sch.Validate(d.String(), key, errs) + sch.Validate(d.String(), dataToVal(key), errs) } } } diff --git a/keywords_strings.go b/keywords_strings.go index d2c8b5b..56ada04 100644 --- a/keywords_strings.go +++ b/keywords_strings.go @@ -18,9 +18,9 @@ func NewMaxLength() Validator { } // Validate implements the Validator interface for MaxLength -func (m MaxLength) Validate(propPath string, data interface{}, errs *[]ValError) { - if str, ok := data.(string); ok { - if utf8.RuneCountInString(str) > int(m) { +func (m MaxLength) Validate(propPath string, data Val, errs *[]ValError) { + if str, ok := data.(StringVal); ok { + if utf8.RuneCountInString(string(str)) > int(m) { AddError(errs, propPath, data, fmt.Sprintf("max length of %d characters exceeded: %s", m, str)) } } @@ -38,9 +38,9 @@ func NewMinLength() Validator { } // Validate implements the Validator interface for MinLength -func (m MinLength) Validate(propPath string, data interface{}, errs *[]ValError) { - if str, ok := data.(string); ok { - if utf8.RuneCountInString(str) < int(m) { +func (m MinLength) Validate(propPath string, data Val, errs *[]ValError) { + if str, ok := data.(StringVal); ok { + if utf8.RuneCountInString(string(str)) < int(m) { AddError(errs, propPath, data, fmt.Sprintf("min length of %d characters required: %s", m, str)) } } @@ -58,9 +58,9 @@ func NewPattern() Validator { } // Validate implements the Validator interface for Pattern -func (p Pattern) Validate(propPath string, data interface{}, errs *[]ValError) { +func (p Pattern) Validate(propPath string, data Val, errs *[]ValError) { re := regexp.Regexp(p) - if str, ok := data.(string); ok { + if str, ok := data.(StringVal); ok { if !re.Match([]byte(str)) { AddError(errs, propPath, data, fmt.Sprintf("regexp pattern %s mismatch on string: %s", re.String(), str)) } diff --git a/schema.go b/schema.go index ee1f939..73ffa36 100644 --- a/schema.go +++ b/schema.go @@ -188,7 +188,7 @@ func (rs *RootSchema) ValidateBytes(data []byte) ([]ValError, error) { if err := json.Unmarshal(data, &doc); err != nil { return errs, fmt.Errorf("error parsing JSON bytes: %s", err.Error()) } - rs.Validate("/", doc, &errs) + rs.Validate("/", dataToVal(doc), &errs) return errs, nil } @@ -365,7 +365,7 @@ func (s *Schema) Path() string { // Validate uses the schema to check an instance, collecting validation // errors in a slice -func (s *Schema) Validate(propPath string, data interface{}, errs *[]ValError) { +func (s *Schema) Validate(propPath string, data Val, errs *[]ValError) { if s.Ref != "" && s.ref != nil { s.ref.Validate(propPath, data, errs) return diff --git a/schema_test.go b/schema_test.go index 32dfed7..8ad1788 100644 --- a/schema_test.go +++ b/schema_test.go @@ -435,7 +435,7 @@ func runJSONTests(t *testing.T, testFilepaths []string) { for i, c := range ts.Tests { tests++ got := []ValError{} - sc.Validate("/", c.Data, &got) + sc.Validate("/", dataToVal(c.Data), &got) valid := len(got) == 0 if valid != c.Valid { t.Errorf("%s: %s test case %d: %s. error: %s", base, ts.Description, i, c.Description, got) @@ -449,28 +449,28 @@ func runJSONTests(t *testing.T, testFilepaths []string) { } func TestDataType(t *testing.T) { - type customObject struct {} + type customObject struct{} type customNumber float64 cases := []struct { data interface{} - expect string + expect T }{ - {nil, "null"}, - {float64(4), "integer"}, - {float64(4.5), "number"}, - {customNumber(4.5), "number"}, - {"foo", "string"}, - {[]interface{}{}, "array"}, - {[0]interface{}{}, "array"}, - {map[string]interface{}{}, "object"}, - {struct{}{}, "object"}, - {customObject{}, "object"}, - {int8(42), "unknown"}, + {nil, Null}, + {float64(4), Integer}, + {float64(4.5), Number}, + {customNumber(4.5), Number}, + {"foo", String}, + {[]interface{}{}, Array}, + {[0]interface{}{}, Array}, + {map[string]interface{}{}, Object}, + {struct{}{}, Object}, + {customObject{}, Object}, + {int8(42), Unknown}, } for i, c := range cases { - got := DataType(c.data) + got := dataToT(c.data) if got != c.expect { t.Errorf("case %d result mismatch. expected: '%s', got: '%s'", i, c.expect, got) } diff --git a/types.go b/types.go new file mode 100644 index 0000000..e0b1361 --- /dev/null +++ b/types.go @@ -0,0 +1,248 @@ +package jsonschema + +import ( + "fmt" + "reflect" + "strings" +) + +type T string + +const ( + Unknown T = "unknown" + Null = "null" + Bool = "boolean" + Object = "object" + Array = "array" + Number = "number" + Integer = "integer" + String = "string" +) + +// dataToT resolves data to a JSON type. +func dataToT(data interface{}) T { + if data == nil { + return Null + } + + knd := reflect.TypeOf(data).Kind() + _ = knd + + switch reflect.TypeOf(data).Kind() { + case reflect.Bool: + return Bool + case reflect.Float64: + number := reflect.ValueOf(data).Float() + if float64(int(number)) == number { + return Integer + } + return Number + case reflect.String: + return String + case reflect.Array, reflect.Slice: + return Array + case reflect.Map, reflect.Struct: + return Object + default: + return Unknown + } +} + +// Ts is a collection of JSON types. +type Ts []T + +func (ts Ts) String() string { + switch len(ts) { + case 0: + return "" + case 1: + return string(ts[0]) + } + + // Calculate string length. + n := len(ts) - 1 + for i := 0; i < len(ts); i++ { + n += len(ts[i]) + } + + var b strings.Builder + b.Grow(n) + b.WriteString(string(ts[0])) + for _, t := range ts[1:] { + b.WriteString(",") + b.WriteString(string(t)) + } + return b.String() +} + +var mapT = map[T]bool{ + Null: true, + Bool: true, + Object: true, + Array: true, + Number: true, + Integer: true, + String: true, +} + +type Val interface { + Type() T + Raw() interface{} +} + +type NullVal struct{} + +func (v NullVal) Type() T { + return Null +} + +func (v NullVal) Raw() interface{} { + return nil +} + +type BoolVal bool + +func (v BoolVal) Type() T { + return Bool +} + +func (v BoolVal) Raw() interface{} { + return bool(v) +} + +type NumberVal float64 + +func (v NumberVal) Type() T { + return Number +} + +func (v NumberVal) Raw() interface{} { + return float64(v) +} + +func (v NumberVal) Value() float64 { + return float64(v) +} + +type IntVal int64 + +func (v IntVal) Type() T { + return Integer +} + +func (v IntVal) Raw() interface{} { + return int64(v) +} + +func (v IntVal) Value() int64 { + return int64(v) +} + +type StringVal string + +func (v StringVal) Type() T { + return String +} + +func (v StringVal) Raw() interface{} { + return string(v) +} + +type ObjectVal struct { + raw interface{} + m map[string]interface{} +} + +func (v *ObjectVal) Type() T { + return Object +} + +func (v *ObjectVal) Raw() interface{} { + return v.raw +} + +func (v *ObjectVal) Map() map[string]interface{} { + v.ensureM() + return v.m +} + +func (v *ObjectVal) ensureM() { + if v.raw == nil || v.m != nil { + return + } + + switch reflect.TypeOf(v.raw).Kind() { + case reflect.Map: + switch m := v.raw.(type) { + case map[string]interface{}: + v.m = m + case map[interface{}]interface{}: + tmp := make(map[string]interface{}, len(m)) + for k, v := range m { + switch str := k.(type) { + case string: + tmp[str] = v + default: + return + } + } + v.m = tmp + } + case reflect.Struct: + val := reflect.ValueOf(v.raw) + tmp := make(map[string]interface{}, val.NumField()) + for i := 0; i < val.NumField(); i++ { + f := val.Field(i) + t := val.Type() + tmp[t.Name()] = f.Interface() + } + v.m = tmp + } +} + +type ArrayVal []interface{} + +func (v ArrayVal) Type() T { + return Array +} + +func (v ArrayVal) Raw() interface{} { + return []interface{}(v) +} + +var _ Val = NullVal{} +var _ Val = BoolVal(false) +var _ Val = NumberVal(0) +var _ Val = IntVal(0) +var _ Val = StringVal("") +var _ Val = &ObjectVal{} +var _ Val = ArrayVal{} + +func dataToVal(data interface{}) Val { + if data == nil { + return NullVal{} + } + + val := reflect.ValueOf(data) + + switch dataToT(data) { + case Bool: + return BoolVal(val.Bool()) + case Number: + return NumberVal(val.Float()) + case Integer: + return IntVal(int64(val.Float())) + case String: + return StringVal(val.String()) + case Object: + return &ObjectVal{raw: data} + case Array: + arr := make([]interface{}, 0, val.Len()) + for i := 0; i < val.Len(); i++ { + arr = append(arr, val.Index(i).Interface()) + } + return ArrayVal(arr) + default: + panic(fmt.Sprintf("unable to handle data of type '%v' (%v)", dataToT(data), data)) + } +} diff --git a/val_error.go b/val_error.go index 75852f6..752118d 100644 --- a/val_error.go +++ b/val_error.go @@ -45,10 +45,10 @@ func InvalidValueString(data interface{}) string { } // AddError creates and appends a ValError to errs -func AddError(errs *[]ValError, propPath string, data interface{}, msg string) { +func AddError(errs *[]ValError, propPath string, data Val, msg string) { *errs = append(*errs, ValError{ PropertyPath: propPath, - InvalidValue: data, + InvalidValue: data.Raw(), Message: msg, }) } diff --git a/validate.go b/validate.go index 57ff528..d05c864 100644 --- a/validate.go +++ b/validate.go @@ -11,7 +11,7 @@ type Validator interface { // Validate checks decoded JSON data and writes // validation errors (if any) to an outparam slice of ValErrors // propPath indicates the position of data in the json tree - Validate(propPath string, data interface{}, errs *[]ValError) + Validate(propPath string, val Val, errs *[]ValError) } // BaseValidator is a foundation for building a validator @@ -30,11 +30,11 @@ func (b BaseValidator) Path() string { } // AddError is a convenience method for appending a new error to an existing error slice -func (b BaseValidator) AddError(errs *[]ValError, propPath string, data interface{}, msg string) { +func (b BaseValidator) AddError(errs *[]ValError, propPath string, data Val, msg string) { *errs = append(*errs, ValError{ PropertyPath: propPath, RulePath: b.Path(), - InvalidValue: data, + InvalidValue: data.Raw(), Message: msg, }) } diff --git a/validate_test.go b/validate_test.go index daf9817..837efa3 100644 --- a/validate_test.go +++ b/validate_test.go @@ -12,8 +12,8 @@ func newIsFoo() Validator { return new(IsFoo) } -func (f IsFoo) Validate(propPath string, data interface{}, errs *[]ValError) { - if str, ok := data.(string); ok { +func (f IsFoo) Validate(propPath string, data Val, errs *[]ValError) { + if str, ok := data.(StringVal); ok { if str != "foo" { AddError(errs, propPath, data, fmt.Sprintf("should be foo. plz make '%s' == foo. plz", str)) } @@ -44,7 +44,7 @@ func ExampleCustomValidator() { type FooValidator uint8 -func (f *FooValidator) Validate(propPath string, data interface{}, errs *[]ValError) {} +func (f *FooValidator) Validate(propPath string, data Val, errs *[]ValError) {} func TestRegisterValidator(t *testing.T) { newFoo := func() Validator { From cdba76df318e5db1fce43e82fc9337bcac2aa268 Mon Sep 17 00:00:00 2001 From: Arvydas Sidorenko Date: Thu, 5 Sep 2019 21:10:22 +0200 Subject: [PATCH 2/4] Improve type system --- keywords.go | 2 + keywords_arrays.go | 8 ++-- keywords_objects.go | 64 ++++++++++++------------- schema.go | 2 +- schema_test.go | 2 +- types.go | 112 +++++++++++++++++++++++++++++--------------- 6 files changed, 114 insertions(+), 76 deletions(-) diff --git a/keywords.go b/keywords.go index 162bab0..4c97ded 100644 --- a/keywords.go +++ b/keywords.go @@ -177,6 +177,8 @@ func (c Const) Validate(propPath string, data Val, errs *[]ValError) { } // Normalize the input to match the constant type. + // We could do it more efficiently by providing Val.Compare(), + // but for now this does the job. var val interface{} b, err := json.Marshal(data.Raw()) if err != nil { diff --git a/keywords_arrays.go b/keywords_arrays.go index 8f5b4ef..ee7a1c1 100644 --- a/keywords_arrays.go +++ b/keywords_arrays.go @@ -40,13 +40,13 @@ func (it Items) Validate(propPath string, data Val, errs *[]ValError) { if it.single { for i, elem := range arr { d, _ := jp.Descendant(strconv.Itoa(i)) - it.Schemas[0].Validate(d.String(), dataToVal(elem), errs) + it.Schemas[0].Validate(d.String(), rawToVal(elem), errs) } } else { for i, vs := range it.Schemas { if i < len(arr) { d, _ := jp.Descendant(strconv.Itoa(i)) - vs.Validate(d.String(), dataToVal(arr[i]), errs) + vs.Validate(d.String(), rawToVal(arr[i]), errs) } } } @@ -127,7 +127,7 @@ func (a *AdditionalItems) Validate(propPath string, data Val, errs *[]ValError) continue } d, _ := jp.Descendant(strconv.Itoa(i)) - a.Schema.Validate(d.String(), dataToVal(elem), errs) + a.Schema.Validate(d.String(), rawToVal(elem), errs) } } } @@ -239,7 +239,7 @@ func (c *Contains) Validate(propPath string, data Val, errs *[]ValError) { if arr, ok := data.(ArrayVal); ok { for _, elem := range arr { test := &[]ValError{} - v.Validate(propPath, dataToVal(elem), test) + v.Validate(propPath, rawToVal(elem), test) if len(*test) == 0 { return } diff --git a/keywords_objects.go b/keywords_objects.go index d990b2d..f8286fe 100644 --- a/keywords_objects.go +++ b/keywords_objects.go @@ -19,9 +19,9 @@ func NewMaxProperties() Validator { // Validate implements the validator interface for MaxProperties func (m MaxProperties) Validate(propPath string, data Val, errs *[]ValError) { - if obj, ok := data.(*ObjectVal); ok { - if len(obj.Map()) > int(m) { - AddError(errs, propPath, data, fmt.Sprintf("%d object Properties exceed %d maximum", len(obj.Map()), m)) + if obj, ok := data.(ObjectVal); ok { + if obj.Len() > int(m) { + AddError(errs, propPath, data, fmt.Sprintf("%d object Properties exceed %d maximum", obj.Len(), m)) } } } @@ -38,9 +38,9 @@ func NewMinProperties() Validator { // Validate implements the validator interface for minProperties func (m minProperties) Validate(propPath string, data Val, errs *[]ValError) { - if obj, ok := data.(*ObjectVal); ok { - if len(obj.Map()) < int(m) { - AddError(errs, propPath, data, fmt.Sprintf("%d object Properties below %d minimum", len(obj.Map()), m)) + if obj, ok := data.(ObjectVal); ok { + if obj.Len() < int(m) { + AddError(errs, propPath, data, fmt.Sprintf("%d object Properties below %d minimum", obj.Len(), m)) } } } @@ -57,9 +57,9 @@ func NewRequired() Validator { // Validate implements the validator interface for Required func (r Required) Validate(propPath string, data Val, errs *[]ValError) { - if obj, ok := data.(*ObjectVal); ok { + if obj, ok := data.(ObjectVal); ok { for _, key := range r { - if val, ok := obj.Map()[key]; val == nil && !ok { + if val, ok := obj.Field(key); val == nil && !ok { AddError(errs, propPath, data, fmt.Sprintf(`"%s" value is required`, key)) } } @@ -99,11 +99,11 @@ func (p Properties) Validate(propPath string, data Val, errs *[]ValError) { return } - if obj, ok := data.(*ObjectVal); ok { - for key, val := range obj.Map() { - if p[key] != nil { - d, _ := jp.Descendant(key) - p[key].Validate(d.String(), dataToVal(val), errs) + if obj, ok := data.(ObjectVal); ok { + for pair := range obj.Iterator(nil) { + if p[pair.Key] != nil { + d, _ := jp.Descendant(pair.Key) + p[pair.Key].Validate(d.String(), rawToVal(pair.Value), errs) } } } @@ -153,12 +153,12 @@ func (p PatternProperties) Validate(propPath string, data Val, errs *[]ValError) return } - if obj, ok := data.(*ObjectVal); ok { - for key, val := range obj.Map() { + if obj, ok := data.(ObjectVal); ok { + for pair := range obj.Iterator(nil) { for _, ptn := range p { - if ptn.re.Match([]byte(key)) { - d, _ := jp.Descendant(key) - ptn.schema.Validate(d.String(), dataToVal(val), errs) + if ptn.re.Match([]byte(pair.Key)) { + d, _ := jp.Descendant(pair.Key) + ptn.schema.Validate(d.String(), rawToVal(pair.Value), errs) } } } @@ -244,26 +244,26 @@ func (ap AdditionalProperties) Validate(propPath string, data Val, errs *[]ValEr return } - if obj, ok := data.(*ObjectVal); ok { + if obj, ok := data.(ObjectVal); ok { KEYS: - for key, val := range obj.Map() { + for pair := range obj.Iterator(nil) { if ap.Properties != nil { for propKey := range *ap.Properties { - if propKey == key { + if propKey == pair.Key { continue KEYS } } } if ap.patterns != nil { for _, ptn := range *ap.patterns { - if ptn.re.Match([]byte(key)) { + if ptn.re.Match([]byte(pair.Key)) { continue KEYS } } } // c := len(*errs) - d, _ := jp.Descendant(key) - ap.Schema.Validate(d.String(), dataToVal(val), errs) + d, _ := jp.Descendant(pair.Key) + ap.Schema.Validate(d.String(), rawToVal(pair.Value), errs) // if len(*errs) > c { // // fmt.Sprintf("object key %s AdditionalProperties error: %s", key, err.Error()) // return @@ -327,9 +327,9 @@ func (d Dependencies) Validate(propPath string, data Val, errs *[]ValError) { return } - if obj, ok := data.(*ObjectVal); ok { + if obj, ok := data.(ObjectVal); ok { for key, val := range d { - if obj.Map()[key] != nil { + if v, _ := obj.Field(key); v != nil { d, _ := jp.Descendant(key) val.Validate(d.String(), obj, errs) } @@ -362,12 +362,12 @@ type Dependency struct { // Validate implements the validator interface for Dependency func (d Dependency) Validate(propPath string, data Val, errs *[]ValError) { - if obj, ok := data.(*ObjectVal); ok { + if obj, ok := data.(ObjectVal); ok { if d.schema != nil { d.schema.Validate(propPath, data, errs) } else if len(d.props) > 0 { for _, k := range d.props { - if obj.Map()[k] == nil { + if v, _ := obj.Field(k); v == nil { AddError(errs, propPath, data, fmt.Sprintf("Dependency property %s is Required", k)) } } @@ -419,11 +419,11 @@ func (p PropertyNames) Validate(propPath string, data Val, errs *[]ValError) { } sch := Schema(p) - if obj, ok := data.(*ObjectVal); ok { - for key := range obj.Map() { + if obj, ok := data.(ObjectVal); ok { + for pair := range obj.Iterator(nil) { // TODO - adjust error message & prop path - d, _ := jp.Descendant(key) - sch.Validate(d.String(), dataToVal(key), errs) + d, _ := jp.Descendant(pair.Key) + sch.Validate(d.String(), rawToVal(pair.Key), errs) } } } diff --git a/schema.go b/schema.go index 73ffa36..42ca8de 100644 --- a/schema.go +++ b/schema.go @@ -188,7 +188,7 @@ func (rs *RootSchema) ValidateBytes(data []byte) ([]ValError, error) { if err := json.Unmarshal(data, &doc); err != nil { return errs, fmt.Errorf("error parsing JSON bytes: %s", err.Error()) } - rs.Validate("/", dataToVal(doc), &errs) + rs.Validate("/", rawToVal(doc), &errs) return errs, nil } diff --git a/schema_test.go b/schema_test.go index 8ad1788..5a9081d 100644 --- a/schema_test.go +++ b/schema_test.go @@ -435,7 +435,7 @@ func runJSONTests(t *testing.T, testFilepaths []string) { for i, c := range ts.Tests { tests++ got := []ValError{} - sc.Validate("/", dataToVal(c.Data), &got) + sc.Validate("/", rawToVal(c.Data), &got) valid := len(got) == 0 if valid != c.Valid { t.Errorf("%s: %s test case %d: %s. error: %s", base, ts.Description, i, c.Description, got) diff --git a/types.go b/types.go index e0b1361..bd25ab8 100644 --- a/types.go +++ b/types.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "strings" + "unicode" ) type T string @@ -150,54 +151,89 @@ func (v StringVal) Raw() interface{} { type ObjectVal struct { raw interface{} - m map[string]interface{} } -func (v *ObjectVal) Type() T { +func (v ObjectVal) Type() T { return Object } -func (v *ObjectVal) Raw() interface{} { +func (v ObjectVal) Raw() interface{} { return v.raw } -func (v *ObjectVal) Map() map[string]interface{} { - v.ensureM() - return v.m +func (v ObjectVal) Len() int { + switch reflect.TypeOf(v.raw).Kind() { + case reflect.Map: + return reflect.ValueOf(v.raw).Len() + case reflect.Struct: + return reflect.ValueOf(v.raw).NumField() + default: + panic(fmt.Sprintf("invalid value: %v", v.raw)) + } } -func (v *ObjectVal) ensureM() { - if v.raw == nil || v.m != nil { - return - } +func (v ObjectVal) Field(name string) (interface{}, bool) { + var val reflect.Value switch reflect.TypeOf(v.raw).Kind() { case reflect.Map: - switch m := v.raw.(type) { - case map[string]interface{}: - v.m = m - case map[interface{}]interface{}: - tmp := make(map[string]interface{}, len(m)) - for k, v := range m { - switch str := k.(type) { - case string: - tmp[str] = v - default: + val = reflect.ValueOf(v.raw).MapIndex(reflect.ValueOf(name)) + case reflect.Struct: + val = reflect.ValueOf(v.raw).FieldByName(name) + default: + panic(fmt.Sprintf("invalid value: %v", v.raw)) + } + + if !val.IsValid() { + return nil, false + } + return val.Interface(), true +} + +type Pair struct { + Key string + Value interface{} +} + +func (v ObjectVal) Iterator(cancelC chan struct{}) <-chan Pair { + ch := make(chan Pair, 50) + + go func() { + defer close(ch) + + switch reflect.TypeOf(v.raw).Kind() { + case reflect.Map: + for i := reflect.ValueOf(v.raw).MapRange(); i.Next(); { + select { + case ch <- Pair{ + Key: i.Key().String(), + Value: i.Value().Interface(), + }: + case <-cancelC: + return + } + } + case reflect.Struct: + val := reflect.ValueOf(v.raw) + valType := val.Type() + for i := 0; i < val.NumField(); i++ { + name := valType.Field(i).Name + if unicode.IsLower(rune(name[0])) { + continue + } + select { + case ch <- Pair{ + Key: valType.Field(i).Name, + Value: val.Field(i).Interface(), + }: + case <-cancelC: return } } - v.m = tmp - } - case reflect.Struct: - val := reflect.ValueOf(v.raw) - tmp := make(map[string]interface{}, val.NumField()) - for i := 0; i < val.NumField(); i++ { - f := val.Field(i) - t := val.Type() - tmp[t.Name()] = f.Interface() } - v.m = tmp - } + }() + + return ch } type ArrayVal []interface{} @@ -215,17 +251,17 @@ var _ Val = BoolVal(false) var _ Val = NumberVal(0) var _ Val = IntVal(0) var _ Val = StringVal("") -var _ Val = &ObjectVal{} +var _ Val = ObjectVal{} var _ Val = ArrayVal{} -func dataToVal(data interface{}) Val { - if data == nil { +func rawToVal(raw interface{}) Val { + if raw == nil { return NullVal{} } - val := reflect.ValueOf(data) + val := reflect.ValueOf(raw) - switch dataToT(data) { + switch dataToT(raw) { case Bool: return BoolVal(val.Bool()) case Number: @@ -235,7 +271,7 @@ func dataToVal(data interface{}) Val { case String: return StringVal(val.String()) case Object: - return &ObjectVal{raw: data} + return ObjectVal{raw: raw} case Array: arr := make([]interface{}, 0, val.Len()) for i := 0; i < val.Len(); i++ { @@ -243,6 +279,6 @@ func dataToVal(data interface{}) Val { } return ArrayVal(arr) default: - panic(fmt.Sprintf("unable to handle data of type '%v' (%v)", dataToT(data), data)) + panic(fmt.Sprintf("unable to handle raw data of type '%v' (%v)", dataToT(raw), raw)) } } From fd226d0bc6611460342e53ecd5ef42982e3e9139 Mon Sep 17 00:00:00 2001 From: Arvydas Sidorenko Date: Fri, 6 Sep 2019 08:21:49 +0200 Subject: [PATCH 3/4] Declare type T as integer --- keywords.go | 26 +++++++++------- keywords_conditionals.go | 4 +-- keywords_format.go | 1 - keywords_numeric.go | 4 +-- keywords_objects.go | 3 +- schema_test.go | 9 +++--- traversal_test.go | 1 - types.go | 64 +++++++++++++++++++++++++--------------- 8 files changed, 66 insertions(+), 46 deletions(-) diff --git a/keywords.go b/keywords.go index 4c97ded..dc51643 100644 --- a/keywords.go +++ b/keywords.go @@ -40,7 +40,7 @@ func (t Type) Validate(propPath string, data Val, errs *[]ValError) { } } if len(t.vals) == 1 { - t.AddError(errs, propPath, data, fmt.Sprintf(`type should be %s`, t.vals[0])) + t.AddError(errs, propPath, data, fmt.Sprintf(`type should be %s`, mapTStr[t.vals[0]])) return } @@ -68,21 +68,27 @@ func (t Type) JSONProp(name string) interface{} { func (t *Type) UnmarshalJSON(data []byte) error { var single string if err := json.Unmarshal(data, &single); err == nil { - *t = Type{strVal: true, vals: Ts{T(single)}} + if v, ok := mapStrT[single]; ok { + *t = Type{strVal: true, vals: Ts{v}} + } else { + return fmt.Errorf(`"%s" is not a valid type`, single) + } } else { - var set Ts + var set []string if err := json.Unmarshal(data, &set); err == nil { - *t = Type{vals: set} + ts := make(Ts, len(set)) + for i, v := range set { + t, ok := mapStrT[v] + if !ok { + return fmt.Errorf(`"%s" is not a valid type`, single) + } + ts[i] = t + } + *t = Type{vals: ts} } else { return err } } - - for _, pr := range t.vals { - if !mapT[pr] { - return fmt.Errorf(`"%s" is not a valid type`, pr) - } - } return nil } diff --git a/keywords_conditionals.go b/keywords_conditionals.go index 5a368ce..092ebbe 100644 --- a/keywords_conditionals.go +++ b/keywords_conditionals.go @@ -1,8 +1,6 @@ package jsonschema -import ( - "encoding/json" -) +import "encoding/json" // If MUST be a valid JSON Schema. // Instances that successfully validate against this keyword's subschema MUST also be valid against the subschema value of the "Then" keyword, if present. diff --git a/keywords_format.go b/keywords_format.go index 2204bfb..cee9519 100644 --- a/keywords_format.go +++ b/keywords_format.go @@ -1,7 +1,6 @@ package jsonschema import ( - // "encoding/json" "fmt" "net" "net/mail" diff --git a/keywords_numeric.go b/keywords_numeric.go index 254fcc3..9afd4e2 100644 --- a/keywords_numeric.go +++ b/keywords_numeric.go @@ -1,8 +1,6 @@ package jsonschema -import ( - "fmt" -) +import "fmt" // MultipleOf MUST be a number, strictly greater than 0. // MultipleOf validates that a numeric instance is valid only if division diff --git a/keywords_objects.go b/keywords_objects.go index f8286fe..3c790de 100644 --- a/keywords_objects.go +++ b/keywords_objects.go @@ -3,9 +3,10 @@ package jsonschema import ( "encoding/json" "fmt" - "github.com/qri-io/jsonpointer" "regexp" "strconv" + + "github.com/qri-io/jsonpointer" ) // MaxProperties MUST be a non-negative integer. diff --git a/schema_test.go b/schema_test.go index 5a9081d..c1b798d 100644 --- a/schema_test.go +++ b/schema_test.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/sergi/go-diff/diffmatchpatch" "io/ioutil" - // "net/http" - // "net/http/httptest" "path/filepath" + "strconv" + "strings" "testing" + + "github.com/sergi/go-diff/diffmatchpatch" ) func ExampleBasic() { @@ -472,7 +473,7 @@ func TestDataType(t *testing.T) { for i, c := range cases { got := dataToT(c.data) if got != c.expect { - t.Errorf("case %d result mismatch. expected: '%s', got: '%s'", i, c.expect, got) + t.Errorf("case %d result mismatch. expected: '%s', got: '%s'", i, mapTStr[c.expect], mapTStr[got]) } } } diff --git a/traversal_test.go b/traversal_test.go index f7d3443..146bf0d 100644 --- a/traversal_test.go +++ b/traversal_test.go @@ -100,5 +100,4 @@ func TestReferenceTraversal(t *testing.T) { t.Errorf("case %d: expected %d references, got: %d", i, c.refs, refs) } } - } diff --git a/types.go b/types.go index bd25ab8..8702e9c 100644 --- a/types.go +++ b/types.go @@ -1,25 +1,53 @@ package jsonschema import ( + "encoding/json" "fmt" "reflect" "strings" "unicode" ) -type T string +type T int const ( - Unknown T = "unknown" - Null = "null" - Bool = "boolean" - Object = "object" - Array = "array" - Number = "number" - Integer = "integer" - String = "string" + Unknown T = iota + Null + Bool + Object + Array + Number + Integer + String ) +// MarshalJSON implements the json.Marshaler interface for T +func (t T) MarshalJSON() ([]byte, error) { + return json.Marshal(mapTStr[t]) +} + +var mapTStr = [8]string{ + Unknown: "unknown", + Null: "null", + Bool: "boolean", + Object: "object", + Array: "array", + Number: "number", + Integer: "integer", + String: "string", +} + +var mapStrT = map[string]T{ + "unknown": Unknown, + "null": Null, + "boolean": Bool, + "object": Object, + "array": Array, + "number": Number, + "integer": Integer, + "string": String, +} + // dataToT resolves data to a JSON type. func dataToT(data interface{}) T { if data == nil { @@ -57,35 +85,25 @@ func (ts Ts) String() string { case 0: return "" case 1: - return string(ts[0]) + return mapTStr[ts[0]] } // Calculate string length. n := len(ts) - 1 for i := 0; i < len(ts); i++ { - n += len(ts[i]) + n += len(mapTStr[ts[i]]) } var b strings.Builder b.Grow(n) - b.WriteString(string(ts[0])) + b.WriteString(mapTStr[ts[0]]) for _, t := range ts[1:] { b.WriteString(",") - b.WriteString(string(t)) + b.WriteString(mapTStr[t]) } return b.String() } -var mapT = map[T]bool{ - Null: true, - Bool: true, - Object: true, - Array: true, - Number: true, - Integer: true, - String: true, -} - type Val interface { Type() T Raw() interface{} From 4bca40ff533c990784140063659ea12f7f704176 Mon Sep 17 00:00:00 2001 From: Arvydas Sidorenko Date: Fri, 6 Sep 2019 08:30:24 +0200 Subject: [PATCH 4/4] Remove unused imports --- schema_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/schema_test.go b/schema_test.go index c1b798d..aeda0ac 100644 --- a/schema_test.go +++ b/schema_test.go @@ -6,8 +6,6 @@ import ( "fmt" "io/ioutil" "path/filepath" - "strconv" - "strings" "testing" "github.com/sergi/go-diff/diffmatchpatch"