Skip to content

Commit f7e9c99

Browse files
smyrmanrs
authored andcommitted
Adding MinLen/MaxLen to Dict and Schema (#60)
Dict and Schema now got MinLen/MaxLen in the same way as Array. For Schema only, the JSON Schema encoding has been updated to include minProperties/maxProperties based on these values. Dict is not yet supported. Dict tests have been rewritten to table-tests, while Schema has received new table-tests in both packages.
1 parent cb188d3 commit f7e9c99

File tree

6 files changed

+264
-33
lines changed

6 files changed

+264
-33
lines changed

schema/dict.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ type Dict struct {
1111
KeysValidator FieldValidator
1212
// ValuesValidator is the validator to apply on dict values
1313
ValuesValidator FieldValidator
14+
// MinLen defines the minimum number of fields (default 0)
15+
MinLen int
16+
// MaxLen defines the maximum number of fields (default no limit)
17+
MaxLen int
1418
}
1519

1620
// Compile implements Compiler interface
@@ -54,5 +58,12 @@ func (v Dict) Validate(value interface{}) (interface{}, error) {
5458
}
5559
dest[key] = val
5660
}
61+
l := len(dest)
62+
if l < v.MinLen {
63+
return nil, fmt.Errorf("has fewer properties than %d", v.MinLen)
64+
}
65+
if v.MaxLen > 0 && l > v.MaxLen {
66+
return nil, fmt.Errorf("has more properties than %d", v.MaxLen)
67+
}
5768
return dest, nil
5869
}

schema/dict_test.go

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,102 @@
1-
package schema
1+
// +build go1.7
2+
3+
package schema_test
24

35
import (
46
"testing"
57

6-
"github.com/stretchr/testify/assert"
8+
"github.com/rs/rest-layer/schema"
79
)
810

911
func TestDictValidatorCompile(t *testing.T) {
10-
v := &Dict{KeysValidator: &String{}, ValuesValidator: &String{}}
11-
err := v.Compile()
12-
assert.NoError(t, err)
13-
v = &Dict{
14-
KeysValidator: &String{Regexp: "[invalid re"},
12+
testCases := []compilerTestCase{
13+
{
14+
Name: "KeysValidator=&String{},ValuesValidator=&String{}",
15+
Compiler: &schema.Dict{
16+
KeysValidator: &schema.String{},
17+
ValuesValidator: &schema.String{},
18+
},
19+
},
20+
{
21+
Name: "KeysValidator=&String{Regexp:invalid}",
22+
Compiler: &schema.Dict{KeysValidator: &schema.String{Regexp: "[invalid re"}},
23+
Error: "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`",
24+
},
25+
{
26+
Name: "ValuesValidator=&String{Regexp:invalid}",
27+
Compiler: &schema.Dict{ValuesValidator: &schema.String{Regexp: "[invalid re"}},
28+
Error: "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`",
29+
},
1530
}
16-
err = v.Compile()
17-
assert.EqualError(t, err, "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`")
18-
v = &Dict{
19-
ValuesValidator: &String{Regexp: "[invalid re"},
31+
for i := range testCases {
32+
testCases[i].Run(t)
2033
}
21-
err = v.Compile()
22-
assert.EqualError(t, err, "invalid regexp: error parsing regexp: missing closing ]: `[invalid re`")
2334
}
2435

2536
func TestDictValidator(t *testing.T) {
26-
v, err := Dict{KeysValidator: &String{}}.Validate(map[string]interface{}{"foo": true, "bar": false})
27-
assert.NoError(t, err)
28-
assert.Equal(t, map[string]interface{}{"foo": true, "bar": false}, v)
29-
v, err = Dict{KeysValidator: &String{MinLen: 3}}.Validate(map[string]interface{}{"foo": true, "ba": false})
30-
assert.EqualError(t, err, "invalid key `ba': is shorter than 3")
31-
assert.Equal(t, nil, v)
32-
v, err = Dict{ValuesValidator: &Bool{}}.Validate(map[string]interface{}{"foo": true, "bar": false})
33-
assert.NoError(t, err)
34-
assert.Equal(t, map[string]interface{}{"foo": true, "bar": false}, v)
35-
v, err = Dict{ValuesValidator: &Bool{}}.Validate(map[string]interface{}{"foo": true, "bar": "value"})
36-
assert.EqualError(t, err, "invalid value for key `bar': not a Boolean")
37-
assert.Equal(t, nil, v)
38-
v, err = Dict{ValuesValidator: &String{}}.Validate("value")
39-
assert.EqualError(t, err, "not a dict")
40-
assert.Equal(t, nil, v)
41-
v, err = Dict{ValuesValidator: &String{}}.Validate([]interface{}{"value"})
42-
assert.EqualError(t, err, "not a dict")
43-
assert.Equal(t, nil, v)
44-
37+
testCases := []fieldValidatorTestCase{
38+
{
39+
Name: `KeysValidator=&String{},Validate(map[string]interface{}{"foo":true,"bar":false})`,
40+
Validator: &schema.Dict{KeysValidator: &schema.String{}},
41+
Input: map[string]interface{}{"foo": true, "bar": false},
42+
Expect: map[string]interface{}{"foo": true, "bar": false},
43+
},
44+
{
45+
Name: `KeysValidator=&String{MinLen:3},Validate(map[string]interface{}{"foo":true,"bar":false})`,
46+
Validator: &schema.Dict{KeysValidator: &schema.String{MinLen: 3}},
47+
Input: map[string]interface{}{"foo": true, "bar": false},
48+
Expect: map[string]interface{}{"foo": true, "bar": false},
49+
},
50+
{
51+
Name: `KeysValidator=&String{MinLen:3},Validate(map[string]interface{}{"foo":true,"ba":false})`,
52+
Validator: &schema.Dict{KeysValidator: &schema.String{MinLen: 3}},
53+
Input: map[string]interface{}{"foo": true, "ba": false},
54+
Error: "invalid key `ba': is shorter than 3",
55+
},
56+
{
57+
Name: `ValuesValidator=&Bool{},Validate(map[string]interface{}{"foo":true,"bar":false})`,
58+
Validator: &schema.Dict{ValuesValidator: &schema.Bool{}},
59+
Input: map[string]interface{}{"foo": true, "bar": false},
60+
Expect: map[string]interface{}{"foo": true, "bar": false},
61+
},
62+
{
63+
Name: `ValuesValidator=&Bool{},Validate(map[string]interface{}{"foo":true,"bar":"value"})`,
64+
Validator: &schema.Dict{ValuesValidator: &schema.Bool{}},
65+
Input: map[string]interface{}{"foo": true, "bar": "value"},
66+
Error: "invalid value for key `bar': not a Boolean",
67+
},
68+
{
69+
Name: `ValuesValidator=&String{},Validate("value")`,
70+
Validator: &schema.Dict{ValuesValidator: &schema.String{}},
71+
Input: "value",
72+
Error: "not a dict",
73+
},
74+
{
75+
Name: `MinLen=2,Validate(map[string]interface{}{"foo":true,"bar":false})`,
76+
Validator: &schema.Dict{MinLen: 2},
77+
Input: map[string]interface{}{"foo": true, "bar": "value"},
78+
Expect: map[string]interface{}{"foo": true, "bar": "value"},
79+
},
80+
{
81+
Name: `MinLen=3,Validate(map[string]interface{}{"foo":true,"bar":false})`,
82+
Validator: &schema.Dict{MinLen: 3},
83+
Input: map[string]interface{}{"foo": true, "bar": "value"},
84+
Error: "has fewer properties than 3",
85+
},
86+
{
87+
Name: `MaxLen=2,Validate(map[string]interface{}{"foo":true,"bar":false})`,
88+
Validator: &schema.Dict{MaxLen: 3},
89+
Input: map[string]interface{}{"foo": true, "bar": "value"},
90+
Expect: map[string]interface{}{"foo": true, "bar": "value"},
91+
},
92+
{
93+
Name: `MaxLen=1,Validate(map[string]interface{}{"foo":true,"bar":false})`,
94+
Validator: &schema.Dict{MaxLen: 1},
95+
Input: map[string]interface{}{"foo": true, "bar": "value"},
96+
Error: "has more properties than 1",
97+
},
98+
}
99+
for i := range testCases {
100+
testCases[i].Run(t)
101+
}
45102
}

schema/encoding/jsonschema/all_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,61 @@ func TestEncoder(t *testing.T) {
175175
}`,
176176
},
177177
{
178-
name: "Required=true(1/1)",
178+
name: "MinLen=2",
179+
schema: schema.Schema{
180+
Fields: schema.Fields{
181+
"foo": schema.Field{
182+
Validator: &schema.Bool{},
183+
},
184+
"bar": schema.Field{
185+
Validator: &schema.Bool{},
186+
},
187+
"baz": schema.Field{
188+
Validator: &schema.Bool{},
189+
},
190+
},
191+
MinLen: 2,
192+
},
193+
expect: `{
194+
"type": "object",
195+
"additionalProperties": false,
196+
"properties": {
197+
"foo": {"type": "boolean"},
198+
"bar": {"type": "boolean"},
199+
"baz": {"type": "boolean"}
200+
},
201+
"minProperties": 2
202+
}`,
203+
},
204+
{
205+
name: "MaxLen=2",
206+
schema: schema.Schema{
207+
Fields: schema.Fields{
208+
"foo": schema.Field{
209+
Validator: &schema.Bool{},
210+
},
211+
"bar": schema.Field{
212+
Validator: &schema.Bool{},
213+
},
214+
"baz": schema.Field{
215+
Validator: &schema.Bool{},
216+
},
217+
},
218+
MaxLen: 2,
219+
},
220+
expect: `{
221+
"type": "object",
222+
"additionalProperties": false,
223+
"properties": {
224+
"foo": {"type": "boolean"},
225+
"bar": {"type": "boolean"},
226+
"baz": {"type": "boolean"}
227+
},
228+
"maxProperties": 2
229+
}`,
230+
},
231+
{
232+
name: "Required=true(1/2)",
179233
schema: schema.Schema{
180234
Fields: schema.Fields{
181235
"name": schema.Field{

schema/encoding/jsonschema/jsonschema.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ func schemaToJSONSchema(w io.Writer, s *schema.Schema) (err error) {
229229
}
230230
}
231231
ew.writeString("}")
232+
if s.MinLen > 0 {
233+
ew.writeFormat(`, "minProperties": %s`, strconv.FormatInt(int64(s.MinLen), 10))
234+
}
235+
if s.MaxLen > 0 {
236+
ew.writeFormat(`, "maxProperties": %s`, strconv.FormatInt(int64(s.MaxLen), 10))
237+
}
232238

233239
if len(required) > 0 {
234240
ew.writeFormat(`, "required": [%s]`, strings.Join(required, ", "))

schema/schema.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ type Schema struct {
1414
Description string
1515
// Fields defines the schema's allowed fields
1616
Fields Fields
17+
// MinLen defines the minimum number of fields (default 0)
18+
MinLen int
19+
// MaxLen defines the maximum number of fields (default no limit)
20+
MaxLen int
1721
}
1822

1923
// Validator is an interface used to validate schema against actual data
@@ -324,5 +328,14 @@ func (s Schema) validate(changes map[string]interface{}, base map[string]interfa
324328
}
325329
}
326330
}
331+
l := len(doc)
332+
if l < s.MinLen {
333+
addFieldError(errs, "", fmt.Sprintf("has fewer properties than %d", s.MinLen))
334+
return nil, errs
335+
}
336+
if s.MaxLen > 0 && l > s.MaxLen {
337+
addFieldError(errs, "", fmt.Sprintf("has more properties than %d", s.MaxLen))
338+
return nil, errs
339+
}
327340
return doc, errs
328341
}

schema/schema_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package schema_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/rs/rest-layer/schema"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestSchemaValidator(t *testing.T) {
11+
minLenSchema := &schema.Schema{
12+
Fields: schema.Fields{
13+
"foo": schema.Field{
14+
Validator: &schema.Bool{},
15+
},
16+
"bar": schema.Field{
17+
Validator: &schema.Bool{},
18+
},
19+
"baz": schema.Field{
20+
Validator: &schema.Bool{},
21+
},
22+
},
23+
MinLen: 2,
24+
}
25+
assert.NoError(t, minLenSchema.Compile())
26+
27+
maxLenSchema := &schema.Schema{
28+
Fields: schema.Fields{
29+
"foo": schema.Field{
30+
Validator: &schema.Bool{},
31+
},
32+
"bar": schema.Field{
33+
Validator: &schema.Bool{},
34+
},
35+
"baz": schema.Field{
36+
Validator: &schema.Bool{},
37+
},
38+
},
39+
MaxLen: 2,
40+
}
41+
assert.NoError(t, maxLenSchema.Compile())
42+
43+
testCases := []struct {
44+
Name string
45+
Schema *schema.Schema
46+
Base, Change, Expect map[string]interface{}
47+
Errors map[string][]interface{}
48+
}{
49+
{
50+
Name: `MinLen=2,Validate(map[string]interface{}{"foo":true,"bar":false})`,
51+
Schema: minLenSchema,
52+
Change: map[string]interface{}{"foo": true, "bar": false},
53+
Expect: map[string]interface{}{"foo": true, "bar": false},
54+
},
55+
{
56+
Name: `MinLen=2,Validate(map[string]interface{}{"foo":true})`,
57+
Schema: minLenSchema,
58+
Change: map[string]interface{}{"foo": true},
59+
Errors: map[string][]interface{}{"": []interface{}{"has fewer properties than 2"}},
60+
},
61+
{
62+
Name: `MaxLen=2,Validate(map[string]interface{}{"foo":true,"bar":false})`,
63+
Schema: maxLenSchema,
64+
Change: map[string]interface{}{"foo": true, "bar": false},
65+
Expect: map[string]interface{}{"foo": true, "bar": false},
66+
},
67+
{
68+
Name: `MaxLen=2,Validate(map[string]interface{}{"foo":true,"bar":true,"baz":false})`,
69+
Schema: maxLenSchema,
70+
Change: map[string]interface{}{"foo": true, "bar": true, "baz": false},
71+
Errors: map[string][]interface{}{"": []interface{}{"has more properties than 2"}},
72+
},
73+
}
74+
75+
for i := range testCases {
76+
tc := testCases[i]
77+
t.Run(tc.Name, func(t *testing.T) {
78+
t.Parallel()
79+
80+
doc, errs := tc.Schema.Validate(tc.Base, tc.Change)
81+
if len(tc.Errors) == 0 {
82+
assert.Len(t, errs, 0)
83+
assert.Equal(t, tc.Expect, doc)
84+
} else {
85+
assert.Equal(t, tc.Errors, errs)
86+
assert.Nil(t, doc)
87+
}
88+
})
89+
}
90+
}

0 commit comments

Comments
 (0)