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

implement ValidateMap #338

Merged
merged 8 commits into from
Jan 8, 2020
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ If you don't know what to do, there are some features and functions that need to
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
- [ ] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [ ] Implement fuzzing testing
- [ ] Implement some struct/map/array utilities
- [ ] Implement map/array validation
Expand Down
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func IsRequestURL(rawurl string) bool
func IsSSN(str string) bool
func IsSemver(str string) bool
func IsTime(str string, format string) bool
func IsType(in interface{}, params ...string) bool
func IsURL(str string) bool
func IsUTFDigit(str string) bool
func IsUTFLetter(str string) bool
Expand Down Expand Up @@ -204,6 +205,7 @@ func ToString(obj interface{}) string
func Trim(str, chars string) string
func Truncate(str string, length int, ending string) string
func UnderscoreToCamelCase(s string) string
func ValidateMap(s map[string]interface{}, o map[string]interface{}) (bool, error)
func ValidateStruct(s interface{}) (bool, error)
func WhiteList(str, chars string) string
type ConditionIterator
Expand All @@ -227,6 +229,27 @@ type Validator
```go
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
```
###### IsType
```go
println(govalidator.IsType("Bob", "string"))
println(govalidator.IsType(1, "int"))
i := 1
println(govalidator.IsType(&i, "*int"))
```

IsType can be used through the tag `type` which is essential for map validation:
```go
type User struct {
Name string `valid:"type(string)"`
Age int `valid:"type(int)"`
Meta interface{} `valid:"type(string)"`
}
result, err := govalidator.ValidateStruct(user{"Bob", 20, "meta"})
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### ToString
```go
type User struct {
Expand Down Expand Up @@ -335,6 +358,11 @@ Validators with parameters
"in(string1|string2|...|stringN)": IsIn,
"rsapub(keylength)" : IsRsaPub,
```
Validators with parameters for any type

```go
"type(type)": IsType,
```

And here is small example of usage:
```go
Expand Down Expand Up @@ -370,6 +398,41 @@ if err != nil {
}
println(result)
```
###### ValidateMap [#2](https://github.com/asaskevich/govalidator/pull/338)
If you want to validate maps, you can use the map to be validated and a validation map that contain the same tags used in ValidateStruct, both maps have to be in the form `map[string]interface{}`

So here is small example of usage:
```go
var mapTemplate = map[string]interface{}{
"name":"required,alpha",
"family":"required,alpha",
"email":"required,email",
"cell-phone":"numeric",
"address":map[string]interface{}{
"line1":"required,alphanum",
"line2":"alphanum",
"postal-code":"numeric",
},
}

var inputMap = map[string]interface{}{
"name":"Bob",
"family":"Smith",
"email":"foo@bar.baz",
"address":map[string]interface{}{
"line1":"",
"line2":"",
"postal-code":"",
},
}

result, err := govalidator.ValidateMap(mapTemplate, inputMap)
if err != nil {
println("error: " + err.Error())
}
println(result)
```

###### WhiteList
```go
// Remove all characters from string ignoring characters between "a" and "z"
Expand Down Expand Up @@ -445,7 +508,7 @@ If you don't know what to do, there are some features and functions that need to
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
- [ ] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [ ] Implement fuzzing testing
- [ ] Implement some struct/map/array utilities
- [ ] Implement map/array validation
Expand Down
11 changes: 11 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type CustomTypeValidator func(i interface{}, o interface{}) bool

// ParamValidator is a wrapper for validator functions that accepts additional parameters.
type ParamValidator func(str string, params ...string) bool
type InterfaceParamValidator func(in interface{}, params ...string) bool
type tagOptionsMap map[string]tagOption

func (t tagOptionsMap) orderedKeys() []string {
Expand Down Expand Up @@ -46,6 +47,16 @@ type UnsupportedTypeError struct {
// It implements the methods to sort by string.
type stringValues []reflect.Value

// InterfaceParamTagMap is a map of functions accept variants parameters for an interface value
var InterfaceParamTagMap = map[string]InterfaceParamValidator{
"type": IsType,
}

// InterfaceParamTagRegexMap maps interface param tags to their respective regexes.
var InterfaceParamTagRegexMap = map[string]*regexp.Regexp{
"type": regexp.MustCompile(`^type\((.*)\)$`),
}

// ParamTagMap is a map of functions accept variants parameters
var ParamTagMap = map[string]ParamValidator{
"length": ByteLength,
Expand Down
151 changes: 151 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,109 @@ func PrependPathToErrors(err error, path string) error {
return err
}

// ValidateMap use validation map for fields.
// result will be equal to `false` if there are any errors.
// m is the validation map in the form
// map[string]interface{}{"name":"required,alpha","address":map[string]interface{}{"line1":"required,alphanum"}}
func ValidateMap(s map[string]interface{}, m map[string]interface{}) (bool, error) {
if s == nil {
return true, nil
}
result := true
var err error
var errs Errors
var index int
val := reflect.ValueOf(s)
for key, value := range s {
presentResult := true
validator, ok := m[key]
if !ok {
presentResult = false
var err error
err = fmt.Errorf("all map keys has to be present in the validation map; got %s", key)
err = PrependPathToErrors(err, key)
errs = append(errs, err)
}
valueField := reflect.ValueOf(value)
mapResult := true
typeResult := true
structResult := true
resultField := true
switch subValidator := validator.(type) {
case map[string]interface{}:
var err error
if v, ok := value.(map[string]interface{}); !ok {
mapResult = false
err = fmt.Errorf("map validator has to be for the map type only; got %s", valueField.Type().String())
err = PrependPathToErrors(err, key)
errs = append(errs, err)
} else {
mapResult, err = ValidateMap(v, subValidator)
if err != nil {
mapResult = false
err = PrependPathToErrors(err, key)
errs = append(errs, err)
}
}
case string:
if (valueField.Kind() == reflect.Struct ||
(valueField.Kind() == reflect.Ptr && valueField.Elem().Kind() == reflect.Struct)) &&
subValidator != "-" {
var err error
structResult, err = ValidateStruct(valueField.Interface())
if err != nil {
err = PrependPathToErrors(err, key)
errs = append(errs, err)
}
}
resultField, err = typeCheck(valueField, reflect.StructField{
Name: key,
PkgPath: "",
Type: val.Type(),
Tag: reflect.StructTag(fmt.Sprintf("%s:%q", tagName, subValidator)),
Offset: 0,
Index: []int{index},
Anonymous: false,
}, val, nil)
if err != nil {
errs = append(errs, err)
}
case nil:
// already handlerd when checked before
default:
typeResult = false
err = fmt.Errorf("map validator has to be either map[string]interface{} or string; got %s", valueField.Type().String())
err = PrependPathToErrors(err, key)
errs = append(errs, err)
}
result = result && presentResult && typeResult && resultField && structResult && mapResult
index++
}
// check required keys
requiredResult := true
for key, value := range m {
if schema, ok := value.(string); ok {
tags := parseTagIntoMap(schema)
if required, ok := tags["required"]; ok {
if _, ok := s[key]; !ok {
requiredResult = false
if required.customErrorMessage != "" {
err = Error{key, fmt.Errorf(required.customErrorMessage), true, "required", []string{}}
} else {
err = Error{key, fmt.Errorf("required field missing"), false, "required", []string{}}
}
errs = append(errs, err)
}
}
}
}

if len(errs) > 0 {
err = errs
}
return result && requiredResult, err
}

// ValidateStruct use tags for fields.
// result will be equal to `false` if there are any errors.
func ValidateStruct(s interface{}) (bool, error) {
Expand Down Expand Up @@ -856,6 +959,15 @@ func IsSemver(str string) bool {
return rxSemver.MatchString(str)
}

// IsType check if interface is of some type
func IsType(v interface{}, params ...string) bool {
if len(params) == 1 {
typ := params[0]
return strings.Replace(reflect.TypeOf(v).String(), " ", "", -1) == strings.Replace(typ, " ", "", -1)
}
return false
}

// IsTime check if string is valid according to given format
func IsTime(str string, format string) bool {
_, err := time.Parse(format, str)
Expand Down Expand Up @@ -1062,6 +1174,45 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
}()
}

for _, validatorSpec := range optionsOrder {
validatorStruct := options[validatorSpec]
var negate bool
validator := validatorSpec
customMsgExists := len(validatorStruct.customErrorMessage) > 0

// Check whether the tag looks like '!something' or 'something'
if validator[0] == '!' {
validator = validator[1:]
negate = true
}

// Check for interface param validators
for key, value := range InterfaceParamTagRegexMap {
ps := value.FindStringSubmatch(validator)
if len(ps) == 0 {
continue
}

validatefunc, ok := InterfaceParamTagMap[key]
if !ok {
continue
}

delete(options, validatorSpec)

field := fmt.Sprint(v)
if result := validatefunc(v.Interface(), ps[1:]...); (!result && !negate) || (result && negate) {
if customMsgExists {
return false, Error{t.Name, TruncatingErrorf(validatorStruct.customErrorMessage, field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
if negate {
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
}
}

switch v.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
Expand Down
Loading