Repository: go-playground/validator
Pull request #758 (2021-04-23 14:00:55)

Add BIC ISO format validator
## Fixes Or Enhances


**Make sure that you've checked the boxes below before you submit PR:**
- [x] Tests exist or have been written that cover this particular change.

Change details:
 - Add BIC (SWIFT code) validation based on [ISO 9362](https://en.wikipedia.org/wiki/ISO_9362)

@go-playground/admins

deankarn: Thanks @yeexel I'll try to look soon. It might take a while for me to find the free time.

Patch:
diff --git a/_examples/map-validation/main.go b/_examples/map-validation/main.go
new file mode 100644
index 0000000..92e282f
--- /dev/null
+++ b/_examples/map-validation/main.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+	"fmt"
+	"github.com/go-playground/validator/v10"
+)
+
+var validate *validator.Validate
+
+func main() {
+	validate = validator.New()
+
+	validateMap()
+	validateNestedMap()
+}
+
+func validateMap() {
+	user := map[string]interface{}{"name": "Arshiya Kiani", "email": "zytel3301@gmail.com"}
+
+	// Every rule will be applied to the item of the data that the offset of rule is pointing to.
+	// So if you have a field "email": "omitempty,required,email", the validator will apply these
+	// rules to offset of email in user data
+	rules := map[string]interface{}{"name": "required,min=8,max=32", "email": "omitempty,required,email"}
+
+	// ValidateMap will return map[string]error.
+	// The offset of every item in errs is the name of invalid field and the value
+	// is the message of error. If there was no error, ValidateMap method will
+	// return an EMPTY map of errors, not nil. If you want to check that
+	// if there was an error or not, you must check the length of the return value
+	errs := validate.ValidateMap(user, rules)
+
+	if len(errs) > 0 {
+		fmt.Println(errs)
+		// The user is invalid
+	}
+
+	// The user is valid
+}
+
+func validateNestedMap() {
+
+	data := map[string]interface{}{
+		"name":  "Arshiya Kiani",
+		"email": "zytel3301@gmail.com",
+		"details": map[string]interface{}{
+			"family_members": map[string]interface{}{
+				"father_name": "Micheal",
+				"mother_name": "Hannah",
+			},
+			"salary": "1000",
+		},
+	}
+
+	// Rules must be set as the structure as the data itself. If you want to dive into the
+	// map, just declare its rules as a map
+	rules := map[string]interface{}{
+		"name":  "min=4,max=32",
+		"email": "required,email",
+		"details": map[string]interface{}{
+			"family_members": map[string]interface{}{
+				"father_name": "required,min=4,max=32",
+				"mother_name": "required,min=4,max=32",
+			},
+			"salary": "number",
+		},
+	}
+
+	if len(validate.ValidateMap(data, rules)) == 0 {
+		// Data is valid
+	}
+
+	// Data is invalid
+}
diff --git a/baked_in.go b/baked_in.go
index dd25705..2cc914f 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -190,6 +190,7 @@ var (
 		"iso3166_1_alpha3":        isIso3166Alpha3,
 		"iso3166_1_alpha_numeric": isIso3166AlphaNumeric,
 		"bcp47_language_tag":      isBCP47LanguageTag,
+		"bic":                     isIsoBicFormat,
 	}
 )
 
@@ -1038,6 +1039,9 @@ func isNeCrossStructField(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(topField.Len()) != int64(field.Len())
 
+	case reflect.Bool:
+		return topField.Bool() != field.Bool()
+
 	case reflect.Struct:
 
 		fieldType := field.Type()
@@ -1085,6 +1089,9 @@ func isEqCrossStructField(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(topField.Len()) == int64(field.Len())
 
+	case reflect.Bool:
+		return topField.Bool() == field.Bool()
+
 	case reflect.Struct:
 
 		fieldType := field.Type()
@@ -1132,6 +1139,9 @@ func isEqField(fl FieldLevel) bool {
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(field.Len()) == int64(currentField.Len())
 
+	case reflect.Bool:
+		return field.Bool() == currentField.Bool()
+
 	case reflect.Struct:
 
 		fieldType := field.Type()
@@ -1446,6 +1456,9 @@ func requireCheckFieldValue(fl FieldLevel, param string, value string, defaultNo
 
 	case reflect.Slice, reflect.Map, reflect.Array:
 		return int64(field.Len()) == asInt(value)
+
+	case reflect.Bool:
+		return field.Bool() == asBool(value)
 	}
 
 	// default reflect.String:
@@ -2300,3 +2313,14 @@ func isBCP47LanguageTag(fl FieldLevel) bool {
 
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
+
+// isIsoBicFormat is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362
+func isIsoBicFormat(fl FieldLevel) bool {
+	bicString := fl.Field().String()
+
+	if !bicRegex.MatchString(bicString) {
+		return false
+	}
+
+	return true
+}
diff --git a/doc.go b/doc.go
index 207d987..eafad0d 100644
--- a/doc.go
+++ b/doc.go
@@ -1228,6 +1228,13 @@ More information on https://pkg.go.dev/golang.org/x/text/language
 
 	Usage: bcp47_language_tag
 
+BIC (SWIFT code)
+
+This validates that a string value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362.
+More information on https://www.iso.org/standard/60390.html
+
+	Usage: bic
+
 TimeZone
 
 This validates that a string value is a valid time zone based on the time zone database present on the system.
diff --git a/regexes.go b/regexes.go
index b741f4e..7b01e13 100644
--- a/regexes.go
+++ b/regexes.go
@@ -49,6 +49,7 @@ const (
 	hTMLEncodedRegexString           = `&#[x]?([0-9a-fA-F]{2})|(&gt)|(&lt)|(&quot)|(&amp)+[;]?`
 	hTMLRegexString                  = `<[/]?([a-zA-Z]+).*?>`
 	splitParamsRegexString           = `'[^']*'|\S+`
+	bicRegexString                   = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$`
 )
 
 var (
@@ -98,4 +99,5 @@ var (
 	hTMLEncodedRegex           = regexp.MustCompile(hTMLEncodedRegexString)
 	hTMLRegex                  = regexp.MustCompile(hTMLRegexString)
 	splitParamsRegex           = regexp.MustCompile(splitParamsRegexString)
+	bicRegex                   = regexp.MustCompile(bicRegexString)
 )
diff --git a/validator_instance.go b/validator_instance.go
index d230a32..8e27707 100644
--- a/validator_instance.go
+++ b/validator_instance.go
@@ -143,6 +143,33 @@ func (v *Validate) SetTagName(name string) {
 	v.tagName = name
 }
 
+// ValidateMapCtx validates a map using a map of validation rules and allows passing of contextual
+// validation validation information via context.Context.
+func (v Validate) ValidateMapCtx(ctx context.Context, data map[string]interface{}, rules map[string]interface{}) map[string]interface{} {
+	errs := make(map[string]interface{})
+	for field, rule := range rules {
+		if reflect.ValueOf(rule).Kind() == reflect.Map && reflect.ValueOf(data[field]).Kind() == reflect.Map {
+			err := v.ValidateMapCtx(ctx, data[field].(map[string]interface{}), rule.(map[string]interface{}))
+			if len(err) > 0 {
+				errs[field] = err
+			}
+		} else if reflect.ValueOf(rule).Kind() == reflect.Map {
+			errs[field] = errors.New("The field: '" + field + "' is not a map to dive")
+		} else {
+			err := v.VarCtx(ctx, data[field], rule.(string))
+			if err != nil {
+				errs[field] = err
+			}
+		}
+	}
+	return errs
+}
+
+// ValidateMap validates map data form a map of tags
+func (v *Validate) ValidateMap(data map[string]interface{}, rules map[string]interface{}) map[string]interface{} {
+	return v.ValidateMapCtx(context.Background(), data, rules)
+}
+
 // RegisterTagNameFunc registers a function to get alternate names for StructFields.
 //
 // eg. to use the names which have been specified for JSON representations of structs, rather than normal Go field names:


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 1b2e39b..19dac53 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -1670,12 +1670,14 @@ func TestCrossStructNeFieldValidation(t *testing.T) {
 	i := 1
 	j = 1
 	k = 1.543
+	b := true
 	arr := []string{"test"}
 
 	s2 := "abcd"
 	i2 := 1
 	j2 = 1
 	k2 = 1.543
+	b2 := true
 	arr2 := []string{"test"}
 	arr3 := []string{"test", "test2"}
 	now2 := now
@@ -1696,6 +1698,10 @@ func TestCrossStructNeFieldValidation(t *testing.T) {
 	NotEqual(t, errs, nil)
 	AssertError(t, errs, "", "", "", "", "necsfield")
 
+	errs = validate.VarWithValue(b2, b, "necsfield")
+	NotEqual(t, errs, nil)
+	AssertError(t, errs, "", "", "", "", "necsfield")
+
 	errs = validate.VarWithValue(arr2, arr, "necsfield")
 	NotEqual(t, errs, nil)
 	AssertError(t, errs, "", "", "", "", "necsfield")
@@ -1834,6 +1840,7 @@ func TestCrossStructEqFieldValidation(t *testing.T) {
 	i := 1
 	j = 1
 	k = 1.543
+	b := true
 	arr := []string{"test"}
 
 	var j2 uint64
@@ -1842,6 +1849,7 @@ func TestCrossStructEqFieldValidation(t *testing.T) {
 	i2 := 1
 	j2 = 1
 	k2 = 1.543
+	b2 := true
 	arr2 := []string{"test"}
 	arr3 := []string{"test", "test2"}
 	now2 := now
@@ -1858,6 +1866,9 @@ func TestCrossStructEqFieldValidation(t *testing.T) {
 	errs = validate.VarWithValue(k2, k, "eqcsfield")
 	Equal(t, errs, nil)
 
+	errs = validate.VarWithValue(b2, b, "eqcsfield")
+	Equal(t, errs, nil)
+
 	errs = validate.VarWithValue(arr2, arr, "eqcsfield")
 	Equal(t, errs, nil)
 
@@ -4829,6 +4840,7 @@ func TestIsEqFieldValidation(t *testing.T) {
 	i := 1
 	j = 1
 	k = 1.543
+	b := true
 	arr := []string{"test"}
 	now := time.Now().UTC()
 
@@ -4838,6 +4850,7 @@ func TestIsEqFieldValidation(t *testing.T) {
 	i2 := 1
 	j2 = 1
 	k2 = 1.543
+	b2 := true
 	arr2 := []string{"test"}
 	arr3 := []string{"test", "test2"}
 	now2 := now
@@ -4854,6 +4867,9 @@ func TestIsEqFieldValidation(t *testing.T) {
 	errs = validate.VarWithValue(k2, k, "eqfield")
 	Equal(t, errs, nil)
 
+	errs = validate.VarWithValue(b2, b, "eqfield")
+	Equal(t, errs, nil)
+
 	errs = validate.VarWithValue(arr2, arr, "eqfield")
 	Equal(t, errs, nil)
 
@@ -10065,12 +10081,15 @@ func TestRequiredUnless(t *testing.T) {
 		Field6  uint              `validate:"required_unless=Field5 2" json:"field_6"`
 		Field7  float32           `validate:"required_unless=Field6 0" json:"field_7"`
 		Field8  float64           `validate:"required_unless=Field7 0.0" json:"field_8"`
+		Field9  bool              `validate:"omitempty" json:"field_9"`
+		Field10 string            `validate:"required_unless=Field9 true" json:"field_10"`
 	}{
 		FieldE: "test",
 		Field2: &fieldVal,
 		Field3: map[string]string{"key": "val"},
 		Field4: "test",
 		Field5: 2,
+		Field9: true,
 	}
 
 	validate := New()
@@ -10090,6 +10109,8 @@ func TestRequiredUnless(t *testing.T) {
 		Field5  string            `validate:"required_unless=Field3 0" json:"field_5"`
 		Field6  string            `validate:"required_unless=Inner.Field test" json:"field_6"`
 		Field7  string            `validate:"required_unless=Inner2.Field test" json:"field_7"`
+		Field8  bool              `validate:"omitempty" json:"field_8"`
+		Field9  string            `validate:"required_unless=Field8 true" json:"field_9"`
 	}{
 		Inner:  &Inner{Field: &fieldVal},
 		FieldE: "test",
@@ -10100,10 +10121,11 @@ func TestRequiredUnless(t *testing.T) {
 	NotEqual(t, errs, nil)
 
 	ve := errs.(ValidationErrors)
-	Equal(t, len(ve), 3)
+	Equal(t, len(ve), 4)
 	AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_unless")
 	AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_unless")
 	AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_unless")
+	AssertError(t, errs, "Field9", "Field9", "Field9", "Field9", "required_unless")
 
 	defer func() {
 		if r := recover(); r == nil {
@@ -11201,3 +11223,44 @@ func TestBCP47LanguageTagValidation(t *testing.T) {
 		_ = validate.Var(2, "bcp47_language_tag")
 	}, "Bad field type int")
 }
+
+func TestBicIsoFormatValidation(t *testing.T) {
+	tests := []struct {
+		value    string `validate:"bic"`
+		tag      string
+		expected bool
+	}{
+		{"SBICKEN1345", "bic", true},
+		{"SBICKEN1", "bic", true},
+		{"SBICKENY", "bic", true},
+		{"SBICKEN1YYP", "bic", true},
+		{"SBIC23NXXX", "bic", false},
+		{"S23CKENXXXX", "bic", false},
+		{"SBICKENXX", "bic", false},
+		{"SBICKENXX9", "bic", false},
+		{"SBICKEN13458", "bic", false},
+		{"SBICKEN", "bic", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.value, test.tag)
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d bic failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d bic failed Error: %s", i, errs)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "bic" {
+					t.Fatalf("Index: %d bic failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+}