Skip to content

Commit c0d3bb3

Browse files
authored
Validate untyped maps similar to structs (#128)
* Validate untyped maps similar to structs * Map validation refinements based on feedback * Flag map keys as optional
1 parent 25eba0b commit c0d3bb3

File tree

4 files changed

+346
-0
lines changed

4 files changed

+346
-0
lines changed

README.md

+47
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,53 @@ And when each field is validated, its rules are also evaluated in the order they
124124
If a rule fails, an error is recorded for that field, and the validation will continue with the next field.
125125

126126

127+
### Validating a Map
128+
129+
Sometimes you might need to work with dynamic data stored in maps rather than a typed model. You can use `validation.Map()`
130+
in this situation. A single map can have rules for multiple keys, and a key can be associated with multiple
131+
rules. For example,
132+
133+
```go
134+
c := map[string]interface{}{
135+
"Name": "Qiang Xue",
136+
"Email": "q",
137+
"Address": map[string]interface{}{
138+
"Street": "123",
139+
"City": "Unknown",
140+
"State": "Virginia",
141+
"Zip": "12345",
142+
},
143+
}
144+
145+
err := validation.Validate(c,
146+
validation.Map(
147+
// Name cannot be empty, and the length must be between 5 and 20.
148+
validation.Key("Name", validation.Required, validation.Length(5, 20)),
149+
// Email cannot be empty and should be in a valid email format.
150+
validation.Key("Email", validation.Required, is.Email),
151+
// Validate Address using its own validation rules
152+
validation.Key("Address", validation.Map(
153+
// Street cannot be empty, and the length must between 5 and 50
154+
validation.Key("Street", validation.Required, validation.Length(5, 50)),
155+
// City cannot be empty, and the length must between 5 and 50
156+
validation.Key("City", validation.Required, validation.Length(5, 50)),
157+
// State cannot be empty, and must be a string consisting of two letters in upper case
158+
validation.Key("State", validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
159+
// State cannot be empty, and must be a string consisting of five digits
160+
validation.Key("Zip", validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
161+
)),
162+
),
163+
)
164+
fmt.Println(err)
165+
// Output:
166+
// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
167+
```
168+
169+
When the map validation is performed, the keys are validated in the order they are specified in `Map`.
170+
And when each key is validated, its rules are also evaluated in the order they are associated with the key.
171+
If a rule fails, an error is recorded for that key, and the validation will continue with the next key.
172+
173+
127174
### Validation Errors
128175

129176
The `validation.ValidateStruct` method returns validation errors found in struct fields in terms of `validation.Errors`

example_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,39 @@ func Example_six() {
154154
// unexpected value
155155
// <nil>
156156
}
157+
158+
func Example_seven() {
159+
c := map[string]interface{}{
160+
"Name": "Qiang Xue",
161+
"Email": "q",
162+
"Address": map[string]interface{}{
163+
"Street": "123",
164+
"City": "Unknown",
165+
"State": "Virginia",
166+
"Zip": "12345",
167+
},
168+
}
169+
170+
err := validation.Validate(c,
171+
validation.Map(
172+
// Name cannot be empty, and the length must be between 5 and 20.
173+
validation.Key("Name", validation.Required, validation.Length(5, 20)),
174+
// Email cannot be empty and should be in a valid email format.
175+
validation.Key("Email", validation.Required, is.Email),
176+
// Validate Address using its own validation rules
177+
validation.Key("Address", validation.Map(
178+
// Street cannot be empty, and the length must between 5 and 50
179+
validation.Key("Street", validation.Required, validation.Length(5, 50)),
180+
// City cannot be empty, and the length must between 5 and 50
181+
validation.Key("City", validation.Required, validation.Length(5, 50)),
182+
// State cannot be empty, and must be a string consisting of two letters in upper case
183+
validation.Key("State", validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
184+
// State cannot be empty, and must be a string consisting of five digits
185+
validation.Key("Zip", validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
186+
)),
187+
),
188+
)
189+
fmt.Println(err)
190+
// Output:
191+
// Address: (State: must be in a valid format; Street: the length must be between 5 and 50.); Email: must be a valid email address.
192+
}

map.go

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package validation
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
)
9+
10+
var (
11+
// ErrNotMap is the error that the value being validated is not a map.
12+
ErrNotMap = errors.New("only a map can be validated")
13+
14+
// ErrKeyWrongType is the error returned in case of an incorrect key type.
15+
ErrKeyWrongType = NewError("validation_key_wrong_type", "key not the correct type")
16+
17+
// ErrKeyMissing is the error returned in case of a missing key.
18+
ErrKeyMissing = NewError("validation_key_missing", "required key is missing")
19+
20+
// ErrKeyUnexpected is the error returned in case of an unexpected key.
21+
ErrKeyUnexpected = NewError("validation_key_unexpected", "key not expected")
22+
)
23+
24+
type (
25+
// MapRule represents a rule set associated with a map.
26+
MapRule struct {
27+
keys []*KeyRules
28+
allowExtraKeys bool
29+
}
30+
31+
// KeyRules represents a rule set associated with a map key.
32+
KeyRules struct {
33+
key interface{}
34+
optional bool
35+
rules []Rule
36+
}
37+
)
38+
39+
// Map returns a validation rule that checks the keys and values of a map.
40+
// This rule should only be used for validating maps, or a validation error will be reported.
41+
// Use Key() to specify map keys that need to be validated. Each Key() call specifies a single key which can
42+
// be associated with multiple rules.
43+
// For example,
44+
// validation.Map(
45+
// validation.Key("Name", validation.Required),
46+
// validation.Key("Value", validation.Required, validation.Length(5, 10)),
47+
// )
48+
//
49+
// A nil value is considered valid. Use the Required rule to make sure a map value is present.
50+
func Map(keys ...*KeyRules) MapRule {
51+
return MapRule{keys: keys}
52+
}
53+
54+
// AllowExtraKeys configures the rule to ignore extra keys.
55+
func (r MapRule) AllowExtraKeys() MapRule {
56+
r.allowExtraKeys = true
57+
return r
58+
}
59+
60+
// Validate checks if the given value is valid or not.
61+
func (r MapRule) Validate(m interface{}) error {
62+
return r.ValidateWithContext(nil, m)
63+
}
64+
65+
// ValidateWithContext checks if the given value is valid or not.
66+
func (r MapRule) ValidateWithContext(ctx context.Context, m interface{}) error {
67+
value := reflect.ValueOf(m)
68+
if value.Kind() == reflect.Ptr {
69+
value = value.Elem()
70+
}
71+
if value.Kind() != reflect.Map {
72+
// must be a map
73+
return NewInternalError(ErrNotMap)
74+
}
75+
if value.IsNil() {
76+
// treat a nil map as valid
77+
return nil
78+
}
79+
80+
errs := Errors{}
81+
kt := value.Type().Key()
82+
83+
var extraKeys map[interface{}]bool
84+
if !r.allowExtraKeys {
85+
extraKeys = make(map[interface{}]bool, value.Len())
86+
for _, k := range value.MapKeys() {
87+
extraKeys[k.Interface()] = true
88+
}
89+
}
90+
91+
for _, kr := range r.keys {
92+
var err error
93+
if kv := reflect.ValueOf(kr.key); !kt.AssignableTo(kv.Type()) {
94+
err = ErrKeyWrongType
95+
} else if vv := value.MapIndex(kv); !vv.IsValid() {
96+
if !kr.optional {
97+
err = ErrKeyMissing
98+
}
99+
} else if ctx == nil {
100+
err = Validate(vv.Interface(), kr.rules...)
101+
} else {
102+
err = ValidateWithContext(ctx, vv.Interface(), kr.rules...)
103+
}
104+
if err != nil {
105+
if ie, ok := err.(InternalError); ok && ie.InternalError() != nil {
106+
return err
107+
}
108+
errs[getErrorKeyName(kr.key)] = err
109+
}
110+
if !r.allowExtraKeys {
111+
delete(extraKeys, kr.key)
112+
}
113+
}
114+
115+
if !r.allowExtraKeys {
116+
for key := range extraKeys {
117+
errs[getErrorKeyName(key)] = ErrKeyUnexpected
118+
}
119+
}
120+
121+
if len(errs) > 0 {
122+
return errs
123+
}
124+
return nil
125+
}
126+
127+
// Key specifies a map key and the corresponding validation rules.
128+
func Key(key interface{}, rules ...Rule) *KeyRules {
129+
return &KeyRules{
130+
key: key,
131+
rules: rules,
132+
}
133+
}
134+
135+
// Optional configures the rule to ignore the key if missing.
136+
func (r *KeyRules) Optional() *KeyRules {
137+
r.optional = true
138+
return r
139+
}
140+
141+
// getErrorKeyName returns the name that should be used to represent the validation error of a map key.
142+
func getErrorKeyName(key interface{}) string {
143+
return fmt.Sprintf("%v", key)
144+
}

map_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package validation
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestMap(t *testing.T) {
11+
var m0 map[string]interface{}
12+
m1 := map[string]interface{}{"A": "abc", "B": "xyz", "c": "abc", "D": (*string)(nil), "F": (*String123)(nil), "H": []string{"abc", "abc"}, "I": map[string]string{"foo": "abc"}}
13+
m2 := map[string]interface{}{"E": String123("xyz"), "F": (*String123)(nil)}
14+
m3 := map[string]interface{}{"M3": Model3{}}
15+
m4 := map[string]interface{}{"M3": Model3{A: "abc"}}
16+
m5 := map[string]interface{}{"A": "internal", "B": ""}
17+
m6 := map[int]string{11: "abc", 22: "xyz"}
18+
tests := []struct {
19+
tag string
20+
model interface{}
21+
rules []*KeyRules
22+
err string
23+
}{
24+
// empty rules
25+
{"t1.1", m1, []*KeyRules{}, ""},
26+
{"t1.2", m1, []*KeyRules{Key("A"), Key("B")}, ""},
27+
// normal rules
28+
{"t2.1", m1, []*KeyRules{Key("A", &validateAbc{}), Key("B", &validateXyz{})}, ""},
29+
{"t2.2", m1, []*KeyRules{Key("A", &validateXyz{}), Key("B", &validateAbc{})}, "A: error xyz; B: error abc."},
30+
{"t2.3", m1, []*KeyRules{Key("A", &validateXyz{}), Key("c", &validateXyz{})}, "A: error xyz; c: error xyz."},
31+
{"t2.4", m1, []*KeyRules{Key("D", Length(0, 5))}, ""},
32+
{"t2.5", m1, []*KeyRules{Key("F", Length(0, 5))}, ""},
33+
{"t2.6", m1, []*KeyRules{Key("H", Each(&validateAbc{})), Key("I", Each(&validateAbc{}))}, ""},
34+
{"t2.7", m1, []*KeyRules{Key("H", Each(&validateXyz{})), Key("I", Each(&validateXyz{}))}, "H: (0: error xyz; 1: error xyz.); I: (foo: error xyz.)."},
35+
{"t2.8", m1, []*KeyRules{Key("I", Map(Key("foo", &validateAbc{})))}, ""},
36+
{"t2.9", m1, []*KeyRules{Key("I", Map(Key("foo", &validateXyz{})))}, "I: (foo: error xyz.)."},
37+
// non-map value
38+
{"t3.1", &m1, []*KeyRules{}, ""},
39+
{"t3.2", nil, []*KeyRules{}, ErrNotMap.Error()},
40+
{"t3.3", m0, []*KeyRules{}, ""},
41+
{"t3.4", &m0, []*KeyRules{}, ""},
42+
{"t3.5", 123, []*KeyRules{}, ErrNotMap.Error()},
43+
// invalid key spec
44+
{"t4.1", m1, []*KeyRules{Key(123)}, "123: key not the correct type."},
45+
{"t4.2", m1, []*KeyRules{Key("X")}, "X: required key is missing."},
46+
{"t4.3", m1, []*KeyRules{Key("X").Optional()}, ""},
47+
// non-string keys
48+
{"t5.1", m6, []*KeyRules{Key(11, &validateAbc{}), Key(22, &validateXyz{})}, ""},
49+
{"t5.2", m6, []*KeyRules{Key(11, &validateXyz{}), Key(22, &validateAbc{})}, "11: error xyz; 22: error abc."},
50+
// validatable value
51+
{"t6.1", m2, []*KeyRules{Key("E")}, "E: error 123."},
52+
{"t6.2", m2, []*KeyRules{Key("E", Skip)}, ""},
53+
{"t6.3", m2, []*KeyRules{Key("E", Skip.When(true))}, ""},
54+
{"t6.4", m2, []*KeyRules{Key("E", Skip.When(false))}, "E: error 123."},
55+
// Required, NotNil
56+
{"t7.1", m2, []*KeyRules{Key("F", Required)}, "F: cannot be blank."},
57+
{"t7.2", m2, []*KeyRules{Key("F", NotNil)}, "F: is required."},
58+
{"t7.3", m2, []*KeyRules{Key("F", Skip, Required)}, ""},
59+
{"t7.4", m2, []*KeyRules{Key("F", Skip, NotNil)}, ""},
60+
{"t7.5", m2, []*KeyRules{Key("F", Skip.When(true), Required)}, ""},
61+
{"t7.6", m2, []*KeyRules{Key("F", Skip.When(true), NotNil)}, ""},
62+
{"t7.7", m2, []*KeyRules{Key("F", Skip.When(false), Required)}, "F: cannot be blank."},
63+
{"t7.8", m2, []*KeyRules{Key("F", Skip.When(false), NotNil)}, "F: is required."},
64+
// validatable structs
65+
{"t8.1", m3, []*KeyRules{Key("M3", Skip)}, ""},
66+
{"t8.2", m3, []*KeyRules{Key("M3")}, "M3: (A: error abc.)."},
67+
{"t8.3", m4, []*KeyRules{Key("M3")}, ""},
68+
// internal error
69+
{"t9.1", m5, []*KeyRules{Key("A", &validateAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"},
70+
}
71+
for _, test := range tests {
72+
err1 := Validate(test.model, Map(test.rules...).AllowExtraKeys())
73+
err2 := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
74+
assertError(t, test.err, err1, test.tag)
75+
assertError(t, test.err, err2, test.tag)
76+
}
77+
78+
a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true}
79+
err := Validate(a, Map(
80+
Key("Name", Required),
81+
Key("Value", Required, Length(5, 10)),
82+
))
83+
assert.EqualError(t, err, "Extra: key not expected; Value: the length must be between 5 and 10.")
84+
}
85+
86+
func TestMapWithContext(t *testing.T) {
87+
m1 := map[string]interface{}{"A": "abc", "B": "xyz", "c": "abc", "g": "xyz"}
88+
m2 := map[string]interface{}{"A": "internal", "B": ""}
89+
tests := []struct {
90+
tag string
91+
model interface{}
92+
rules []*KeyRules
93+
err string
94+
}{
95+
// normal rules
96+
{"t1.1", m1, []*KeyRules{Key("A", &validateContextAbc{}), Key("B", &validateContextXyz{})}, ""},
97+
{"t1.2", m1, []*KeyRules{Key("A", &validateContextXyz{}), Key("B", &validateContextAbc{})}, "A: error xyz; B: error abc."},
98+
{"t1.3", m1, []*KeyRules{Key("A", &validateContextXyz{}), Key("c", &validateContextXyz{})}, "A: error xyz; c: error xyz."},
99+
{"t1.4", m1, []*KeyRules{Key("g", &validateContextAbc{})}, "g: error abc."},
100+
// skip rule
101+
{"t2.1", m1, []*KeyRules{Key("g", Skip, &validateContextAbc{})}, ""},
102+
{"t2.2", m1, []*KeyRules{Key("g", &validateContextAbc{}, Skip)}, "g: error abc."},
103+
// internal error
104+
{"t3.1", m2, []*KeyRules{Key("A", &validateContextAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"},
105+
}
106+
for _, test := range tests {
107+
err := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
108+
assertError(t, test.err, err, test.tag)
109+
}
110+
111+
a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true}
112+
err := ValidateWithContext(context.Background(), a, Map(
113+
Key("Name", Required),
114+
Key("Value", Required, Length(5, 10)),
115+
))
116+
if assert.NotNil(t, err) {
117+
assert.Equal(t, "Extra: key not expected; Value: the length must be between 5 and 10.", err.Error())
118+
}
119+
}

0 commit comments

Comments
 (0)