Skip to content

Commit

Permalink
Support query on dict/object values
Browse files Browse the repository at this point in the history
This commit adds support for looking up field definitions inside
schema.Dict and schema.Object instances. To achieve this,
Dict.ValuesValidator has been deprecated in favour of a new Dict.Values
property that holds a full schema.Field.

As a result, it should be possible to filter on schema.Dict and
schema.Object sub-fields.

PS! This commit does not add support for filtering on fields inside
schema.Array, schema.AnyOf or schema.AllOf; this will come later.
  • Loading branch information
smyrman committed Oct 5, 2017
1 parent 0df3129 commit 3ae6b0c
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 84 deletions.
36 changes: 31 additions & 5 deletions schema/dict.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import (
type Dict struct {
// KeysValidator is the validator to apply on dict keys.
KeysValidator FieldValidator
// ValuesValidator is the validator to apply on dict values.
ValuesValidator FieldValidator

// ValuesValidator is deprecated, use Values instead to specify a Field. If
// used, Values will be set to a required, non-filterable filed on Compile.
// ValuesValidator FieldValidator

// Values describes the properties for each dict value.
Values Field
// MinLen defines the minimum number of fields (default 0).
MinLen int
// MaxLen defines the maximum number of fields (default no limit).
Expand All @@ -23,8 +28,19 @@ func (v *Dict) Compile(rc ReferenceChecker) (err error) {
if err = c.Compile(rc); err != nil {
return
}

}
if c, ok := v.ValuesValidator.(Compiler); ok {
/*
// Populate v.Values from v.ValuesValidator for backwards-compatibility.
if val := v.ValuesValidator; val != nil {
log.Println("Deprecated: schema.Dict.ValuesValidator is deprecated in favour of schema.Dict.Values")
v.Values = schema.Field{
Required: true,
Validator: val,
}
}*/

if c, ok := v.Values.Validator.(Compiler); ok {
if err = c.Compile(rc); err != nil {
return
}
Expand All @@ -49,9 +65,9 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
return nil, errors.New("key validator does not return string")
}
}
if v.ValuesValidator != nil {
if v.Values.Validator != nil {
var err error
val, err = v.ValuesValidator.Validate(val)
val, err = v.Values.Validator.Validate(val)
if err != nil {
return nil, fmt.Errorf("invalid value for key `%s': %s", key, err)
}
Expand All @@ -67,3 +83,13 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
}
return dest, nil
}

// GetField implements the FieldGetter interface.
func (v Dict) GetField(name string) *Field {
if _, err := v.KeysValidator.Validate(name); err != nil {
return nil
}
// TODO: As part of issue #77, replace Dict.ValuesValidator with a
// schema.Field.
return &v.Values
}
8 changes: 5 additions & 3 deletions schema/dict_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ func ExampleDict() {
Allowed: []string{"foo", "bar"},
},
// Allow either string or integer as dict value
ValuesValidator: &schema.AnyOf{
0: &schema.String{},
1: &schema.Integer{},
Values: schema.Field{
Validator: &schema.AnyOf{
0: &schema.String{},
1: &schema.Integer{},
},
},
},
},
Expand Down
30 changes: 16 additions & 14 deletions schema/dict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ func TestDictCompile(t *testing.T) {
{
Name: "{KeysValidator:String,ValuesValidator:String}",
Compiler: &schema.Dict{
KeysValidator: &schema.String{},
ValuesValidator: &schema.String{},
KeysValidator: &schema.String{},
Values: schema.Field{
Validator: &schema.String{},
},
},
ReferenceChecker: fakeReferenceChecker{},
},
Expand All @@ -25,19 +27,19 @@ func TestDictCompile(t *testing.T) {
Error: "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`",
},
{
Name: "{ValuesValidator:String{Regexp:invalid}}",
Compiler: &schema.Dict{ValuesValidator: &schema.String{Regexp: "[invalid re"}},
Name: "{Values.Validator:String{Regexp:invalid}}",
Compiler: &schema.Dict{Values: schema.Field{Validator: &schema.String{Regexp: "[invalid re"}}},
ReferenceChecker: fakeReferenceChecker{},
Error: "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`",
},
{
Name: "{ValuesValidator:Reference{Path:valid}}",
Compiler: &schema.Dict{ValuesValidator: &schema.Reference{Path: "foo"}},
Name: "{Values.Validator:Reference{Path:valid}}",
Compiler: &schema.Dict{Values: schema.Field{Validator: &schema.Reference{Path: "foo"}}},
ReferenceChecker: fakeReferenceChecker{"foo": {}},
},
{
Name: "{ValuesValidator:Reference{Path:invalid}}",
Compiler: &schema.Dict{ValuesValidator: &schema.Reference{Path: "bar"}},
Name: "{Values.Validator:Reference{Path:invalid}}",
Compiler: &schema.Dict{Values: schema.Field{Validator: &schema.Reference{Path: "bar"}}},
ReferenceChecker: fakeReferenceChecker{"foo": {}},
Error: "can't find resource 'bar'",
},
Expand Down Expand Up @@ -68,20 +70,20 @@ func TestDictValidate(t *testing.T) {
Error: "invalid key `ba': is shorter than 3",
},
{
Name: `{ValuesValidator:Bool}.Validate(valid)`,
Validator: &schema.Dict{ValuesValidator: &schema.Bool{}},
Name: `{Values.Validator:Bool}.Validate(valid)`,
Validator: &schema.Dict{Values: schema.Field{Validator: &schema.Bool{}}},
Input: map[string]interface{}{"foo": true, "bar": false},
Expect: map[string]interface{}{"foo": true, "bar": false},
},
{
Name: `{ValuesValidator:Bool}.Validate({"foo":true,"bar":"value"})`,
Validator: &schema.Dict{ValuesValidator: &schema.Bool{}},
Name: `{Values.Validator:Bool}.Validate({"foo":true,"bar":"value"})`,
Validator: &schema.Dict{Values: schema.Field{Validator: &schema.Bool{}}},
Input: map[string]interface{}{"foo": true, "bar": "value"},
Error: "invalid value for key `bar': not a Boolean",
},
{
Name: `{ValuesValidator:String}.Validate("")`,
Validator: &schema.Dict{ValuesValidator: &schema.String{}},
Name: `{Values.Validator:String}.Validate("")`,
Validator: &schema.Dict{Values: schema.Field{Validator: &schema.String{}}},
Input: "",
Error: "not a dict",
},
Expand Down
4 changes: 1 addition & 3 deletions schema/encoding/jsonschema/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,7 @@ func complexSchema1() schema.Schema {
"m": schema.Field{
Description: "m",
Validator: &schema.Array{
ValuesValidator: &schema.Object{
Schema: mSchema,
},
ValuesValidator: &schema.Object{Schema: mSchema},
},
},
},
Expand Down
5 changes: 3 additions & 2 deletions schema/encoding/jsonschema/dict.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ func (v dictBuilder) BuildJSONSchema() (map[string]interface{}, error) {

// Retrieve values validator JSON schema.
var valuesSchema map[string]interface{}
if v.ValuesValidator != nil {
b, err := ValidatorBuilder(v.ValuesValidator)
if v.Values.Validator != nil {
b, err := ValidatorBuilder(v.Values.Validator)
if err != nil {
return nil, err
}
Expand All @@ -68,6 +68,7 @@ func (v dictBuilder) BuildJSONSchema() (map[string]interface{}, error) {
} else {
valuesSchema = map[string]interface{}{}
}
addFieldProperties(valuesSchema, v.Values)

// Compose JSON Schema.
switch len(patterns) {
Expand Down
18 changes: 12 additions & 6 deletions schema/encoding/jsonschema/dict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestDictValidatorEncode(t *testing.T) {
testCases := []encoderTestCase{
{
name: `KeysValidator=nil,ValuesValidator=nil}"`,
name: `KeysValidator=nil,Values.Validator=nil}"`,
schema: schema.Schema{
Fields: schema.Fields{
"d": {
Expand Down Expand Up @@ -46,12 +46,14 @@ func TestDictValidatorEncode(t *testing.T) {
customValidate: fieldValidator("d", `{"type": "object", "additionalProperties": true}`),
},
{
name: `ValuesValidator=Integer{}"`,
name: `Values.Validator=Integer{}"`,
schema: schema.Schema{
Fields: schema.Fields{
"d": {
Validator: &schema.Dict{
ValuesValidator: &schema.Integer{},
Values: schema.Field{
Validator: &schema.Integer{},
},
},
},
},
Expand Down Expand Up @@ -88,8 +90,10 @@ func TestDictValidatorEncode(t *testing.T) {
Fields: schema.Fields{
"d": {
Validator: &schema.Dict{
KeysValidator: &schema.String{Regexp: "re"},
ValuesValidator: &schema.Integer{},
KeysValidator: &schema.String{Regexp: "re"},
Values: schema.Field{
Validator: &schema.Integer{},
},
},
},
},
Expand Down Expand Up @@ -179,7 +183,9 @@ func TestDictValidatorEncode(t *testing.T) {
Regexp: "tch",
Allowed: []string{"match1", "match2"},
},
ValuesValidator: &schema.Integer{},
Values: schema.Field{
Validator: &schema.Integer{},
},
},
},
},
Expand Down
61 changes: 36 additions & 25 deletions schema/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,31 @@ type Field struct {
Schema *Schema
}

// FieldHandler is the piece of logic modifying the field value based on passed parameters
// Compile implements the ReferenceCompiler interface and recursively compile sub schemas
// and validators when they implement Compiler interface.
func (f Field) Compile(rc ReferenceChecker) error {
// TODO check field name format (alpha num + _ and -).
if f.Schema != nil {
// Recursively compile sub schema if any.
if err := f.Schema.Compile(rc); err != nil {
return fmt.Errorf(".%v", err)
}
} else if f.Validator != nil {
// Compile validator if it implements the ReferenceCompiler or Compiler interface.
if c, ok := f.Validator.(Compiler); ok {
if err := c.Compile(rc); err != nil {
return fmt.Errorf(": %v", err)
}
}
if reflect.ValueOf(f.Validator).Kind() != reflect.Ptr {
return errors.New(": not a schema.Validator pointer")
}
}
return nil
}

// FieldHandler is the piece of logic modifying the field value based on passed
// parameters
type FieldHandler func(ctx context.Context, value interface{}, params map[string]interface{}) (interface{}, error)

// FieldValidator is an interface for all individual validators. It takes a
Expand All @@ -73,9 +97,9 @@ type FieldValidator interface {
Validate(value interface{}) (interface{}, error)
}

//FieldValidatorFunc is an adapter to allow the use of ordinary functions as field validators.
// If f is a function with the appropriate signature, FieldValidatorFunc(f) is a FieldValidator
// that calls f.
//FieldValidatorFunc is an adapter to allow the use of ordinary functions as
// field validators. If f is a function with the appropriate signature,
// FieldValidatorFunc(f) is a FieldValidator that calls f.
type FieldValidatorFunc func(value interface{}) (interface{}, error)

// Validate calls f(value).
Expand All @@ -93,25 +117,12 @@ type FieldSerializer interface {
Serialize(value interface{}) (interface{}, error)
}

// Compile implements the ReferenceCompiler interface and recursively compile sub schemas
// and validators when they implement Compiler interface.
func (f Field) Compile(rc ReferenceChecker) error {
// TODO check field name format (alpha num + _ and -).
if f.Schema != nil {
// Recursively compile sub schema if any.
if err := f.Schema.Compile(rc); err != nil {
return fmt.Errorf(".%v", err)
}
} else if f.Validator != nil {
// Compile validator if it implements the ReferenceCompiler or Compiler interface.
if c, ok := f.Validator.(Compiler); ok {
if err := c.Compile(rc); err != nil {
return fmt.Errorf(": %v", err)
}
}
if reflect.ValueOf(f.Validator).Kind() != reflect.Ptr {
return errors.New(": not a schema.Validator pointer")
}
}
return nil
// FieldGetter is used to describe a FieldValidator (or Schema) which allow JSON
// object values.
type FieldGetter interface {
// GetField returns the validator for the field if the given field name is
// present in the schema.
//
// You may reference sub field using dotted notation field.subfield.
GetField(name string) *Field
}
5 changes: 5 additions & 0 deletions schema/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ func (v Object) Validate(value interface{}) (interface{}, error) {
return dest, nil
}

// GetField implements the FieldGetter interface.
func (v Object) GetField(name string) *Field {
return v.Schema.GetField(name)
}

// ErrorMap contains a map of errors by field name.
type ErrorMap map[string][]interface{}

Expand Down
52 changes: 26 additions & 26 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@ import (
"fmt"
"log"
"reflect"
"strings"
)

type internal struct{}

// Tombstone is used to mark a field for removal
// Tombstone is used to mark a field for removal.
var Tombstone = internal{}

// Validator is an interface used to validate schema against actual data
// Validator is an interface used to validate schema against actual data.
type Validator interface {
GetField(name string) *Field
Prepare(ctx context.Context, payload map[string]interface{}, original *map[string]interface{}, replace bool) (changes map[string]interface{}, base map[string]interface{})
Validate(changes map[string]interface{}, base map[string]interface{}) (doc map[string]interface{}, errs map[string][]interface{})
}

// Schema defines fields for a document
// Schema defines fields for a document.
type Schema struct {
// Description of the object described by this schema.
Description string
Expand Down Expand Up @@ -50,32 +49,33 @@ func (s Schema) Compile(rc ReferenceChecker) error {
return nil
}

// GetField returns the validator for the field if the given field name is
// present in the schema.
//
// You may reference sub field using dotted notation field.subfield.
// GetField implements the FieldGetter interface.
func (s Schema) GetField(name string) *Field {
// Split the name to get the current level name on first element and the
// rest of the path as second element if dot notation is used (i.e.:
// field.subfield.subsubfield -> field, subfield.subsubfield)
if i := strings.IndexByte(name, '.'); i != -1 {
remaining := name[i+1:]
name = name[:i]
field, found := s.Fields[name]
if !found {
// Invalid node
return nil
}
if field.Schema == nil {
// Invalid path
return nil
}
// Recursively call has field to consume the whole path.
return field.Schema.GetField(remaining)
name, remaining, wasSplit := splitFieldPath(name)

field, found := s.Fields[name]

if !found {
// invalid name.
return nil
}
if field, found := s.Fields[name]; found {

if !wasSplit {
// no remaining, return field.
return &field
}

if field.Schema != nil {
// Recursively call GetField to consume whole path.
// TODO: This will be removed when implementing issue #77.
return field.Schema.GetField(remaining)
}

if fg, ok := field.Validator.(FieldGetter); ok {
// Recursively call GetField to consume whole path.
return fg.GetField(remaining)
}

return nil
}

Expand Down
Loading

0 comments on commit 3ae6b0c

Please sign in to comment.