Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optional key, dynamic map; update ReadMe #151

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,35 @@ 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())
err2 := ValidateWithContext(context.Background(), test.model, Map(test.rules...).AllowExtraKeys())
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(
Key("Name", Required),
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) {
Expand Down