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

feat(*): implement json type system #55

Closed
wants to merge 4 commits into from
Closed
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
94 changes: 37 additions & 57 deletions keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -68,25 +29,24 @@ 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
}
}
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
}

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]))
Expand All @@ -108,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: []string{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 []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 !primitiveTypes[pr] {
return fmt.Errorf(`"%s" is not a valid type`, pr)
}
}
return nil
}

Expand Down Expand Up @@ -160,7 +126,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)
Expand Down Expand Up @@ -209,14 +175,28 @@ 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.
// 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 {
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)))
}
}
Expand Down
32 changes: 16 additions & 16 deletions keywords_arrays.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(), rawToVal(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(), rawToVal(arr[i]), errs)
}
}
}
Expand Down Expand Up @@ -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(), rawToVal(elem), errs)
}
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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, rawToVal(elem), test)
if len(*test) == 0 {
return
}
Expand Down
8 changes: 4 additions & 4 deletions keywords_booleans.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 4 additions & 6 deletions keywords_conditionals.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,7 +18,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 {
Expand Down Expand Up @@ -76,7 +74,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{} {
Expand Down Expand Up @@ -114,7 +112,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{} {
Expand Down
Loading