Skip to content

Commit

Permalink
POC: Implement an Object validator
Browse files Browse the repository at this point in the history
- leverages Schema validation to validate a sub document using Schema
- allows you to create arrays of objects which have their fields
  validated correctly.
- add more tests for output of error map
- ensure ErrorMap.Error() output is deterministic
- simplify ErrorMap implementation (rs)
  • Loading branch information
Yan-Fa Li committed Aug 2, 2016
1 parent 2890f65 commit 7538bd2
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 0 deletions.
55 changes: 55 additions & 0 deletions schema/object.go
@@ -0,0 +1,55 @@
package schema

import (
"errors"
"fmt"
"sort"
"strings"
)

// Object validates objects which are defined by Schemas.
type Object struct {
Schema *Schema
}

// Compile implements Compiler interface
func (v *Object) Compile() error {
if v.Schema == nil {
return fmt.Errorf("No schema defined for object")
}

if err := compileDependencies(*v.Schema, v.Schema); err != nil {
return err
}
return nil
}

// ErrorMap to return lots of errors
type ErrorMap map[string][]interface{}

func (e ErrorMap) Error() string {
errs := make([]string, 0, len(e))
for key := range e {
errs = append(errs, key)
}
sort.Strings(errs)
for i, key := range errs {
errs[i] = fmt.Sprintf("%s is %s", key, e[key])
}
return strings.Join(errs, ", ")
}

// Validate implements FieldValidator interface
func (v Object) Validate(value interface{}) (interface{}, error) {
dict, ok := value.(map[string]interface{})
if !ok {
return nil, errors.New("not a dict")
}
dest, errs := v.Schema.Validate(nil, dict)
if len(errs) > 0 {
errMap := ErrorMap{}
errMap = errs
return nil, errMap
}
return dest, nil
}
203 changes: 203 additions & 0 deletions schema/object_test.go
@@ -0,0 +1,203 @@
package schema

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestInvalidObjectValidatorCompile(t *testing.T) {
v := &Object{}
err := v.Compile()
assert.Error(t, err)
}

func TestObjectValidatorCompile(t *testing.T) {
v := &Object{
Schema: &Schema{},
}
err := v.Compile()
assert.NoError(t, err)
}

func TestObjectWithSchemaValidatorCompile(t *testing.T) {
v := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
err := v.Compile()
assert.NoError(t, err)
}

func TestObjectValidator(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = "hello"
v := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
doc, err := v.Validate(obj)
assert.NoError(t, err)
assert.Equal(t, obj, doc)
}

func TestInvalidObjectValidator(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = 1
v := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
_, err := v.Validate(obj)
assert.Error(t, err)
}

func TestErrorObjectCast(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = 1
v := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
_, err := v.Validate(obj)
switch errMap := err.(type) {
case ErrorMap:
assert.True(t, true)
assert.Len(t, errMap, 1)
default:
assert.True(t, false)
}
}

func TestArrayOfObject(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = "a"
objb := make(map[string]interface{})
objb["test"] = "b"
value := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
array := Array{ValuesValidator: value}
a := []interface{}{obj, objb}
_, err := array.Validate(a)
assert.NoError(t, err)
}

func TestErrorArrayOfObject(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = "a"
objb := make(map[string]interface{})
objb["test"] = 1
value := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
array := Array{ValuesValidator: value}
a := []interface{}{obj, objb}
_, err := array.Validate(a)
assert.Error(t, err)
}

func TestErrorBasicMessage(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = 1
v := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
},
},
}
_, err := v.Validate(obj)
errMap, ok := err.(ErrorMap)
assert.True(t, ok)
assert.Len(t, errMap, 1)
assert.Equal(t, "test is [not a string]", errMap.Error())
}

func Test2ErrorFieldMessages(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = 1
obj["count"] = "blah"
v := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
"count": Field{
Validator: Integer{},
},
},
},
}
v.Compile()
_, err := v.Validate(obj)
errMap, ok := err.(ErrorMap)
assert.True(t, ok)
assert.Len(t, errMap, 2)
// can't guarentee order of keys in range over map so test them individually
assert.Equal(t, errMap.Error(), "count is [not an integer], test is [not a string]")
}

func TestErrorMessagesForObjectValidatorEmbeddedInArray(t *testing.T) {
obj := make(map[string]interface{})
obj["test"] = 1
obj["isUp"] = "false"
value := &Object{
Schema: &Schema{
Fields: Fields{
"test": Field{
Validator: String{},
},
"isUp": Field{
Validator: Bool{},
},
},
},
}
value.Compile()

array := Array{ValuesValidator: value}

// Not testing multiple array values being errors because Array
// implementation stops validating on first error found in array.
a := []interface{}{obj}
_, err := array.Validate(a)
assert.Error(t, err)
// can't guarantee order of keys in range over map so test them individually
assert.Equal(t, err.Error(), "invalid value at #1: isUp is [not a Boolean], test is [not a string]")
}

0 comments on commit 7538bd2

Please sign in to comment.