From 721d96690d941e2172db4fc8664793ca0bc3ffdb Mon Sep 17 00:00:00 2001 From: Mikolaj Gasior Date: Tue, 24 Dec 2024 00:38:26 +0100 Subject: [PATCH] Add code from previous repository --- .github/workflows/tests.yml | 23 +++ LICENSE | 37 ++-- README.md | 38 +++- go.mod | 3 + html_input_gen.go | 212 +++++++++++++++++++++ html_input_gen_test.go | 112 ++++++++++++ validator.go | 258 ++++++++++++++++++++++++++ validator_reflectvalue_test.go | 58 ++++++ validator_test.go | 325 +++++++++++++++++++++++++++++++++ version.go | 3 + 10 files changed, 1048 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 go.mod create mode 100644 html_input_gen.go create mode 100644 html_input_gen_test.go create mode 100644 validator.go create mode 100644 validator_reflectvalue_test.go create mode 100644 validator_test.go create mode 100644 version.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a322702 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: Test build + +on: + pull_request: + branches: + - main + +jobs: + main: + name: Build and run + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Run tests + run: | + go test + + - name: Check if binary builds + run: | + go build . + diff --git a/LICENSE b/LICENSE index ebd4008..3a6dc56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,21 @@ -BSD 2-Clause License +MIT License -Copyright (c) 2024, Go Phings +Copyright (c) 2023, 2024 Mikolaj Gasior -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index aa7f156..0842c7e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ # struct-validator -Verify the values of struct fields using tags + +[![Go Reference](https://pkg.go.dev/badge/github.com/go-phings/struct-validator.svg)](https://pkg.go.dev/github.com/go-phings/struct-validator) [![Go Report Card](https://goreportcard.com/badge/github.com/go-phings/struct-validator)](https://goreportcard.com/report/github.com/go-phings/struct-validator) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/go-phings/struct-validator?sort=semver) + +Verify the values of struct fields using tags + +### Example code + +``` +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"` +} + +s := &Test1{ + FirstName: "Name that is way too long and certainly not valid", + ... +} + +o := structvalidator.&ValidationOptions{ + RestrictFields: map[string]bool{ + "FirstName": true, + "LastName": true, + ... + }, + ... +} + +isValid, fieldsWithInvalidValue := structvalidator.Validate(s, &o) +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..83895a8 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/go-phings/struct-validator + +go 1.23.4 diff --git a/html_input_gen.go b/html_input_gen.go new file mode 100644 index 0000000..bf04c31 --- /dev/null +++ b/html_input_gen.go @@ -0,0 +1,212 @@ +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 new file mode 100644 index 0000000..146f451 --- /dev/null +++ b/html_input_gen_test.go @@ -0,0 +1,112 @@ +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/validator.go b/validator.go new file mode 100644 index 0000000..918dc6d --- /dev/null +++ b/validator.go @@ -0,0 +1,258 @@ +package structvalidator + +import ( + "reflect" + "regexp" + "strconv" + "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 + +// 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) { + 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 != nil && options.OverwriteTagName != "" { + tagName = options.OverwriteTagName + } + + invalidFields := map[string]int{} + 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 options != nil && len(options.RestrictFields) > 0 && !options.RestrictFields[field.Name] { + continue + } + + // validate only ints and string + if !isInt(fieldKind) && !isString(fieldKind) { + 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) + } + + 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 + } + } + + var fieldValue reflect.Value + if options != nil && len(options.OverwriteFieldValues) > 0 && isKeyInMap(field.Name, options.OverwriteFieldValues) { + fieldValue = reflect.ValueOf(options.OverwriteFieldValues[field.Name]) + } else { + fieldValue = v.Elem().FieldByName(field.Name) + } + + fieldValid, failureFlags := validateValue(fieldValue, &validation) + if !fieldValid { + valid = false + invalidFields[field.Name] = failureFlags + } + } + + 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) { + 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 + } + } + } + } + } +} + +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 isString(k reflect.Kind) bool { + if k == reflect.String { + return true + } + return false +} + +func isBool(k reflect.Kind) bool { + if k == reflect.Bool { + 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 + } + } + return false +} diff --git a/validator_reflectvalue_test.go b/validator_reflectvalue_test.go new file mode 100644 index 0000000..6fa1cad --- /dev/null +++ b/validator_reflectvalue_test.go @@ -0,0 +1,58 @@ +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 new file mode 100644 index 0000000..5a408e5 --- /dev/null +++ b/validator_test.go @@ -0,0 +1,325 @@ +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/version.go b/version.go new file mode 100644 index 0000000..49e0e74 --- /dev/null +++ b/version.go @@ -0,0 +1,3 @@ +package structvalidator + +const VERSION = "0.4.7"