diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/LICENSE b/LICENSE index 1f29df8..4f9ab81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023, 2024 Mikolaj Gasior +Copyright (c) 2023, 2024, 2025 Mikołaj Gąsior Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ee6a462..ad509e1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# structvalidator - -[![Go Reference](https://pkg.go.dev/badge/github.com/keenbytes/structvalidator.svg)](https://pkg.go.dev/github.com/keenbytes/structvalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/keenbytes/structvalidator)](https://goreportcard.com/report/github.com/keenbytes/structvalidator) +# struct-validator Verify the values of struct fields using tags @@ -8,10 +6,10 @@ Verify the values of struct fields using tags Use the package with the following URL: ``` -import "github.com/keenbytes/structvalidator" +import "github.com/keenbytes/struct-validator" ``` -And see below code snippet: +And see the below code snippet: ``` type Test1 struct { FirstName string `validation:"req lenmin:5 lenmax:25"` @@ -31,7 +29,7 @@ s := &Test1{ ... } -o := structvalidator.&ValidationOptions{ +o := validator.&ValidationOptions{ RestrictFields: map[string]bool{ "FirstName": true, "LastName": true, @@ -40,5 +38,5 @@ o := structvalidator.&ValidationOptions{ ... } -isValid, fieldsWithInvalidValue := structvalidator.Validate(s, &o) +isValid, fieldViolations, err := validator.Validate(s, &o) ``` diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..dc21ae8 --- /dev/null +++ b/consts.go @@ -0,0 +1,17 @@ +package validator + +import "regexp" + +const ( + FailLenMin = 2 << iota + FailLenMax + FailValMin + FailValMax + FailRegExp + FailEmail + FailReq + FailType +) + +// pre‑compiled e‑mail regexp (RFC‑5322‑ish, good enough for most cases) +var emailRegexp = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) diff --git a/go.mod b/go.mod index 493336f..c4b81f2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/keenbytes/structvalidator +module github.com/keenbytes/struct-validator -go 1.22.1 +go 1.24.5 diff --git a/html_input_gen.go b/html_input_gen.go deleted file mode 100644 index 2870961..0000000 --- a/html_input_gen.go +++ /dev/null @@ -1,212 +0,0 @@ -package structvalidator - -import ( - "fmt" - "html" - "reflect" - "strconv" - "strings" -) - -const TypeText = 1 -const TypeTextarea = 2 -const TypePassword = 3 -const TypeEmail = 4 - -// Optional configuration for validation: -// * RestrictFields defines what struct fields should be generated -// * ExcludeFields defines fields that should be skipped (also from RestrictFields) -// * OverwriteFieldTags can be used to overwrite tags for specific fields -// * OverwriteTagName sets tag used to define validation (default is "validation") -// * ValidateWhenSuffix will validate certain fields based on their name, eg. "PrimaryEmail" field will need to be a valid email -// * OverwriteFieldValues is to use overwrite values for fields, so these values are validated not the ones in struct -// * IDPrefix - if added, an element will contain an 'id' attribute in form of prefix + field name -// * NamePrefix - use this to put a prefix in the 'name' attribute -// * OverwriteValues - fill inputs with the specified values -// * FieldValues - when true then fill inputs with struct instance values -type HTMLOptions struct { - RestrictFields map[string]bool - ExcludeFields map[string]bool - OverwriteFieldTags map[string]map[string]string - OverwriteTagName string - ValidateWhenSuffix bool - IDPrefix string - NamePrefix string - OverwriteValues map[string]string - FieldValues bool -} - -// GenerateHTMLInput takes a struct and generates HTML inputs for each of the fields, eg. or `, fieldNameAttr, fieldIDAttr, validationAttrs, patternAttr, html.EscapeString(value)) - continue - } - fieldTypeAttr := ` type="text"` - if inputType == TypeEmail { - fieldTypeAttr = ` type="email"` - } - if inputType == TypePassword { - fieldTypeAttr = ` type="password"` - fieldValue = "" - } - fields[field.Name] = fmt.Sprintf(``, fieldTypeAttr, fieldNameAttr, fieldIDAttr, validationAttrs, patternAttr, fieldValue) - continue - } - } - - return fields -} - -func getHTMLAttributesFromTag(tag string) (string, int) { - attrs := "" - inputType := TypeText - - opts := strings.SplitN(tag, " ", -1) - for _, opt := range opts { - if opt == "req" { - attrs = attrs + " required" - } - if opt == "email" { - inputType = TypeEmail - continue - } - if opt == "uitextarea" { - inputType = TypeTextarea - } - if opt == "uipassword" { - inputType = TypePassword - } - for _, valOpt := range []string{"lenmin", "lenmax", "valmin", "valmax", "regexp"} { - if strings.HasPrefix(opt, valOpt+":") { - val := strings.Replace(opt, valOpt+":", "", 1) - if valOpt == "regexp" { - attrs = attrs + fmt.Sprintf(` pattern="%s"`, html.EscapeString(val)) - continue - } - - i, err := strconv.Atoi(val) - if err != nil { - continue - } - switch valOpt { - case "lenmin": - attrs = attrs + fmt.Sprintf(` minlength="%d"`, i) - case "lenmax": - attrs = attrs + fmt.Sprintf(` maxlength="%d"`, i) - case "valmin": - attrs = attrs + fmt.Sprintf(` min="%d"`, i) - case "valmax": - attrs = attrs + fmt.Sprintf(` max="%d"`, i) - } - } - } - } - - return attrs, inputType -} diff --git a/html_input_gen_test.go b/html_input_gen_test.go deleted file mode 100644 index 146f451..0000000 --- a/html_input_gen_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package structvalidator - -import ( - "testing" -) - -func TestGenerateHTML(t *testing.T) { - s := &Test1{} - fieldsHTMLInputs := GenerateHTML(s, &HTMLOptions{ - RestrictFields: map[string]bool{ - "FirstName": true, - "Age": true, - "PostCode": true, - "Email": true, - "Country": true, - "County": true, - }, - IDPrefix: "id_", - NamePrefix: "name_", - }) - - if fieldsHTMLInputs["FirstName"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'FirstName' field") - } - if fieldsHTMLInputs["Age"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Age' field") - } - if fieldsHTMLInputs["PostCode"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'PostCode' field") - } - if fieldsHTMLInputs["Email"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Email' field") - } - if fieldsHTMLInputs["Country"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Country' field") - } - if fieldsHTMLInputs["County"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'County' field") - } -} - -func TestGenerateHTMLWithValues(t *testing.T) { - s := &Test1{} - fieldsHTMLInputs := GenerateHTML(s, &HTMLOptions{ - RestrictFields: map[string]bool{ - "FirstName": true, - "Age": true, - "PostCode": true, - "Email": true, - "Country": true, - "County": true, - }, - IDPrefix: "id_", - NamePrefix: "name_", - OverwriteValues: map[string]string{ - "FirstName": `Joe "Joe"`, - "Age": "40", - "Email": "email@example.com", - "Country": "XX", - }, - }) - - if fieldsHTMLInputs["FirstName"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'FirstName' field") - } - if fieldsHTMLInputs["Age"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Age' field") - } - if fieldsHTMLInputs["PostCode"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'PostCode' field") - } - if fieldsHTMLInputs["Email"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Email' field") - } - if fieldsHTMLInputs["Country"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Country' field") - } - if fieldsHTMLInputs["County"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'County' field") - } -} - -func TestGenerateHTMLWithFieldValues(t *testing.T) { - s := &Test1{ - FirstName: "Joe", - Age: 60, - Email: "joe@example.com", - } - fieldsHTMLInputs := GenerateHTML(s, &HTMLOptions{ - RestrictFields: map[string]bool{ - "FirstName": true, - "Age": true, - "Email": true, - }, - IDPrefix: "id_", - NamePrefix: "name_", - OverwriteValues: map[string]string{ - "FirstName": `Joe "Joe"`, - }, - FieldValues: true, - }) - - if fieldsHTMLInputs["FirstName"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'FirstName' field") - } - if fieldsHTMLInputs["Age"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Age' field") - } - if fieldsHTMLInputs["Email"] != `` { - t.Fatal("GenerateHTML failed to output HTML for 'Email' field") - } -} diff --git a/internal.go b/internal.go new file mode 100644 index 0000000..cfb7183 --- /dev/null +++ b/internal.go @@ -0,0 +1,52 @@ +package validator + +import ( + "reflect" + "regexp" + "strings" + "sync" +) + +// dereferenceKind walks through pointer indirections until it reaches a non‑pointer type. +// It returns the final reflect.Kind and the reflect.Value that points to that concrete value. +// If the original value is a nil pointer, the returned value will be the zero Value of the +// element type (so Kind() will be the element’s kind, but IsValid() will be false). +func dereferenceKind(v reflect.Value) (bool, reflect.Kind, reflect.Value) { + for v.Kind() == reflect.Pointer { + if v.IsNil() { + // Nil pointer – we cannot Elem() safely, so break and return the zero value. + // The caller can decide whether a nil pointer is acceptable. + return true, v.Type().Elem().Kind(), reflect.Zero(v.Type().Elem()) + } + v = v.Elem() + } + return false, v.Kind(), v +} + +func parseRule(tok string) (name, arg string) { + parts := strings.SplitN(tok, ":", 2) + name = parts[0] + if len(parts) == 2 { + arg = parts[1] + } + return +} + +var ( + // pattern → compiled *regexp.Regexp* + regexCache sync.Map +) + +// getCompiledRegexp returns a compiled *regexp.Regexp for the given pattern. +// It caches the result so subsequent calls are O(1). +func getCompiledRegexp(pattern string) (*regexp.Regexp, error) { + if v, ok := regexCache.Load(pattern); ok { + return v.(*regexp.Regexp), nil + } + compiled, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + regexCache.Store(pattern, compiled) + return compiled, nil +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..63ae43f --- /dev/null +++ b/options.go @@ -0,0 +1,9 @@ +package validator + +// Optional configuration for validation: +// * RestrictFields defines what struct fields should be validated +// * TagName sets tag used to define validation (default is "validation") +type ValidateOptions struct { + RestrictFields map[string]bool + TagName string +} diff --git a/validate.go b/validate.go new file mode 100644 index 0000000..c736f18 --- /dev/null +++ b/validate.go @@ -0,0 +1,171 @@ +package validator + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" +) + +// Validate takes a struct and validates values of its fields based on their tags. +func Validate(obj interface{}, options *ValidateOptions) (bool, map[string]int, error) { + if options == nil { + options = &ValidateOptions{} // use defaults + } + + tagName := "validation" + if options.TagName != "" { + tagName = options.TagName + } + + val := reflect.ValueOf(obj) + + // If we received a pointer to a pointer, keep dereferencing until we hit a non‑pointer. + // This also normalizes the case where the caller passed a pointer to a struct. + for val.Kind() == reflect.Pointer { + if val.IsNil() { + return false, nil, errors.New("nil pointer supplied") + } + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return false, nil, fmt.Errorf("obj is not a struct or pointer to struct, got %s", val.Kind()) + } + + typ := val.Type() + + numField := typ.NumField() + fieldViolations := make(map[string]int, numField) + overallOk := true + + for i := 0; i < numField; i++ { + structField := typ.Field(i) + fieldValue := val.Field(i) + + // unexported fields are not validated + if !structField.IsExported() { + continue + } + + // check if only specified field should be checked + if len(options.RestrictFields) > 0 && !options.RestrictFields[structField.Name] { + continue + } + + ok, viol := ValidateField(structField, fieldValue, tagName) + if !ok { + overallOk = false + fieldViolations[structField.Name] = viol + } + } + + return overallOk, fieldViolations, nil +} + +// ValidateField takes a reflected struct field, its value and a tagname and validates the values against the requirements in the tag. +func ValidateField(structField reflect.StructField, fieldValue reflect.Value, tagName string) (bool, int) { + violations := 0 + + tag := structField.Tag.Get(tagName) + + isNilPointer, kind, concrete := dereferenceKind(fieldValue) + + if tag != "" { + tokens := strings.Fields(tag) + + // check req first + for _, token := range tokens { + name, _ := parseRule(token) + if name == "req" && isNilPointer { + violations += FailReq + break + } + } + + // make further checks only if not nil + if !isNilPointer { + for _, token := range tokens { + name, arg := parseRule(token) + switch name { + case "email": + if kind != reflect.String { + // Wrong kind → treat as violation (helps catch config errors) + violations += FailType + break + } + if !emailRegexp.MatchString(concrete.String()) { + violations += FailEmail + } + + case "lenmin", "lenmax": + if kind != reflect.String { + violations += FailType + break + } + minMax, _ := strconv.Atoi(arg) // arg is guaranteed to be numeric by convention + strLen := len(concrete.String()) + if name == "lenmin" && strLen < minMax { + violations += FailLenMin + } + if name == "lenmax" && strLen > minMax { + violations += FailLenMax + } + case "valmin", "valmax": + // First, make sure we are dealing with a numeric kind. + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + limit, _ := strconv.ParseInt(arg, 10, 64) + val := concrete.Int() + if name == "valmin" && val < limit { + violations += FailValMin + } + if name == "valmax" && val > limit { + violations += FailValMax + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + limit, _ := strconv.ParseUint(arg, 10, 64) + val := concrete.Uint() + if name == "valmin" && val < limit { + violations += FailValMin + } + if name == "valmax" && val > limit { + violations += FailValMax + } + case reflect.Float32, reflect.Float64: + limit, _ := strconv.ParseFloat(arg, 64) + val := concrete.Float() + if name == "valmin" && val < limit { + violations += FailValMin + } + if name == "valmax" && val > limit { + violations += FailValMax + } + default: + // Not a numeric type → record a mismatch. + violations = +FailType + } + default: + } + } + } + } + + // regexp checked only if field value is not a nil pointer + if !isNilPointer { + regexpTagName := tagName + "_regexp" + if pattern := structField.Tag.Get(regexpTagName); pattern != "" { + if kind == reflect.String { + compiled, err := getCompiledRegexp(pattern) + if err != nil || !compiled.MatchString(concrete.String()) { + // configuration error is considered a failed regexp + violations += FailRegExp + } + } + } + } + + ok := violations == 0 + return ok, violations +} diff --git a/validate_test.go b/validate_test.go new file mode 100644 index 0000000..ad33e2e --- /dev/null +++ b/validate_test.go @@ -0,0 +1,252 @@ +package validator + +import ( + "log" + "testing" +) + +type Test1 struct { + FirstName string `validation:"lenmin:5 lenmax:25"` + LastName string `validation:"lenmin:2 lenmax:50"` + Age int `validation:"valmin:18 valmax:150"` + Price int `validation:"valmin:0 valmax:9999"` + PostCode string `validation:"" validation_regexp:"^[0-9][0-9]-[0-9][0-9][0-9]$"` + Email string `validation:"email"` + BelowZero int `validation:"valmin:-6 valmax:-2"` + DiscountPrice int `validation:"valmin:0 valmax:8000"` + Country string `validation_regexp:"^[A-Z][A-Z]$"` + County string `validation:"lenmax:40"` +} + +type Test2 struct { + FirstName string `mytag:"req lenmin:5 lenmax:25"` + LastName string `mytag:"req lenmin:2 lenmax:50"` + Age int `mytag:"req valmin:18 valmax:150"` + Price int `mytag:"req valmin:0 valmax:9999"` + PostCode string `mytag:"req" mytag_regexp:"^[0-9][0-9]-[0-9][0-9][0-9]$"` + Email string `mytag:"req email"` + BelowZero int `mytag:"valmin:-6 valmax:-2"` + DiscountPrice int `mytag:"valmin:0 valmax:8000"` + Country string `mytag_regexp:"^[A-Z][A-Z]$"` + County string `mytag:"lenmax:40"` +} + +type Test3 struct { + FirstName *string `mytag:"req lenmin:5 lenmax:25"` + LastName *string `mytag:"lenmin:2 lenmax:50"` + Age *int `mytag:"req valmin:18 valmax:150"` + Price *int `mytag:"valmin:0 valmax:9999"` + PostCode *string `mytag:"req" mytag_regexp:"^[0-9][0-9]-[0-9][0-9][0-9]$"` + Email *string `mytag:"req email"` + BelowZero *int `mytag:"valmin:-6 valmax:-2"` + DiscountPrice *int `mytag:"valmin:0 valmax:8000"` + Country *string `mytag:"^[A-Z][A-Z]$"` + County *string `mytag:"lenmax:40"` +} + +func TestWithDefaultValues(t *testing.T) { + s := Test1{} + + expectedViolations := map[string]int{ + "FirstName": FailLenMin, + "LastName": FailLenMin, + "Age": FailValMin, + "PostCode": FailRegExp, + "Email": FailEmail, + "Country": FailRegExp, + "BelowZero": FailValMax, + } + + ok, violations, _ := Validate(s, &ValidateOptions{}) + if ok { + t.Fatalf("validation should have failed") + } + + compareViolations(violations, expectedViolations, t) +} + +func TestWithInvalidValues(t *testing.T) { + s := Test1{ + FirstName: "123456789012345678901234567890", + LastName: "b", + Age: 15, + Price: 0, + PostCode: "AA123", + Email: "invalidEmail", + BelowZero: 8, + DiscountPrice: 9999, + Country: "Tokelau", + County: "", + } + + expectedViolations := map[string]int{ + "FirstName": FailLenMax, + "LastName": FailLenMin, + "Age": FailValMin, + "PostCode": FailRegExp, + "Email": FailEmail, + "BelowZero": FailValMax, + "DiscountPrice": FailValMax, + "Country": FailRegExp, + } + + ok, violations, _ := Validate(s, &ValidateOptions{}) + if ok { + t.Fatalf("validation should have failed") + } + + compareViolations(violations, expectedViolations, t) +} + +func TestWithValidValues(t *testing.T) { + s := Test1{ + FirstName: "Johnny", + LastName: "Smith", + Age: 35, + Price: 0, + PostCode: "43-155", + Email: "john@example.com", + BelowZero: -4, + DiscountPrice: 8000, + Country: "GB", + County: "Enfield", + } + + expectedViolations := map[string]int{} + + ok, violations, _ := Validate(s, &ValidateOptions{}) + if !ok { + t.Fatalf("validation should have succeeded") + } + + compareViolations(violations, expectedViolations, t) +} + +func TestWithInvalidValuesAndFieldRestriction(t *testing.T) { + s := Test1{ + FirstName: "123456789012345678901234567890", + LastName: "b", + Age: 15, + Price: 0, + PostCode: "AA123", + Email: "invalidEmail", + BelowZero: 8, + DiscountPrice: 9999, + Country: "Tokelau", + County: "", + } + + expectedViolations := map[string]int{ + "FirstName": FailLenMax, + "LastName": FailLenMin, + } + opts := &ValidateOptions{ + RestrictFields: map[string]bool{ + "FirstName": true, + "LastName": true, + }, + } + + ok, violations, _ := Validate(s, opts) + if ok { + t.Fatalf("validation should have failed") + } + + compareViolations(violations, expectedViolations, t) +} + +func TestWithInvalidValuesAndOverwrittenTagName(t *testing.T) { + s := Test2{ + FirstName: "123456789012345678901234567890", + LastName: "b", + Age: 15, + Price: 0, + PostCode: "AA123", + Email: "invalidEmail", + BelowZero: 8, + DiscountPrice: 9999, + Country: "Tokelau", + County: "", + } + expectedViolations := map[string]int{ + "FirstName": FailLenMax, + "LastName": FailLenMin, + "Age": FailValMin, + "PostCode": FailRegExp, + "Email": FailEmail, + "BelowZero": FailValMax, + "DiscountPrice": FailValMax, + "Country": FailRegExp, + } + opts := &ValidateOptions{ + TagName: "mytag", + } + ok, violations, _ := Validate(s, opts) + if ok { + t.Fatalf("validation should have failed") + } + + compareViolations(violations, expectedViolations, t) +} + +func TestWithAllInvalidValuesAndPointerFields(t *testing.T) { + s := Test3{} + + expectedViolations := map[string]int{ + "FirstName": FailReq, + "Age": FailReq, + "PostCode": FailReq, + "Email": FailReq, + } + opts := &ValidateOptions{ + TagName: "mytag", + } + ok, violations, _ := Validate(s, opts) + if ok { + t.Fatalf("validation should have failed") + } + + compareViolations(violations, expectedViolations, t) +} + +func TestWithInvalidValuesAndPointerFields(t *testing.T) { + firstName := "a" + age := 3 + postCode := "a123" + + s := Test3{ + FirstName: &firstName, + Age: &age, + PostCode: &postCode, + } + + expectedViolations := map[string]int{ + "FirstName": FailLenMin, + "Age": FailValMin, + "PostCode": FailRegExp, + "Email": FailReq, + } + opts := &ValidateOptions{ + TagName: "mytag", + } + ok, violations, _ := Validate(s, opts) + if ok { + t.Fatalf("validation should have failed") + } + + compareViolations(violations, expectedViolations, t) +} + +func compareViolations(violations map[string]int, expectedViolations map[string]int, t *testing.T) { + if len(violations) != len(expectedViolations) { + for k, v := range violations { + log.Printf("%s %d", k, v) + } + t.Fatalf("Validate returned invalid number of failed fields %d where it should be %d", len(violations), len(expectedViolations)) + } + for k, v := range expectedViolations { + if violations[k] != v { + t.Fatalf("Validate returned invalid failure flag of %d where it should be %d for %s", violations[k], v, k) + } + } +} diff --git a/validator.go b/validator.go deleted file mode 100644 index 52bb8a6..0000000 --- a/validator.go +++ /dev/null @@ -1,184 +0,0 @@ -package structvalidator - -import ( - "reflect" - "regexp" - "strconv" - "strings" -) - -// values for invalid field flags -const ( - _ = iota - FailLenMin = 1 << iota - FailLenMax - FailValMin - FailValMax - FailEmpty - FailRegexp - FailEmail - FailZero -) - -// Optional configuration for validation: -// * RestrictFields defines what struct fields should be validated -// * OverwriteFieldTags can be used to overwrite tags for specific fields -// * OverwriteTagName sets tag used to define validation (default is "validation") -// * ValidateWhenSuffix will validate certain fields based on their name, eg. "PrimaryEmail" field will need to be a valid email -// * OverwriteFieldValues is to use overwrite values for fields, so these values are validated not the ones in struct -type ValidationOptions struct { - RestrictFields map[string]bool - OverwriteFieldTags map[string]map[string]string - OverwriteTagName string - ValidateWhenSuffix bool - OverwriteFieldValues map[string]interface{} -} - -// Validate validates fields of a struct. Currently only fields which are string or int (any) are validated. -// 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() - - // TODO: Fix this to traverse the pointer behind reflect.Value properly. Current this is made to support - // struct-db-postgres module that uses this validator. - if s.String() == "reflect.Value" { - s = reflect.ValueOf(obj.(reflect.Value).Interface()).Type().Elem().Elem() - } - - tagName := "validation" - if options.OverwriteTagName != "" { - tagName = options.OverwriteTagName - } - - invalidFields := make(map[string]int, s.NumField()) - valid := true - - for j := 0; j < s.NumField(); j++ { - field := s.Field(j) - fieldKind := field.Type.Kind() - - // check if only specified field should be checked - if len(options.RestrictFields) > 0 && !options.RestrictFields[field.Name] { - continue - } - - // validate only ints and string - if !isInt(fieldKind) && fieldKind != reflect.String { - continue - } - - validation := NewValueValidation() - - 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 - overwriteVal, ok := options.OverwriteFieldValues[field.Name] - if ok { - fieldValue = reflect.ValueOf(overwriteVal) - } else { - fieldValue = v.Elem().FieldByName(field.Name) - } - - ok, failureFlags := validation.ValidateReflectValue(fieldValue) - if !ok { - valid = false - invalidFields[field.Name] = failureFlags - } - } - - return valid, invalidFields -} - -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 - } - if opt == "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) - continue - } - - i, err := strconv.Atoi(val) - if err != nil { - continue - } - switch valOpt { - case "lenmin": - v.LenMin = i - case "lenmax": - v.LenMax = i - case "valmin": - v.ValMin = int64(i) - if i == 0 { - v.Flags = v.Flags | ValMinNotNil - } - case "valmax": - v.ValMax = int64(i) - if i == 0 { - v.Flags = v.Flags | ValMaxNotNil - } - } - } - } - } - - if tagRegexp != "" { - v.Regexp = regexp.MustCompile(tagRegexp) - } -} - -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 - } -} - -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 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 -} diff --git a/validator_reflectvalue_test.go b/validator_reflectvalue_test.go deleted file mode 100644 index 6fa1cad..0000000 --- a/validator_reflectvalue_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package structvalidator - -import ( - "reflect" - "testing" -) - -// In the struct-db-postgres, struct-validator is used to validate map of values against reflect.Value which is actually -// a pointer of a pointer to struct. -// TODO: This should be revisited at some point. -type Wrapper struct { - DoesntMatter string - UseMeToValidate []*Test1 -} - -func TestWithInvalidValuesOnReflectValue(t *testing.T) { - o := &Wrapper{} - v := reflect.ValueOf(o) - i := reflect.Indirect(v) - s := i.Type() - - for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - k := f.Type.Kind() - - // Only field which are slices of pointers to struct instances - if k != reflect.Slice || f.Type.Elem().Kind() != reflect.Ptr || f.Type.Elem().Elem().Kind() != reflect.Struct { - continue - } - - expectedBool := false - expectedFailedFields := map[string]int{ - "FirstName": FailLenMax, - "LastName": FailLenMin, - "Age": FailValMin, - "PostCode": FailRegexp, - "Email": FailEmail, - "BelowZero": FailValMax, - "DiscountPrice": FailValMax, - "Country": FailRegexp, - } - opts := &ValidationOptions{ - OverwriteFieldValues: map[string]interface{}{ - "FirstName": "123456789012345678901234567890", - "LastName": "b", - "Age": 15, - "Price": 0, - "PostCode": "AA123", - "Email": "invalidEmail", - "BelowZero": 8, - "DiscountPrice": 9999, - "Country": "Tokelau", - "County": "", - }, - } - compare(reflect.New(f.Type.Elem()), expectedBool, expectedFailedFields, opts, t) - } -} diff --git a/validator_test.go b/validator_test.go deleted file mode 100644 index 5a408e5..0000000 --- a/validator_test.go +++ /dev/null @@ -1,325 +0,0 @@ -package structvalidator - -import ( - "log" - "testing" -) - -type Test1 struct { - FirstName string `validation:"req lenmin:5 lenmax:25"` - LastName string `validation:"req lenmin:2 lenmax:50"` - Age int `validation:"req valmin:18 valmax:150"` - Price int `validation:"req valmin:0 valmax:9999"` - PostCode string `validation:"req" validation_regexp:"^[0-9][0-9]-[0-9][0-9][0-9]$"` - Email string `validation:"req email"` - BelowZero int `validation:"valmin:-6 valmax:-2"` - DiscountPrice int `validation:"valmin:0 valmax:8000"` - Country string `validation_regexp:"^[A-Z][A-Z]$"` - County string `validation:"lenmax:40 uitextarea"` -} - -type Test2 struct { - FirstName string `mytag:"req lenmin:5 lenmax:25"` - LastName string `mytag:"req lenmin:2 lenmax:50"` - Age int `mytag:"req valmin:18 valmax:150"` - Price int `mytag:"req valmin:0 valmax:9999"` - PostCode string `mytag:"req" mytag_regexp:"^[0-9][0-9]-[0-9][0-9][0-9]$"` - Email string `mytag:"req email"` - BelowZero int `mytag:"valmin:-6 valmax:-2"` - DiscountPrice int `mytag:"valmin:0 valmax:8000"` - Country string `mytag_regexp:"^[A-Z][A-Z]$"` - County string `mytag:"lenmax:40"` -} - -type Test3 struct { - ZeroMin int `mytag:"valmin:0 valmax:5"` - ZeroMax int `mytag:"valmax:0"` - ZeroBoth int `mytag:"valmin:0 valmax:0"` - NotZero int `mytag:"valmin:4 valmax:6"` - OnlyMin int `mytag:"valmin:3"` - OnlyMax int `mytag:"valmax:7"` -} - -type Test4 struct { - PrimaryEmail string `` -} - -func TestWithDefaultValues(t *testing.T) { - s := Test1{} - expectedBool := false - expectedFailedFields := map[string]int{ - "FirstName": FailEmpty, - "LastName": FailEmpty, - "Age": FailValMin, - "PostCode": FailEmpty, - "Email": FailEmpty, - "Country": FailRegexp, - "BelowZero": FailValMax, - } - opts := &ValidationOptions{} - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithInvalidValues(t *testing.T) { - s := Test1{ - FirstName: "123456789012345678901234567890", - LastName: "b", - Age: 15, - Price: 0, - PostCode: "AA123", - Email: "invalidEmail", - BelowZero: 8, - DiscountPrice: 9999, - Country: "Tokelau", - County: "", - } - expectedBool := false - expectedFailedFields := map[string]int{ - "FirstName": FailLenMax, - "LastName": FailLenMin, - "Age": FailValMin, - "PostCode": FailRegexp, - "Email": FailEmail, - "BelowZero": FailValMax, - "DiscountPrice": FailValMax, - "Country": FailRegexp, - } - opts := &ValidationOptions{} - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithValidValues(t *testing.T) { - s := Test1{ - FirstName: "Johnny", - LastName: "Smith", - Age: 35, - Price: 0, - PostCode: "43-155", - Email: "john@example.com", - BelowZero: -4, - DiscountPrice: 8000, - Country: "GB", - County: "Enfield", - } - expectedBool := true - expectedFailedFields := map[string]int{} - opts := &ValidationOptions{} - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithInvalidValuesAndFieldRestriction(t *testing.T) { - s := Test1{ - FirstName: "123456789012345678901234567890", - LastName: "b", - Age: 15, - Price: 0, - PostCode: "AA123", - Email: "invalidEmail", - BelowZero: 8, - DiscountPrice: 9999, - Country: "Tokelau", - County: "", - } - expectedBool := false - expectedFailedFields := map[string]int{ - "FirstName": FailLenMax, - "LastName": FailLenMin, - } - opts := &ValidationOptions{ - RestrictFields: map[string]bool{ - "FirstName": true, - "LastName": true, - }, - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithInvalidValuesAndFieldRestrictionAndOverwrittenFieldTags(t *testing.T) { - s := Test1{ - FirstName: "123456789012345678901234567890", - LastName: "b", - Age: 15, - Price: 0, - PostCode: "AA123", - Email: "invalidEmail", - BelowZero: 8, - DiscountPrice: 9999, - Country: "Tokelau", - County: "", - } - expectedBool := false - expectedFailedFields := map[string]int{ - "LastName": FailLenMin, - } - opts := &ValidationOptions{ - RestrictFields: map[string]bool{ - "FirstName": true, - "LastName": true, - }, - OverwriteFieldTags: map[string]map[string]string{ - "FirstName": map[string]string{ - "validation": "req lenmin:4 lenmax:100", - }, - }, - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithInvalidValuesAndOverwrittenTagName(t *testing.T) { - s := Test2{ - FirstName: "123456789012345678901234567890", - LastName: "b", - Age: 15, - Price: 0, - PostCode: "AA123", - Email: "invalidEmail", - BelowZero: 8, - DiscountPrice: 9999, - Country: "Tokelau", - County: "", - } - expectedBool := false - expectedFailedFields := map[string]int{ - "FirstName": FailLenMax, - "LastName": FailLenMin, - "Age": FailValMin, - "PostCode": FailRegexp, - "Email": FailEmail, - "BelowZero": FailValMax, - "DiscountPrice": FailValMax, - "Country": FailRegexp, - } - opts := &ValidationOptions{ - OverwriteTagName: "mytag", - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestValMinMaxWithDefault(t *testing.T) { - s := Test3{} - expectedBool := false - expectedFailedFields := map[string]int{ - "NotZero": FailValMin, - "OnlyMin": FailValMin, - } - opts := &ValidationOptions{ - OverwriteTagName: "mytag", - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestValMinMaxWithValid(t *testing.T) { - s := Test3{ - NotZero: 4, - OnlyMin: 3, - OnlyMax: 7, - } - expectedBool := true - expectedFailedFields := map[string]int{} - opts := &ValidationOptions{ - OverwriteTagName: "mytag", - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestValMinMaxWithInvalid(t *testing.T) { - s := Test3{ - ZeroMin: -4, - ZeroMax: -6, - ZeroBoth: -6, - NotZero: 2, - OnlyMin: -5, - OnlyMax: -6, - } - expectedBool := false - expectedFailedFields := map[string]int{ - "ZeroMin": FailValMin, - "ZeroBoth": FailValMin, - "NotZero": FailValMin, - "OnlyMin": FailValMin, - } - opts := &ValidationOptions{ - OverwriteTagName: "mytag", - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithInvalidValuesWithSuffixValidation(t *testing.T) { - s := Test4{ - PrimaryEmail: "invalidemail", - } - expectedBool := false - expectedFailedFields := map[string]int{ - "PrimaryEmail": FailEmail, - } - opts := &ValidationOptions{ - ValidateWhenSuffix: true, - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithInvalidValuesWithoutSuffixValidation(t *testing.T) { - s := Test4{ - PrimaryEmail: "invalidemail", - } - expectedBool := true - expectedFailedFields := map[string]int{} - opts := &ValidationOptions{ - ValidateWhenSuffix: false, - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func TestWithOverwrittenValues(t *testing.T) { - s := Test1{ - FirstName: "123456789012345678901234567890", - LastName: "b", - Age: 15, - Price: 0, - PostCode: "AA123", - Email: "invalidEmail", - BelowZero: 8, - DiscountPrice: 9999, - Country: "Tokelau", - County: "", - } - expectedBool := false - expectedFailedFields := map[string]int{ - "Age": FailValMax, - } - opts := &ValidationOptions{ - RestrictFields: map[string]bool{ - "FirstName": true, - "LastName": true, - "Age": true, - }, - OverwriteFieldValues: map[string]interface{}{ - "FirstName": "123456", - "LastName": "123", - "Age": 300, - }, - } - compare(&s, expectedBool, expectedFailedFields, opts, t) -} - -func compare(s interface{}, expectedBool bool, expectedFailedFields map[string]int, options *ValidationOptions, t *testing.T) { - valid, failedFields := Validate(s, options) - if valid != expectedBool { - t.Fatalf("Validate returned invalid boolean value") - } - compareFailedFields(failedFields, expectedFailedFields, t) -} - -func compareFailedFields(failedFields map[string]int, expectedFailedFields map[string]int, t *testing.T) { - if len(failedFields) != len(expectedFailedFields) { - for k, v := range failedFields { - log.Printf("%s %d", k, v) - } - t.Fatalf("Validate returned invalid number of failed fields %d where it should be %d", len(failedFields), len(expectedFailedFields)) - } - for k, v := range expectedFailedFields { - if failedFields[k] != v { - t.Fatalf("Validate returned invalid failure flag of %d where it should be %d for %s", failedFields[k], v, k) - } - } -} diff --git a/value_validation.go b/value_validation.go deleted file mode 100644 index 6dee11f..0000000 --- a/value_validation.go +++ /dev/null @@ -1,84 +0,0 @@ -package structvalidator - -import ( - "reflect" - "regexp" -) - -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 isInt(value.Kind()) && 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 isInt(value.Kind()) { - 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 deleted file mode 100644 index bb56d20..0000000 --- a/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package structvalidator - -const VERSION = "0.6.1"