From ff642d82b1b6fcf5a1425f9e874cf8086afc52ea Mon Sep 17 00:00:00 2001 From: Mikolaj Gasior Date: Sun, 16 Feb 2025 19:05:17 +0100 Subject: [PATCH 1/4] Move validation to a separate ValueValidation struct --- html_input_gen.go | 10 +-- validator.go | 206 ++++++++++++++------------------------------ value_validation.go | 85 ++++++++++++++++++ version.go | 2 +- 4 files changed, 157 insertions(+), 146 deletions(-) create mode 100644 value_validation.go diff --git a/html_input_gen.go b/html_input_gen.go index bf04c31..2870961 100644 --- a/html_input_gen.go +++ b/html_input_gen.go @@ -65,7 +65,7 @@ func GenerateHTML(obj interface{}, options *HTMLOptions) map[string]string { } // generate only ints, string and bool - if !isInt(fieldKind) && !isString(fieldKind) && !isBool(fieldKind) { + if !isInt(fieldKind) && fieldKind != reflect.String && fieldKind != reflect.Bool { continue } @@ -73,10 +73,10 @@ func GenerateHTML(obj interface{}, options *HTMLOptions) map[string]string { value := "" if options != nil && options.FieldValues { - if isBool(fieldKind) && elem.Field(j).Bool() { + if fieldKind == reflect.Bool && elem.Field(j).Bool() { value = "true" } - if isString(fieldKind) { + if fieldKind == reflect.String { value = elem.Field(j).String() } if isInt(fieldKind) { @@ -95,7 +95,7 @@ func GenerateHTML(obj interface{}, options *HTMLOptions) map[string]string { } fieldNameAttr := fmt.Sprintf(` name="%s%s"`, options.NamePrefix, field.Name) - if isBool(fieldKind) { + if fieldKind == reflect.Bool { fieldChecked := "" if value == "true" { fieldChecked = " checked" @@ -142,7 +142,7 @@ func GenerateHTML(obj interface{}, options *HTMLOptions) map[string]string { continue } - if isString(fieldKind) { + if fieldKind == reflect.String { if inputType == TypeTextarea { fields[field.Name] = fmt.Sprintf(`%s`, fieldNameAttr, fieldIDAttr, validationAttrs, patternAttr, html.EscapeString(value)) continue diff --git a/validator.go b/validator.go index 918dc6d..d636f21 100644 --- a/validator.go +++ b/validator.go @@ -7,30 +7,18 @@ import ( "strings" ) -type FieldValidation struct { - lenMin int - lenMax int - valMin int64 - valMax int64 - regexp *regexp.Regexp - flags int64 -} - -// values used with flags -const ValMinNotNil = 2 -const ValMaxNotNil = 4 -const Required = 8 -const Email = 16 - // values for invalid field flags -const FailLenMin = 2 -const FailLenMax = 4 -const FailValMin = 8 -const FailValMax = 16 -const FailEmpty = 32 -const FailRegexp = 64 -const FailEmail = 128 -const FailZero = 256 +const ( + _ = iota + FailLenMin = 1 << iota + FailLenMax + FailValMin + FailValMax + FailEmpty + FailRegexp + FailEmail + FailZero +) // Optional configuration for validation: // * RestrictFields defines what struct fields should be validated @@ -50,6 +38,11 @@ type ValidationOptions struct { // Func returns boolean value that determines whether value is true or false, and a map of fields that failed // validation. See Fail* constants for the values. func Validate(obj interface{}, options *ValidationOptions) (bool, map[string]int) { + // ValidationOptions is required + if options == nil { + panic("ValidationOptions cannot be nil") + } + v := reflect.ValueOf(obj) i := reflect.Indirect(v) s := i.Type() @@ -61,11 +54,11 @@ func Validate(obj interface{}, options *ValidationOptions) (bool, map[string]int } tagName := "validation" - if options != nil && options.OverwriteTagName != "" { + if options.OverwriteTagName != "" { tagName = options.OverwriteTagName } - invalidFields := map[string]int{} + invalidFields := make(map[string]int, s.NumField()) valid := true for j := 0; j < s.NumField(); j++ { @@ -73,57 +66,34 @@ func Validate(obj interface{}, options *ValidationOptions) (bool, map[string]int fieldKind := field.Type.Kind() // check if only specified field should be checked - if options != nil && len(options.RestrictFields) > 0 && !options.RestrictFields[field.Name] { + if len(options.RestrictFields) > 0 && !options.RestrictFields[field.Name] { continue } // validate only ints and string - if !isInt(fieldKind) && !isString(fieldKind) { + if !isInt(fieldKind) && fieldKind != reflect.String { continue } - validation := FieldValidation{} - validation.lenMin = -1 - validation.lenMax = -1 - - // get tag values - tagVal := field.Tag.Get(tagName) - tagRegexpVal := field.Tag.Get(tagName + "_regexp") - if options != nil && len(options.OverwriteFieldTags) > 0 { - if len(options.OverwriteFieldTags[field.Name]) > 0 { - if options.OverwriteFieldTags[field.Name][tagName] != "" { - tagVal = options.OverwriteFieldTags[field.Name][tagName] - } - if options.OverwriteFieldTags[field.Name][tagName+"_regexp"] != "" { - tagRegexpVal = options.OverwriteFieldTags[field.Name][tagName+"_regexp"] - } - } - } - - setValidationFromTag(&validation, tagVal) - if tagRegexpVal != "" { - validation.regexp = regexp.MustCompile(tagRegexpVal) - } + validation := NewValueValidation() - if options != nil && options.ValidateWhenSuffix { - if strings.HasSuffix(field.Name, "Email") { - validation.flags = validation.flags | Email - } - if strings.HasSuffix(field.Name, "Price") && validation.valMin == 0 && validation.valMax == 0 && validation.flags&ValMinNotNil == 0 && validation.flags&ValMaxNotNil == 0 { - validation.valMin = 0 - validation.flags = validation.flags | ValMinNotNil - } + tagVal, tagRegexpVal := getFieldTagValues(&field, tagName, options.OverwriteFieldTags) + setValidationFromTags(validation, tagVal, tagRegexpVal) + if options.ValidateWhenSuffix { + setValidationFromSuffix(validation, &field) } + // field value can be overwritten in ValidationOptions var fieldValue reflect.Value - if options != nil && len(options.OverwriteFieldValues) > 0 && isKeyInMap(field.Name, options.OverwriteFieldValues) { - fieldValue = reflect.ValueOf(options.OverwriteFieldValues[field.Name]) + overwriteVal, ok := options.OverwriteFieldValues[field.Name] + if ok { + fieldValue = reflect.ValueOf(overwriteVal) } else { fieldValue = v.Elem().FieldByName(field.Name) } - fieldValid, failureFlags := validateValue(fieldValue, &validation) - if !fieldValid { + ok, failureFlags := validation.ValidateReflectValue(fieldValue) + if !ok { valid = false invalidFields[field.Name] = failureFlags } @@ -132,73 +102,20 @@ func Validate(obj interface{}, options *ValidationOptions) (bool, map[string]int return valid, invalidFields } -func validateValue(value reflect.Value, validation *FieldValidation) (bool, int) { - minCanBeZero := false - maxCanBeZero := false - if validation.flags&ValMinNotNil > 0 { - minCanBeZero = true - } - if validation.flags&ValMaxNotNil > 0 { - maxCanBeZero = true - } - - if validation.flags&Required > 0 { - if value.Type().Name() == "string" && value.String() == "" { - return false, FailEmpty - } - if strings.HasPrefix(value.Type().Name(), "int") && value.Int() == 0 && !minCanBeZero && !maxCanBeZero && validation.valMin == 0 && validation.valMax == 0 { - return false, FailZero - } - } - - if value.Type().Name() == "string" { - if validation.lenMin > 0 && len(value.String()) < validation.lenMin { - return false, FailLenMin - } - if validation.lenMax > 0 && len(value.String()) > validation.lenMax { - return false, FailLenMax - } - - if validation.regexp != nil { - if !validation.regexp.MatchString(value.String()) { - return false, FailRegexp - } - } - - if validation.flags&Email > 0 { - var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - if !emailRegex.MatchString(value.String()) { - return false, FailEmail - } - } - } - - if strings.HasPrefix(value.Type().Name(), "int") { - if (validation.valMin != 0 || minCanBeZero) && validation.valMin > value.Int() { - return false, FailValMin - } - if (validation.valMax != 0 || maxCanBeZero) && validation.valMax < value.Int() { - return false, FailValMax - } - } - - return true, 0 -} - -func setValidationFromTag(v *FieldValidation, tag string) { +func setValidationFromTags(v *ValueValidation, tag string, tagRegexp string) { opts := strings.SplitN(tag, " ", -1) for _, opt := range opts { if opt == "req" { - v.flags = v.flags | Required + v.Flags = v.Flags | Required } if opt == "email" { - v.flags = v.flags | Email + v.Flags = v.Flags | Email } for _, valOpt := range []string{"lenmin", "lenmax", "valmin", "valmax", "regexp"} { if strings.HasPrefix(opt, valOpt+":") { val := strings.Replace(opt, valOpt+":", "", 1) if valOpt == "regexp" { - v.regexp = regexp.MustCompile(val) + v.Regexp = regexp.MustCompile(val) continue } @@ -208,51 +125,60 @@ func setValidationFromTag(v *FieldValidation, tag string) { } switch valOpt { case "lenmin": - v.lenMin = i + v.LenMin = i case "lenmax": - v.lenMax = i + v.LenMax = i case "valmin": - v.valMin = int64(i) + v.ValMin = int64(i) if i == 0 { - v.flags = v.flags | ValMinNotNil + v.Flags = v.Flags | ValMinNotNil } case "valmax": - v.valMax = int64(i) + v.ValMax = int64(i) if i == 0 { - v.flags = v.flags | ValMaxNotNil + v.Flags = v.Flags | ValMaxNotNil } } } } } -} -func isInt(k reflect.Kind) bool { - if k == reflect.Int64 || k == reflect.Int32 || k == reflect.Int16 || k == reflect.Int8 || k == reflect.Int || k == reflect.Uint64 || k == reflect.Uint32 || k == reflect.Uint16 || k == reflect.Uint8 || k == reflect.Uint { - return true + if tagRegexp != "" { + v.Regexp = regexp.MustCompile(tagRegexp) } - return false } -func isString(k reflect.Kind) bool { - if k == reflect.String { - return true +func setValidationFromSuffix(v *ValueValidation, field *reflect.StructField) { + if strings.HasSuffix(field.Name, "Email") { + v.Flags = v.Flags | Email + } + if strings.HasSuffix(field.Name, "Price") && v.ValMin == 0 && v.ValMax == 0 && v.Flags&ValMinNotNil == 0 && v.Flags&ValMaxNotNil == 0 { + v.ValMin = 0 + v.Flags = v.Flags | ValMinNotNil } - return false } -func isBool(k reflect.Kind) bool { - if k == reflect.Bool { +func isInt(k reflect.Kind) bool { + if k == reflect.Int64 || k == reflect.Int32 || k == reflect.Int16 || k == reflect.Int8 || k == reflect.Int || k == reflect.Uint64 || k == reflect.Uint32 || k == reflect.Uint16 || k == reflect.Uint8 || k == reflect.Uint { return true } return false } -func isKeyInMap(k string, m map[string]interface{}) bool { - for _, key := range reflect.ValueOf(m).MapKeys() { - if key.String() == k { - return true +func getFieldTagValues(field *reflect.StructField, tagName string, overwriteFieldTags map[string]map[string]string) (tagVal string, tagRegexpVal string) { + tagVal = field.Tag.Get(tagName) + tagRegexpVal = field.Tag.Get(tagName + "_regexp") + + overwriteTags, ok := overwriteFieldTags[field.Name] + if ok { + overwriteTagVal, ok2 := overwriteTags[tagName] + if ok2 { + tagVal = overwriteTagVal + } + overwriteTagVal, ok2 = overwriteTags[tagName + "_regexp"] + if ok2 { + tagRegexpVal = overwriteTagVal } } - return false + return } diff --git a/value_validation.go b/value_validation.go new file mode 100644 index 0000000..4c41937 --- /dev/null +++ b/value_validation.go @@ -0,0 +1,85 @@ +package structvalidator + +import ( + "reflect" + "regexp" + "strings" +) + +type ValueValidation struct { + LenMin int + LenMax int + ValMin int64 + ValMax int64 + Regexp *regexp.Regexp + Flags int64 +} + +// values used with flags +const ( + _ = iota + ValMinNotNil = 1 << iota + ValMaxNotNil + Required + Email +) + +func (v *ValueValidation) ValidateReflectValue(value reflect.Value) (ok bool, failureFlags int) { + minCanBeZero := false + maxCanBeZero := false + if v.Flags&ValMinNotNil > 0 { + minCanBeZero = true + } + if v.Flags&ValMaxNotNil > 0 { + maxCanBeZero = true + } + + if v.Flags&Required > 0 { + if value.Type().Name() == "string" && value.String() == "" { + return false, FailEmpty + } + if strings.HasPrefix(value.Type().Name(), "int") && value.Int() == 0 && !minCanBeZero && !maxCanBeZero && v.ValMin == 0 && v.ValMax == 0 { + return false, FailZero + } + } + + if value.Type().Name() == "string" { + if v.LenMin > 0 && len(value.String()) < v.LenMin { + return false, FailLenMin + } + if v.LenMax > 0 && len(value.String()) > v.LenMax { + return false, FailLenMax + } + + if v.Regexp != nil { + if !v.Regexp.MatchString(value.String()) { + return false, FailRegexp + } + } + + if v.Flags&Email > 0 { + var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + if !emailRegex.MatchString(value.String()) { + return false, FailEmail + } + } + } + + if strings.HasPrefix(value.Type().Name(), "int") { + if (v.ValMin != 0 || minCanBeZero) && v.ValMin > value.Int() { + return false, FailValMin + } + if (v.ValMax != 0 || maxCanBeZero) && v.ValMax < value.Int() { + return false, FailValMax + } + } + + return true, 0 +} + +func NewValueValidation() *ValueValidation { + return &ValueValidation{ + LenMin: -1, + LenMax: -1, + } +} diff --git a/version.go b/version.go index 1397b7b..00ad58b 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package structvalidator -const VERSION = "0.5.0" +const VERSION = "0.6.0" From 0f8b7a166bd0b7e7945fe357b2fd99ae928258f5 Mon Sep 17 00:00:00 2001 From: Mikolaj Gasior Date: Sun, 16 Feb 2025 19:09:05 +0100 Subject: [PATCH 2/4] Fix formatting --- validator.go | 18 +++++++++--------- value_validation.go | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/validator.go b/validator.go index d636f21..52bb8a6 100644 --- a/validator.go +++ b/validator.go @@ -9,15 +9,15 @@ import ( // values for invalid field flags const ( - _ = iota + _ = iota FailLenMin = 1 << iota - FailLenMax - FailValMin - FailValMax - FailEmpty - FailRegexp - FailEmail - FailZero + FailLenMax + FailValMin + FailValMax + FailEmpty + FailRegexp + FailEmail + FailZero ) // Optional configuration for validation: @@ -175,7 +175,7 @@ func getFieldTagValues(field *reflect.StructField, tagName string, overwriteFiel if ok2 { tagVal = overwriteTagVal } - overwriteTagVal, ok2 = overwriteTags[tagName + "_regexp"] + overwriteTagVal, ok2 = overwriteTags[tagName+"_regexp"] if ok2 { tagRegexpVal = overwriteTagVal } diff --git a/value_validation.go b/value_validation.go index 4c41937..29a479c 100644 --- a/value_validation.go +++ b/value_validation.go @@ -17,7 +17,7 @@ type ValueValidation struct { // values used with flags const ( - _ = iota + _ = iota ValMinNotNil = 1 << iota ValMaxNotNil Required From 6a404d4f0a5ad0f52c7e6e16f0123c592ddbdc4a Mon Sep 17 00:00:00 2001 From: Mikolaj Gasior Date: Mon, 17 Feb 2025 08:03:47 +0100 Subject: [PATCH 3/4] Update README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7feafaa..ce049f4 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,13 @@ Verify the values of struct fields using tags ### Example code +Use the package with the following URL: ``` -package main - import "gopkg.pl/phings/struct-validator" +``` +And see below code snippet: +``` type Test1 struct { FirstName string `validation:"req lenmin:5 lenmax:25"` LastName string `validation:"req lenmin:2 lenmax:50"` From 00d7524a4de0711896c347a8f69c81eb508fdf72 Mon Sep 17 00:00:00 2001 From: Mikolaj Gasior Date: Tue, 18 Feb 2025 22:28:29 +0100 Subject: [PATCH 4/4] Use isInt() to check for the value type --- value_validation.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/value_validation.go b/value_validation.go index 29a479c..6dee11f 100644 --- a/value_validation.go +++ b/value_validation.go @@ -3,7 +3,6 @@ package structvalidator import ( "reflect" "regexp" - "strings" ) type ValueValidation struct { @@ -38,7 +37,7 @@ func (v *ValueValidation) ValidateReflectValue(value reflect.Value) (ok bool, fa if value.Type().Name() == "string" && value.String() == "" { return false, FailEmpty } - if strings.HasPrefix(value.Type().Name(), "int") && value.Int() == 0 && !minCanBeZero && !maxCanBeZero && v.ValMin == 0 && v.ValMax == 0 { + if isInt(value.Kind()) && value.Int() == 0 && !minCanBeZero && !maxCanBeZero && v.ValMin == 0 && v.ValMax == 0 { return false, FailZero } } @@ -65,7 +64,7 @@ func (v *ValueValidation) ValidateReflectValue(value reflect.Value) (ok bool, fa } } - if strings.HasPrefix(value.Type().Name(), "int") { + if isInt(value.Kind()) { if (v.ValMin != 0 || minCanBeZero) && v.ValMin > value.Int() { return false, FailValMin }