From 7538bd2e9619b17ee74f0210826bbffef4bde596 Mon Sep 17 00:00:00 2001 From: Yan-Fa Li Date: Thu, 14 Jul 2016 18:11:50 -0700 Subject: [PATCH] POC: Implement an Object validator - 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) --- schema/object.go | 55 ++++++++++++ schema/object_test.go | 203 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 schema/object.go create mode 100644 schema/object_test.go diff --git a/schema/object.go b/schema/object.go new file mode 100644 index 00000000..f1ebff8c --- /dev/null +++ b/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 +} diff --git a/schema/object_test.go b/schema/object_test.go new file mode 100644 index 00000000..b65a8c5e --- /dev/null +++ b/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]") +}