Skip to content

Commit 424d5a6

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 58b1ad3 commit 424d5a6

File tree

11 files changed

+148
-85
lines changed

11 files changed

+148
-85
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The REST Layer framework is composed of several sub-packages:
2828

2929
# Documentation
3030

31+
- [Breaking Changes](#breaking-changes)
3132
- [Features](#features)
3233
- [Extensions](#extensions)
3334
- [Storage Handlers](#storage-handlers)
@@ -64,6 +65,16 @@ The REST Layer framework is composed of several sub-packages:
6465
- [Hystrix](#hystrix)
6566
- [JSONSchema](#jsonschema)
6667

68+
## Breaking Changes
69+
70+
Until version 1.0 of rest-layer, breaking changes may occur at any time if you rely on the latest master version.
71+
72+
Below is an overview over recent breaking changes, starting from an arbitrary point with PR #151:
73+
74+
- PR #151: `ValuesValidator FieldValidator` attribute in the `scheam.Dict` struct replaced by `Values Field`.
75+
76+
From the next release and onwards (0.2), this list will summarize breaking changes done to master since the last release.
77+
6778
## Features
6879

6980
- [x] Automatic handling of REST resource operations

schema/dict.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
"fmt"
66
)
77

8-
// Dict validates array values
8+
// Dict validates objects with variadic keys.
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+
// Values describes the properties for each dict value.
14+
Values Field
1415
// MinLen defines the minimum number of fields (default 0).
1516
MinLen int
1617
// MaxLen defines the maximum number of fields (default no limit).
@@ -23,8 +24,10 @@ func (v *Dict) Compile(rc ReferenceChecker) (err error) {
2324
if err = c.Compile(rc); err != nil {
2425
return
2526
}
27+
2628
}
27-
if c, ok := v.ValuesValidator.(Compiler); ok {
29+
30+
if c, ok := v.Values.Validator.(Compiler); ok {
2831
if err = c.Compile(rc); err != nil {
2932
return
3033
}
@@ -49,9 +52,9 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
4952
return nil, errors.New("key validator does not return string")
5053
}
5154
}
52-
if v.ValuesValidator != nil {
55+
if v.Values.Validator != nil {
5356
var err error
54-
val, err = v.ValuesValidator.Validate(val)
57+
val, err = v.Values.Validator.Validate(val)
5558
if err != nil {
5659
return nil, fmt.Errorf("invalid value for key `%s': %s", key, err)
5760
}
@@ -67,3 +70,11 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
6770
}
6871
return dest, nil
6972
}
73+
74+
// GetField implements the FieldGetter interface.
75+
func (v Dict) GetField(name string) *Field {
76+
if _, err := v.KeysValidator.Validate(name); err != nil {
77+
return nil
78+
}
79+
return &v.Values
80+
}

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 defines an interface for fetching sub-fields from a Schema or
121+
// FieldValidator implementation that allows (JSON) object values.
122+
type FieldGetter interface {
123+
// GetField returns a Field for the given name if the name is allowed by
124+
// the schema. The field is expected to validate query values.
125+
//
126+
// You may reference a sub-field using dotted notation, e.g. 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

0 commit comments

Comments
 (0)