Skip to content

Commit 4d81ee0

Browse files
committed
Support query on dict/object values
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.
1 parent 0df3129 commit 4d81ee0

File tree

9 files changed

+135
-84
lines changed

9 files changed

+135
-84
lines changed

schema/dict.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ import (
99
type Dict struct {
1010
// KeysValidator is the validator to apply on dict keys.
1111
KeysValidator FieldValidator
12-
// ValuesValidator is the validator to apply on dict values.
13-
ValuesValidator FieldValidator
12+
13+
// ValuesValidator is deprecated, use Values instead to specify a Field. If
14+
// used, Values will be set to a required, non-filterable filed on Compile.
15+
// ValuesValidator FieldValidator
16+
17+
// Values describes the properties for each dict value.
18+
Values Field
1419
// MinLen defines the minimum number of fields (default 0).
1520
MinLen int
1621
// MaxLen defines the maximum number of fields (default no limit).
@@ -23,8 +28,19 @@ func (v *Dict) Compile(rc ReferenceChecker) (err error) {
2328
if err = c.Compile(rc); err != nil {
2429
return
2530
}
31+
2632
}
27-
if c, ok := v.ValuesValidator.(Compiler); ok {
33+
/*
34+
// Populate v.Values from v.ValuesValidator for backwards-compatibility.
35+
if val := v.ValuesValidator; val != nil {
36+
log.Println("Deprecated: schema.Dict.ValuesValidator is deprecated in favour of schema.Dict.Values")
37+
v.Values = schema.Field{
38+
Required: true,
39+
Validator: val,
40+
}
41+
}*/
42+
43+
if c, ok := v.Values.Validator.(Compiler); ok {
2844
if err = c.Compile(rc); err != nil {
2945
return
3046
}
@@ -49,9 +65,9 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
4965
return nil, errors.New("key validator does not return string")
5066
}
5167
}
52-
if v.ValuesValidator != nil {
68+
if v.Values.Validator != nil {
5369
var err error
54-
val, err = v.ValuesValidator.Validate(val)
70+
val, err = v.Values.Validator.Validate(val)
5571
if err != nil {
5672
return nil, fmt.Errorf("invalid value for key `%s': %s", key, err)
5773
}
@@ -67,3 +83,13 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
6783
}
6884
return dest, nil
6985
}
86+
87+
// GetField implements the FieldGetter interface.
88+
func (v Dict) GetField(name string) *Field {
89+
if _, err := v.KeysValidator.Validate(name); err != nil {
90+
return nil
91+
}
92+
// TODO: As part of issue #77, replace Dict.ValuesValidator with a
93+
// schema.Field.
94+
return &v.Values
95+
}

schema/dict_example_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ func ExampleDict() {
1212
Allowed: []string{"foo", "bar"},
1313
},
1414
// Allow either string or integer as dict value
15-
ValuesValidator: &schema.AnyOf{
16-
0: &schema.String{},
17-
1: &schema.Integer{},
15+
Values: schema.Field{
16+
Validator: &schema.AnyOf{
17+
0: &schema.String{},
18+
1: &schema.Integer{},
19+
},
1820
},
1921
},
2022
},

schema/dict_test.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ func TestDictCompile(t *testing.T) {
1313
{
1414
Name: "{KeysValidator:String,ValuesValidator:String}",
1515
Compiler: &schema.Dict{
16-
KeysValidator: &schema.String{},
17-
ValuesValidator: &schema.String{},
16+
KeysValidator: &schema.String{},
17+
Values: schema.Field{
18+
Validator: &schema.String{},
19+
},
1820
},
1921
ReferenceChecker: fakeReferenceChecker{},
2022
},
@@ -25,19 +27,19 @@ func TestDictCompile(t *testing.T) {
2527
Error: "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`",
2628
},
2729
{
28-
Name: "{ValuesValidator:String{Regexp:invalid}}",
29-
Compiler: &schema.Dict{ValuesValidator: &schema.String{Regexp: "[invalid re"}},
30+
Name: "{Values.Validator:String{Regexp:invalid}}",
31+
Compiler: &schema.Dict{Values: schema.Field{Validator: &schema.String{Regexp: "[invalid re"}}},
3032
ReferenceChecker: fakeReferenceChecker{},
3133
Error: "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`",
3234
},
3335
{
34-
Name: "{ValuesValidator:Reference{Path:valid}}",
35-
Compiler: &schema.Dict{ValuesValidator: &schema.Reference{Path: "foo"}},
36+
Name: "{Values.Validator:Reference{Path:valid}}",
37+
Compiler: &schema.Dict{Values: schema.Field{Validator: &schema.Reference{Path: "foo"}}},
3638
ReferenceChecker: fakeReferenceChecker{"foo": {}},
3739
},
3840
{
39-
Name: "{ValuesValidator:Reference{Path:invalid}}",
40-
Compiler: &schema.Dict{ValuesValidator: &schema.Reference{Path: "bar"}},
41+
Name: "{Values.Validator:Reference{Path:invalid}}",
42+
Compiler: &schema.Dict{Values: schema.Field{Validator: &schema.Reference{Path: "bar"}}},
4143
ReferenceChecker: fakeReferenceChecker{"foo": {}},
4244
Error: "can't find resource 'bar'",
4345
},
@@ -68,20 +70,20 @@ func TestDictValidate(t *testing.T) {
6870
Error: "invalid key `ba': is shorter than 3",
6971
},
7072
{
71-
Name: `{ValuesValidator:Bool}.Validate(valid)`,
72-
Validator: &schema.Dict{ValuesValidator: &schema.Bool{}},
73+
Name: `{Values.Validator:Bool}.Validate(valid)`,
74+
Validator: &schema.Dict{Values: schema.Field{Validator: &schema.Bool{}}},
7375
Input: map[string]interface{}{"foo": true, "bar": false},
7476
Expect: map[string]interface{}{"foo": true, "bar": false},
7577
},
7678
{
77-
Name: `{ValuesValidator:Bool}.Validate({"foo":true,"bar":"value"})`,
78-
Validator: &schema.Dict{ValuesValidator: &schema.Bool{}},
79+
Name: `{Values.Validator:Bool}.Validate({"foo":true,"bar":"value"})`,
80+
Validator: &schema.Dict{Values: schema.Field{Validator: &schema.Bool{}}},
7981
Input: map[string]interface{}{"foo": true, "bar": "value"},
8082
Error: "invalid value for key `bar': not a Boolean",
8183
},
8284
{
83-
Name: `{ValuesValidator:String}.Validate("")`,
84-
Validator: &schema.Dict{ValuesValidator: &schema.String{}},
85+
Name: `{Values.Validator:String}.Validate("")`,
86+
Validator: &schema.Dict{Values: schema.Field{Validator: &schema.String{}}},
8587
Input: "",
8688
Error: "not a dict",
8789
},

schema/encoding/jsonschema/benchmark_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ func complexSchema1() schema.Schema {
9999
"m": schema.Field{
100100
Description: "m",
101101
Validator: &schema.Array{
102-
ValuesValidator: &schema.Object{
103-
Schema: mSchema,
104-
},
102+
ValuesValidator: &schema.Object{Schema: mSchema},
105103
},
106104
},
107105
},

schema/encoding/jsonschema/dict.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ func (v dictBuilder) BuildJSONSchema() (map[string]interface{}, error) {
5656

5757
// Retrieve values validator JSON schema.
5858
var valuesSchema map[string]interface{}
59-
if v.ValuesValidator != nil {
60-
b, err := ValidatorBuilder(v.ValuesValidator)
59+
if v.Values.Validator != nil {
60+
b, err := ValidatorBuilder(v.Values.Validator)
6161
if err != nil {
6262
return nil, err
6363
}
@@ -68,6 +68,7 @@ func (v dictBuilder) BuildJSONSchema() (map[string]interface{}, error) {
6868
} else {
6969
valuesSchema = map[string]interface{}{}
7070
}
71+
addFieldProperties(valuesSchema, v.Values)
7172

7273
// Compose JSON Schema.
7374
switch len(patterns) {

schema/encoding/jsonschema/dict_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func TestDictValidatorEncode(t *testing.T) {
1010
testCases := []encoderTestCase{
1111
{
12-
name: `KeysValidator=nil,ValuesValidator=nil}"`,
12+
name: `KeysValidator=nil,Values.Validator=nil}"`,
1313
schema: schema.Schema{
1414
Fields: schema.Fields{
1515
"d": {
@@ -46,12 +46,14 @@ func TestDictValidatorEncode(t *testing.T) {
4646
customValidate: fieldValidator("d", `{"type": "object", "additionalProperties": true}`),
4747
},
4848
{
49-
name: `ValuesValidator=Integer{}"`,
49+
name: `Values.Validator=Integer{}"`,
5050
schema: schema.Schema{
5151
Fields: schema.Fields{
5252
"d": {
5353
Validator: &schema.Dict{
54-
ValuesValidator: &schema.Integer{},
54+
Values: schema.Field{
55+
Validator: &schema.Integer{},
56+
},
5557
},
5658
},
5759
},
@@ -88,8 +90,10 @@ func TestDictValidatorEncode(t *testing.T) {
8890
Fields: schema.Fields{
8991
"d": {
9092
Validator: &schema.Dict{
91-
KeysValidator: &schema.String{Regexp: "re"},
92-
ValuesValidator: &schema.Integer{},
93+
KeysValidator: &schema.String{Regexp: "re"},
94+
Values: schema.Field{
95+
Validator: &schema.Integer{},
96+
},
9397
},
9498
},
9599
},
@@ -179,7 +183,9 @@ func TestDictValidatorEncode(t *testing.T) {
179183
Regexp: "tch",
180184
Allowed: []string{"match1", "match2"},
181185
},
182-
ValuesValidator: &schema.Integer{},
186+
Values: schema.Field{
187+
Validator: &schema.Integer{},
188+
},
183189
},
184190
},
185191
},

schema/field.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,31 @@ type Field struct {
6363
Schema *Schema
6464
}
6565

66-
// FieldHandler is the piece of logic modifying the field value based on passed parameters
66+
// Compile implements the ReferenceCompiler interface and recursively compile sub schemas
67+
// and validators when they implement Compiler interface.
68+
func (f Field) Compile(rc ReferenceChecker) error {
69+
// TODO check field name format (alpha num + _ and -).
70+
if f.Schema != nil {
71+
// Recursively compile sub schema if any.
72+
if err := f.Schema.Compile(rc); err != nil {
73+
return fmt.Errorf(".%v", err)
74+
}
75+
} else if f.Validator != nil {
76+
// Compile validator if it implements the ReferenceCompiler or Compiler interface.
77+
if c, ok := f.Validator.(Compiler); ok {
78+
if err := c.Compile(rc); err != nil {
79+
return fmt.Errorf(": %v", err)
80+
}
81+
}
82+
if reflect.ValueOf(f.Validator).Kind() != reflect.Ptr {
83+
return errors.New(": not a schema.Validator pointer")
84+
}
85+
}
86+
return nil
87+
}
88+
89+
// FieldHandler is the piece of logic modifying the field value based on passed
90+
// parameters
6791
type FieldHandler func(ctx context.Context, value interface{}, params map[string]interface{}) (interface{}, error)
6892

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

76-
//FieldValidatorFunc is an adapter to allow the use of ordinary functions as field validators.
77-
// If f is a function with the appropriate signature, FieldValidatorFunc(f) is a FieldValidator
78-
// that calls f.
100+
//FieldValidatorFunc is an adapter to allow the use of ordinary functions as
101+
// field validators. If f is a function with the appropriate signature,
102+
// FieldValidatorFunc(f) is a FieldValidator that calls f.
79103
type FieldValidatorFunc func(value interface{}) (interface{}, error)
80104

81105
// Validate calls f(value).
@@ -93,25 +117,12 @@ type FieldSerializer interface {
93117
Serialize(value interface{}) (interface{}, error)
94118
}
95119

96-
// Compile implements the ReferenceCompiler interface and recursively compile sub schemas
97-
// and validators when they implement Compiler interface.
98-
func (f Field) Compile(rc ReferenceChecker) error {
99-
// TODO check field name format (alpha num + _ and -).
100-
if f.Schema != nil {
101-
// Recursively compile sub schema if any.
102-
if err := f.Schema.Compile(rc); err != nil {
103-
return fmt.Errorf(".%v", err)
104-
}
105-
} else if f.Validator != nil {
106-
// Compile validator if it implements the ReferenceCompiler or Compiler interface.
107-
if c, ok := f.Validator.(Compiler); ok {
108-
if err := c.Compile(rc); err != nil {
109-
return fmt.Errorf(": %v", err)
110-
}
111-
}
112-
if reflect.ValueOf(f.Validator).Kind() != reflect.Ptr {
113-
return errors.New(": not a schema.Validator pointer")
114-
}
115-
}
116-
return nil
120+
// FieldGetter is used to describe a FieldValidator (or Schema) which allow JSON
121+
// object values.
122+
type FieldGetter interface {
123+
// GetField returns the validator for the field if the given field name is
124+
// present in the schema.
125+
//
126+
// You may reference sub field using dotted notation field.subfield.
127+
GetField(name string) *Field
117128
}

schema/object.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func (v Object) Validate(value interface{}) (interface{}, error) {
3838
return dest, nil
3939
}
4040

41+
// GetField implements the FieldGetter interface.
42+
func (v Object) GetField(name string) *Field {
43+
return v.Schema.GetField(name)
44+
}
45+
4146
// ErrorMap contains a map of errors by field name.
4247
type ErrorMap map[string][]interface{}
4348

schema/schema.go

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,21 @@ import (
55
"fmt"
66
"log"
77
"reflect"
8-
"strings"
98
)
109

1110
type internal struct{}
1211

13-
// Tombstone is used to mark a field for removal
12+
// Tombstone is used to mark a field for removal.
1413
var Tombstone = internal{}
1514

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

23-
// Schema defines fields for a document
22+
// Schema defines fields for a document.
2423
type Schema struct {
2524
// Description of the object described by this schema.
2625
Description string
@@ -50,32 +49,33 @@ func (s Schema) Compile(rc ReferenceChecker) error {
5049
return nil
5150
}
5251

53-
// GetField returns the validator for the field if the given field name is
54-
// present in the schema.
55-
//
56-
// You may reference sub field using dotted notation field.subfield.
52+
// GetField implements the FieldGetter interface.
5753
func (s Schema) GetField(name string) *Field {
58-
// Split the name to get the current level name on first element and the
59-
// rest of the path as second element if dot notation is used (i.e.:
60-
// field.subfield.subsubfield -> field, subfield.subsubfield)
61-
if i := strings.IndexByte(name, '.'); i != -1 {
62-
remaining := name[i+1:]
63-
name = name[:i]
64-
field, found := s.Fields[name]
65-
if !found {
66-
// Invalid node
67-
return nil
68-
}
69-
if field.Schema == nil {
70-
// Invalid path
71-
return nil
72-
}
73-
// Recursively call has field to consume the whole path.
74-
return field.Schema.GetField(remaining)
54+
name, remaining, wasSplit := splitFieldPath(name)
55+
56+
field, found := s.Fields[name]
57+
58+
if !found {
59+
// invalid name.
60+
return nil
7561
}
76-
if field, found := s.Fields[name]; found {
62+
63+
if !wasSplit {
64+
// no remaining, return field.
7765
return &field
7866
}
67+
68+
if field.Schema != nil {
69+
// Recursively call GetField to consume whole path.
70+
// TODO: This will be removed when implementing issue #77.
71+
return field.Schema.GetField(remaining)
72+
}
73+
74+
if fg, ok := field.Validator.(FieldGetter); ok {
75+
// Recursively call GetField to consume whole path.
76+
return fg.GetField(remaining)
77+
}
78+
7979
return nil
8080
}
8181

0 commit comments

Comments
 (0)