diff --git a/README.md b/README.md index 6c14bbf..6ee4b11 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,123 @@ When the map validation is performed, the keys are validated in the order they a And when each key is validated, its rules are also evaluated in the order they are associated with the key. If a rule fails, an error is recorded for that key, and the validation will continue with the next key. +#### Allowing Extra Keys + +By default, `validation.Map()` will return an `Extra: key not expected` error if there's unexpected key inside the map (you have to specify all expected keys in the validation rules). + +```go +c := map[string]interface{}{ + "Name": "Qiang Xue", + "Email": "q", + "Address": map[string]interface{}{ + "Street": "123", + "City": "Unknown", + }, +} + +err := validation.Validate(c, + validation.Map( + // Name cannot be empty, and the length must be between 5 and 20. + validation.Key("Name", validation.Required, validation.Length(5, 20)), + // Validate Address using its own validation rules + validation.Key("Address", validation.Map( + // Street cannot be empty. + validation.Key("Street", validation.Required), + )), + ), +) +fmt.Println(err) +// Output: +// Address: (City: key not expected); Email: key not expected. +``` + +If you need to allow extra keys, you can achieve this by using `validation.Map().AllowExtraKeys()`, or `validation.DynamicMap()`. + +```go +err := validation.Validate(c, + validation.Map( + // Name cannot be empty, and the length must be between 5 and 20. + validation.Key("Name", validation.Required, validation.Length(5, 20)), + // Validate Address using its own validation rules + validation.Key("Address", validation.Map( + // Street cannot be empty. + validation.Key("Street", validation.Required), + ).AllowExtraKeys()), + ).AllowExtraKeys(), +) +fmt.Println(err) +// Output: +// "" + +err2 := validation.Validate(c, + validation.DynamicMap( + // Name cannot be empty, and the length must be between 5 and 20. + validation.Key("Name", validation.Required, validation.Length(5, 20)), + // Validate Address using its own validation rules + validation.Key("Address", validation.DynamicMap( + // Street cannot be empty. + validation.Key("Street", validation.Required), + )), + ), +) +fmt.Println(err2) +// Output: +// "" +``` + +#### Allowing Optional Keys + +By default, `validation.Key()` expect the key to be provided and will return an `XXX: required key is missing.` error if the key doesn't exist in the map. + +```go +c := map[string]interface{}{ + "Name": "Qiang Xue", +} + +err := validation.Validate(c, + validation.Map( + // Name cannot be empty, and the length must be between 5 and 20. + validation.Key("Name", validation.Required, validation.Length(5, 20)), + // Email cannot be empty and should be in a valid email format. + validation.Key("Email", validation.Required, is.Email), + ), +) +fmt.Println(err) +// Output: +// Email: required key is missing. +``` + +If you need to allow optional key, you can achieve this by using `validation.Key().Optional()` or `validation.OptionalKey()`. + +```go +c := map[string]interface{}{ + "Name": "Qiang Xue", +} + +err := validation.Validate(c, + validation.Map( + // Name cannot be empty, and the length must be between 5 and 20. + validation.Key("Name", validation.Required, validation.Length(5, 20)), + // Email is optional, when it exists, it cannot be empty and should be in a valid email format. + validation.Key("Email", validation.Required, is.Email).Optional(), + ), +) +fmt.Println(err) +// Output: +// "" + +err2 := validation.Validate(c, + validation.Map( + // Name cannot be empty, and the length must be between 5 and 20. + validation.Key("Name", validation.Required, validation.Length(5, 20)), + // Email is optional, when it exists, it cannot be empty and should be in a valid email format. + validation.OptionalKey("Email", validation.Required, is.Email), + ), +) +fmt.Println(err2) +// Output: +// "" +``` ### Validation Errors diff --git a/map.go b/map.go index 106b6d8..4f1bf30 100644 --- a/map.go +++ b/map.go @@ -51,6 +51,22 @@ func Map(keys ...*KeyRules) MapRule { return MapRule{keys: keys} } +// DynamicMap returns a validation rule that checks the keys and values of a map. +// The map is allowed to have extra keys by default, compared to original `Map(...)` function that will return an error when unspecified key(s) existed in the map. +// This rule should only be used for validating maps, or a validation error will be reported. +// Use Key() to specify map keys that need to be validated. Each Key() call specifies a single key which can +// be associated with multiple rules. +// For example, +// validation.DynamicMap( +// validation.Key("Name", validation.Required), +// validation.Key("Value", validation.Required, validation.Length(5, 10)), +// ) +// +// A nil value is considered valid. Use the Required rule to make sure a map value is present. +func DynamicMap(keys ...*KeyRules) MapRule { + return MapRule{keys: keys, allowExtraKeys: true} +} + // AllowExtraKeys configures the rule to ignore extra keys. func (r MapRule) AllowExtraKeys() MapRule { r.allowExtraKeys = true @@ -132,6 +148,16 @@ func Key(key interface{}, rules ...Rule) *KeyRules { } } +// Key specifies an optional map key and the corresponding validation rules. +// the rule will be ignored if the key is missing. +func OptionalKey(key interface{}, rules ...Rule) *KeyRules { + return &KeyRules{ + key: key, + rules: rules, + optional: true, + } +} + // Optional configures the rule to ignore the key if missing. func (r *KeyRules) Optional() *KeyRules { r.optional = true diff --git a/map_test.go b/map_test.go index 174c673..34d5ab1 100644 --- a/map_test.go +++ b/map_test.go @@ -67,6 +67,9 @@ func TestMap(t *testing.T) { {"t8.3", m4, []*KeyRules{Key("M3")}, ""}, // internal error {"t9.1", m5, []*KeyRules{Key("A", &validateAbc{}), Key("B", Required), Key("A", &validateInternalError{})}, "error internal"}, + // optional keys + {"t10.1", m2, []*KeyRules{OptionalKey("G"), OptionalKey("H")}, ""}, + {"t10.2", m2, []*KeyRules{OptionalKey("G"), Key("H")}, "H: required key is missing."}, } for _, test := range tests { err1 := Validate(test.model, Map(test.rules...).AllowExtraKeys()) @@ -74,6 +77,12 @@ func TestMap(t *testing.T) { assertError(t, test.err, err1, test.tag) assertError(t, test.err, err2, test.tag) } + for _, test := range tests { + err1 := Validate(test.model, DynamicMap(test.rules...)) + err2 := ValidateWithContext(context.Background(), test.model, DynamicMap(test.rules...)) + assertError(t, test.err, err1, test.tag) + assertError(t, test.err, err2, test.tag) + } a := map[string]interface{}{"Name": "name", "Value": "demo", "Extra": true} err := Validate(a, Map( @@ -81,6 +90,12 @@ func TestMap(t *testing.T) { Key("Value", Required, Length(5, 10)), )) assert.EqualError(t, err, "Extra: key not expected; Value: the length must be between 5 and 10.") + + err = Validate(a, DynamicMap( + Key("Name", Required), + Key("Value", Required, Length(5, 10)), + )) + assert.EqualError(t, err, "Value: the length must be between 5 and 10.") } func TestMapWithContext(t *testing.T) {