├── doc.go ├── generator ├── dummy_template.tmpl ├── types.go ├── example_support_test.go ├── assets_test.go ├── dive_test.go ├── other_pkg_test.go ├── len_test.go ├── bcp47_test.go ├── required_test.go ├── hex_test.go ├── uuid_test.go ├── notnil_test.go ├── template_funcs.go ├── gencheck_test.go ├── example_test.go └── gencheck.go ├── template ├── notnil.tmpl ├── containsany.tmpl ├── dive.tmpl ├── hex.tmpl ├── bcp47.tmpl ├── gt.tmpl ├── lt.tmpl ├── lte.tmpl ├── gte.tmpl ├── len.tmpl ├── uuid.tmpl ├── required.tmpl ├── ne.tmpl ├── eq.tmpl ├── max.tmpl ├── min.tmpl ├── main.tmpl ├── contains.tmpl ├── url.tmpl └── cidr.tmpl ├── tools.go ├── bcp47.go ├── .gitignore ├── hex.go ├── deps.mk ├── validate.go ├── bcp47_test.go ├── hex_test.go ├── .circleci └── config.yml ├── internal ├── example │ ├── example_other_file.go │ ├── example.go │ └── example_test.go └── benchmark │ ├── types.go │ ├── benchmark_test.go │ └── types_validators.go ├── benchmark.sh ├── go.mod ├── Makefile ├── benchmark_playground.md ├── uuid.go ├── benchmark_nooptions.md ├── benchmark_failfast.md ├── benchmark_noprealloc.md ├── uuid_test.go ├── validations.md ├── validate_test.go ├── errors.go ├── errors_test.go ├── gencheck └── main.go ├── go.sum ├── README.md └── LICENSE /doc.go: -------------------------------------------------------------------------------- 1 | // Package gencheck is used for defining validation functions that are called in generated Validate functions. 2 | package gencheck 3 | -------------------------------------------------------------------------------- /generator/dummy_template.tmpl: -------------------------------------------------------------------------------- 1 | {{define "dummy"}} 2 | {{ $keyType := getMapKeyType . }} 3 | {{ if isStructPtr . }}//Struct Pointer{{end}} 4 | () 5 | {{end}} 6 | -------------------------------------------------------------------------------- /template/notnil.tmpl: -------------------------------------------------------------------------------- 1 | {{define "notnil"}} 2 | {{ if isNullable . -}} 3 | if s.{{.FieldName}} == nil { 4 | {{ addError . "errors.New(\"is Nil\")" }} 5 | } 6 | {{ else }} 7 | {{ generationError (printf "notnil is not valid on non nullable field '%s %s'" .FieldName .FieldType) }} 8 | {{ end }} 9 | {{- end -}} 10 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | _ "github.com/golang/mock/mockgen" 7 | _ "github.com/golang/mock/mockgen/model" 8 | _ "github.com/kevinburke/go-bindata/go-bindata" 9 | _ "github.com/mattn/goveralls" 10 | _ "golang.org/x/tools/cmd/cover" 11 | _ "golang.org/x/tools/cmd/goimports" 12 | ) 13 | -------------------------------------------------------------------------------- /bcp47.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import "golang.org/x/text/language" 4 | 5 | // IsBCP47 will return an error if the string is not able to be parsed as a 6 | // language descriptor. 7 | // http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers 8 | func IsBCP47(s *string) error { 9 | if s == nil { 10 | return nil 11 | } 12 | 13 | _, err := language.Parse(*s) 14 | 15 | return err 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | coverage/ 27 | vendor/ 28 | bin/ 29 | *.diff 30 | /secrets.yml 31 | -------------------------------------------------------------------------------- /hex.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var hexMatcher = regexp.MustCompile("^[0-9a-fA-F]+$") 10 | 11 | // IsHex validates that the given string is a hex value 12 | func IsHex(s *string) error { 13 | if s == nil { 14 | return nil 15 | } 16 | 17 | matches := hexMatcher.MatchString(strings.TrimPrefix(*s, "0x")) 18 | if !matches { 19 | return fmt.Errorf("'%s' is not a hexadecimal string", *s) 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /deps.mk: -------------------------------------------------------------------------------- 1 | GOBINDATA=bin/go/github.com/kevinburke/go-bindata/go-bindata 2 | GOIMPORTS=bin/go/golang.org/x/tools/cmd/goimports 3 | GOVERALLS=bin/go/github.com/mattn/goveralls 4 | MOCKGEN=bin/go/github.com/golang/mock/mockgen 5 | TOOLS = $(GOBINDATA) \ 6 | $(GOIMPORTS) \ 7 | $(MOCKGEN) \ 8 | $(GOVERALLS) 9 | 10 | cleandeps: 11 | if [ -d "./bin" ]; then rm -rf "./bin"; fi 12 | 13 | freshdeps: cleandeps deps 14 | 15 | deps: $(TOOLS) 16 | bin/go/%: 17 | @echo "installing $*" 18 | $(GO) build -o bin/go/$* $* 19 | -------------------------------------------------------------------------------- /template/containsany.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{define "containsany"}} 3 | {{ if eq .FieldType "string" "*string" -}} 4 | {{ if isPtr . }}if s.{{.FieldName}} != nil{ {{ end -}} 5 | if !strings.ContainsAny({{if (isPtr . )}}*{{end}}s.{{.FieldName}}, "{{.Param}}"){ 6 | {{ addError . (printf "errors.New(\"%s did not contain any of %s\")" .FieldName .Param) }} 7 | } 8 | {{ if isPtr .}} } 9 | {{ end -}} 10 | {{ else }} 11 | {{ generationError (printf "containsany is not valid on field '%s %s'" .FieldName .FieldType) }} 12 | {{- end -}} 13 | {{- end -}} 14 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | // Validateable specifies a generic error return type instead of an 4 | // ErrorMap return type in order to allow for handwritten Validate 5 | // methods to work in tandem with gencheck generated Validate methods. 6 | type Validateable interface { 7 | Validate() error 8 | } 9 | 10 | // Validate calls validate on structs that implement the Validateable 11 | // interface. If they do not, then that struct is valid. 12 | func Validate(i interface{}) error { 13 | if v, ok := i.(Validateable); ok { 14 | return v.Validate() 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /template/dive.tmpl: -------------------------------------------------------------------------------- 1 | {{/* dive will call gencheck validate on nested structs */ -}} 2 | {{define "dive"}} 3 | {{ if or (isStruct .) (isStructPtr .) -}} 4 | {{ if isPtr . }}if s.{{.FieldName}} != nil { {{- end -}} 5 | if err := gencheck.Validate(s.{{.FieldName}}); err != nil { 6 | {{ addError . "err" }} 7 | } 8 | {{- if isPtr . }}}{{end }} 9 | {{else if or ( isMap . ) ( isArray . ) -}} 10 | for i, e := range s.{{.FieldName}} { 11 | if err := gencheck.Validate(e); err != nil { 12 | {{ addIndexedError . "i" "err" }} 13 | } 14 | } 15 | {{ else }} 16 | {{ generationError (printf "Dive is not valid on field '%s %s'" .FieldName .FieldType) }} 17 | {{- end -}} 18 | {{- end -}} 19 | -------------------------------------------------------------------------------- /bcp47_test.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | // BCP47TextSuite 10 | type BCP47TextSuite struct { 11 | suite.Suite 12 | } 13 | 14 | // TestBCP47TextSuite 15 | func TestBCP47TextSuite(t *testing.T) { 16 | suite.Run(t, new(BCP47TextSuite)) 17 | } 18 | 19 | // TestIsBCP47_Nil 20 | func (s *BCP47TextSuite) TestIsBCP47_Nil() { 21 | s.Nil(IsBCP47(nil)) 22 | } 23 | 24 | // TestIsBCP47_English 25 | func (s *BCP47TextSuite) TestIsBCP47_English() { 26 | str := "English" 27 | s.NotNil(IsBCP47(&str)) 28 | } 29 | 30 | // TestIsBCP47_en 31 | func (s *BCP47TextSuite) TestIsBCP47_en() { 32 | str := "en" 33 | s.Nil(IsBCP47(&str)) 34 | } 35 | -------------------------------------------------------------------------------- /template/hex.tmpl: -------------------------------------------------------------------------------- 1 | {{define "hexadecimal"}} 2 | {{- template "hex" . -}} 3 | {{end}} 4 | 5 | {{define "hex"}} 6 | {{ if eq .FieldType "string" "*string" -}} 7 | if err := gencheck.IsHex({{if not (isPtr . )}}&{{end}}s.{{.FieldName}}); err != nil { 8 | {{ addError . "err" }} 9 | } 10 | {{ else if eq .FieldType "[]string" "[]*string" -}} 11 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 12 | if err := gencheck.IsHex({{if eq .FieldType "[]string"}}&{{end}}single{{.FieldName}}); err != nil { 13 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 14 | } 15 | } 16 | {{ else }} 17 | {{ generationError (printf "hex is not valid on field '%s %s'" .FieldName .FieldType) }} 18 | {{end}} 19 | {{- end -}} 20 | -------------------------------------------------------------------------------- /generator/types.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import "go/ast" 4 | 5 | // Validation is a holder for a validation rule within the generation templates 6 | // It actually has the same information as the Field struct, simply for ease of 7 | // access from within the templates. 8 | type Validation struct { 9 | Name string 10 | Param string 11 | FieldName string 12 | F *ast.Field 13 | FieldType string 14 | StructName string 15 | FailFast bool 16 | Prealloc bool 17 | } 18 | 19 | // Field is used for storing field information. It holds a reference to the 20 | // original AST field information to help out if needed. 21 | type Field struct { 22 | Name string 23 | F *ast.Field 24 | Rules []Validation 25 | Type string 26 | FailFast bool 27 | } 28 | -------------------------------------------------------------------------------- /template/bcp47.tmpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | BCP47Validator generates code that will verify if a field is a BCP47 compatible string 3 | https://tools.ietf.org/html/bcp47 4 | */}} 5 | {{define "bcp47"}} 6 | {{ if eq .FieldType "string" "*string" -}} 7 | if err := gencheck.IsBCP47({{if not (isPtr . )}}&{{end}}s.{{.FieldName}}); err != nil { 8 | {{ addError . "err" }} 9 | } 10 | {{ else if eq .FieldType "[]string" "[]*string" -}} 11 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 12 | if err := gencheck.IsBCP47({{if eq .FieldType "[]string"}}&{{end}}single{{.FieldName}}); err != nil { 13 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 14 | } 15 | } 16 | {{ else }} 17 | {{ generationError (printf "bcp47 is not valid on field '%s %s'" .FieldName .FieldType) }} 18 | {{end}} 19 | {{- end -}} 20 | -------------------------------------------------------------------------------- /hex_test.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type HexTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestHexTestSuite(t *testing.T) { 15 | suite.Run(t, new(HexTestSuite)) 16 | } 17 | 18 | // TestNilString 19 | func (s *HexTestSuite) TestNilString() { 20 | var str *string 21 | s.Require().Nil(IsHex(str)) 22 | } 23 | 24 | // TestIsHex_No0x 25 | func (s *HexTestSuite) TestIsHex_No0x() { 26 | str := "1a3F" 27 | s.Nil(IsHex(&str)) 28 | } 29 | 30 | // TestIsHex_0x 31 | func (s *HexTestSuite) TestIsHex_0x() { 32 | str := "0x1a3F" 33 | s.Nil(IsHex(&str)) 34 | } 35 | 36 | // TestIsHex_NotHex 37 | func (s *HexTestSuite) TestIsHex_NotHex() { 38 | str := "0x1Gbcq" 39 | s.Equal(errors.New("'0x1Gbcq' is not a hexadecimal string"), IsHex(&str)) 40 | } 41 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobBase: &jobBase 4 | working_directory: /go/src/github.com/abice/gencheck 5 | steps: 6 | - checkout 7 | - run: 8 | name: Build 9 | command: | 10 | make build 11 | - run: 12 | name: Test 13 | command: | 14 | make test 15 | make cover 16 | make coveralls 17 | 18 | jobs: 19 | golang_1.11: 20 | <<: *jobBase 21 | docker: 22 | - image: circleci/golang:1.11 23 | golang_1.12: 24 | <<: *jobBase 25 | docker: 26 | - image: circleci/golang:1.12 27 | golang_1.13: 28 | <<: *jobBase 29 | docker: 30 | - image: circleci/golang:1.13 31 | 32 | workflows: 33 | version: 2 34 | build_and_test: 35 | jobs: 36 | - golang_1.11 37 | - golang_1.12 38 | - golang_1.13 39 | -------------------------------------------------------------------------------- /generator/example_support_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/abice/gencheck" 7 | ) 8 | 9 | // OtherFile is another example struct for testing 10 | type OtherFile struct { 11 | EqCSFieldString string `valid:"required"` 12 | NeCSFieldString string 13 | GtCSFieldString string 14 | GteCSFieldString string 15 | LtCSFieldString string 16 | LteCSFieldString string 17 | } 18 | 19 | func (s OtherFile) Validate() error { 20 | 21 | vErrors := make(gencheck.ValidationErrors, 0, 1) 22 | 23 | // BEGIN EqCSFieldString Validations 24 | // required 25 | if s.EqCSFieldString == "" { 26 | vErrors = append(vErrors, gencheck.NewFieldError("OtherFile", "EqCSFieldString", "required", errors.New("is required"))) 27 | } 28 | // END EqCSFieldString Validations 29 | 30 | if len(vErrors) > 0 { 31 | return vErrors 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /template/gt.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "gt" -}} 2 | {{ if eq (trimPrefix "*" .FieldType) "time.Time" -}} 3 | {{- template "gt_Time" . }} 4 | {{ else -}} 5 | {{- template "min" . -}} 6 | {{- end -}} 7 | {{- end -}} 8 | 9 | 10 | {{ define "gt_Time" -}} 11 | {{$accessor := (printf "s.%s" .FieldName) -}} 12 | {{ if isPtr . }}if {{$accessor}} != nil { {{- end}} 13 | {{ if eq "" .Param -}} 14 | t{{.FieldName}} := time.Now().UTC() 15 | if !{{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.After(t{{.FieldName}}) { 16 | {{ addError . (printf "errors.New(\"is before %s\")" "now") }} 17 | } 18 | {{- else}} 19 | t{{.FieldName}} := time.Now().UTC().Add({{.Param}}) 20 | if !{{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.After(t{{.FieldName}}) { 21 | {{ addError . (printf "fmt.Errorf(\"is before %%s\", t%s)" .FieldName) }} 22 | } 23 | {{- end -}} 24 | {{ if isPtr . }}}{{- end -}}{{- end -}} 25 | -------------------------------------------------------------------------------- /template/lt.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "lt" -}} 2 | {{ if eq (trimPrefix "*" .FieldType) "time.Time" -}} 3 | {{ template "lt_Time" . }} 4 | {{ else -}} 5 | {{- template "max" . -}} 6 | {{- end -}} 7 | {{- end -}} 8 | 9 | 10 | {{ define "lt_Time" -}} 11 | {{$accessor := (printf "s.%s" .FieldName) -}} 12 | {{ if isPtr . }}if {{$accessor}} != nil { {{- end}} 13 | {{ if eq "" .Param -}} 14 | t{{.FieldName}} := time.Now().UTC() 15 | if !{{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.Before(t{{.FieldName}}) { 16 | {{ addError . (printf "errors.New(\"is after %s\")" "now") }} 17 | } 18 | {{- else -}} 19 | t{{.FieldName}} := time.Now().UTC().Add({{.Param}}) 20 | if !{{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.Before(t{{.FieldName}}) { 21 | {{ addError . (printf "fmt.Errorf(\"is after %%s\", t%s)" .FieldName) }} 22 | } 23 | {{- end -}} 24 | {{ if isPtr . }}}{{- end -}}{{- end -}} 25 | -------------------------------------------------------------------------------- /template/lte.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "lte" -}} 2 | {{ if eq (trimPrefix "*" .FieldType) "time.Time" -}} 3 | {{- template "lte_Time" . }} 4 | {{ else -}} 5 | {{- template "max" . -}} 6 | {{- end -}} 7 | {{- end -}} 8 | 9 | 10 | {{ define "lte_Time" -}} 11 | {{$accessor := (printf "s.%s" .FieldName) -}} 12 | {{ if isPtr . }}if {{$accessor}} != nil { {{- end}} 13 | {{ if eq "" .Param -}} 14 | t{{.FieldName}} := time.Now().UTC() 15 | if {{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.After(t{{.FieldName}}) { 16 | {{ addError . (printf "errors.New(\"is after %s\")" "now") }} 17 | } 18 | {{- else}} 19 | t{{.FieldName}} := time.Now().UTC().Add({{.Param}}) 20 | if {{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.After(t{{.FieldName}}) { 21 | {{ addError . (printf "fmt.Errorf(\"is after %%s\", t%s)" .FieldName) }} 22 | } 23 | {{- end -}} 24 | {{ if isPtr . }}}{{- end -}} 25 | {{- end -}} 26 | -------------------------------------------------------------------------------- /template/gte.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "gte" -}} 2 | {{ if eq (trimPrefix "*" .FieldType) "time.Time" -}} 3 | {{ template "gte_Time" . }} 4 | {{ else -}} 5 | {{- template "min" . -}} 6 | {{- end -}} 7 | {{- end -}} 8 | 9 | 10 | {{ define "gte_Time" -}} 11 | {{$accessor := (printf "s.%s" .FieldName) -}} 12 | {{ if isPtr . }} 13 | if {{$accessor}} != nil { {{end -}} 14 | {{if eq "" .Param }} 15 | t{{.FieldName}} := time.Now().UTC() 16 | if {{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.Before(t{{.FieldName}}) { 17 | {{ addError . (printf "errors.New(\"is before %s\")" "now") }} 18 | } 19 | {{- else }} 20 | t{{.FieldName}} := time.Now().UTC().Add({{.Param}}) 21 | if {{ if isPtr .}}(*{{$accessor}}){{else}}{{$accessor}}{{end}}.Before(t{{.FieldName}}) { 22 | {{ addError . (printf "fmt.Errorf(\"is before %%s\", t%s)" .FieldName) }} 23 | } 24 | {{- end -}} 25 | {{ if isPtr . }}}{{- end -}} 26 | {{- end -}} 27 | -------------------------------------------------------------------------------- /internal/example/example_other_file.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/abice/gencheck" 7 | ) 8 | 9 | // OtherFile is another example struct for testing 10 | type OtherFile struct { 11 | EqCSFieldString string `valid:"required"` 12 | NeCSFieldString string 13 | GtCSFieldString string 14 | GteCSFieldString string 15 | LtCSFieldString string 16 | LteCSFieldString string 17 | } 18 | 19 | type ExternalEmbedded struct { 20 | EmbeddedString string `valid:"required"` 21 | } 22 | 23 | func (s OtherFile) Validate() error { 24 | 25 | vErrors := make(gencheck.ValidationErrors, 0, 1) 26 | 27 | // BEGIN EqCSFieldString Validations 28 | // required 29 | if s.EqCSFieldString == "" { 30 | vErrors = append(vErrors, gencheck.NewFieldError("OtherFile", "EqCSFieldString", "required", errors.New("is required"))) 31 | } 32 | // END EqCSFieldString Validations 33 | 34 | if len(vErrors) > 0 { 35 | return vErrors 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gencheckBench(){ 4 | echo "# GENCHECK $options" 5 | echo "# GENCHECK $options" > "$benchMD" 6 | echo "\`\`\`" >> "$benchMD" 7 | f="-f=./internal/benchmark/types.go" 8 | if [[ -n $options ]]; then 9 | ./bin/gencheck "$f" "$options" 10 | else 11 | ./bin/gencheck "$f" 12 | fi 13 | go test -v -run=XXX -bench=BenchmarkCompareGencheck -benchmem ./internal/benchmark >> "$benchMD" 14 | echo "\`\`\`" >> "$benchMD" 15 | } 16 | 17 | benchMD=benchmark_nooptions.md 18 | options= 19 | gencheckBench 20 | 21 | benchMD=benchmark_noprealloc.md 22 | options="--noprealloc" 23 | gencheckBench 24 | 25 | benchMD=benchmark_failfast.md 26 | options="--failfast" 27 | gencheckBench 28 | 29 | benchMD=benchmark_playground.md 30 | 31 | echo "# PLAYGROUND" 32 | echo "# PLAYGROUND" > "$benchMD" 33 | echo "\`\`\`" >> "$benchMD" 34 | go test -v -run=XXX -bench=BenchmarkComparePlayground -benchmem ./internal/benchmark >> "$benchMD" 35 | echo "\`\`\`" >> "$benchMD" 36 | -------------------------------------------------------------------------------- /template/len.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{define "len" -}} 3 | {{/* Use the built in len function for the types that support it. */}} 4 | {{ if or ( isMap . ) ( isArray . ) (eq .FieldType "string") (hasPrefix "chan" .FieldType) -}} 5 | if !(len(s.{{.FieldName}}) == {{.Param}}) { 6 | {{ addError . "errors.New(\"length mismatch\")" }} 7 | } 8 | {{- /* Adding checks for integer values to coincide with the precedent set by go-playground/validator */}} 9 | {{ else if eq .FieldType "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64" "byte" "rune" -}} 10 | if !(s.{{.FieldName}} == {{.Param}}) { 11 | {{ addError . "errors.New(\"length mismatch\")" }} 12 | } 13 | {{ else if eq .FieldType "float" "float32" "float64" "complex64" "complex128" -}} 14 | if !(s.{{.FieldName}} == {{.Param}}) { 15 | {{ addError . "errors.New(\"length mismatch\")" }} 16 | } 17 | {{ else -}} 18 | {{ generationError (printf "len is not valid on field '%s %s'" .FieldName .FieldType) }} 19 | {{ end -}} 20 | {{- end -}} 21 | -------------------------------------------------------------------------------- /template/uuid.tmpl: -------------------------------------------------------------------------------- 1 | {{define "uuid3"}} 2 | {{- template "uuid" . -}} 3 | {{- end -}} 4 | {{define "uuid4"}} 5 | {{- template "uuid" . -}} 6 | {{- end -}} 7 | {{define "uuid5"}} 8 | {{- template "uuid" . -}} 9 | {{- end -}} 10 | 11 | {{define "uuid" }} 12 | {{ $version := (trimPrefix "uuid" .Name) -}} 13 | {{ if eq .FieldType "string" "*string" -}} 14 | if err := gencheck.IsUUID{{if ne $version ""}}v{{$version}}{{end}}({{if not (isPtr . )}}&{{end}}s.{{.FieldName}}); err != nil { 15 | {{ addError . "err" }} 16 | } 17 | {{ else if eq .FieldType "[]string" "[]*string" -}} 18 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 19 | if err := gencheck.IsUUID{{if ne $version ""}}v{{$version}}{{end}}({{if eq .FieldType "[]string"}}&{{end}}single{{.FieldName}}); err != nil { 20 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 21 | } 22 | } 23 | {{ else }} 24 | {{ generationError (printf "uuid is not valid on field '%s %s'" .FieldName .FieldType) }} 25 | {{end}} 26 | {{- end -}} 27 | -------------------------------------------------------------------------------- /template/required.tmpl: -------------------------------------------------------------------------------- 1 | {{define "required"}} 2 | {{ if isNullable . -}} 3 | if s.{{.FieldName}} == nil { 4 | {{ addError . "errors.New(\"is required\")" }} 5 | } 6 | {{ else if eq .FieldType "string" -}} 7 | if s.{{.FieldName}} == "" { 8 | {{ addError . "errors.New(\"is required\")" }} 9 | } 10 | {{ else if eq .FieldType "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64" "byte" "rune" -}} 11 | {{ generationError "Required on integer values is not supported."}} 12 | {{ else if eq .FieldType "float" "float32" "float64" "complex64" "complex128" -}} 13 | {{ generationError "Required on numerical values is not supported." -}} 14 | {{ else if eq .FieldType "bool" }} 15 | {{ generationError "Required on boolean values is not supported." -}} 16 | {{ else if isStruct . -}} 17 | var zero{{.FieldName}} {{.FieldType}} 18 | if s.{{.FieldName}} == zero{{.FieldName}} { 19 | {{ addError . "errors.New(\"is required\")" }} 20 | } 21 | {{ else -}} 22 | var zero{{.FieldName}} {{.FieldType}} 23 | if s.{{.FieldName}} == zero{{.FieldName}} { 24 | {{ addError . "errors.New(\"is required\")" }} 25 | } 26 | {{ end }} 27 | {{- end -}} 28 | -------------------------------------------------------------------------------- /template/ne.tmpl: -------------------------------------------------------------------------------- 1 | {{define "ne" -}} 2 | {{/* Use the built in len function for the types that support it. */}} 3 | {{if eq .FieldType "string" "*string" -}} 4 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} == "{{.Param}}" { 5 | {{ addError . (printf "errors.New(\"%s equaled %s\")" .FieldName .Param) }} 6 | } 7 | {{else if or ( isMap . ) ( isArray . ) -}} 8 | if len(s.{{.FieldName}}) == {{.Param}} { 9 | {{ addError . (printf "errors.New(\"Length of %s equaled %s\")" .FieldName .Param) }} 10 | } 11 | {{ else if eq (trimPrefix "*" .FieldType) "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64" "byte" "rune" -}} 12 | {{ template "ne_BuiltIn" . }} 13 | {{ else if eq (trimPrefix "*" .FieldType) "float" "float32" "float64" "complex64" "complex128" -}} 14 | {{ template "ne_BuiltIn" . }} 15 | {{ else }} 16 | {{ generationError (printf "%s is not valid on field '%s %s'" .Name .FieldName .FieldType) }} 17 | {{- end -}} 18 | {{- end -}} 19 | 20 | {{ define "ne_BuiltIn" -}} 21 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} == {{.Param}} { 22 | {{ addError . (printf "errors.New(\"%s equaled %s\")" .FieldName .Param) }} 23 | } 24 | {{- end -}} 25 | -------------------------------------------------------------------------------- /template/eq.tmpl: -------------------------------------------------------------------------------- 1 | {{define "eq" -}} 2 | {{/* Use the built in len function for the types that support it. */}} 3 | {{if eq .FieldType "string" "*string" -}} 4 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} != "{{.Param}}" { 5 | {{ addError . (printf "errors.New(\"%s did not equal %s\")" .FieldName .Param) }} 6 | } 7 | {{else if or ( isMap . ) ( isArray . ) -}} 8 | if len(s.{{.FieldName}}) != {{.Param}} { 9 | {{ addError . (printf "errors.New(\"Length of %s did not equal %s\")" .FieldName .Param) }} 10 | } 11 | {{ else if eq (trimPrefix "*" .FieldType) "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64" "byte" "rune" -}} 12 | {{ template "eq_BuiltIn" . }} 13 | {{ else if eq (trimPrefix "*" .FieldType) "float" "float32" "float64" "complex64" "complex128" -}} 14 | {{ template "eq_BuiltIn" . }} 15 | {{ else }} 16 | {{ generationError (printf "%s is not valid on field '%s %s'" .Name .FieldName .FieldType) }} 17 | {{- end -}} 18 | {{- end -}} 19 | 20 | {{ define "eq_BuiltIn" -}} 21 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} != {{.Param}} { 22 | {{ addError . (printf "errors.New(\"%s did not equal %s\")" .FieldName .Param) }} 23 | } 24 | {{- end -}} 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abice/gencheck 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Bowery/prompt v0.0.0-20180817134258-8a1d5376df1c // indirect 7 | github.com/Masterminds/semver v1.2.2 // indirect 8 | github.com/Masterminds/sprig v2.17.1+incompatible 9 | github.com/aokoli/goutils v1.1.0 // indirect 10 | github.com/go-playground/universal-translator v0.17.0 // indirect 11 | github.com/golang/mock v1.3.1 12 | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c 13 | github.com/huandu/xstrings v1.2.0 // indirect 14 | github.com/imdario/mergo v0.0.0-20171009183408-7fe0c75c13ab // indirect 15 | github.com/kevinburke/go-bindata v3.16.0+incompatible 16 | github.com/labstack/gommon v0.2.9-0.20190125185610-82ef680aef51 // indirect 17 | github.com/leodido/go-urn v1.2.0 // indirect 18 | github.com/mattn/go-colorable v0.1.0 // indirect 19 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 // indirect 20 | github.com/mattn/goveralls v0.0.5 21 | github.com/mkideal/cli v0.0.3-0.20190117035342-a48c2cee5b5e 22 | github.com/mkideal/pkg v0.0.0-20170503154153-3e188c9e7ecc // indirect 23 | github.com/pkg/errors v0.8.0 24 | github.com/stretchr/testify v1.4.0 25 | golang.org/x/text v0.3.2 26 | golang.org/x/tools v0.0.0-20200117173607-7ad9cd8f3189 27 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 28 | gopkg.in/go-playground/validator.v9 v9.31.0 29 | ) 30 | -------------------------------------------------------------------------------- /generator/assets_test.go: -------------------------------------------------------------------------------- 1 | // Silly Test to get coverage up 2 | 3 | package generator 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestAssets(t *testing.T) { 14 | assets, err := AssetDir("template") 15 | assert.NoError(t, err, "Error getting template dir") 16 | 17 | for _, a := range assets { 18 | fmt.Printf("%s\n", a) 19 | info, _ := AssetInfo("template/" + a) 20 | AssetDigest("template/" + a) 21 | info.IsDir() 22 | info.ModTime() 23 | info.Mode() 24 | info.Name() 25 | info.Size() 26 | info.Sys() 27 | } 28 | 29 | _, err = AssetDir("x") 30 | assert.Error(t, err, "Should have an error for a missing dir") 31 | 32 | _, err = AssetDigest("x") 33 | assert.Error(t, err, "Should have an error for a missing file") 34 | 35 | digests, derr := Digests() 36 | assert.NoError(t, derr, "Should not have an error getting digests") 37 | assert.Len(t, digests, 20, "Did you add or remove an template?") 38 | 39 | _, err = AssetInfo("x") 40 | assert.Error(t, err, "Should have an error for a missing dir") 41 | assert.Panics(t, func() { MustAsset("x") }) 42 | assert.Panics(t, func() { MustAssetString("x") }) 43 | 44 | RestoreAssets(os.TempDir(), "x") 45 | tmpDir := os.TempDir() 46 | err = RestoreAssets(tmpDir, "template") 47 | if err != nil { 48 | t.Logf("Error restoring assets: %s", err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /template/max.tmpl: -------------------------------------------------------------------------------- 1 | {{define "max" -}} 2 | {{/* Use the built in len function for the types that support it. */}} 3 | {{ if or ( isMap . ) ( isArray . ) (eq .FieldType "string") (hasPrefix "chan" .FieldType) -}} 4 | if len(s.{{.FieldName}}) >{{ if eq .Name "lt" }}={{end}} {{.Param}} { 5 | {{ addError . (printf "errors.New(\"length failed check for %s=%s\")" .Name .Param) }} 6 | } 7 | {{- /* Checks for integer values */}} 8 | {{ else if eq (trimPrefix "*" .FieldType) "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64" "byte" "rune" -}} 9 | {{ template "max_Int" . }} 10 | {{ else if eq (trimPrefix "*" .FieldType) "float" "float32" "float64" "complex64" "complex128" -}} 11 | {{ template "max_Float" . }} 12 | {{ else }} 13 | {{ generationError (printf "max is not valid on field '%s %s'" .FieldName .FieldType) }} 14 | {{- end -}} 15 | {{- end -}} 16 | 17 | 18 | {{ define "max_Int" -}} 19 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} >{{ if eq .Name "lt" }}={{end}} {{.Param}} { 20 | {{ addError . (printf "errors.New(\"failed check for %s=%s\")" .Name .Param) }} 21 | } 22 | {{- end -}} 23 | 24 | {{ define "max_Float" -}} 25 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} >{{ if eq .Name "lt" }}={{end}} {{.Param}} { 26 | {{ addError . (printf "errors.New(\"failed check for %s=%s\")" .Name .Param) }} 27 | } 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /template/min.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{define "min" -}} 3 | {{/* Use the built in len function for the types that support it. */}} 4 | {{if or ( isMap . ) ( isArray . ) (eq .FieldType "string") (hasPrefix "chan" .FieldType) -}} 5 | if len(s.{{.FieldName}}) <{{ if eq .Name "gt" }}={{end}} {{.Param}} { 6 | {{ addError . (printf "errors.New(\"length failed check for %s=%s\")" .Name .Param) }} 7 | } 8 | {{- /* Adding checks for integer values to coincide with the precedent set by go-playground/validator */}} 9 | {{ else if eq (trimPrefix "*" .FieldType) "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64" "byte" "rune" -}} 10 | {{ template "min_Int" . }} 11 | {{ else if eq (trimPrefix "*" .FieldType) "float" "float32" "float64" "complex64" "complex128" -}} 12 | {{ template "min_Float" . }} 13 | {{ else }} 14 | {{ generationError (printf "min is not valid on field '%s %s'" .FieldName .FieldType) }} 15 | {{- end -}} 16 | {{- end -}} 17 | 18 | {{ define "min_Int" -}} 19 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} <{{ if eq .Name "gt" }}={{end}} {{.Param}} { 20 | {{ addError . (printf "errors.New(\"failed check for %s=%s\")" .Name .Param) }} 21 | } 22 | {{- end -}} 23 | 24 | {{ define "min_Float" -}} 25 | if {{if isPtr .}}s.{{.FieldName}} != nil && *{{end}}s.{{.FieldName}} <{{ if eq .Name "gt" }}={{end}} {{.Param}} { 26 | {{ addError . (printf "errors.New(\"failed check for %s=%s\")" .Name .Param) }} 27 | } 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /template/main.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "header"}} 2 | // Code generated by gencheck 3 | // DO NOT EDIT! 4 | 5 | package {{.package}} 6 | 7 | import ( 8 | "github.com/abice/gencheck" 9 | ) 10 | {{end -}} 11 | 12 | {{- define "struct"}} 13 | {{$ruleMap := .rules -}} 14 | // Validate is an automatically generated validation method provided by 15 | // gencheck. 16 | // See https://github.com/abice/gencheck for more details. 17 | func (s {{if .ptrMethod }}*{{end}}{{.name}}) Validate () (error) { 18 | {{/* Only make the errors map if we're going to use it. */}} 19 | {{- if gt (len .rules) 0 -}} 20 | {{if (or .globalFailFast (not .prealloc))}} 21 | var vErrors gencheck.ValidationErrors 22 | {{else}} 23 | vErrors := make(gencheck.ValidationErrors, 0, {{len .rules}}) 24 | {{end}} 25 | {{ end }} 26 | {{/* Iterate through the rule map to create the rules */}} 27 | {{range $index, $field := .rules -}} 28 | {{ $name := $field.Name -}} 29 | {{ $rules := $field.Rules -}} 30 | {{ if gt (len $rules) 0 -}} 31 | // BEGIN {{ $name }} Validations 32 | {{- range $rIndex, $rule := $rules }} 33 | // {{ $rule.Name -}} 34 | {{- CallTemplate $rule $rule }} 35 | {{- end -}}// END {{ $name }} Validations 36 | 37 | {{ end }} 38 | {{- end}} 39 | {{/* Only check the errors map if we have rules. */}} 40 | {{ if gt (len .rules) 0 }} 41 | {{/* Only add check if we're not always failing fast. */}} 42 | {{if not .globalFailFast}} 43 | if len(vErrors) >0 { 44 | return vErrors 45 | } 46 | {{end}} 47 | {{- end}} 48 | return nil 49 | } 50 | {{end}} 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL:=all 2 | ifdef VERBOSE 3 | V = -v 4 | else 5 | .SILENT: 6 | endif 7 | 8 | GO ?= GO111MODULE=on go 9 | 10 | include $(wildcard *.mk) 11 | 12 | COVERAGEDIR = coverage 13 | ifdef CIRCLE_ARTIFACTS 14 | COVERAGEDIR = $(CIRCLE_ARTIFACTS) 15 | SERVICE=circle-ci 16 | endif 17 | 18 | .PHONY: all 19 | all: generate fmt build test cover 20 | 21 | .PHONY: benchmarks 22 | benchmarks: generate fmt build 23 | ./benchmark.sh 24 | 25 | build: generate 26 | if [ ! -d bin ]; then mkdir bin; fi 27 | $(GO) build -v -o bin/gencheck ./gencheck 28 | fmt: 29 | gofmt -l -w -s $$(find . -type f -name '*.go' -not -path "./vendor/*") 30 | test: generate gen-test 31 | if [ ! -d coverage ]; then mkdir coverage; fi 32 | $(GO) test -v ./... -race -cover -coverprofile=$(COVERAGEDIR)/coverage.out 33 | 34 | cover: 35 | $(GO) tool cover -html=$(COVERAGEDIR)/coverage.out -o $(COVERAGEDIR)/coverage.html 36 | 37 | tc: test cover 38 | coveralls: $(GOVERALLS) 39 | $(GOVERALLS) -coverprofile=$(COVERAGEDIR)/coverage.out -service=$(SERVICE) -repotoken=$(COVERALLS_TOKEN) 40 | 41 | clean: cleandeps 42 | $(GO) clean 43 | rm -f bin/gencheck 44 | rm -rf coverage/ 45 | 46 | # go-bindata will take all of the template files and create readable assets from them in the executable. 47 | # This way the templates are readable in atom (or another text editor with golang template language support) 48 | generate: deps 49 | $(GOBINDATA) -o generator/assets.go -nometadata -pkg=generator template/*.tmpl 50 | 51 | gen-test: build 52 | $(GO) generate ./... 53 | 54 | install: 55 | $(GO) install ./gencheck 56 | 57 | phony: clean tc build 58 | -------------------------------------------------------------------------------- /benchmark_playground.md: -------------------------------------------------------------------------------- 1 | # PLAYGROUND 2 | ``` 3 | goos: darwin 4 | goarch: amd64 5 | pkg: github.com/abice/gencheck/internal/benchmark 6 | BenchmarkComparePlayground/UUID_Pass-12 2187891 594 ns/op 16 B/op 1 allocs/op 7 | BenchmarkComparePlayground/UUID_Fail-12 1668792 754 ns/op 224 B/op 5 allocs/op 8 | BenchmarkComparePlayground/Hex_Pass-12 2723215 434 ns/op 0 B/op 0 allocs/op 9 | BenchmarkComparePlayground/Hex_Fail-12 1905054 644 ns/op 208 B/op 4 allocs/op 10 | BenchmarkComparePlayground/ContainsAny_Pass-12 9700926 126 ns/op 0 B/op 0 allocs/op 11 | BenchmarkComparePlayground/ContainsAny_Fail-12 3581966 323 ns/op 224 B/op 4 allocs/op 12 | BenchmarkComparePlayground/TestStrings_Pass-12 3550527 342 ns/op 16 B/op 1 allocs/op 13 | BenchmarkComparePlayground/TestStrings_Fail-12 1000000 1058 ns/op 816 B/op 13 allocs/op 14 | --- SKIP: BenchmarkComparePlayground/TestMap_Pass 15 | --- SKIP: BenchmarkComparePlayground/TestMap_Fail 16 | BenchmarkComparePlayground/TestDive_Pass-12 4978272 240 ns/op 32 B/op 2 allocs/op 17 | BenchmarkComparePlayground/TestDive_Fail-12 2600906 479 ns/op 272 B/op 7 allocs/op 18 | BenchmarkComparePlayground/TestDive_Nil-12 4445193 273 ns/op 208 B/op 4 allocs/op 19 | BenchmarkComparePlayground/TestAll_Pass-12 339356 3064 ns/op 408 B/op 11 allocs/op 20 | BenchmarkComparePlayground/TestAll_Fail-12 223174 5596 ns/op 4211 B/op 51 allocs/op 21 | PASS 22 | ok github.com/abice/gencheck/internal/benchmark 19.918s 23 | ``` 24 | -------------------------------------------------------------------------------- /uuid.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | var uuidMatcher = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") 9 | var uuid3Matcher = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") 10 | var uuid4Matcher = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$") 11 | var uuid5Matcher = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$") 12 | 13 | // IsUUID validates that the given string is a UUID value 14 | func IsUUID(s *string) error { 15 | if s == nil { 16 | return nil 17 | } 18 | 19 | matches := uuidMatcher.MatchString(*s) 20 | if !matches { 21 | return fmt.Errorf("'%s' is not a UUID", *s) 22 | } 23 | 24 | return nil 25 | } 26 | 27 | // IsUUIDv3 validates that the given string is a UUIDv3 value 28 | func IsUUIDv3(s *string) error { 29 | if s == nil { 30 | return nil 31 | } 32 | 33 | matches := uuid3Matcher.MatchString(*s) 34 | if !matches { 35 | return fmt.Errorf("'%s' is not a UUIDv3", *s) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // IsUUIDv4 validates that the given string is a UUIDv4 value 42 | func IsUUIDv4(s *string) error { 43 | if s == nil { 44 | return nil 45 | } 46 | 47 | matches := uuid4Matcher.MatchString(*s) 48 | if !matches { 49 | return fmt.Errorf("'%s' is not a UUIDv4", *s) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // IsUUIDv5 validates that the given string is a UUIDv5 value 56 | func IsUUIDv5(s *string) error { 57 | if s == nil { 58 | return nil 59 | } 60 | 61 | matches := uuid5Matcher.MatchString(*s) 62 | if !matches { 63 | return fmt.Errorf("'%s' is not a UUIDv5", *s) 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /template/contains.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{define "contains"}} 3 | {{ if eq .FieldType "string" "*string" -}} 4 | {{ if isPtr . }}if s.{{.FieldName}} != nil{ {{ end -}} 5 | if !strings.Contains({{ accessor . "s." }}, "{{.Param}}"){ 6 | {{ addError . (printf "errors.New(\"%s did not contain %s\")" .FieldName .Param) }} 7 | } 8 | {{ if isPtr .}} } 9 | {{ end -}} 10 | {{ else if eq .FieldType "[]string" "[]*string" -}} 11 | found{{.FieldName}} := false 12 | for _, single{{.FieldName}} := range s.{{.FieldName}} { 13 | if "{{.Param}}" == {{if eq .FieldType "[]*string"}}*{{end}}single{{.FieldName}}{ 14 | found{{.FieldName}} = true 15 | break 16 | } 17 | } 18 | if !found{{.FieldName}} { 19 | {{ addError . (printf "errors.New(\"%s did not contain %s\")" .FieldName .Param) }} 20 | } 21 | {{ else if (isMap .) -}} 22 | {{$keyType := getMapKeyType . -}} 23 | {{ if eq $keyType "string" -}} 24 | if _, found{{.FieldName}} := s.{{.FieldName}}["{{.Param}}"]; !found{{.FieldName}} { 25 | {{ addError . (printf "errors.New(\"%s did not contain %s\")" .FieldName .Param) }} 26 | } 27 | {{else if eq $keyType "int" "int8" "int16" "int32" "int64" "uint" "uint8" "uint16" "uint32" "uint64"}} 28 | {{if not (isParamInt .Param)}} 29 | {{ generationError (printf "cannot use a non integer value on an integer keyed map '%s %s'" .FieldName .FieldType) }} 30 | {{end}} 31 | if _, found{{.FieldName}} := s.{{.FieldName}}[{{.Param}}]; !found{{.FieldName}} { 32 | {{ addError . (printf "errors.New(\"%s did not contain %s\")" .FieldName .Param) }} 33 | } 34 | {{ else }} 35 | {{ generationError (printf "contains is not valid on field '%s %s'" .FieldName .FieldType) }} 36 | {{- end -}} 37 | {{ else }} 38 | {{ generationError (printf "contains is not valid on field '%s %s'" .FieldName .FieldType) }} 39 | {{- end -}} 40 | {{- end -}} 41 | -------------------------------------------------------------------------------- /benchmark_nooptions.md: -------------------------------------------------------------------------------- 1 | # GENCHECK 2 | ``` 3 | goos: darwin 4 | goarch: amd64 5 | pkg: github.com/abice/gencheck/internal/benchmark 6 | BenchmarkCompareGencheck/UUID_Pass-12 2869220 407 ns/op 16 B/op 1 allocs/op 7 | BenchmarkCompareGencheck/UUID_Fail-12 1858568 645 ns/op 208 B/op 6 allocs/op 8 | BenchmarkCompareGencheck/Hex_Pass-12 3280252 365 ns/op 16 B/op 1 allocs/op 9 | BenchmarkCompareGencheck/Hex_Fail-12 1954568 632 ns/op 192 B/op 6 allocs/op 10 | BenchmarkCompareGencheck/ContainsAny_Pass-12 21148444 56.3 ns/op 16 B/op 1 allocs/op 11 | BenchmarkCompareGencheck/ContainsAny_Fail-12 7321660 167 ns/op 128 B/op 4 allocs/op 12 | BenchmarkCompareGencheck/TestStrings_Pass-12 19691707 65.2 ns/op 64 B/op 1 allocs/op 13 | BenchmarkCompareGencheck/TestStrings_Fail-12 3345627 363 ns/op 416 B/op 10 allocs/op 14 | BenchmarkCompareGencheck/TestMap_Pass-12 39226683 31.0 ns/op 16 B/op 1 allocs/op 15 | BenchmarkCompareGencheck/TestMap_Fail-12 9257938 131 ns/op 128 B/op 4 allocs/op 16 | BenchmarkCompareGencheck/TestDive_Pass-12 18120820 67.3 ns/op 32 B/op 2 allocs/op 17 | BenchmarkCompareGencheck/TestDive_Fail-12 1685066 704 ns/op 592 B/op 13 allocs/op 18 | BenchmarkCompareGencheck/TestDive_Nil-12 9715989 130 ns/op 128 B/op 4 allocs/op 19 | BenchmarkCompareGencheck/TestAll_Pass-12 644622 1933 ns/op 632 B/op 8 allocs/op 20 | BenchmarkCompareGencheck/TestAll_Fail-12 316861 4088 ns/op 2722 B/op 54 allocs/op 21 | PASS 22 | ok github.com/abice/gencheck/internal/benchmark 22.311s 23 | ``` 24 | -------------------------------------------------------------------------------- /benchmark_failfast.md: -------------------------------------------------------------------------------- 1 | # GENCHECK --failfast 2 | ``` 3 | goos: darwin 4 | goarch: amd64 5 | pkg: github.com/abice/gencheck/internal/benchmark 6 | BenchmarkCompareGencheck/UUID_Pass-12 3019042 413 ns/op 0 B/op 0 allocs/op 7 | BenchmarkCompareGencheck/UUID_Fail-12 1584471 756 ns/op 208 B/op 6 allocs/op 8 | BenchmarkCompareGencheck/Hex_Pass-12 3540170 350 ns/op 0 B/op 0 allocs/op 9 | BenchmarkCompareGencheck/Hex_Fail-12 1829170 651 ns/op 192 B/op 6 allocs/op 10 | BenchmarkCompareGencheck/ContainsAny_Pass-12 31153759 33.7 ns/op 0 B/op 0 allocs/op 11 | BenchmarkCompareGencheck/ContainsAny_Fail-12 6937201 172 ns/op 128 B/op 4 allocs/op 12 | BenchmarkCompareGencheck/TestStrings_Pass-12 169096743 7.32 ns/op 0 B/op 0 allocs/op 13 | BenchmarkCompareGencheck/TestStrings_Fail-12 8132941 143 ns/op 128 B/op 4 allocs/op 14 | BenchmarkCompareGencheck/TestMap_Pass-12 197221968 6.07 ns/op 0 B/op 0 allocs/op 15 | BenchmarkCompareGencheck/TestMap_Fail-12 8839633 140 ns/op 128 B/op 4 allocs/op 16 | BenchmarkCompareGencheck/TestDive_Pass-12 80319295 15.1 ns/op 0 B/op 0 allocs/op 17 | BenchmarkCompareGencheck/TestDive_Fail-12 1629925 751 ns/op 592 B/op 13 allocs/op 18 | BenchmarkCompareGencheck/TestDive_Nil-12 9048465 137 ns/op 128 B/op 4 allocs/op 19 | BenchmarkCompareGencheck/TestAll_Pass-12 714092 1758 ns/op 328 B/op 6 allocs/op 20 | BenchmarkCompareGencheck/TestAll_Fail-12 8258488 154 ns/op 128 B/op 4 allocs/op 21 | PASS 22 | ok github.com/abice/gencheck/internal/benchmark 23.385s 23 | ``` 24 | -------------------------------------------------------------------------------- /benchmark_noprealloc.md: -------------------------------------------------------------------------------- 1 | # GENCHECK --noprealloc 2 | ``` 3 | goos: darwin 4 | goarch: amd64 5 | pkg: github.com/abice/gencheck/internal/benchmark 6 | BenchmarkCompareGencheck/UUID_Pass-12 3060320 388 ns/op 0 B/op 0 allocs/op 7 | BenchmarkCompareGencheck/UUID_Fail-12 1633210 675 ns/op 208 B/op 6 allocs/op 8 | BenchmarkCompareGencheck/Hex_Pass-12 3488574 345 ns/op 0 B/op 0 allocs/op 9 | BenchmarkCompareGencheck/Hex_Fail-12 1812626 663 ns/op 192 B/op 6 allocs/op 10 | BenchmarkCompareGencheck/ContainsAny_Pass-12 36120229 34.1 ns/op 0 B/op 0 allocs/op 11 | BenchmarkCompareGencheck/ContainsAny_Fail-12 6942110 171 ns/op 128 B/op 4 allocs/op 12 | BenchmarkCompareGencheck/TestStrings_Pass-12 168171955 7.15 ns/op 0 B/op 0 allocs/op 13 | BenchmarkCompareGencheck/TestStrings_Fail-12 2564859 461 ns/op 464 B/op 12 allocs/op 14 | BenchmarkCompareGencheck/TestMap_Pass-12 188108667 6.45 ns/op 0 B/op 0 allocs/op 15 | BenchmarkCompareGencheck/TestMap_Fail-12 8653820 141 ns/op 128 B/op 4 allocs/op 16 | BenchmarkCompareGencheck/TestDive_Pass-12 77069458 15.9 ns/op 0 B/op 0 allocs/op 17 | BenchmarkCompareGencheck/TestDive_Fail-12 1583066 727 ns/op 592 B/op 13 allocs/op 18 | BenchmarkCompareGencheck/TestDive_Nil-12 8740198 132 ns/op 128 B/op 4 allocs/op 19 | BenchmarkCompareGencheck/TestAll_Pass-12 695394 1772 ns/op 328 B/op 6 allocs/op 20 | BenchmarkCompareGencheck/TestAll_Fail-12 286522 4457 ns/op 3443 B/op 59 allocs/op 21 | PASS 22 | ok github.com/abice/gencheck/internal/benchmark 23.473s 23 | ``` 24 | -------------------------------------------------------------------------------- /uuid_test.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type UUIDTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func TestUUIDTestSuite(t *testing.T) { 16 | suite.Run(t, new(UUIDTestSuite)) 17 | } 18 | 19 | func (s *UUIDTestSuite) TestIsUUID_No0x() { 20 | str := "603c9a2a-38dB-4987-932a-2f57733a29f1" 21 | s.Nil(IsUUID(&str)) 22 | } 23 | 24 | // TestNilUUID tests to see if the string passed in is nil 25 | func (s *UUIDTestSuite) TestNilUUID() { 26 | var str *string 27 | s.Require().Nil(IsUUID(str)) 28 | s.Require().Nil(IsUUIDv3(str)) 29 | s.Require().Nil(IsUUIDv4(str)) 30 | s.Require().Nil(IsUUIDv5(str)) 31 | } 32 | 33 | // TestIsUUID_NotMatch 34 | func (s *UUIDTestSuite) TestIsUUID_NotMatch() { 35 | str := "603c9a2a-38db-4987-932a-2f57733a29fQ" 36 | s.Require().Equal(errors.New("'603c9a2a-38db-4987-932a-2f57733a29fQ' is not a UUID"), IsUUID(&str)) 37 | } 38 | 39 | func (s *UUIDTestSuite) TestIsUUID_NotUUIDTooLong() { 40 | str := "AB603c9a2a-38db-4987-932a-2f57733a29fQ" 41 | s.Require().Equal(errors.New("'AB603c9a2a-38db-4987-932a-2f57733a29fQ' is not a UUID"), IsUUID(&str)) 42 | } 43 | func (s *UUIDTestSuite) TestIsUUID_v3() { 44 | str := uuid.NewMD5(uuid.New(), []byte("test")).String() 45 | s.NoError(IsUUID(&str)) 46 | s.NoError(IsUUIDv3(&str)) 47 | s.Error(IsUUIDv4(&str)) 48 | s.Error(IsUUIDv5(&str)) 49 | } 50 | 51 | func (s *UUIDTestSuite) TestIsUUID_v4() { 52 | str := uuid.New().String() 53 | s.NoError(IsUUID(&str)) 54 | s.NoError(IsUUIDv4(&str)) 55 | s.Error(IsUUIDv3(&str)) 56 | s.Error(IsUUIDv5(&str)) 57 | } 58 | 59 | func (s *UUIDTestSuite) TestIsUUID_v5() { 60 | str := uuid.NewSHA1(uuid.NameSpaceDNS, []byte("test")).String() 61 | s.NoError(IsUUID(&str)) 62 | s.NoError(IsUUIDv5(&str)) 63 | s.Error(IsUUIDv3(&str)) 64 | s.Error(IsUUIDv4(&str)) 65 | } 66 | -------------------------------------------------------------------------------- /template/url.tmpl: -------------------------------------------------------------------------------- 1 | {{define "uri"}} 2 | {{ if eq .FieldType "string" "*string" -}} 3 | {{if (isPtr . )}}if s.{{.FieldName}} != nil { {{end -}} 4 | if {{if (isPtr . )}}*{{end}}s.{{.FieldName}} != "" { 5 | _, {{.FieldName}}urierr := url.ParseRequestURI({{ accessor . "s." }}) 6 | if {{.FieldName}}urierr != nil { 7 | {{ addError . (printf "%surierr" .FieldName) }} 8 | } 9 | } 10 | {{if (isPtr . )}} } {{end -}} 11 | {{ else if eq .FieldType "[]string" "[]*string" -}} 12 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 13 | _, err := url.ParseRequestURI({{if eq .FieldType "[]string"}}&{{end}}single{{.FieldName}}) 14 | if err != nil { 15 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 16 | } 17 | } 18 | {{ else }} 19 | {{ generationError (printf "uri is not valid on field '%s %s'" .FieldName .FieldType) }} 20 | {{end}} 21 | {{- end -}} 22 | 23 | 24 | {{define "url"}} 25 | {{ if eq .FieldType "string" "*string" -}} 26 | {{if (isPtr . )}}if s.{{.FieldName}} != nil { {{end -}} 27 | if {{ accessor . "s." }} != "" { 28 | {{.FieldName}}URL, {{.FieldName}}urlerr := url.ParseRequestURI({{ accessor . "s." }}) 29 | if {{.FieldName}}urlerr != nil { 30 | {{ addError . (printf "%surlerr" .FieldName) }} 31 | } else if {{.FieldName}}URL.Scheme == "" { 32 | {{ addError . (printf `errors.New("%s is missing a scheme")` .FieldName) }} 33 | } 34 | } 35 | {{if (isPtr . )}} } {{end -}} 36 | {{ else if eq .FieldType "[]string" "[]*string" -}} 37 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 38 | u, err := url.ParseRequestURI({{if eq .FieldType "[]string"}}&{{end}}single{{.FieldName}}) 39 | if err != nil { 40 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 41 | } else if u.Scheme == "" { 42 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - missing scheme", index, single%s)` .FieldName) }} 43 | } 44 | } 45 | {{ else }} 46 | {{ generationError (printf "url is not valid on field '%s %s'" .FieldName .FieldType) }} 47 | {{end}} 48 | {{- end -}} -------------------------------------------------------------------------------- /validations.md: -------------------------------------------------------------------------------- 1 | # Gencheck validations 2 | 3 | Name | Params | Field Type | Description 4 | ---- | ------------------- | ------ | ----------- 5 | hex | N/A | `(*)string` | Checks if a string field is a valid hexadecimal format number (0x prefix optional) 6 | notnil | N/A | pointers, interfaces, chans, maps, slices | Checks and fails if a pointer is nil 7 | len | 1 | `(*)string, slices, maps, chans(?)` | Uses golang's built in `len` method to determine the length of the field 8 | len | 1 | `int` | Checks if the value equals the parameter 9 | uuid | N/A | `(*)string` | Checks and fails if a string is not a valid UUID 10 | required | N/A | Any Nullable Value (ptr, `map`, `slice`, `chan`) | Checks to see if the pointer value is nil 11 | required | N/A | `string` | Checks to see if the `string != ""` 12 | required | N/A | `struct` | Checks to see if the struct equals the empty value struct 13 | min | 1 | `(*)string, slices, maps, chans` | Checks minimum length using `len(value)` 14 | min | 1 | `numbers` | Checks a simple `value >= param` 15 | max | 1 | `(*)string, slices, maps, chans` | Checks minimum length using `len(value)` 16 | max | 1 | `numbers` | Checks a simple `value <= param` 17 | lt(e) | 1 | See max | See max 18 | lt(e) | 1 | `time` | Checks that the field is less than (or equal to) `time.Now().UTC()` 19 | gt(e) | 1 | See min | See min 20 | gt(e) | 1 | `time` | Checks that the field is greater than (or equal to) `time.Now().UTC()` 21 | dive | N/A | Any | Dive will call `gencheck.Validate()` on the struct/ptr/interface passed in. That function will only call validate if it can be cast to the `gencheck.Validateable` interface. 22 | contains | 1 | `(*)string` | Checks to see if the field contains the parameter string value 23 | contains | 1 | `[](*)string` | Checks to see if any of the array elements **equal** the parameter value 24 | bcp47 | N/A | `(*)string` | Checks to see if the string value is one of the languages defined in https://tools.ietf.org/html/bcp47 25 | ff | N/A | Any | Fail Fast allows you to specify that if any validation within this field is invalid see [Fail Fast](#Fail Fast Flag) for more information 26 | -------------------------------------------------------------------------------- /validate_test.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type ValidateTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func TestValidateTestSuite(t *testing.T) { 15 | suite.Run(t, new(ValidateTestSuite)) 16 | } 17 | 18 | type CanValidate struct { 19 | Valid bool 20 | } 21 | 22 | func (c CanValidate) Validate() error { 23 | if c.Valid { 24 | return nil 25 | } 26 | return ValidationErrors{ 27 | NewFieldError("CanValidate", "Valid", "", fmt.Errorf("Valid when true")), 28 | NewFieldError("CanValidate", "Valid", "valid", nil), 29 | } 30 | } 31 | 32 | type NotValidateable struct { 33 | Whatever string 34 | } 35 | 36 | // TestCanValidate_Happy 37 | func (s *ValidateTestSuite) TestCanValidate_Happy() { 38 | uut := &CanValidate{Valid: true} 39 | s.Require().Nil(Validate(uut)) 40 | } 41 | 42 | // TestCanValidate_ValidationError 43 | func (s *ValidateTestSuite) TestCanValidate_ValidationError() { 44 | uut := CanValidate{Valid: false} 45 | err := Validate(uut) 46 | s.Require().NotNil(err) 47 | 48 | if ve, ok := err.(ValidationErrors); ok { 49 | s.Equal("validation: field validation failed for 'CanValidate.Valid': Valid when true\nvalidation: field validation failed for 'CanValidate.Valid': tag='valid'", ve.Error()) 50 | s.Require().Len(ve, 2, "Should have 2 errors in the CanValidate Error") 51 | 52 | fe := ve[0] 53 | s.Equal("CanValidate", fe.Struct()) 54 | s.Equal("Valid", fe.Field()) 55 | s.Equal("", fe.Tag()) 56 | s.Equal("Valid when true", fe.Message()) 57 | s.Equal("validation: field validation failed for 'CanValidate.Valid': Valid when true", fe.Error()) 58 | 59 | fe = ve[1] 60 | s.Equal("CanValidate", fe.Struct()) 61 | s.Equal("Valid", fe.Field()) 62 | s.Equal("valid", fe.Tag()) 63 | s.Equal("", fe.Message()) 64 | s.Equal("validation: field validation failed for 'CanValidate.Valid': tag='valid'", fe.Error()) 65 | 66 | } else { 67 | s.FailNow("Error returned should have been validation errors") 68 | } 69 | } 70 | 71 | // TestNotValidateable tests to see if the string passed in is nil 72 | func (s *ValidateTestSuite) TestNotValidateable() { 73 | uut := &NotValidateable{} 74 | s.Require().Nil(Validate(uut)) 75 | } 76 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | errMsgFormat = `validation: field validation failed for '%s.%s': %s` 11 | errTagFormat = `validation: field validation failed for '%s.%s': tag='%s'` 12 | ) 13 | 14 | // Error returns a string of the validation errors 15 | func (ve ValidationErrors) Error() string { 16 | buff := bytes.NewBufferString("") 17 | 18 | var fe *fieldError 19 | 20 | for i := 0; i < len(ve); i++ { 21 | 22 | fe = ve[i].(*fieldError) 23 | buff.WriteString(fe.Error()) 24 | buff.WriteString("\n") 25 | } 26 | 27 | return strings.TrimSpace(buff.String()) 28 | } 29 | 30 | // ValidationErrors is an array of Field Errors 31 | type ValidationErrors []FieldError 32 | 33 | // FieldError provide error information for a specific field validation. 34 | type FieldError interface { 35 | Tag() string 36 | Field() string 37 | Struct() string 38 | Message() string 39 | error 40 | } 41 | 42 | // fieldError provide error information for a specific field validation. 43 | type fieldError struct { 44 | Ftag string `json:"tag"` 45 | Ffield string `json:"field"` 46 | FstructName string `json:"structName"` 47 | Fmsg string `json:"msg,omitempty"` 48 | } 49 | 50 | func (fe fieldError) Error() string { 51 | if fe.Fmsg == "" { 52 | return fmt.Sprintf(errTagFormat, fe.FstructName, fe.Ffield, fe.Ftag) 53 | } 54 | return fmt.Sprintf(errMsgFormat, fe.FstructName, fe.Ffield, fe.Fmsg) 55 | } 56 | 57 | func (fe fieldError) Tag() string { 58 | return fe.Ftag 59 | } 60 | 61 | func (fe fieldError) Field() string { 62 | return fe.Ffield 63 | } 64 | 65 | func (fe fieldError) Struct() string { 66 | return fe.FstructName 67 | } 68 | 69 | func (fe fieldError) Message() string { 70 | return fe.Fmsg 71 | } 72 | 73 | // NewFieldError returns a newly created immutable FieldError 74 | // Tag is normally the rule that was invalid, but can be used for whatever 75 | // message you want. 76 | func NewFieldError(st, field, tag string, err error) FieldError { 77 | var msg string 78 | if err != nil { 79 | msg = err.Error() 80 | } 81 | return &fieldError{ 82 | FstructName: st, 83 | Ffield: field, 84 | Ftag: tag, 85 | Fmsg: msg, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/benchmark/types.go: -------------------------------------------------------------------------------- 1 | //go:generate ../../bin/gencheck -f=types.go 2 | 3 | package benchmark 4 | 5 | import "time" 6 | 7 | type TestString struct { 8 | Required string `valid:"required" validate:"required"` 9 | Len string `valid:"len=10" validate:"len=10"` 10 | Min string `valid:"min=5" validate:"min=5"` 11 | Max string `valid:"max=100" validate:"max=100"` 12 | } 13 | 14 | type SingleString struct { 15 | Entry string `valid:"required" validate:"required"` 16 | } 17 | 18 | type TestUUID struct { 19 | UUID string `valid:"required,uuid" validate:"required,uuid"` 20 | } 21 | 22 | type TestContainsAny struct { 23 | Any string `valid:"containsany=@#!" validate:"containsany=@#!"` 24 | } 25 | 26 | type TestHex struct { 27 | Value string `valid:"hex" validate:"hexadecimal"` 28 | } 29 | 30 | type TestMap struct { 31 | Value map[string]string `valid:"contains=test" validate:"contains=test"` //Playground doesn't really support this usecase. 32 | } 33 | 34 | type TestDive struct { 35 | Value *SingleString `valid:"required,dive" validate:"dive"` // Added required flag to have the same logic as playground 36 | } 37 | 38 | type TestAll struct { 39 | Required string `valid:"required" validate:"required"` 40 | Len string `valid:"len=10" validate:"len=10"` 41 | Min string `valid:"min=5" validate:"min=5"` 42 | Max string `valid:"max=100" validate:"max=100"` 43 | CIDR string `valid:"required,cidr" validate:"required,cidr"` 44 | LteTime time.Time `valid:"lte" validate:"lte"` 45 | GteTime time.Time `valid:"gte" validate:"gte"` 46 | Gte float64 `valid:"gte=1.2345" validate:"gte=1.2345"` 47 | NotNil *string `valid:"required" validate:"required"` 48 | Contains string `valid:"contains=fox" validate:"contains=fox"` 49 | Hex string `valid:"hex" validate:"hexadecimal"` 50 | UUID string `valid:"uuid" validate:"uuid"` 51 | MinInt int64 `valid:"min=12345" validate:"min=12345"` 52 | MaxInt int64 `valid:"max=12345" validate:"max=12345"` 53 | Dive *SingleString `valid:"required,dive" validate:"dive"` // Added required flag to have the same logic as playground 54 | URL string `valid:"url" validate:"url"` 55 | URI string `valid:"uri" validate:"uri"` 56 | } 57 | -------------------------------------------------------------------------------- /template/cidr.tmpl: -------------------------------------------------------------------------------- 1 | {{define "cidr"}} 2 | {{ if eq .FieldType "string" "*string" -}} 3 | {{if (isPtr . )}}if s.{{.FieldName}} != nil { {{end -}} 4 | _, _, {{.FieldName}}err := net.ParseCIDR({{ accessor . "s." }}) 5 | if {{.FieldName}}err != nil { 6 | {{ addError . (printf "%serr" .FieldName) }} 7 | } 8 | {{if (isPtr . )}} } {{end -}} 9 | {{ else if eq .FieldType "[]string" "[]*string" -}} 10 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 11 | _, _, err := net.ParseCIDR({{if eq .FieldType "[]string"}}&{{end}}single{{.FieldName}}) 12 | if err != nil { 13 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 14 | } 15 | } 16 | {{ else }} 17 | {{ generationError (printf "cidr is not valid on field '%s %s'" .FieldName .FieldType) }} 18 | {{end}} 19 | {{- end -}} 20 | 21 | {{define "cidrv4"}} 22 | {{ if eq .FieldType "string" "*string" -}} 23 | ip, _, {{.FieldName}}err := net.ParseCIDR({{ accessor . "s." }}) 24 | if {{.FieldName}}err != nil || ip.To4() == nil { 25 | {{ addError . (printf "%serr" .FieldName) }} 26 | } 27 | {{ else if eq .FieldType "[]string" "[]*string" -}} 28 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 29 | ip, _, err := net.ParseCIDR({{if eq .FieldType "[]*string"}}*{{end}}single{{.FieldName}}) 30 | if err != nil || ip.To4() == nil { 31 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 32 | } 33 | } 34 | {{ else }} 35 | {{ generationError (printf "cidrv4 is not valid on field '%s %s'" .FieldName .FieldType) }} 36 | {{end}} 37 | {{- end -}} 38 | 39 | {{define "cidrv6"}} 40 | {{ if eq .FieldType "string" "*string" -}} 41 | {{if (isPtr . )}}if s.{{.FieldName}} != nil { {{end -}} 42 | ip, _, {{.FieldName}}err := net.ParseCIDR({{ accessor . "s." }}) 43 | if {{.FieldName}}err != nil || ip.To4() != nil { 44 | {{ addError . (printf "%serr" .FieldName) }} 45 | } 46 | {{if (isPtr . )}} } {{end}} 47 | {{ else if eq .FieldType "[]string" "[]*string" -}} 48 | for index, single{{.FieldName}} := range s.{{.FieldName}} { 49 | ip, _, err := net.ParseCIDR({{if eq .FieldType "[]*string"}}*{{end}}single{{.FieldName}}) 50 | if err != nil || ip.To4() != nil { 51 | {{ addError . (printf `fmt.Errorf("%%s[%%d] - %%s", index, single%s, err)` .FieldName) }} 52 | } 53 | } 54 | {{ else }} 55 | {{ generationError (printf "cidrv6 is not valid on field '%s %s'" .FieldName .FieldType) }} 56 | {{end}} 57 | {{- end -}} 58 | -------------------------------------------------------------------------------- /generator/dive_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // DiveTestSuite 12 | type DiveTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *DiveTestSuite) SetupSuite() { 18 | } 19 | 20 | // TestDiveTestSuite 21 | func TestDiveTestSuite(t *testing.T) { 22 | suite.Run(t, new(DiveTestSuite)) 23 | } 24 | 25 | var diveErrorCases = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"bool", `Dive is not valid on field 'TestField bool'`}, 30 | {"*int", `Dive is not valid on field 'TestField *int'`}, 31 | //{"[]*string", `Dive is not valid on field 'TestField []*string'`}, //dive does not reject validating slice elements missing validators 32 | //{"map[string]int", `Dive is not valid on field 'TestField map[string]int'`}, //dive does not reject validating map elements missing validators 33 | } 34 | 35 | func (s *DiveTestSuite) TestErrorCases() { 36 | 37 | format := `package test 38 | // SomeStruct 39 | type SomeStruct struct { 40 | TestField %s %s 41 | }` 42 | for _, testCase := range diveErrorCases { 43 | 44 | g := NewGenerator() 45 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"dive\"`") 46 | 47 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 48 | s.Nil(err, "Error parsing input string") 49 | 50 | _, err = g.Generate(f) 51 | s.Require().NotNil(err, "Error for '%s' missing", testCase.inType) 52 | s.Contains(err.Error(), testCase.errorString) 53 | } 54 | } 55 | 56 | var diveSuccessCases = []struct { 57 | inType string 58 | fieldCheck string 59 | }{ 60 | {"SomeOtherStruct", `gencheck.Validate(s.TestField)`}, 61 | {"*SomeOtherStruct", `gencheck.Validate(s.TestField)`}, 62 | {"[]SomeOtherStruct", `gencheck.Validate(e)`}, 63 | {"[]*SomeOtherStruct", `gencheck.Validate(e)`}, 64 | {"map[string]SomeOtherStruct", `gencheck.Validate(e)`}, 65 | {"map[string]*SomeOtherStruct", `gencheck.Validate(e)`}, 66 | } 67 | 68 | func (s *DiveTestSuite) TestSuccessCases() { 69 | 70 | format := `package test 71 | // SomeOtherStruct 72 | type SomeOtherStruct struct{ 73 | RequiredField string %s 74 | } 75 | // SomeStruct 76 | type SomeStruct struct { 77 | TestField %s %s 78 | }` 79 | for _, testCase := range diveSuccessCases { 80 | 81 | g := NewGenerator() 82 | input := fmt.Sprintf(format, "`valid:\"required\"`", testCase.inType, "`valid:\"dive\"`") 83 | 84 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 85 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 86 | 87 | output, err := g.Generate(f) 88 | s.Nil(err, "Error generating code for input string") 89 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /generator/other_pkg_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var otherPkgDiveTests = []struct { 12 | inType string 13 | fieldCheck string 14 | }{ 15 | {"example.Inner", `gencheck.Validate(s.TestField)`}, 16 | {"*example.Inner", `gencheck.Validate(s.TestField)`}, 17 | {"example.IFace", `gencheck.Validate(s.TestField)`}, 18 | } 19 | 20 | func TestIsFuncSelector(t *testing.T) { 21 | format := `package test 22 | 23 | // SomeStruct 24 | type SomeStruct struct { 25 | TestField %s %s 26 | }` 27 | for _, testCase := range otherPkgDiveTests { 28 | 29 | g := NewGenerator() 30 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"dive\"`") 31 | 32 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 33 | assert.Nil(t, err, "Error parsing input string for type '%s'", testCase.inType) 34 | 35 | output, err := g.Generate(f) 36 | assert.Nil(t, err, "Error generating code for input string") 37 | // fmt.Println(string(output)) 38 | assert.Contains(t, string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 39 | } 40 | } 41 | 42 | var mapContainsTests = []struct { 43 | inType string 44 | validTag string 45 | fieldCheck string 46 | }{ 47 | {"map[string]interface{}", "`valid:\"contains=myKey\"`", `s.MapOfSomething["myKey"]; !foundMapOfSomething`}, 48 | {"map[int]interface{}", "`valid:\"contains=10\"`", `s.MapOfSomething[10]; !foundMapOfSomething`}, 49 | } 50 | 51 | func TestMapContains(t *testing.T) { 52 | format := `package test 53 | 54 | // SomeStruct 55 | type SomeStruct struct { 56 | MapOfSomething %s %s 57 | }` 58 | for _, testCase := range mapContainsTests { 59 | 60 | g := NewGenerator() 61 | input := fmt.Sprintf(format, testCase.inType, testCase.validTag) 62 | 63 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 64 | assert.Nil(t, err, "Error parsing input string for type '%s'", testCase.inType) 65 | 66 | output, err := g.Generate(f) 67 | assert.Nil(t, err, "Error generating code for input string") 68 | // fmt.Println(string(output)) 69 | assert.Contains(t, string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 70 | } 71 | } 72 | 73 | var failedMapContainsTests = []struct { 74 | inType string 75 | validTag string 76 | errorString string 77 | }{ 78 | {"map[int]interface{}", "`valid:\"contains=myKey\"`", `cannot use a non integer value on an integer keyed map`}, 79 | } 80 | 81 | func TestFailedMapContains(t *testing.T) { 82 | format := `package test 83 | 84 | // SomeStruct 85 | type SomeStruct struct { 86 | MapOfSomething %s %s 87 | }` 88 | for _, testCase := range failedMapContainsTests { 89 | 90 | g := NewGenerator() 91 | input := fmt.Sprintf(format, testCase.inType, testCase.validTag) 92 | 93 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 94 | assert.Nil(t, err, "Error parsing input string for type '%s'", testCase.inType) 95 | 96 | _, err = g.Generate(f) 97 | assert.NotNil(t, err, "Error on required int missing") 98 | assert.Contains(t, err.Error(), testCase.errorString) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package gencheck 2 | 3 | // 4 | // import ( 5 | // "encoding/json" 6 | // "errors" 7 | // "testing" 8 | // 9 | // "github.com/stretchr/testify/suite" 10 | // ) 11 | // 12 | // // NoValidate 13 | // type NoValidate struct{} 14 | // 15 | // // HasValidate 16 | // type HasValidate struct{} 17 | // 18 | // // Validate 19 | // func (s HasValidate) Validate() error { 20 | // return errors.New("Validating 'HasValidate' instance") 21 | // } 22 | // 23 | // // gencheckErrorsTestSuite 24 | // type gencheckErrorsTestSuite struct { 25 | // suite.Suite 26 | // } 27 | // 28 | // // TestgencheckTypesTestSuite 29 | // func TestgencheckTypesTestSuite(t *testing.T) { 30 | // suite.Run(t, new(gencheckErrorsTestSuite)) 31 | // } 32 | // 33 | // // TestValidate 34 | // func (s *gencheckErrorsTestSuite) TestValidate() { 35 | // a := HasValidate{} 36 | // b := &HasValidate{} 37 | // c := NoValidate{} 38 | // 39 | // err := Validate(a) 40 | // s.Equal(errors.New("Validating 'HasValidate' instance"), err) 41 | // 42 | // err = Validate(b) 43 | // s.Equal(errors.New("Validating 'HasValidate' instance"), err) 44 | // 45 | // err = Validate(c) 46 | // s.Nil(err) 47 | // } 48 | // 49 | // // TestErrorSliceError_Empty 50 | // func (s *gencheckErrorsTestSuite) TestErrorSliceError_Empty() { 51 | // ea := gencheck.ErrorSlice{} 52 | // 53 | // s.Equal("[]", ea.Error()) 54 | // } 55 | // 56 | // // TestErrorSliceError_MultiElements 57 | // func (s *gencheckErrorsTestSuite) TestErrorSliceError_MultiElements() { 58 | // ea := gencheck.ErrorSlice{ 59 | // errors.New("foo"), 60 | // errors.New("bar"), 61 | // nil, 62 | // gencheck.ErrorSlice{errors.New("this is"), errors.New("nested")}, 63 | // } 64 | // 65 | // s.Equal("[\"foo\",\"bar\",null,[\"this is\",\"nested\"]]", ea.Error()) 66 | // } 67 | // 68 | // // TestErrorMapError_Empty 69 | // func (s *gencheckErrorsTestSuite) TestErrorMapError_Empty() { 70 | // em := gencheck.ErrorMap{} 71 | // s.Equal("{}", em.Error()) 72 | // } 73 | // 74 | // // TestErrorMapError_NilValue 75 | // func (s *gencheckErrorsTestSuite) TestErrorMapError_NilValue() { 76 | // em := gencheck.ErrorMap{ 77 | // "flat": nil, 78 | // "nestedErrorSlice": gencheck.ErrorSlice{errors.New("this is"), errors.New("nested")}, 79 | // "nestedEmptyErrorMap": make(gencheck.ErrorMap), 80 | // } 81 | // 82 | // expectedJSONAsMap := make(map[string]interface{}) 83 | // actualJSONAsMap := make(map[string]interface{}) 84 | // json.Unmarshal([]byte(`{"flat": null,"nestedErrorSlice": ["this is","nested"],"nestedEmptyErrorMap": {}}`), &expectedJSONAsMap) 85 | // json.Unmarshal([]byte(em.Error()), &actualJSONAsMap) 86 | // 87 | // s.Equal(expectedJSONAsMap, actualJSONAsMap) 88 | // } 89 | // 90 | // // TestErrorMapError_MultipleValues 91 | // func (s *gencheckErrorsTestSuite) TestErrorMapError_MultipleValues() { 92 | // em := gencheck.ErrorMap{ 93 | // "flat": errors.New(`"flat" "error"`), 94 | // "nestedErrorSlice": gencheck.ErrorSlice{errors.New("this is"), errors.New("nested")}, 95 | // "nestedEmptyErrorMap": make(gencheck.ErrorMap), 96 | // } 97 | // 98 | // expectedJSONAsMap := make(map[string]interface{}) 99 | // actualJSONAsMap := make(map[string]interface{}) 100 | // json.Unmarshal([]byte(`{"flat": "\"flat\" \"error\"","nestedErrorSlice": ["this is","nested"],"nestedEmptyErrorMap": {}}`), &expectedJSONAsMap) 101 | // json.Unmarshal([]byte(em.Error()), &actualJSONAsMap) 102 | // 103 | // s.Equal(expectedJSONAsMap, actualJSONAsMap) 104 | // } 105 | -------------------------------------------------------------------------------- /gencheck/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/abice/gencheck/generator" 11 | "github.com/mkideal/cli" 12 | ) 13 | 14 | type rootT struct { 15 | cli.Helper 16 | FileNames []string `cli:"*f,file" usage:"The file(s) to generate validations for. Use more than one flag for more files."` 17 | SupportFiles []string `cli:"supp" usage:"Any supplemental files needed for parsing. Use more than one flag for more files."` 18 | PointerReceiver bool `cli:"ptr" usage:"turn on pointer receiver generation for validation method." dft:"false"` 19 | CustomTemplates []string `cli:"t,template" usage:"custom template files"` 20 | TemplateDirs []string `cli:"d,template-dir" usage:"custom template folders"` 21 | FailFast bool `cli:"failfast" usage:"Tell the generator to fail all structs fast"` 22 | NoPrealloc bool `cli:"noprealloc" usage:"Tell the generator to not preallocate a buffer for errors and let the array grow as needed."` 23 | } 24 | 25 | func main() { 26 | cli.Run(new(rootT), func(ctx *cli.Context) error { 27 | argv := ctx.Argv().(*rootT) 28 | g := generator.NewGenerator() 29 | 30 | if argv.PointerReceiver { 31 | g.WithPointerMethod() 32 | } 33 | 34 | if argv.FailFast { 35 | g.WithFailFast() 36 | } 37 | 38 | if argv.NoPrealloc { 39 | g.WithoutPrealloc() 40 | } 41 | 42 | if len(argv.CustomTemplates) > 0 { 43 | ctx.String("Adding templates=%s\n", ctx.Color().Grey(argv.CustomTemplates)) 44 | 45 | // Add them one by one, because it's really just a warning if we can't load one of their templates. 46 | for _, templ := range argv.CustomTemplates { 47 | err := g.AddTemplateFiles(argv.CustomTemplates...) 48 | if err != nil { 49 | ctx.String("Unable to add template '%s': %s\n", ctx.Color().Cyan(templ), ctx.Color().Yellow(err)) 50 | } 51 | } 52 | } 53 | 54 | for _, dir := range argv.TemplateDirs { 55 | ctx.String("Scanning templates in %s\n", ctx.Color().Grey(dir)) 56 | 57 | // Walk the directory and add each file one by one. 58 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 59 | if !info.IsDir() { 60 | ctx.String("Adding template %s\n", ctx.Color().Grey(path)) 61 | err = g.AddTemplateFiles(path) 62 | if err != nil { 63 | ctx.String("Unable to add template '%s': %s\n", ctx.Color().Cyan(path), ctx.Color().Yellow(err)) 64 | } 65 | } 66 | return err 67 | }) 68 | } 69 | 70 | for _, fileName := range argv.SupportFiles { 71 | supportFileName, _ := filepath.Abs(fileName) 72 | g.AddSupportFiles(supportFileName) 73 | } 74 | 75 | for _, fileName := range argv.FileNames { 76 | 77 | originalName := fileName 78 | 79 | ctx.String("gencheck started. file: %s\n", ctx.Color().Cyan(originalName)) 80 | fileName, _ = filepath.Abs(fileName) 81 | outFilePath := fmt.Sprintf("%s_validators.go", strings.TrimSuffix(fileName, filepath.Ext(fileName))) 82 | 83 | // Parse the file given in arguments 84 | raw, err := g.GenerateFromFile(fileName) 85 | if err != nil { 86 | return fmt.Errorf("Error while generating validators\nInputFile=%s\nError=%s\n", ctx.Color().Cyan(fileName), ctx.Color().RedBg(err)) 87 | } 88 | 89 | err = ioutil.WriteFile(outFilePath, raw, os.ModePerm) 90 | if err != nil { 91 | return fmt.Errorf("Error while writing to file %s: %s\n", ctx.Color().Cyan(outFilePath), ctx.Color().Red(err)) 92 | } 93 | ctx.String("gencheck finished. file: %s\n", ctx.Color().Cyan(originalName)) 94 | } 95 | 96 | return nil 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /generator/len_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // LenTestSuite 12 | type LenTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *LenTestSuite) SetupSuite() { 18 | } 19 | 20 | // TestLengthTestSuite 21 | func TestLenTestSuite(t *testing.T) { 22 | suite.Run(t, new(LenTestSuite)) 23 | } 24 | 25 | var lenErrorCases = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"bool", `len is not valid on field 'TestField bool'`}, 30 | {"func()()", `len is not valid on field 'TestField func()'`}, 31 | {"*int", `len is not valid on field 'TestField *int'`}, 32 | {"*string", `len is not valid on field 'TestField *string'`}, 33 | {"SomeInterface", `len is not valid on field 'TestField SomeInterface'`}, 34 | } 35 | 36 | func (s *LenTestSuite) TestErrorCases() { 37 | 38 | format := `package test 39 | // SomeStruct 40 | type SomeStruct struct { 41 | TestField %s %s 42 | }` 43 | for _, testCase := range lenErrorCases { 44 | 45 | g := NewGenerator() 46 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"len=12\"`") 47 | 48 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 49 | s.Nil(err, "Error parsing input string") 50 | 51 | _, err = g.Generate(f) 52 | s.Require().NotNil(err, "Error for '%s' missing", testCase.inType) 53 | s.Contains(err.Error(), testCase.errorString) 54 | } 55 | } 56 | 57 | var lenSuccessCases = []struct { 58 | inType string 59 | fieldCheck string 60 | }{ 61 | {"int", `!(s.TestField == 12)`}, 62 | {"int8", `!(s.TestField == 12)`}, 63 | {"int16", `!(s.TestField == 12)`}, 64 | {"int32", `!(s.TestField == 12)`}, 65 | {"int64", `!(s.TestField == 12)`}, 66 | {"uint", `!(s.TestField == 12)`}, 67 | {"uint8", `!(s.TestField == 12)`}, 68 | {"uint16", `!(s.TestField == 12)`}, 69 | {"uint32", `!(s.TestField == 12)`}, 70 | {"uint64", `!(s.TestField == 12)`}, 71 | {"float", `!(s.TestField == 12)`}, 72 | {"float32", `!(s.TestField == 12)`}, 73 | {"float64", `!(s.TestField == 12)`}, 74 | {"complex64", `!(s.TestField == 12)`}, 75 | {"complex128", `!(s.TestField == 12)`}, 76 | {"string", `!(len(s.TestField) == 12)`}, 77 | {"chan int", `!(len(s.TestField) == 12)`}, 78 | {"[]string", `!(len(s.TestField) == 12)`}, 79 | {"map[string]string", `!(len(s.TestField) == 12)`}, 80 | {"[]SomeOtherStruct", `!(len(s.TestField) == 12)`}, 81 | {"[]SomeInterface", `!(len(s.TestField) == 12)`}, 82 | } 83 | 84 | // TestRequiredFields will cycle through the test cases for successful calls 85 | // to the required template and validate that the correct validation has been produced. 86 | func (s *LenTestSuite) TestSuccessCases() { 87 | 88 | format := `package test 89 | // SomeInterface 90 | type SomeInterface interface{ 91 | 92 | } 93 | // SomeOtherStruct 94 | type SomeOtherStruct struct{ 95 | 96 | } 97 | // SomeStruct 98 | type SomeStruct struct { 99 | TestField %s %s 100 | }` 101 | for _, testCase := range lenSuccessCases { 102 | 103 | g := NewGenerator() 104 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"len=12\"`") 105 | 106 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 107 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 108 | 109 | output, err := g.Generate(f) 110 | s.Nil(err, "Error generating code for input string") 111 | // fmt.Println(string(output)) 112 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /generator/bcp47_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // BCP47TestSuite 12 | type BCP47TestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *BCP47TestSuite) SetupSuite() { 18 | } 19 | 20 | // TestLengthTestSuite 21 | func TestBCP47TestSuite(t *testing.T) { 22 | suite.Run(t, new(BCP47TestSuite)) 23 | } 24 | 25 | var BCP47ErrorCases = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"int", `bcp47 is not valid on field 'TestField int'`}, 30 | {"int8", `bcp47 is not valid on field 'TestField int8'`}, 31 | {"int16", `bcp47 is not valid on field 'TestField int16'`}, 32 | {"int32", `bcp47 is not valid on field 'TestField int32'`}, 33 | {"int64", `bcp47 is not valid on field 'TestField int64'`}, 34 | {"uint", `bcp47 is not valid on field 'TestField uint'`}, 35 | {"uint8", `bcp47 is not valid on field 'TestField uint8'`}, 36 | {"uint16", `bcp47 is not valid on field 'TestField uint16'`}, 37 | {"uint32", `bcp47 is not valid on field 'TestField uint32'`}, 38 | {"uint64", `bcp47 is not valid on field 'TestField uint64'`}, 39 | {"bool", `bcp47 is not valid on field 'TestField bool'`}, 40 | {"float", `bcp47 is not valid on field 'TestField float'`}, 41 | {"float32", `bcp47 is not valid on field 'TestField float32'`}, 42 | {"float64", `bcp47 is not valid on field 'TestField float64'`}, 43 | {"complex64", `bcp47 is not valid on field 'TestField complex64'`}, 44 | {"complex128", `bcp47 is not valid on field 'TestField complex128'`}, 45 | } 46 | 47 | func (s *BCP47TestSuite) TestErrorCases() { 48 | 49 | format := `package test 50 | // SomeStruct 51 | type SomeStruct struct { 52 | TestField %s %s 53 | }` 54 | for _, testCase := range BCP47ErrorCases { 55 | 56 | g := NewGenerator() 57 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"bcp47\"`") 58 | 59 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 60 | s.Nil(err, "Error parsing input string") 61 | 62 | _, err = g.Generate(f) 63 | s.NotNil(err, "Error for '%s' missing", testCase.inType) 64 | if err != nil { 65 | s.Contains(err.Error(), testCase.errorString) 66 | } 67 | } 68 | } 69 | 70 | var BCP47SuccessCases = []struct { 71 | inType string 72 | fieldCheck string 73 | }{ 74 | {"*string", `gencheck.IsBCP47(s.TestField); err != nil`}, 75 | {"string", `gencheck.IsBCP47(&s.TestField); err != nil`}, 76 | {"[]string", `gencheck.IsBCP47(&singleTestField); err != nil`}, 77 | {"[]*string", `gencheck.IsBCP47(singleTestField); err != nil`}, 78 | } 79 | 80 | // TestRequiredFields will cycle through the test cases for successful calls 81 | // to the required template and validate that the correct validation has been produced. 82 | func (s *BCP47TestSuite) TestSuccessCases() { 83 | 84 | format := `package test 85 | // SomeInterface 86 | type SomeInterface interface{ 87 | 88 | } 89 | // SomeOtherStruct 90 | type SomeOtherStruct struct{ 91 | 92 | } 93 | // SomeStruct 94 | type SomeStruct struct { 95 | TestField %s %s 96 | }` 97 | for _, testCase := range BCP47SuccessCases { 98 | 99 | g := NewGenerator() 100 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"bcp47\"`") 101 | 102 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 103 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 104 | 105 | output, err := g.Generate(f) 106 | s.Nil(err, "Error generating code for input string") 107 | // fmt.Println(string(output)) 108 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /generator/required_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // RequiredTestSuite 12 | type RequiredTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *RequiredTestSuite) SetupSuite() { 18 | } 19 | 20 | // TestLengthTestSuite 21 | func TestRequiredTestSuite(t *testing.T) { 22 | suite.Run(t, new(RequiredTestSuite)) 23 | } 24 | 25 | var requiredErroringTests = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"int", `Required on integer values is not supported.`}, 30 | {"int8", `Required on integer values is not supported.`}, 31 | {"int16", `Required on integer values is not supported.`}, 32 | {"int32", `Required on integer values is not supported.`}, 33 | {"int64", `Required on integer values is not supported.`}, 34 | {"uint", `Required on integer values is not supported.`}, 35 | {"uint8", `Required on integer values is not supported.`}, 36 | {"uint16", `Required on integer values is not supported.`}, 37 | {"uint32", `Required on integer values is not supported.`}, 38 | {"uint64", `Required on integer values is not supported.`}, 39 | {"bool", `Required on boolean values is not supported.`}, 40 | {"float", `Required on numerical values is not supported.`}, 41 | {"float32", `Required on numerical values is not supported.`}, 42 | {"float64", `Required on numerical values is not supported.`}, 43 | {"complex64", `Required on numerical values is not supported.`}, 44 | {"complex128", `Required on numerical values is not supported.`}, 45 | } 46 | 47 | func (s *RequiredTestSuite) TestRequiredErrors() { 48 | 49 | format := `package test 50 | // SomeStruct 51 | type SomeStruct struct { 52 | Test%s %s %s 53 | }` 54 | for _, testCase := range requiredErroringTests { 55 | 56 | g := NewGenerator() 57 | input := fmt.Sprintf(format, testCase.inType, testCase.inType, "`valid:\"required\"`") 58 | 59 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 60 | s.Nil(err, "Error parsing input string") 61 | 62 | _, err = g.Generate(f) 63 | s.NotNil(err, "Error on required int missing") 64 | s.Contains(err.Error(), testCase.errorString) 65 | } 66 | } 67 | 68 | var requiredSuccessTests = []struct { 69 | inType string 70 | fieldCheck string 71 | }{ 72 | {"chan int", `s.TestField == nil`}, 73 | {"func()()", `s.TestField == nil`}, 74 | {"*int", `s.TestField == nil`}, 75 | {"*string", `s.TestField == nil`}, 76 | {"SomeInterface", `s.TestField == nil`}, 77 | {"[]string", `s.TestField == nil`}, 78 | {"map[string]string", `s.TestField == nil`}, 79 | {"string", `s.TestField == ""`}, 80 | {"SomeOtherStruct", `s.TestField == zeroTestField`}, 81 | } 82 | 83 | // TestRequiredFields will cycle through the test cases for successful calls 84 | // to the required template and validate that the correct validation has been produced. 85 | func (s *RequiredTestSuite) TestRequiredFields() { 86 | 87 | format := `package test 88 | // SomeInterface 89 | type SomeInterface interface{ 90 | 91 | } 92 | // SomeOtherStruct 93 | type SomeOtherStruct struct{ 94 | 95 | } 96 | // SomeStruct 97 | type SomeStruct struct { 98 | TestField %s %s 99 | }` 100 | for _, testCase := range requiredSuccessTests { 101 | 102 | g := NewGenerator() 103 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"required\"`") 104 | 105 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 106 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 107 | 108 | output, err := g.Generate(f) 109 | s.Nil(err, "Error generating code for input string") 110 | // fmt.Println(string(output)) 111 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /generator/hex_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // HexTestSuite 12 | type HexTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *HexTestSuite) SetupSuite() { 18 | } 19 | 20 | // TestLengthTestSuite 21 | func TestHexTestSuite(t *testing.T) { 22 | suite.Run(t, new(HexTestSuite)) 23 | } 24 | 25 | var HexErrorCases = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"int", `hex is not valid on field 'TestField int'`}, 30 | {"int8", `hex is not valid on field 'TestField int8'`}, 31 | {"int16", `hex is not valid on field 'TestField int16'`}, 32 | {"int32", `hex is not valid on field 'TestField int32'`}, 33 | {"int64", `hex is not valid on field 'TestField int64'`}, 34 | {"uint", `hex is not valid on field 'TestField uint'`}, 35 | {"uint8", `hex is not valid on field 'TestField uint8'`}, 36 | {"uint16", `hex is not valid on field 'TestField uint16'`}, 37 | {"uint32", `hex is not valid on field 'TestField uint32'`}, 38 | {"uint64", `hex is not valid on field 'TestField uint64'`}, 39 | {"bool", `hex is not valid on field 'TestField bool'`}, 40 | {"float", `hex is not valid on field 'TestField float'`}, 41 | {"float32", `hex is not valid on field 'TestField float32'`}, 42 | {"float64", `hex is not valid on field 'TestField float64'`}, 43 | {"complex64", `hex is not valid on field 'TestField complex64'`}, 44 | {"complex128", `hex is not valid on field 'TestField complex128'`}, 45 | {"SomeOtherStruct", `hex is not valid on field 'TestField SomeOtherStruct'`}, 46 | {"*SomeOtherStruct", `hex is not valid on field 'TestField *SomeOtherStruct'`}, 47 | {"SomeInterface", `hex is not valid on field 'TestField SomeInterface'`}, 48 | } 49 | 50 | func (s *HexTestSuite) TestErrorCases() { 51 | 52 | format := `package test 53 | // SomeStruct 54 | type SomeStruct struct { 55 | TestField %s %s 56 | }` 57 | for _, testCase := range HexErrorCases { 58 | 59 | g := NewGenerator() 60 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"hex\"`") 61 | 62 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 63 | s.Nil(err, "Error parsing input string") 64 | 65 | _, err = g.Generate(f) 66 | s.NotNil(err, "Error for '%s' missing", testCase.inType) 67 | if err != nil { 68 | s.Contains(err.Error(), testCase.errorString) 69 | } 70 | } 71 | } 72 | 73 | var HexSuccessCases = []struct { 74 | inType string 75 | fieldCheck string 76 | }{ 77 | {"*string", `gencheck.IsHex(s.TestField); err != nil`}, 78 | {"string", `gencheck.IsHex(&s.TestField); err != nil`}, 79 | {"[]string", `gencheck.IsHex(&singleTestField); err != nil`}, 80 | {"[]*string", `gencheck.IsHex(singleTestField); err != nil`}, 81 | } 82 | 83 | // TestRequiredFields will cycle through the test cases for successful calls 84 | // to the required template and validate that the correct validation has been produced. 85 | func (s *HexTestSuite) TestSuccessCases() { 86 | 87 | format := `package test 88 | // SomeInterface 89 | type SomeInterface interface{ 90 | 91 | } 92 | // SomeOtherStruct 93 | type SomeOtherStruct struct{ 94 | 95 | } 96 | // SomeStruct 97 | type SomeStruct struct { 98 | TestField %s %s 99 | }` 100 | for _, testCase := range HexSuccessCases { 101 | 102 | g := NewGenerator() 103 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"hex\"`") 104 | 105 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 106 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 107 | 108 | output, err := g.Generate(f) 109 | s.Nil(err, "Error generating code for input string") 110 | // fmt.Println(string(output)) 111 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /generator/uuid_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // UUIDTestSuite 12 | type UUIDTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *UUIDTestSuite) SetupSuite() { 18 | } 19 | 20 | // TestLengthTestSuite 21 | func TestUUIDTestSuite(t *testing.T) { 22 | suite.Run(t, new(UUIDTestSuite)) 23 | } 24 | 25 | var UUIDErrorCases = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"int", `uuid is not valid on field 'TestField int'`}, 30 | {"int8", `uuid is not valid on field 'TestField int8'`}, 31 | {"int16", `uuid is not valid on field 'TestField int16'`}, 32 | {"int32", `uuid is not valid on field 'TestField int32'`}, 33 | {"int64", `uuid is not valid on field 'TestField int64'`}, 34 | {"uint", `uuid is not valid on field 'TestField uint'`}, 35 | {"uint8", `uuid is not valid on field 'TestField uint8'`}, 36 | {"uint16", `uuid is not valid on field 'TestField uint16'`}, 37 | {"uint32", `uuid is not valid on field 'TestField uint32'`}, 38 | {"uint64", `uuid is not valid on field 'TestField uint64'`}, 39 | {"bool", `uuid is not valid on field 'TestField bool'`}, 40 | {"float", `uuid is not valid on field 'TestField float'`}, 41 | {"float32", `uuid is not valid on field 'TestField float32'`}, 42 | {"float64", `uuid is not valid on field 'TestField float64'`}, 43 | {"complex64", `uuid is not valid on field 'TestField complex64'`}, 44 | {"complex128", `uuid is not valid on field 'TestField complex128'`}, 45 | {"SomeOtherStruct", `uuid is not valid on field 'TestField SomeOtherStruct'`}, 46 | {"*SomeOtherStruct", `uuid is not valid on field 'TestField *SomeOtherStruct'`}, 47 | {"SomeInterface", `uuid is not valid on field 'TestField SomeInterface'`}, 48 | } 49 | 50 | func (s *UUIDTestSuite) TestErrorCases() { 51 | 52 | format := `package test 53 | // SomeStruct 54 | type SomeStruct struct { 55 | TestField %s %s 56 | }` 57 | for _, testCase := range UUIDErrorCases { 58 | 59 | g := NewGenerator() 60 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"uuid\"`") 61 | 62 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 63 | s.Nil(err, "Error parsing input string") 64 | 65 | _, err = g.Generate(f) 66 | s.NotNil(err, "Error for '%s' missing", testCase.inType) 67 | if err != nil { 68 | s.Contains(err.Error(), testCase.errorString) 69 | } 70 | } 71 | } 72 | 73 | var UUIDSuccessCases = []struct { 74 | inType string 75 | fieldCheck string 76 | }{ 77 | {"*string", `gencheck.IsUUID(s.TestField); err != nil`}, 78 | {"string", `gencheck.IsUUID(&s.TestField); err != nil`}, 79 | {"[]string", `gencheck.IsUUID(&singleTestField); err != nil`}, 80 | {"[]*string", `gencheck.IsUUID(singleTestField); err != nil`}, 81 | } 82 | 83 | // TestRequiredFields will cycle through the test cases for successful calls 84 | // to the required template and validate that the correct validation has been produced. 85 | func (s *UUIDTestSuite) TestSuccessCases() { 86 | 87 | format := `package test 88 | // SomeInterface 89 | type SomeInterface interface{ 90 | 91 | } 92 | // SomeOtherStruct 93 | type SomeOtherStruct struct{ 94 | 95 | } 96 | // SomeStruct 97 | type SomeStruct struct { 98 | TestField %s %s 99 | }` 100 | for _, testCase := range UUIDSuccessCases { 101 | 102 | g := NewGenerator() 103 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"uuid\"`") 104 | 105 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 106 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 107 | 108 | output, err := g.Generate(f) 109 | s.Nil(err, "Error generating code for input string") 110 | // fmt.Println(string(output)) 111 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /generator/notnil_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | // notnilTestSuite 12 | type notnilTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | // SetupSuite 17 | func (s *notnilTestSuite) SetupSuite() { 18 | } 19 | 20 | // TestLengthTestSuite 21 | func TestNotnilTestSuite(t *testing.T) { 22 | suite.Run(t, new(notnilTestSuite)) 23 | } 24 | 25 | var notnilErrorCases = []struct { 26 | inType string 27 | errorString string 28 | }{ 29 | {"int", `notnil is not valid on non nullable field 'TestField int'`}, 30 | {"int8", `notnil is not valid on non nullable field 'TestField int8'`}, 31 | {"int16", `notnil is not valid on non nullable field 'TestField int16'`}, 32 | {"int32", `notnil is not valid on non nullable field 'TestField int32'`}, 33 | {"int64", `notnil is not valid on non nullable field 'TestField int64'`}, 34 | {"uint", `notnil is not valid on non nullable field 'TestField uint'`}, 35 | {"uint8", `notnil is not valid on non nullable field 'TestField uint8'`}, 36 | {"uint16", `notnil is not valid on non nullable field 'TestField uint16'`}, 37 | {"uint32", `notnil is not valid on non nullable field 'TestField uint32'`}, 38 | {"uint64", `notnil is not valid on non nullable field 'TestField uint64'`}, 39 | {"bool", `notnil is not valid on non nullable field 'TestField bool'`}, 40 | {"float", `notnil is not valid on non nullable field 'TestField float'`}, 41 | {"float32", `notnil is not valid on non nullable field 'TestField float32'`}, 42 | {"float64", `notnil is not valid on non nullable field 'TestField float64'`}, 43 | {"complex64", `notnil is not valid on non nullable field 'TestField complex64'`}, 44 | {"complex128", `notnil is not valid on non nullable field 'TestField complex128'`}, 45 | {"string", `notnil is not valid on non nullable field 'TestField string'`}, 46 | } 47 | 48 | func (s *notnilTestSuite) TestErrorCases() { 49 | 50 | format := `package test 51 | // SomeStruct 52 | type SomeStruct struct { 53 | TestField %s %s 54 | }` 55 | for _, testCase := range notnilErrorCases { 56 | 57 | g := NewGenerator() 58 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"notnil\"`") 59 | 60 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 61 | s.Nil(err, "Error parsing input string") 62 | 63 | _, err = g.Generate(f) 64 | s.NotNil(err, "Error on required int missing") 65 | if err != nil { 66 | s.Contains(err.Error(), testCase.errorString) 67 | } 68 | } 69 | } 70 | 71 | var notnilSuccessCases = []struct { 72 | inType string 73 | fieldCheck string 74 | }{ 75 | {"chan int", `s.TestField == nil`}, 76 | {"func()()", `s.TestField == nil`}, 77 | {"*int", `s.TestField == nil`}, 78 | {"*string", `s.TestField == nil`}, 79 | {"SomeInterface", `s.TestField == nil`}, 80 | {"[]string", `s.TestField == nil`}, 81 | {"map[string]string", `s.TestField == nil`}, 82 | {"*SomeOtherStruct", `s.TestField == nil`}, 83 | } 84 | 85 | // TestRequiredFields will cycle through the test cases for successful calls 86 | // to the required template and validate that the correct validation has been produced. 87 | func (s *notnilTestSuite) TestSuccessCases() { 88 | 89 | format := `package test 90 | // SomeInterface 91 | type SomeInterface interface{ 92 | 93 | } 94 | // SomeOtherStruct 95 | type SomeOtherStruct struct{ 96 | 97 | } 98 | // SomeStruct 99 | type SomeStruct struct { 100 | TestField %s %s 101 | }` 102 | for _, testCase := range notnilSuccessCases { 103 | 104 | g := NewGenerator() 105 | input := fmt.Sprintf(format, testCase.inType, "`valid:\"notnil\"`") 106 | 107 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 108 | s.Nil(err, "Error parsing input string for type '%s'", testCase.inType) 109 | 110 | output, err := g.Generate(f) 111 | s.Nil(err, "Error generating code for input string") 112 | // fmt.Println(string(output)) 113 | s.Contains(string(output), testCase.fieldCheck, "RequiredField Type='%s' ExpectedOutput='%s'", testCase.inType, testCase.fieldCheck) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /generator/template_funcs.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | errorFormat = `vErrors = append(vErrors, gencheck.NewFieldError("%s","%s","%s",%s))` 13 | failFastErrorFormat = `return append(vErrors, gencheck.NewFieldError("%s","%s","%s",%s))` 14 | indexedErrorFormat = `vErrors = append(vErrors, gencheck.NewFieldError("%s",fmt.Sprintf("%s[%%v]", %s),"%s",%s))` 15 | indexedFailFastErrorFormat = `return append(vErrors, gencheck.NewFieldError("%s",fmt.Sprintf("%s[%%v]", %s),"%s",%s))` 16 | ) 17 | 18 | // addFieldError is a helper method for templates to add an error to a field. 19 | func addFieldError(v Validation, eString string) (ret string, err error) { 20 | if v.FailFast { 21 | ret = fmt.Sprintf(failFastErrorFormat, v.StructName, v.FieldName, v.Name, eString) 22 | } else { 23 | ret = fmt.Sprintf(errorFormat, v.StructName, v.FieldName, v.Name, eString) 24 | } 25 | return 26 | } 27 | 28 | // addIndexedFieldError is a helper method for templates to add an indexed error to a field. 29 | func addIndexedFieldError(v Validation, fieldIndex, eString string) (ret string, err error) { 30 | if v.FailFast { 31 | ret = fmt.Sprintf(indexedFailFastErrorFormat, v.StructName, v.FieldName, fieldIndex, v.Name, eString) 32 | } else { 33 | ret = fmt.Sprintf(indexedErrorFormat, v.StructName, v.FieldName, fieldIndex, v.Name, eString) 34 | } 35 | return 36 | } 37 | 38 | // accessor is a helper method for templates to use to remove a lot of `if isPtr *` 39 | func accessor(f Validation, prefix string) (string, error) { 40 | isPtr, err := isPtr(f) 41 | if err != nil || !isPtr { 42 | return fmt.Sprintf("%s%s", prefix, f.FieldName), nil 43 | } 44 | return fmt.Sprintf("*%s%s", prefix, f.FieldName), nil 45 | } 46 | 47 | // IsPtr is a helper method for templates to use to determine if a field is a pointer. 48 | func isPtr(f Validation) (ret bool, err error) { 49 | ret = false 50 | field := f.F 51 | // fmt.Printf("ast.Field: %#v\n", field.Type) 52 | _, ret = field.Type.(*ast.StarExpr) 53 | return 54 | } 55 | 56 | // IsNullable is a helper method for templates to use to determine if a field is a nullable 57 | func isNullable(f Validation) (ret bool, err error) { 58 | ret = false 59 | field := f.F 60 | fType := field.Type 61 | if _, ok := fType.(*ast.Ident); ok { 62 | fType = getIdentType(fType.(*ast.Ident)) 63 | } 64 | 65 | // fmt.Printf("IsNullable: %s=>%#v\n", f.FieldType, fType) 66 | switch fType.(type) { 67 | case *ast.StarExpr: 68 | ret = true 69 | case *ast.MapType: 70 | ret = true 71 | case *ast.ArrayType: 72 | ret = true 73 | case *ast.ChanType: 74 | ret = true 75 | case *ast.FuncType: 76 | ret = true 77 | case *ast.InterfaceType: 78 | ret = true 79 | } 80 | return 81 | } 82 | 83 | // isArray is a helper method for templates to determine if the field is an array 84 | func isArray(f Validation) (ret bool, err error) { 85 | ret = false 86 | field := f.F 87 | switch field.Type.(type) { 88 | case *ast.ArrayType: 89 | ret = true 90 | } 91 | return 92 | } 93 | 94 | // isMap is a helper method for templates to determine if the field is a map 95 | func isMap(f Validation) (ret bool, err error) { 96 | ret = false 97 | field := f.F 98 | switch field.Type.(type) { 99 | case *ast.MapType: 100 | ret = true 101 | } 102 | return 103 | } 104 | 105 | // GenerationError is a helper method for templates to generate an error if something 106 | // is unsupported. 107 | func GenerationError(s string) (ret interface{}, err error) { 108 | err = errors.New(s) 109 | return 110 | } 111 | 112 | // isStruct is a helper method for templates to determine if the field is a struct (not a pointer to a struct) 113 | func (g *Generator) tmplIsStruct(f Validation) (ret bool, err error) { 114 | ret = g.isStruct(f.F) 115 | return 116 | } 117 | 118 | // isStruct is a helper method for templates to determine if the field is a struct (not a pointer to a struct) 119 | func (g *Generator) isStruct(field *ast.Field) (ret bool) { 120 | ret = false 121 | fType := field.Type 122 | typeString := g.getStringForExpr(field.Type) 123 | if strings.HasPrefix(typeString, `*`) { 124 | // Quit early if it's a pointer 125 | return 126 | } 127 | if _, knownStruct := g.knownStructs[typeString]; knownStruct { 128 | // We know it's not a pointer, so if we can find it, it's a struct 129 | ret = true 130 | return 131 | } 132 | 133 | if _, ok := fType.(*ast.Ident); ok { 134 | fType = getIdentType(fType.(*ast.Ident)) 135 | } 136 | 137 | switch fType.(type) { 138 | case *ast.StructType: 139 | ret = true 140 | case *ast.SelectorExpr: 141 | ret = true 142 | } 143 | return 144 | } 145 | 146 | // tmplIsStructPtr is the template helper method to determine if a field is a *Struct 147 | func (g *Generator) tmplIsStructPtr(f Validation) (ret bool, err error) { 148 | ret = g.isStructPtr(f.F) 149 | return 150 | } 151 | 152 | // isStructPtr is a helper method for templates to determine if the field is a pointer to a struct 153 | func (g *Generator) isStructPtr(field *ast.Field) (ret bool) { 154 | ret = false 155 | fType := field.Type 156 | typeString := g.getStringForExpr(field.Type) 157 | if !strings.HasPrefix(typeString, `*`) { 158 | // Quit early if it's not a pointer at all 159 | return 160 | } 161 | if _, knownStruct := g.knownStructs[strings.TrimPrefix(typeString, `*`)]; knownStruct { 162 | // We already know it's a pointer, so if it's found, return true 163 | ret = true 164 | return 165 | } 166 | 167 | if star, ok := fType.(*ast.StarExpr); ok { 168 | switch star.X.(type) { 169 | case *ast.Ident: 170 | fType = getIdentType(star.X.(*ast.Ident)) 171 | case *ast.SelectorExpr: 172 | ret = true 173 | // fType = getIdentType(star.X.(*ast.SelectorExpr).Sel) 174 | // fmt.Printf("Got a selector, inner type is: %#v\n", fType) 175 | } 176 | } 177 | 178 | switch fType.(type) { 179 | case *ast.StructType: 180 | ret = true 181 | } 182 | return 183 | } 184 | 185 | func getIdentType(ident *ast.Ident) ast.Expr { 186 | if ident.Obj != nil { 187 | switch ident.Obj.Decl.(type) { 188 | case *ast.TypeSpec: 189 | return ident.Obj.Decl.(*ast.TypeSpec).Type 190 | } 191 | } 192 | return ident 193 | } 194 | 195 | // tmplIsStructPtr is the template helper method to determine if a field is a *Struct 196 | func (g *Generator) tmplGetMapKeyType(f Validation) (ret string, err error) { 197 | expr := g.getMapKeyType(f.F) 198 | if expr != nil { 199 | ret = g.getStringForExpr(expr) 200 | } 201 | return 202 | } 203 | 204 | func (g *Generator) getMapKeyType(field *ast.Field) ast.Expr { 205 | switch field.Type.(type) { 206 | case *ast.MapType: 207 | mt := field.Type.(*ast.MapType) 208 | 209 | switch mt.Key.(type) { 210 | case *ast.Ident: 211 | return getIdentType(mt.Key.(*ast.Ident)) 212 | } 213 | } 214 | return nil 215 | } 216 | 217 | func tmplIsParamInt(param string) bool { 218 | _, err := strconv.Atoi(param) 219 | if err != nil { 220 | return false 221 | } 222 | return true 223 | } 224 | -------------------------------------------------------------------------------- /internal/benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/abice/gencheck" 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/assert" 11 | "gopkg.in/go-playground/validator.v9" 12 | ) 13 | 14 | var result bool 15 | var errResult error 16 | var EmptyStructs = []TestString{ 17 | {}, 18 | {}, 19 | {}, 20 | {}, 21 | {}, 22 | {}, 23 | {}, 24 | {}, 25 | {}, 26 | {}, 27 | } 28 | 29 | // BenchmarkEmptyInt is a quick benchmark to determine performance of just using an empty var 30 | // for zero value comparison 31 | func BenchmarkValidString(b *testing.B) { 32 | uut := TestString{ 33 | Required: "Required", 34 | Len: "1234567890", 35 | Min: "12345", 36 | Max: "1231232", 37 | } 38 | var err error 39 | 40 | b.ResetTimer() 41 | for x := 0; x < b.N; x++ { 42 | err = uut.Validate() 43 | } 44 | errResult = err 45 | } 46 | 47 | // BenchmarkEmptyStruct is a quick benchmark to determine performance of just using an empty var 48 | // for zero value comparison 49 | func BenchmarkFailing1TestString(b *testing.B) { 50 | uut := TestString{ 51 | Required: "", 52 | Len: "1234567890", 53 | Min: "12345", 54 | Max: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", 55 | } 56 | var err error 57 | 58 | b.ResetTimer() 59 | for x := 0; x < b.N; x++ { 60 | err = uut.Validate() 61 | } 62 | errResult = err 63 | } 64 | 65 | // BenchmarkEmptyStruct is a quick benchmark to determine performance of just using an empty var 66 | // for zero value comparison 67 | func BenchmarkFailing2TestString(b *testing.B) { 68 | uut := TestString{ 69 | Required: "", 70 | Len: "12345678901", 71 | Min: "12345", 72 | Max: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", 73 | } 74 | var err error 75 | 76 | b.ResetTimer() 77 | for x := 0; x < b.N; x++ { 78 | err = uut.Validate() 79 | } 80 | errResult = err 81 | } 82 | 83 | // BenchmarkEmptyStruct is a quick benchmark to determine performance of just using an empty var 84 | // for zero value comparison 85 | func BenchmarkFailingAllTestString(b *testing.B) { 86 | uut := TestString{ 87 | Required: "", 88 | Len: "12345678901", 89 | Min: "1234", 90 | Max: "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901", 91 | } 92 | var err error 93 | 94 | b.ResetTimer() 95 | for x := 0; x < b.N; x++ { 96 | err = uut.Validate() 97 | } 98 | errResult = err 99 | } 100 | 101 | const ( 102 | containsAnyPassingString = `Some string to do a contains any@ search# on!` 103 | containsAnyFailingString = `Some string to do a contains any search on.` 104 | ) 105 | 106 | const ( 107 | hexPassingString = `1234567890ABCDEF` 108 | hexFailingString = `1234567890ABCDEFG` 109 | ) 110 | 111 | var dummyString = "nothing but emptiness" 112 | 113 | var benchmarks = []struct { 114 | name string 115 | hasError bool 116 | uut gencheck.Validateable 117 | }{ 118 | {"UUID Pass", false, TestUUID{UUID: uuid.New().String()}}, 119 | {"UUID Fail", true, TestUUID{UUID: uuid.New().String() + "notauuid"}}, 120 | {"Hex Pass", false, TestHex{Value: hexPassingString}}, 121 | {"Hex Fail", true, TestHex{Value: hexFailingString}}, 122 | {"ContainsAny Pass", false, TestContainsAny{Any: containsAnyPassingString}}, 123 | {"ContainsAny Fail", true, TestContainsAny{Any: containsAnyFailingString}}, 124 | {"TestStrings Pass", false, TestString{ 125 | Required: "Required", 126 | Len: "1234567890", 127 | Min: "12345", 128 | Max: "1231232", 129 | }}, 130 | {"TestStrings Fail", true, TestString{ 131 | Required: "", 132 | Len: "12345678901", 133 | Min: "1234", 134 | Max: "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901", 135 | }}, 136 | {"TestMap Pass", false, TestMap{Value: map[string]string{"test": "something"}}}, //Playground doesn't support this test. 137 | {"TestMap Fail", true, TestMap{Value: map[string]string{"test2": "something"}}}, 138 | {"TestDive Pass", false, TestDive{Value: &SingleString{"This is a test"}}}, 139 | {"TestDive Fail", true, TestDive{Value: &SingleString{""}}}, 140 | {"TestDive Nil", true, TestDive{}}, 141 | {"TestAll Pass", false, TestAll{ 142 | Required: "Required", 143 | Len: "1234567890", 144 | Min: "12345", 145 | Max: "1231232", 146 | CIDR: "192.168.1.0/24", 147 | LteTime: time.Now().Add(-1 * time.Second), 148 | GteTime: time.Now().Add(60 * time.Second), // Make this a large number so that it's still after now when the test comes around. 149 | Gte: 1.23451, 150 | NotNil: &dummyString, 151 | Contains: "The quick brown fox jumped over the lazy dog", 152 | Hex: "1234567890AbCdEf", 153 | UUID: uuid.New().String(), 154 | MinInt: 12345, 155 | MaxInt: 12345, 156 | URL: "http://test.com/health?whatislife=something", 157 | URI: "http://test.com/health?whatislife=something", 158 | Dive: &SingleString{Entry: "This is a test"}, 159 | }}, 160 | {"TestAll Fail", true, TestAll{ 161 | Required: "", 162 | Len: "123456789", 163 | Min: "1234", 164 | Max: "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901", 165 | CIDR: "192.168.1.0", 166 | LteTime: time.Now().Add(60 * time.Second), // Make this a large number so that it's still after now when the test comes around. 167 | GteTime: time.Now().Add(-1 * time.Second), 168 | Gte: 1.2344, 169 | NotNil: nil, 170 | Contains: "The quick brown cat jumped over the lazy dog", 171 | Hex: "1234567890AbCdEfG", 172 | UUID: strings.ToUpper(uuid.New().String() + "adsf"), 173 | MinInt: 12344, 174 | MaxInt: 12346, 175 | URL: "/health?whatislife=something", 176 | URI: "?whatislife=something", 177 | Dive: &SingleString{Entry: ""}, 178 | }}, 179 | } 180 | 181 | // BenchmarkCompareGencheckContainsAnyFail is a quick benchmark to determine performance of gencheck vs goplayground/validator 182 | func BenchmarkCompareGencheck(b *testing.B) { 183 | var err error 184 | 185 | b.ResetTimer() 186 | for _, bm := range benchmarks { 187 | b.Run(bm.name, func(b *testing.B) { 188 | for i := 0; i < b.N; i++ { 189 | err = bm.uut.Validate() 190 | } 191 | errResult = err 192 | if bm.hasError { 193 | assert.Error(b, err, "No Error when expected one.") 194 | } else { 195 | assert.NoError(b, err, "Error when there shouldn't be.") 196 | } 197 | }) 198 | } 199 | } 200 | 201 | // BenchmarkCompareGencheckContainsAnyFail is a quick benchmark to determine performance of gencheck vs goplayground/validator 202 | func BenchmarkComparePlayground(b *testing.B) { 203 | var err error 204 | validate := validator.New() 205 | 206 | b.ResetTimer() 207 | for _, bm := range benchmarks { 208 | b.Run(bm.name, func(b *testing.B) { 209 | if strings.HasPrefix(bm.name, "TestMap") { 210 | b.SkipNow() 211 | } 212 | for i := 0; i < b.N; i++ { 213 | err = validate.Struct(bm.uut) 214 | } 215 | errResult = err 216 | if bm.hasError { 217 | assert.Error(b, err, "No Error when expected one.") 218 | } else { 219 | assert.NoError(b, err, "Error when there shouldn't be.") 220 | } 221 | }) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Bowery/prompt v0.0.0-20180817134258-8a1d5376df1c h1:fAMg70P5ydy1uiIj6CdA69h6nmQKbv18VlVOXhKNrcM= 2 | github.com/Bowery/prompt v0.0.0-20180817134258-8a1d5376df1c/go.mod h1:4/6eNcqZ09BZ9wLK3tZOjBA1nDj+B0728nlX5YRlSmQ= 3 | github.com/Masterminds/semver v1.2.2 h1:ptelpryog9A0pR4TGFvIAvw2c8SaNrYkFtfrxhSviss= 4 | github.com/Masterminds/semver v1.2.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 5 | github.com/Masterminds/sprig v2.17.1+incompatible h1:PChbxFGKTWsg9IWh+pSZRCSj3zQkVpL6Hd9uWsFwxtc= 6 | github.com/Masterminds/sprig v2.17.1+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 7 | github.com/aokoli/goutils v1.1.0 h1:jy4ghdcYvs5EIoGssZNslIASX5m+KNMfyyKvRQ0TEVE= 8 | github.com/aokoli/goutils v1.1.0/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 13 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 14 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 15 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 16 | github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= 17 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 18 | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c= 19 | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 20 | github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= 21 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 22 | github.com/imdario/mergo v0.0.0-20171009183408-7fe0c75c13ab h1:k/Biv+LJL35wkk0Hveko1nj7as4tSHkHdZaNlzn/gcQ= 23 | github.com/imdario/mergo v0.0.0-20171009183408-7fe0c75c13ab/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 24 | github.com/kevinburke/go-bindata v3.16.0+incompatible h1:TFzFZop2KxGhqNwsyjgmIh5JOrpG940MZlm5gNbxr8g= 25 | github.com/kevinburke/go-bindata v3.16.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM= 26 | github.com/labstack/gommon v0.2.9-0.20190125185610-82ef680aef51 h1:kqWediUZSpRhIW6ZeTClKd1wIT3Hc1PlO3NQIMtKSaw= 27 | github.com/labstack/gommon v0.2.9-0.20190125185610-82ef680aef51/go.mod h1:pnGpdM3THFBN3cctRYV09Bn7QqeQJp4ptXl7VIh2p4I= 28 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 29 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 30 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 31 | github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= 32 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 33 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 34 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 h1:USWjF42jDCSEeikX/G1g40ZWnsPXN5WkZ4jMHZWyBK4= 35 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 36 | github.com/mattn/goveralls v0.0.5 h1:spfq8AyZ0cCk57Za6/juJ5btQxeE1FaEGMdfcI+XO48= 37 | github.com/mattn/goveralls v0.0.5/go.mod h1:Xg2LHi51faXLyKXwsndxiW6uxEEQT9+3sjGzzwU4xy0= 38 | github.com/mkideal/cli v0.0.3-0.20190117035342-a48c2cee5b5e h1:cBVYCHL7QOJRvphH9/81e/rDgW64AkdJSIwx4zgwG4o= 39 | github.com/mkideal/cli v0.0.3-0.20190117035342-a48c2cee5b5e/go.mod h1:HLuSls75T7LFlTgByGeuLwcvdUmmx/aUQxnnEKxoZzY= 40 | github.com/mkideal/pkg v0.0.0-20170503154153-3e188c9e7ecc h1:eyN9UWVX+CeeCQZPudCUAPc84xQYTjEu9MWNa2HuJrs= 41 | github.com/mkideal/pkg v0.0.0-20170503154153-3e188c9e7ecc/go.mod h1:DECgB56amjU/mmmsKuooNPQ1856HASOMC3D4ntSVU70= 42 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 43 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 48 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 49 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 50 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 51 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 52 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= 53 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 54 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 55 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 56 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 57 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 58 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 60 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20181217223516-dcdaa6325bcb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 64 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 67 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg= 70 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 71 | golang.org/x/tools v0.0.0-20200113040837-eac381796e91 h1:OOkytthzFBKHY5EfEgLUabprb0LtJVkQtNxAQ02+UE4= 72 | golang.org/x/tools v0.0.0-20200113040837-eac381796e91/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 73 | golang.org/x/tools v0.0.0-20200117173607-7ad9cd8f3189 h1:8208oB0AyZ5ZA7xACIuGkDVU+o6u+E1ha6CMRcxL2ew= 74 | golang.org/x/tools v0.0.0-20200117173607-7ad9cd8f3189/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 75 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 79 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 80 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 81 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 82 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 83 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gencheck 2 | [![CircleCI](https://circleci.com/gh/abice/gencheck.svg?style=svg&circle-token=db146e1d9c8d935d7bd05ac879f818801c432ea4)](https://circleci.com/gh/abice/gencheck) 3 | [![Coverage Status](https://coveralls.io/repos/github/abice/gencheck/badge.svg)](https://coveralls.io/github/abice/gencheck) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/abice/gencheck)](https://goreportcard.com/report/github.com/abice/gencheck) 5 | 6 | ### Validation generation for go. 7 | 8 | ### Built-in validations 9 | See [Validations](validations.md) 10 | 11 | ## How it works 12 | gencheck was built using the idea of [zencoder/gokay](https://github.com/zencoder/gokay), but uses templates to create validations for a struct. 13 | 14 | gencheck will use the `valid` tag within a struct to generate a `Validate()` method, which is will store in a `file_validators.go` file 15 | next to the input file. 16 | 17 | gencheck's `Validate()` method will return a `ValidationErrors` type, which is an array of `FieldError`s. 18 | 19 | Given the struct: 20 | 21 | ```go 22 | type MyStruct struct{ 23 | MyField string `valid:"required"` 24 | } 25 | ``` 26 | A Validate method is generated: 27 | 28 | ```go 29 | func (s MyStruct) Validate() error { 30 | var vErrors gencheck.ValidationErrors 31 | 32 | // BEGIN MyField Validations 33 | // required 34 | if s.MyField == "" { 35 | vErrors = append(vErrors, gencheck.NewFieldError("MyStruct", "MyField", "required", errors.New("is required"))) 36 | } 37 | // END MyField Validations 38 | 39 | if len(vErrors) > 0 { 40 | return vErrors 41 | } 42 | return nil 43 | } 44 | ``` 45 | 46 | ## Installing 47 | 48 | First use `go get` to install the latest version of the library. 49 | 50 | `go get -v github.com/abice/gencheck/gencheck` 51 | 52 | Normally the above command will build and install the binary, but just to be sure it is installed in your `GOPATH` for use on the command line, or build args: 53 | 54 | `go install github.com/abice/gencheck/gencheck` 55 | 56 | ## Running 57 | ### Command line 58 | ```sh 59 | gencheck -f=file.go -t="SomeTemplate.tmpl" --template="SomeOtherTemplate.tmpl" -d="some/dir" --template-dir="some/dir/that/has/templates" 60 | ``` 61 | 62 | ### Using with `go generate` 63 | Add a `//go:generate` tag to the top of your file that you want to generate for, including the file name. 64 | 65 | ```go 66 | //go:generate gencheck -f=this_file.go 67 | ``` 68 | 69 | ## Adding Validations 70 | Add validations to `valid` tag in struct def: 71 | 72 | ```go 73 | type ExampleStruct struct { 74 | HexStringPtr *string `valid:"len=16,notnil,hex"` 75 | HexString string `valid:"len=12,hex"` 76 | CanBeNilWithConstraints *string `valid:"len=12"` 77 | } 78 | ``` 79 | 80 | 81 | ### Tag syntax 82 | Validation tags are comma separated, with any validation parameter specified after an equal sign. 83 | 84 | `valid:"ValidationName1,ValidationName2=vn2param"` 85 | 86 | In the above example, the `hex` and `notnil` Validations are parameterless, whereas len requires 1 parameter. 87 | 88 | 89 | ### Time comparisons 90 | Since the addition of `gt(e)` and `lt(e)`, there are now comparisons for `time.Time` values. If no arguments are specified to those, then it calculates whether the 91 | field time is After and Before `time.Now().UTC()` respectively. You can specify a parameter for those validations if you choose. The parameter will be interpreted as 92 | the offset to use with respect to `time.Now().UTC()` by utilizing the `Add()` function. 93 | 94 | ```go 95 | requestTime time.Time `valid:"gte=-1*time.Second"` 96 | ``` 97 | 98 | ```go 99 | tGteTimeVal := time.Now().UTC().Add(-1 * time.Second) 100 | if s.GteTimeVal.Before(tGteTimeVal) { 101 | vErrors = append(vErrors, gencheck.NewFieldError("Test", "GteTimeVal", "gte", fmt.Errorf("is before %s", tGteTimeVal))) 102 | } 103 | ``` 104 | 105 | ### Fail Fast flag 106 | The fail fast flag is a built-in validation flag that will allow you to return immediately on an invalid check. This allows you to not waste time checking the rest of the struct if a vital field is wrong. It can be placed anywhere within the `valid` tag, and will be applied to **all** rules within that field. 107 | 108 | There is also a `--failfast` flag on the cli that will allow you to make **all** validations within **all** structs found in the files to be fail fast. 109 | 110 | ### Writing your own Validations 111 | gencheck allows developers to write and attach their own Validation templates to the generator. 112 | 113 | 1. Write a template that creates a validation for a given field making sure to define the template as the validation tag you want to use: 114 | 115 | ```gotemplate 116 | {{define "mycheck" -}} 117 | if err := gencheck.IsUUID({{.Param}}, {{if not (IsPtr . )}}&{{end}}s.{{.FieldName}}); err != nil { 118 | {{ AddError . "err" }} 119 | } 120 | {{end -}} 121 | ``` 122 | 123 | 1. Import that template when running gencheck 124 | 1. Write tests for your struct's constraints 125 | 1. Add `valid` tags to your struct fields 126 | 1. Run gencheck: `gencheck -f=file.go -t=MyTemplate` 127 | 128 | NOTES: 129 | 130 | - In your template, the . pipeline is an instance of the [`generator.Validation`](generator/types.go#L27) struct. 131 | - The template functions from [Sprig](https://github.com/Masterminds/sprig) have been included. 132 | - There are some custom functions provided for you to help in determining the ast field type 133 | - isPtr 134 | - addError 135 | - isNullable 136 | - isMap 137 | - isArray 138 | - isStruct 139 | - isStructPtr 140 | - isStructPtr 141 | - generationError 142 | - Allows you to fail code generation with a specific error message 143 | 144 | [More Examples](internal/example/) 145 | 146 | 147 | ## Useless Benchmarks 148 | 149 | I know benchmarks are always skewed to show what the creators want you to see, but here's a quick benchmark of the cost of using validation to check. 150 | 151 | I've also added some comparison benchmark output from the `./internal/benchmark_test.go` to compare the different options with gencheck and how it holds up to the go playground validator. 152 | 153 | - [Playground](benchmark_playground.md) 154 | - [No Options](benchmark_nooptions.md) 155 | - [No Preallocated error array](benchmark_noprealloc.md) 156 | - [Fail Fast](benchmark_failfast.md) 157 | 158 | ``` 159 | BenchmarkReflectionInt-8 20000000 104 ns/op 160 | BenchmarkEmptyInt-8 2000000000 0.29 ns/op 161 | BenchmarkReflectionStruct-8 5000000 262 ns/op 162 | BenchmarkEmptyStruct-8 50000000 28.3 ns/op 163 | BenchmarkReflectionString-8 10000000 159 ns/op 164 | BenchmarkEmptyString-8 200000000 9.49 ns/op 165 | 166 | ``` 167 | Benchmarks using fail fast flag 168 | ``` 169 | BenchmarkValidString-8 300000000 5.02 ns/op 170 | BenchmarkFailing1TestString-8 10000000 158 ns/op 171 | BenchmarkFailing2TestString-8 10000000 159 ns/op 172 | BenchmarkFailingAllTestString-8 10000000 164 ns/op 173 | ``` 174 | 175 | Benchmarks without fail fast flag and preallocated capacity for errors 176 | ``` 177 | BenchmarkValidString-8 20000000 68.7 ns/op 178 | BenchmarkFailing1TestString-8 10000000 189 ns/op 179 | BenchmarkFailing2TestString-8 5000000 272 ns/op 180 | BenchmarkFailingAllTestString-8 3000000 418 ns/op 181 | ``` 182 | 183 | ## Development 184 | 185 | ### Dependencies 186 | 187 | Tested on go 1.7.3. 188 | 189 | ### Build and run unit tests 190 | 191 | make test 192 | 193 | ### TODO 194 | - [x] Testing for templates 195 | - [x] Prevent duplicate validations on the same field 196 | - [x] Update Required tag to error out on numerical or boolean fields 197 | - [ ] Support for sub-validations? `Struct fields: generated code will call static Validate method on any field that implements Validateable interface` Maybe use a deep check 198 | - [x] Readme info for what information is available within the templates. 199 | - [ ] Contains for other slice types. 200 | - [ ] Contains for maps. 201 | - [ ] Add support for build tags for generated file. 202 | - [ ] Cross field validation (i.e. x.start <= x.end) 203 | 204 | ### CI 205 | 206 | [This library builds on Circle CI, here.](https://circleci.com/gh/abice/gencheck/) 207 | 208 | ## License 209 | 210 | [Apache License Version 2.0](LICENSE) 211 | -------------------------------------------------------------------------------- /generator/gencheck_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "go/parser" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | const ( 13 | testExample = `example_test.go` 14 | testSupport = `example_support_test.go` 15 | ) 16 | 17 | // generatorTestSuite 18 | type generatorTestSuite struct { 19 | suite.Suite 20 | } 21 | 22 | // SetupSuite 23 | func (s *generatorTestSuite) SetupSuite() { 24 | } 25 | 26 | // TestLengthTestSuite 27 | func TestGeneratorTestSuite(t *testing.T) { 28 | suite.Run(t, new(generatorTestSuite)) 29 | } 30 | 31 | // TestNoStructInputFile 32 | func (s *generatorTestSuite) TestNoStructFile() { 33 | input := `package test 34 | // SomeInterface 35 | type SomeInterface interface{ 36 | 37 | } 38 | ` 39 | g := NewGenerator() 40 | f, err := parser.ParseFile(g.fileSet, "TestRequiredErrors", input, parser.ParseComments) 41 | s.Nil(err, "Error parsing no struct input") 42 | 43 | output, err := g.Generate(f) 44 | s.Nil(err, "Error generating formatted code") 45 | if false { // Debugging statement 46 | fmt.Println(string(output)) 47 | } 48 | } 49 | 50 | // TestNoFile 51 | func (s *generatorTestSuite) TestNoFile() { 52 | g := NewGenerator() 53 | // Parse the file given in arguments 54 | _, err := g.GenerateFromFile("") 55 | s.NotNil(err, "Error generating formatted code") 56 | } 57 | 58 | // TestExampleFile 59 | func (s *generatorTestSuite) TestExampleFile() { 60 | g := NewGenerator() 61 | // Parse the file given in arguments 62 | g.AddSupportFiles(testSupport) 63 | imported, err := g.GenerateFromFile(testExample) 64 | s.Nil(err, "Error generating formatted code") 65 | if false { 66 | fmt.Println(string(imported)) 67 | } 68 | } 69 | 70 | func (s *generatorTestSuite) TestDuplicateRuleFailure() { 71 | g := NewGenerator() 72 | input := `package test 73 | // SomeStruct 74 | type SomeStruct struct { 75 | TestString string ` + "`valid:\"len=0,len=1\"`" + ` 76 | } 77 | ` 78 | f, err := parser.ParseFile(g.fileSet, "TestStringInput", input, parser.ParseComments) 79 | s.Nil(err, "Error parsing input string") 80 | 81 | _, err = g.Generate(f) 82 | s.EqualError(err, "Duplicate rules are not allowed: 'len' on field 'TestString'") 83 | } 84 | 85 | func (s *generatorTestSuite) TestNoTemplate() { 86 | g := NewGenerator() 87 | input := `package test 88 | // SomeStruct 89 | type SomeStruct struct { 90 | TestString string ` + "`valid:\"xyz\"`" + ` 91 | } 92 | ` 93 | f, err := parser.ParseFile(g.fileSet, "TestStringInput", input, parser.ParseComments) 94 | s.Nil(err, "Error parsing input string") 95 | 96 | output, err := g.Generate(f) 97 | s.Require().Nil(err, "Error generating output") 98 | expected := "// Code generated by gencheck\n// DO NOT EDIT!\n\npackage test\n" 99 | s.Equal(len(expected), len(output)) 100 | s.Equal(expected, string(output)) 101 | } 102 | 103 | func (s *generatorTestSuite) TestFailFastFlag() { 104 | g := NewGenerator() 105 | input := `package test 106 | // SomeStruct 107 | type SomeStruct struct { 108 | TestString string ` + "`valid:\"required,ff\"`" + ` 109 | } 110 | ` 111 | f, err := parser.ParseFile(g.fileSet, "TestStringInput", input, parser.ParseComments) 112 | s.Nil(err, "Error parsing input string") 113 | 114 | output, err := g.Generate(f) 115 | s.Require().Nil(err, "Error generating output") 116 | 117 | s.Contains(string(output), "return append(vErrors,", string(output)) 118 | } 119 | 120 | func (s *generatorTestSuite) TestPointerFunc() { 121 | g := NewGenerator().WithPointerMethod() 122 | input := `package test 123 | // SomeStruct 124 | type SomeStruct struct { 125 | TestString string ` + "`valid:\"required\"`" + ` 126 | } 127 | ` 128 | f, err := parser.ParseFile(g.fileSet, "TestStringInput", input, parser.ParseComments) 129 | s.Nil(err, "Error parsing input string") 130 | 131 | output, err := g.Generate(f) 132 | s.Require().Nil(err, "Error generating output") 133 | 134 | s.Contains(string(output), "func (s *SomeStruct) Validate()") 135 | 136 | } 137 | 138 | func (s *generatorTestSuite) TestFailFast() { 139 | g := NewGenerator().WithFailFast() 140 | input := `package test 141 | // SomeStruct 142 | type SomeStruct struct { 143 | TestString string ` + "`valid:\"required\"`" + ` 144 | } 145 | ` 146 | f, err := parser.ParseFile(g.fileSet, "TestStringInput", input, parser.ParseComments) 147 | s.Nil(err, "Error parsing input string") 148 | 149 | output, err := g.Generate(f) 150 | s.Require().Nil(err, "Error generating output") 151 | 152 | s.Contains(string(output), "return append(vErrors,") 153 | 154 | } 155 | 156 | func (s *generatorTestSuite) TestAddTemplateFile() { 157 | g := NewGenerator() 158 | input := `package test 159 | // Inner 160 | type Inner struct{ 161 | 162 | } 163 | 164 | // SomeStruct 165 | type SomeStruct struct { 166 | TestString *Inner ` + "`valid:\"dummy\"`" + ` 167 | } 168 | ` 169 | 170 | err := g.AddTemplateFiles("dummy_template.tmpl") 171 | s.Require().Nil(err, "Error adding dummy template") 172 | f, err := parser.ParseFile(g.fileSet, "TestStringInput", input, parser.ParseComments) 173 | s.Nil(err, "Error parsing input string") 174 | 175 | output, err := g.Generate(f) 176 | s.Require().NotNil(err, fmt.Sprintf("Should have had an error generating output")) 177 | s.Contains(err.Error(), "generate: error formatting code") 178 | if false { 179 | fmt.Printf("Output: %s\n", string(output)) 180 | } 181 | } 182 | 183 | var result bool 184 | 185 | var EmptyStructs = []Inner{ 186 | {}, 187 | {}, 188 | {}, 189 | {}, 190 | {}, 191 | {}, 192 | {}, 193 | {}, 194 | {}, 195 | {}, 196 | } 197 | 198 | // BenchmarkReflection is a quick test to see how much of an impact reflection has 199 | // in the performance of an application 200 | func BenchmarkReflectionInt(b *testing.B) { 201 | match := false 202 | for x := 0; x < b.N; x++ { 203 | zeroInt := reflect.Zero(reflect.ValueOf(x).Type()) 204 | if reflect.ValueOf(x).Interface() == zeroInt.Interface() { 205 | match = true 206 | } 207 | match = false 208 | } 209 | result = match 210 | } 211 | 212 | // BenchmarkEmptyInt is a quick benchmark to determine performance of just using an empty var 213 | // for zero value comparison 214 | func BenchmarkEmptyInt(b *testing.B) { 215 | match := false 216 | for x := 0; x < b.N; x++ { 217 | var zeroInt int 218 | if x == zeroInt { 219 | match = true 220 | } 221 | match = false 222 | } 223 | result = match 224 | } 225 | 226 | // BenchmarkReflectionStruct is a quick test to see how much of an impact reflection has 227 | // in the performance of an application 228 | func BenchmarkReflectionStruct(b *testing.B) { 229 | match := false 230 | count := len(EmptyStructs) 231 | 232 | for x := 0; x < b.N; x++ { 233 | uut := EmptyStructs[x%count] 234 | zeroInner := reflect.Zero(reflect.ValueOf(uut).Type()) 235 | if reflect.ValueOf(uut).Interface() == zeroInner.Interface() { 236 | match = true 237 | } 238 | match = false 239 | } 240 | result = match 241 | } 242 | 243 | // BenchmarkEmptyStruct is a quick benchmark to determine performance of just using an empty var 244 | // for zero value comparison 245 | func BenchmarkEmptyStruct(b *testing.B) { 246 | match := false 247 | count := len(EmptyStructs) 248 | 249 | for x := 0; x < b.N; x++ { 250 | var zeroExample Inner 251 | if EmptyStructs[x%count] == zeroExample { 252 | match = true 253 | } 254 | match = false 255 | } 256 | result = match 257 | } 258 | 259 | var Strings = []string{ 260 | "", 261 | "a", 262 | "abcdefghijklmnopqrstuvwxyz", 263 | "1234567890", 264 | "qwerty", 265 | "zxcvb", 266 | "chickens", 267 | "cows", 268 | "trains", 269 | } 270 | 271 | // BenchmarkReflectionString is a quick test to see how much of an impact reflection has 272 | // in the performance of an application 273 | func BenchmarkReflectionString(b *testing.B) { 274 | match := false 275 | count := len(Strings) 276 | 277 | for x := 0; x < b.N; x++ { 278 | uut := Strings[x%count] 279 | zeroInner := reflect.Zero(reflect.ValueOf(uut).Type()) 280 | if reflect.ValueOf(uut).Interface() == zeroInner.Interface() { 281 | match = true 282 | } 283 | match = false 284 | } 285 | result = match 286 | } 287 | 288 | // BenchmarkEmptyString is a quick benchmark to determine performance of just using an empty var 289 | // for zero value comparison 290 | func BenchmarkEmptyString(b *testing.B) { 291 | match := false 292 | count := len(Strings) 293 | 294 | for x := 0; x < b.N; x++ { 295 | var zeroExample string 296 | if Strings[x%count] == zeroExample { 297 | match = true 298 | } 299 | match = false 300 | } 301 | result = match 302 | } 303 | -------------------------------------------------------------------------------- /internal/example/example.go: -------------------------------------------------------------------------------- 1 | //go:generate ../../bin/gencheck --supp=example_other_file.go -f=example.go 2 | 3 | package example 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/abice/gencheck/internal/benchmark" 9 | ) 10 | 11 | // Example struct for testing 12 | type Example struct { 13 | MapOfInterfaces map[string]interface{} `valid:"notnil"` 14 | } 15 | 16 | // IFace is a sample interface for testing. 17 | type IFace interface { 18 | SomeMethod() error 19 | } 20 | 21 | // Inner is another example struct for testing 22 | type Inner struct { 23 | EqCSFieldString string `valid:"required"` 24 | NeCSFieldString string 25 | GtCSFieldString string 26 | GteCSFieldString string 27 | LtCSFieldString string 28 | LteCSFieldString string 29 | } 30 | 31 | type Embedded struct { 32 | FieldString string `valid:"required"` 33 | } 34 | 35 | // Test is a test struct for testing. 36 | type Test struct { 37 | Embedded 38 | ExternalEmbedded 39 | Inner Inner 40 | // RequiredNumber int `valid:"required"` 41 | RequiredString string `valid:"required,ff"` 42 | RequiredMultiple []string `valid:"required"` 43 | LenString string `valid:"len=1"` 44 | LenNumber float64 `valid:"len=1113.00"` 45 | LenMultiple []string `valid:"len=7"` 46 | MinString string `valid:"min=1"` 47 | MinNumber float64 `valid:"min=1113.00"` 48 | MinMultiple []string `valid:"min=7"` 49 | MaxString string `valid:"max=3"` 50 | MaxNumber float64 `valid:"max=1113.00"` 51 | MaxMultiple []string `valid:"max=7"` 52 | EqString string `valid:"eq=3"` 53 | EqNumber float64 `valid:"eq=2.33"` 54 | EqMultiple []string `valid:"eq=7"` 55 | NeString string `valid:"ne="` 56 | NeNumber float64 `valid:"ne=0.00"` 57 | NeMultiple []string `valid:"ne=0"` 58 | LtString string `valid:"lt=3"` 59 | LtNumber float64 `valid:"lt=5.56"` 60 | LtMultiple []string `valid:"lt=2"` 61 | LtTime time.Time `valid:"lt"` 62 | LteString string `valid:"lte=3"` 63 | LteNumber float64 `valid:"lte=5.56"` 64 | LteMultiple []string `valid:"lte=2"` 65 | LteTime time.Time `valid:"lte"` 66 | GtString string `valid:"gt=3"` 67 | GtNumber float64 `valid:"gt=5.56"` 68 | GtMultiple []string `valid:"gt=2"` 69 | GtTime time.Time `valid:"gt"` 70 | GteString string `valid:"gte=3"` 71 | GteNumber float64 `valid:"gte=5.56"` 72 | GteMultiple []string `valid:"gte=2"` 73 | GteTime time.Time `valid:"gte"` 74 | GteTimeVal time.Time `valid:"gte=1*time.Second"` 75 | GteTimePtr *time.Time `valid:"gte"` 76 | EqFieldString string `valid:"eqfield=MaxString"` 77 | EqCSFieldString string `valid:"eqcsfield=Inner.EqCSFieldString"` 78 | NeCSFieldString string `valid:"necsfield=Inner.NeCSFieldString"` 79 | GtCSFieldString string `valid:"gtcsfield=Inner.GtCSFieldString"` 80 | GteCSFieldString string `valid:"gtecsfield=Inner.GteCSFieldString"` 81 | LtCSFieldString string `valid:"ltcsfield=Inner.LtCSFieldString"` 82 | LteCSFieldString string `valid:"ltecsfield=Inner.LteCSFieldString"` 83 | NeFieldString string `valid:"nefield=EqFieldString"` 84 | GtFieldString string `valid:"gtfield=MaxString"` 85 | GteFieldString string `valid:"gtefield=MaxString"` 86 | LtFieldString string `valid:"ltfield=MaxString"` 87 | LteFieldString string `valid:"ltefield=MaxString"` 88 | AlphaString string `valid:"alpha"` 89 | AlphanumString string `valid:"alphanum"` 90 | NumericString string `valid:"numeric"` 91 | NumberString string `valid:"number"` 92 | HexadecimalString string `valid:"hexadecimal"` 93 | HexColorString string `valid:"hexcolor"` 94 | RGBColorString string `valid:"rgb"` 95 | RGBAColorString string `valid:"rgba"` 96 | HSLColorString string `valid:"hsl"` 97 | HSLAColorString string `valid:"hsla"` 98 | Email string `valid:"email"` 99 | URL string `valid:"url"` 100 | URI string `valid:"uri"` 101 | Base64 string `valid:"base64"` 102 | Contains string `valid:"contains=purpose"` 103 | ContainsPtr *string `valid:"contains=purpose"` 104 | ContainsArray []string `valid:"contains=nonsense"` 105 | ContainsAny string `valid:"containsany=!@#$"` 106 | Excludes string `valid:"excludes=text"` 107 | ExcludesAll string `valid:"excludesall=!@#$"` 108 | ExcludesRune string `valid:"excludesrune=☻"` 109 | ISBN string `valid:"isbn"` 110 | ISBN10 string `valid:"isbn10"` 111 | ISBN13 string `valid:"isbn13"` 112 | UUID string `valid:"uuid"` 113 | UUID3 string `valid:"uuid3"` 114 | UUID4 string `valid:"uuid4"` 115 | UUID5 string `valid:"uuid5"` 116 | ASCII string `valid:"ascii"` 117 | PrintableASCII string `valid:"printascii"` 118 | MultiByte string `valid:"multibyte"` 119 | DataURI string `valid:"datauri"` 120 | Latitude string `valid:"latitude"` 121 | Longitude string `valid:"longitude"` 122 | SSN string `valid:"ssn"` 123 | IP string `valid:"ip"` 124 | IPv4 string `valid:"ipv4"` 125 | IPv6 string `valid:"ipv6"` 126 | CIDR string `valid:"cidr"` 127 | CIDRv4 string `valid:"cidrv4"` 128 | CIDRv6 string `valid:"cidrv6"` 129 | TCPAddr string `valid:"tcp_addr"` 130 | TCPAddrv4 string `valid:"tcp4_addr"` 131 | TCPAddrv6 string `valid:"tcp6_addr"` 132 | UDPAddr string `valid:"udp_addr"` 133 | UDPAddrv4 string `valid:"udp4_addr"` 134 | UDPAddrv6 string `valid:"udp6_addr"` 135 | IPAddr string `valid:"ip_addr"` 136 | IPAddrv4 string `valid:"ip4_addr"` 137 | IPAddrv6 string `valid:"ip6_addr"` 138 | UnixAddr string `valid:"unix_addr"` // can't fail from within Go's net package currently, but maybe in the future 139 | MAC string `valid:"mac"` 140 | IsColor string `valid:"iscolor"` 141 | MinIntPtr *int64 `valid:"required,min=1234"` 142 | InnerDive Inner `valid:"dive"` 143 | InnerDivePtr *Inner `valid:"dive"` 144 | InnerDiveSlice []Inner `valid:"dive"` 145 | InnerDiveSlicePtr []*Inner `valid:"dive"` 146 | InnerDiveMap map[string]Inner `valid:"dive"` 147 | InnerDiveMapPtr map[string]*Inner `valid:"dive"` 148 | OtherFileDive OtherFile `valid:"dive"` 149 | OtherFileDivePtr *OtherFile `valid:"dive"` 150 | OtherFileDiveSlice []OtherFile `valid:"dive"` 151 | OtherFileDiveSlicePtr []*OtherFile `valid:"dive"` 152 | OtherFileDiveMap map[string]OtherFile `valid:"dive"` 153 | OtherFileDiveMapPtr map[string]*OtherFile `valid:"dive"` 154 | MapContains map[string]interface{} `valid:"contains=key"` 155 | TestString benchmark.TestString `valid:"dive"` 156 | TestStringPtr *benchmark.TestString `valid:"dive"` 157 | } 158 | -------------------------------------------------------------------------------- /generator/example_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/abice/gencheck" 8 | ) 9 | 10 | type NoImplicitvalid struct { 11 | StringField string 12 | 13 | NonStringKeyMap map[int]Testvalid 14 | NestedNonStringKeyMap map[string]map[int]map[string]*Testvalid 15 | NestedMapWithNonStructValue map[string]map[string]map[string]*int64 16 | 17 | SliceOfBuiltins []string 18 | NestedSliceofBuiltins [][][]*int 19 | } 20 | 21 | type HasvalidImplicit struct { 22 | InvalidStruct *Testvalid 23 | ValidStruct AlwaysValid 24 | 25 | MapOfStruct map[string]Testvalid 26 | MapOfStructPtrs map[string]*Testvalid 27 | MapOfMaps map[string]map[string]*Testvalid 28 | MapMapsOfSlices map[string]map[string][]*Testvalid 29 | MapOfInterfaces map[string]interface{} 30 | 31 | SimpleSlice []*Testvalid 32 | SliceOfSlicesOfSlices [][][]*Testvalid 33 | 34 | MapOfSlicesOfMaps map[string][]map[string]*Testvalid 35 | } 36 | 37 | // Testvalid 38 | type Testvalid struct { 39 | Valid bool 40 | } 41 | 42 | // valid 43 | func (s Testvalid) valid() error { 44 | if !s.Valid { 45 | return gencheck.ValidationErrors{gencheck.NewFieldError("TestValid", "Valid", "", fmt.Errorf("invalid when false"))} 46 | } 47 | return nil 48 | } 49 | 50 | // AlwaysValid 51 | type AlwaysValid struct{} 52 | 53 | // valid 54 | func (s AlwaysValid) valid() error { 55 | return nil 56 | } 57 | 58 | // Example 59 | type MapContains struct { 60 | MapOfInterfaces map[string]interface{} `valid:"contains=myKey"` 61 | } 62 | 63 | // Example 64 | type Example struct { 65 | MapOfInterfaces map[string]interface{} `valid:"notnil"` 66 | } 67 | 68 | type Inner struct { 69 | EqCSFieldString string 70 | NeCSFieldString string 71 | GtCSFieldString string 72 | GteCSFieldString string 73 | LtCSFieldString string 74 | LteCSFieldString string 75 | } 76 | 77 | type Embedded struct { 78 | FieldString string `valid:"required"` 79 | } 80 | 81 | type Test struct { 82 | Embedded `valid:"dive"` 83 | Inner Inner 84 | RequiredString string `valid:"required"` 85 | RequiredMultiple []string `valid:"required"` 86 | LenString string `valid:"len=1"` 87 | LenNumber float64 `valid:"len=1113.00"` 88 | LenMultiple []string `valid:"len=7"` 89 | MinString string `valid:"min=1"` 90 | MinNumber float64 `valid:"min=1113.00"` 91 | MinMultiple []string `valid:"min=7"` 92 | MaxString string `valid:"max=3"` 93 | MaxNumber float64 `valid:"max=1113.00"` 94 | MaxMultiple []string `valid:"max=7"` 95 | EqString string `valid:"eq=3"` 96 | EqNumber float64 `valid:"eq=2.33"` 97 | EqMultiple []string `valid:"eq=7"` 98 | NeString string `valid:"ne="` 99 | NeNumber float64 `valid:"ne=0.00"` 100 | NeMultiple []string `valid:"ne=0"` 101 | LtString string `valid:"lt=3"` 102 | LtNumber float64 `valid:"lt=5.56"` 103 | LtMultiple []string `valid:"lt=2"` 104 | LtTime time.Time `valid:"lt"` 105 | LteString string `valid:"lte=3"` 106 | LteNumber float64 `valid:"lte=5.56"` 107 | LteMultiple []string `valid:"lte=2"` 108 | LteTime time.Time `valid:"lte"` 109 | GtString string `valid:"gt=3"` 110 | GtNumber float64 `valid:"gt=5.56"` 111 | GtMultiple []string `valid:"gt=2"` 112 | GtTime time.Time `valid:"gt"` 113 | GteString string `valid:"gte=3"` 114 | GteNumber float64 `valid:"gte=5.56"` 115 | GteMultiple []string `valid:"gte=2"` 116 | GteTime time.Time `valid:"gte"` 117 | EqFieldString string `valid:"eqfield=MaxString"` 118 | EqCSFieldString string `valid:"eqcsfield=Inner.EqCSFieldString"` 119 | NeCSFieldString string `valid:"necsfield=Inner.NeCSFieldString"` 120 | GtCSFieldString string `valid:"gtcsfield=Inner.GtCSFieldString"` 121 | GteCSFieldString string `valid:"gtecsfield=Inner.GteCSFieldString"` 122 | LtCSFieldString string `valid:"ltcsfield=Inner.LtCSFieldString"` 123 | LteCSFieldString string `valid:"ltecsfield=Inner.LteCSFieldString"` 124 | NeFieldString string `valid:"nefield=EqFieldString"` 125 | GtFieldString string `valid:"gtfield=MaxString"` 126 | GteFieldString string `valid:"gtefield=MaxString"` 127 | LtFieldString string `valid:"ltfield=MaxString"` 128 | LteFieldString string `valid:"ltefield=MaxString"` 129 | AlphaString string `valid:"alpha"` 130 | AlphanumString string `valid:"alphanum"` 131 | NumericString string `valid:"numeric"` 132 | NumberString string `valid:"number"` 133 | HexadecimalString string `valid:"hexadecimal"` 134 | HexColorString string `valid:"hexcolor"` 135 | RGBColorString string `valid:"rgb"` 136 | RGBAColorString string `valid:"rgba"` 137 | HSLColorString string `valid:"hsl"` 138 | HSLAColorString string `valid:"hsla"` 139 | Email string `valid:"email"` 140 | URL string `valid:"url"` 141 | URI string `valid:"uri"` 142 | Base64 string `valid:"base64"` 143 | Contains string `valid:"contains=purpose"` 144 | ContainsAny string `valid:"containsany=!@#$"` 145 | Excludes string `valid:"excludes=text"` 146 | ExcludesAll string `valid:"excludesall=!@#$"` 147 | ExcludesRune string `valid:"excludesrune=☻"` 148 | ISBN string `valid:"isbn"` 149 | ISBN10 string `valid:"isbn10"` 150 | ISBN13 string `valid:"isbn13"` 151 | UUID string `valid:"uuid"` 152 | UUID3 string `valid:"uuid3"` 153 | UUID4 string `valid:"uuid4"` 154 | UUID5 string `valid:"uuid5"` 155 | ASCII string `valid:"ascii"` 156 | PrintableASCII string `valid:"printascii"` 157 | MultiByte string `valid:"multibyte"` 158 | DataURI string `valid:"datauri"` 159 | Latitude string `valid:"latitude"` 160 | Longitude string `valid:"longitude"` 161 | SSN string `valid:"ssn"` 162 | IP string `valid:"ip"` 163 | IPv4 string `valid:"ipv4"` 164 | IPv6 string `valid:"ipv6"` 165 | CIDR string `valid:"cidr"` 166 | CIDRPtr *string `valid:"cidr"` 167 | CIDRv4 string `valid:"cidrv4"` 168 | CIDRv6 string `valid:"cidrv6"` 169 | TCPAddr string `valid:"tcp_addr"` 170 | TCPAddrv4 string `valid:"tcp4_addr"` 171 | TCPAddrv6 string `valid:"tcp6_addr"` 172 | UDPAddr string `valid:"udp_addr"` 173 | UDPAddrv4 string `valid:"udp4_addr"` 174 | UDPAddrv6 string `valid:"udp6_addr"` 175 | IPAddr string `valid:"ip_addr"` 176 | IPAddrv4 string `valid:"ip4_addr"` 177 | IPAddrv6 string `valid:"ip6_addr"` 178 | UnixAddr string `valid:"unix_addr"` // can't fail from within Go's net package currently, but maybe in the future 179 | MAC string `valid:"mac"` 180 | IsColor string `valid:"iscolor"` 181 | InnerDive Inner `valid:"dive"` 182 | InnerDivePtr *Inner `valid:"dive"` 183 | InnerDiveSlice []Inner `valid:"dive"` 184 | InnerDiveSlicePtr []*Inner `valid:"dive"` 185 | InnerDiveMap map[string]Inner `valid:"dive"` 186 | InnerDiveMapPtr map[string]*Inner `valid:"dive"` 187 | OtherFileDive OtherFile `valid:"dive"` 188 | OtherFileDivePtr *OtherFile `valid:"dive"` 189 | OtherFileDiveSlice []OtherFile `valid:"dive"` 190 | OtherFileDiveSlicePtr []*OtherFile `valid:"dive"` 191 | OtherFileDiveMap map[string]OtherFile `valid:"dive"` 192 | OtherFileDiveMapPtr map[string]*OtherFile `valid:"dive,ff"` 193 | } 194 | -------------------------------------------------------------------------------- /internal/benchmark/types_validators.go: -------------------------------------------------------------------------------- 1 | // Code generated by gencheck 2 | // DO NOT EDIT! 3 | 4 | package benchmark 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/abice/gencheck" 14 | ) 15 | 16 | // Validate is an automatically generated validation method provided by 17 | // gencheck. 18 | // See https://github.com/abice/gencheck for more details. 19 | func (s SingleString) Validate() error { 20 | 21 | vErrors := make(gencheck.ValidationErrors, 0, 1) 22 | 23 | // BEGIN Entry Validations 24 | // required 25 | if s.Entry == "" { 26 | vErrors = append(vErrors, gencheck.NewFieldError("SingleString", "Entry", "required", errors.New("is required"))) 27 | } 28 | // END Entry Validations 29 | 30 | if len(vErrors) > 0 { 31 | return vErrors 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // Validate is an automatically generated validation method provided by 38 | // gencheck. 39 | // See https://github.com/abice/gencheck for more details. 40 | func (s TestAll) Validate() error { 41 | 42 | vErrors := make(gencheck.ValidationErrors, 0, 17) 43 | 44 | // BEGIN Required Validations 45 | // required 46 | if s.Required == "" { 47 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Required", "required", errors.New("is required"))) 48 | } 49 | // END Required Validations 50 | 51 | // BEGIN Len Validations 52 | // len 53 | if !(len(s.Len) == 10) { 54 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Len", "len", errors.New("length mismatch"))) 55 | } 56 | // END Len Validations 57 | 58 | // BEGIN Min Validations 59 | // min 60 | if len(s.Min) < 5 { 61 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Min", "min", errors.New("length failed check for min=5"))) 62 | } 63 | // END Min Validations 64 | 65 | // BEGIN Max Validations 66 | // max 67 | if len(s.Max) > 100 { 68 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Max", "max", errors.New("length failed check for max=100"))) 69 | } 70 | // END Max Validations 71 | 72 | // BEGIN CIDR Validations 73 | // required 74 | if s.CIDR == "" { 75 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "CIDR", "required", errors.New("is required"))) 76 | } 77 | 78 | // cidr 79 | _, _, CIDRerr := net.ParseCIDR(s.CIDR) 80 | if CIDRerr != nil { 81 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "CIDR", "cidr", CIDRerr)) 82 | } 83 | // END CIDR Validations 84 | 85 | // BEGIN LteTime Validations 86 | // lte 87 | tLteTime := time.Now().UTC() 88 | if s.LteTime.After(tLteTime) { 89 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "LteTime", "lte", errors.New("is after now"))) 90 | } 91 | // END LteTime Validations 92 | 93 | // BEGIN GteTime Validations 94 | // gte 95 | tGteTime := time.Now().UTC() 96 | if s.GteTime.Before(tGteTime) { 97 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "GteTime", "gte", errors.New("is before now"))) 98 | } 99 | // END GteTime Validations 100 | 101 | // BEGIN Gte Validations 102 | // gte 103 | if s.Gte < 1.2345 { 104 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Gte", "gte", errors.New("failed check for gte=1.2345"))) 105 | } 106 | // END Gte Validations 107 | 108 | // BEGIN NotNil Validations 109 | // required 110 | if s.NotNil == nil { 111 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "NotNil", "required", errors.New("is required"))) 112 | } 113 | // END NotNil Validations 114 | 115 | // BEGIN Contains Validations 116 | // contains 117 | if !strings.Contains(s.Contains, "fox") { 118 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Contains", "contains", errors.New("Contains did not contain fox"))) 119 | } 120 | // END Contains Validations 121 | 122 | // BEGIN Hex Validations 123 | // hex 124 | if err := gencheck.IsHex(&s.Hex); err != nil { 125 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Hex", "hex", err)) 126 | } 127 | // END Hex Validations 128 | 129 | // BEGIN UUID Validations 130 | // uuid 131 | if err := gencheck.IsUUID(&s.UUID); err != nil { 132 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "UUID", "uuid", err)) 133 | } 134 | // END UUID Validations 135 | 136 | // BEGIN MinInt Validations 137 | // min 138 | if s.MinInt < 12345 { 139 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "MinInt", "min", errors.New("failed check for min=12345"))) 140 | } 141 | // END MinInt Validations 142 | 143 | // BEGIN MaxInt Validations 144 | // max 145 | if s.MaxInt > 12345 { 146 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "MaxInt", "max", errors.New("failed check for max=12345"))) 147 | } 148 | // END MaxInt Validations 149 | 150 | // BEGIN Dive Validations 151 | // required 152 | if s.Dive == nil { 153 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Dive", "required", errors.New("is required"))) 154 | } 155 | 156 | // dive 157 | if s.Dive != nil { 158 | if err := gencheck.Validate(s.Dive); err != nil { 159 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "Dive", "dive", err)) 160 | } 161 | } 162 | // END Dive Validations 163 | 164 | // BEGIN URL Validations 165 | // url 166 | if s.URL != "" { 167 | URLURL, URLurlerr := url.ParseRequestURI(s.URL) 168 | if URLurlerr != nil { 169 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "URL", "url", URLurlerr)) 170 | } else if URLURL.Scheme == "" { 171 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "URL", "url", errors.New("URL is missing a scheme"))) 172 | } 173 | } 174 | // END URL Validations 175 | 176 | // BEGIN URI Validations 177 | // uri 178 | if s.URI != "" { 179 | _, URIurierr := url.ParseRequestURI(s.URI) 180 | if URIurierr != nil { 181 | vErrors = append(vErrors, gencheck.NewFieldError("TestAll", "URI", "uri", URIurierr)) 182 | } 183 | } 184 | // END URI Validations 185 | 186 | if len(vErrors) > 0 { 187 | return vErrors 188 | } 189 | 190 | return nil 191 | } 192 | 193 | // Validate is an automatically generated validation method provided by 194 | // gencheck. 195 | // See https://github.com/abice/gencheck for more details. 196 | func (s TestContainsAny) Validate() error { 197 | 198 | vErrors := make(gencheck.ValidationErrors, 0, 1) 199 | 200 | // BEGIN Any Validations 201 | // containsany 202 | if !strings.ContainsAny(s.Any, "@#!") { 203 | vErrors = append(vErrors, gencheck.NewFieldError("TestContainsAny", "Any", "containsany", errors.New("Any did not contain any of @#!"))) 204 | } 205 | // END Any Validations 206 | 207 | if len(vErrors) > 0 { 208 | return vErrors 209 | } 210 | 211 | return nil 212 | } 213 | 214 | // Validate is an automatically generated validation method provided by 215 | // gencheck. 216 | // See https://github.com/abice/gencheck for more details. 217 | func (s TestDive) Validate() error { 218 | 219 | vErrors := make(gencheck.ValidationErrors, 0, 1) 220 | 221 | // BEGIN Value Validations 222 | // required 223 | if s.Value == nil { 224 | vErrors = append(vErrors, gencheck.NewFieldError("TestDive", "Value", "required", errors.New("is required"))) 225 | } 226 | 227 | // dive 228 | if s.Value != nil { 229 | if err := gencheck.Validate(s.Value); err != nil { 230 | vErrors = append(vErrors, gencheck.NewFieldError("TestDive", "Value", "dive", err)) 231 | } 232 | } 233 | // END Value Validations 234 | 235 | if len(vErrors) > 0 { 236 | return vErrors 237 | } 238 | 239 | return nil 240 | } 241 | 242 | // Validate is an automatically generated validation method provided by 243 | // gencheck. 244 | // See https://github.com/abice/gencheck for more details. 245 | func (s TestHex) Validate() error { 246 | 247 | vErrors := make(gencheck.ValidationErrors, 0, 1) 248 | 249 | // BEGIN Value Validations 250 | // hex 251 | if err := gencheck.IsHex(&s.Value); err != nil { 252 | vErrors = append(vErrors, gencheck.NewFieldError("TestHex", "Value", "hex", err)) 253 | } 254 | // END Value Validations 255 | 256 | if len(vErrors) > 0 { 257 | return vErrors 258 | } 259 | 260 | return nil 261 | } 262 | 263 | // Validate is an automatically generated validation method provided by 264 | // gencheck. 265 | // See https://github.com/abice/gencheck for more details. 266 | func (s TestMap) Validate() error { 267 | 268 | vErrors := make(gencheck.ValidationErrors, 0, 1) 269 | 270 | // BEGIN Value Validations 271 | // contains 272 | if _, foundValue := s.Value["test"]; !foundValue { 273 | vErrors = append(vErrors, gencheck.NewFieldError("TestMap", "Value", "contains", errors.New("Value did not contain test"))) 274 | } 275 | // END Value Validations 276 | 277 | if len(vErrors) > 0 { 278 | return vErrors 279 | } 280 | 281 | return nil 282 | } 283 | 284 | // Validate is an automatically generated validation method provided by 285 | // gencheck. 286 | // See https://github.com/abice/gencheck for more details. 287 | func (s TestString) Validate() error { 288 | 289 | vErrors := make(gencheck.ValidationErrors, 0, 4) 290 | 291 | // BEGIN Required Validations 292 | // required 293 | if s.Required == "" { 294 | vErrors = append(vErrors, gencheck.NewFieldError("TestString", "Required", "required", errors.New("is required"))) 295 | } 296 | // END Required Validations 297 | 298 | // BEGIN Len Validations 299 | // len 300 | if !(len(s.Len) == 10) { 301 | vErrors = append(vErrors, gencheck.NewFieldError("TestString", "Len", "len", errors.New("length mismatch"))) 302 | } 303 | // END Len Validations 304 | 305 | // BEGIN Min Validations 306 | // min 307 | if len(s.Min) < 5 { 308 | vErrors = append(vErrors, gencheck.NewFieldError("TestString", "Min", "min", errors.New("length failed check for min=5"))) 309 | } 310 | // END Min Validations 311 | 312 | // BEGIN Max Validations 313 | // max 314 | if len(s.Max) > 100 { 315 | vErrors = append(vErrors, gencheck.NewFieldError("TestString", "Max", "max", errors.New("length failed check for max=100"))) 316 | } 317 | // END Max Validations 318 | 319 | if len(vErrors) > 0 { 320 | return vErrors 321 | } 322 | 323 | return nil 324 | } 325 | 326 | // Validate is an automatically generated validation method provided by 327 | // gencheck. 328 | // See https://github.com/abice/gencheck for more details. 329 | func (s TestUUID) Validate() error { 330 | 331 | vErrors := make(gencheck.ValidationErrors, 0, 1) 332 | 333 | // BEGIN UUID Validations 334 | // required 335 | if s.UUID == "" { 336 | vErrors = append(vErrors, gencheck.NewFieldError("TestUUID", "UUID", "required", errors.New("is required"))) 337 | } 338 | 339 | // uuid 340 | if err := gencheck.IsUUID(&s.UUID); err != nil { 341 | vErrors = append(vErrors, gencheck.NewFieldError("TestUUID", "UUID", "uuid", err)) 342 | } 343 | // END UUID Validations 344 | 345 | if len(vErrors) > 0 { 346 | return vErrors 347 | } 348 | 349 | return nil 350 | } 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /generator/gencheck.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/printer" 9 | "go/token" 10 | "sort" 11 | "strings" 12 | "text/template" 13 | 14 | "github.com/Masterminds/sprig" 15 | "golang.org/x/tools/imports" 16 | ) 17 | 18 | const ( 19 | validateTag = `valid:` 20 | failFastFlag = `ff` 21 | ) 22 | 23 | // Generator is responsible for generating validation files for the given in a go source file. 24 | type Generator struct { 25 | t *template.Template 26 | knownTemplates map[string]*template.Template 27 | fileSet *token.FileSet 28 | generatePointerMethod bool 29 | failFast bool 30 | noPrealloc bool 31 | knownStructs map[string]*ast.StructType 32 | } 33 | 34 | // NewGenerator is a constructor method for creating a new Generator with default 35 | // templates loaded. 36 | func NewGenerator() *Generator { 37 | g := &Generator{ 38 | knownTemplates: make(map[string]*template.Template), 39 | t: template.New("generator"), 40 | fileSet: token.NewFileSet(), 41 | generatePointerMethod: false, 42 | failFast: false, 43 | noPrealloc: false, 44 | knownStructs: map[string]*ast.StructType{}, 45 | } 46 | 47 | funcs := sprig.TxtFuncMap() 48 | 49 | funcs["CallTemplate"] = g.CallTemplate 50 | funcs["isPtr"] = isPtr 51 | funcs["addError"] = addFieldError 52 | funcs["addIndexedError"] = addIndexedFieldError 53 | funcs["isNullable"] = isNullable 54 | funcs["isMap"] = isMap 55 | funcs["isArray"] = isArray 56 | funcs["generationError"] = GenerationError 57 | funcs["isStruct"] = g.tmplIsStruct 58 | funcs["isStructPtr"] = g.tmplIsStructPtr 59 | funcs["getMapKeyType"] = g.tmplGetMapKeyType 60 | funcs["isParamInt"] = tmplIsParamInt 61 | funcs["accessor"] = accessor 62 | 63 | g.t.Funcs(funcs) 64 | 65 | for _, assets := range AssetNames() { 66 | g.t = template.Must(g.t.Parse(string(MustAsset(assets)))) 67 | } 68 | 69 | g.updateTemplates() 70 | 71 | return g 72 | } 73 | 74 | // WithPointerMethod is used to change the method generated for a struct to use a pointer receiver rather than a value receiver. 75 | func (g *Generator) WithPointerMethod() *Generator { 76 | g.generatePointerMethod = true 77 | return g 78 | } 79 | 80 | // WithFailFast is used to change all error checks to return immediately on failure. 81 | func (g *Generator) WithFailFast() *Generator { 82 | g.failFast = true 83 | return g 84 | } 85 | 86 | // WithoutPrealloc is used to remove preallocation of error array. 87 | func (g *Generator) WithoutPrealloc() *Generator { 88 | g.noPrealloc = true 89 | return g 90 | } 91 | 92 | // CallTemplate is a helper method for the template to call a parsed template but with 93 | // a dynamic name. 94 | func (g *Generator) CallTemplate(rule Validation, data interface{}) (ret string, err error) { 95 | buf := bytes.NewBuffer([]byte{}) 96 | 97 | // We don't need an else statement here because we already filter out unknown templates 98 | // during the parsing phase. 99 | if _, found := g.knownTemplates[rule.Name]; found { 100 | err = g.t.ExecuteTemplate(buf, rule.Name, data) 101 | } 102 | ret = buf.String() 103 | 104 | return 105 | } 106 | 107 | // GenerateFromFile is responsible for orchestrating the Code generation. It results in a byte array 108 | // that can be written to any file desired. It has already had goimports run on the code before being returned. 109 | func (g *Generator) GenerateFromFile(inputFile string) ([]byte, error) { 110 | f, err := g.parseFile(inputFile) 111 | if err != nil { 112 | return nil, fmt.Errorf("generate: error parsing input file '%s': %s", inputFile, err) 113 | } 114 | return g.Generate(f) 115 | 116 | } 117 | 118 | // AddSupportFiles will add the file to the ast.FileSet for parsing, but won't generate anything for the file. 119 | func (g *Generator) AddSupportFiles(inputFiles ...string) error { 120 | for _, inputFile := range inputFiles { 121 | f, err := g.parseFile(inputFile) 122 | if err != nil { 123 | return err 124 | } 125 | // Add known structs to the context 126 | g.inspect(f) 127 | } 128 | return nil 129 | } 130 | 131 | type byPosition []*ast.Field 132 | 133 | func (a byPosition) Len() int { return len(a) } 134 | func (a byPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 135 | func (a byPosition) Less(i, j int) bool { return a[i].Pos() < a[j].Pos() } 136 | 137 | func (g *Generator) expandEmbeddedFields(structs map[string]*ast.StructType, fields []*ast.Field) []*ast.Field { 138 | sort.Sort(byPosition(fields)) 139 | 140 | actualFieldList := make([]*ast.Field, 0, len(fields)) 141 | for _, field := range fields { 142 | if len(field.Names) < 1 { 143 | fieldName := g.getStringForExpr(field.Type) 144 | if embedded, ok := g.knownStructs[fieldName]; ok { 145 | expanded := g.expandEmbeddedFields(g.knownStructs, embedded.Fields.List) 146 | actualFieldList = append(actualFieldList, expanded...) 147 | } 148 | } else { 149 | actualFieldList = append(actualFieldList, field) 150 | } 151 | } 152 | 153 | return actualFieldList 154 | } 155 | 156 | // Generate does the heavy lifting for the code generation starting from the parsed AST file. 157 | func (g *Generator) Generate(f *ast.File) ([]byte, error) { 158 | var err error 159 | structs := g.inspect(f) 160 | if len(structs) <= 0 { 161 | return nil, nil 162 | } 163 | 164 | pkg := f.Name.Name 165 | 166 | vBuff := bytes.NewBuffer([]byte{}) 167 | g.t.ExecuteTemplate(vBuff, "header", map[string]interface{}{"package": pkg}) 168 | 169 | // Make the output more consistent by iterating over sorted keys of map 170 | var keys []string 171 | for key := range structs { 172 | keys = append(keys, key) 173 | } 174 | sort.Strings(keys) 175 | 176 | for _, name := range keys { 177 | st := structs[name] 178 | 179 | var rules []Field 180 | 181 | // Only expands when we find an embedded struct 182 | fieldList := g.expandEmbeddedFields(structs, st.Fields.List) 183 | 184 | // Go through the fields in the struct and find all the validated tags 185 | for _, field := range fieldList { 186 | fieldName := `` 187 | // Support for embedded structs that don't have a field name associated with it 188 | if len(field.Names) > 0 { 189 | fieldName = field.Names[0].Name 190 | } else { 191 | fieldName = g.getStringForExpr(field.Type) 192 | } 193 | // Make a field holder 194 | f := Field{ 195 | F: field, 196 | Name: fieldName, 197 | } 198 | g.getTypeString(&f) 199 | 200 | // Skip untagged fields 201 | if field.Tag == nil { 202 | continue 203 | } 204 | 205 | // Skip fields that aren't validated 206 | if !strings.Contains(field.Tag.Value, validateTag) { 207 | continue 208 | } 209 | 210 | // The AST keeps the rune marker on the string, so we trim them off 211 | str := strings.Trim(field.Tag.Value, "`") 212 | // Separate tag types are separated by spaces, so split on that 213 | vals := strings.Split(str, " ") 214 | for _, val := range vals { 215 | // Only parse out the valid: tag 216 | if !strings.HasPrefix(val, validateTag) { 217 | continue 218 | } 219 | // Strip off the valid: prefix and the quotation marks 220 | ruleStr := val[len(validateTag)+1 : len(val)-1] 221 | // Split on commas for multiple validations 222 | fieldRules := strings.Split(ruleStr, ",") 223 | 224 | parseErr := g.parseFieldRules(&f, name, fieldRules) 225 | if parseErr != nil { 226 | return nil, parseErr 227 | } 228 | } 229 | 230 | // If we have any rules for the field, add it to the map 231 | if len(f.Rules) > 0 { 232 | rules = append(rules, f) 233 | } 234 | } 235 | 236 | data := map[string]interface{}{ 237 | "st": st, 238 | "name": name, 239 | "rules": rules, 240 | "ptrMethod": g.generatePointerMethod, 241 | "globalFailFast": g.failFast, 242 | "prealloc": !g.noPrealloc, 243 | } 244 | 245 | if len(rules) > 0 { 246 | err = g.t.ExecuteTemplate(vBuff, "struct", data) 247 | 248 | if err != nil { 249 | if te, ok := err.(template.ExecError); ok { 250 | return nil, fmt.Errorf("generate: error executing template: %s", te.Err) 251 | } 252 | 253 | return nil, fmt.Errorf("generate: error executing template: %s", err) 254 | } 255 | } 256 | } 257 | 258 | formatted, err := imports.Process(pkg, vBuff.Bytes(), nil) 259 | if err != nil { 260 | err = fmt.Errorf("generate: error formatting code %s\n\n%s\n", err, vBuff.String()) 261 | } 262 | return formatted, err 263 | } 264 | 265 | func (g *Generator) parseFieldRules(f *Field, structName string, fieldRules []string) error { 266 | // Store the validation as the duplication check so that *if* in the future we want to support certain rules as 267 | // having duplicates, we can do that easier. 268 | dupecheck := make(map[string]Validation) 269 | for _, rule := range fieldRules { 270 | // Check for fail fast and ignore that rule 271 | if rule == failFastFlag { 272 | f.FailFast = true 273 | continue 274 | } 275 | // Rules are able to have parameters, 276 | // but will have an = in them if that is the case. 277 | 278 | v := Validation{ 279 | Name: rule, 280 | FieldName: f.Name, 281 | F: f.F, 282 | FieldType: f.Type, 283 | StructName: structName, 284 | Prealloc: g.noPrealloc, 285 | } 286 | 287 | if strings.Contains(rule, `=`) { 288 | // There is a parameter, so get the rule name, and the parameter 289 | temp := strings.Split(rule, `=`) 290 | v.Name = temp[0] 291 | v.Param = temp[1] 292 | } 293 | 294 | // Only keep the rule if it is a known template 295 | if _, ok := g.knownTemplates[v.Name]; ok { 296 | // Make sure it's not a duplicate rule 297 | if _, isDupe := dupecheck[v.Name]; isDupe { 298 | return fmt.Errorf("Duplicate rules are not allowed: '%s' on field '%s'", v.Name, f.Name) 299 | } 300 | dupecheck[v.Name] = v 301 | 302 | f.Rules = append(f.Rules, v) 303 | } else { 304 | fmt.Printf("Skipping unknown validation template: '%s'\n", v.Name) 305 | } 306 | } 307 | 308 | if f.FailFast || g.failFast { 309 | for index, val := range f.Rules { 310 | val.FailFast = true 311 | f.Rules[index] = val 312 | } 313 | } 314 | 315 | return nil 316 | } 317 | 318 | func (g *Generator) getTypeString(f *Field) { 319 | tString := g.getStringForExpr(f.F.Type) 320 | f.Type = tString 321 | } 322 | 323 | func (g *Generator) getStringForExpr(f ast.Expr) string { 324 | typeBuff := bytes.NewBuffer([]byte{}) 325 | pErr := printer.Fprint(typeBuff, g.fileSet, f) 326 | if pErr != nil { 327 | fmt.Printf("Error getting Type: %s\n", pErr) 328 | } 329 | return typeBuff.String() 330 | } 331 | 332 | // AddTemplateFiles will be used during generation when the command line accepts 333 | // user templates to add to the generation. 334 | func (g *Generator) AddTemplateFiles(filenames ...string) (err error) { 335 | g.t, err = g.t.ParseFiles(filenames...) 336 | if err == nil { 337 | g.updateTemplates() 338 | } 339 | return 340 | } 341 | 342 | // updateTemplates will update the lookup map for validation checks that are 343 | // allowed within the template engine. 344 | func (g *Generator) updateTemplates() { 345 | for _, template := range g.t.Templates() { 346 | g.knownTemplates[template.Name()] = template 347 | } 348 | } 349 | 350 | // parseFile simply calls the go/parser ParseFile function with an empty token.FileSet 351 | func (g *Generator) parseFile(fileName string) (*ast.File, error) { 352 | // Parse the file given in arguments 353 | return parser.ParseFile(g.fileSet, fileName, nil, parser.ParseComments) 354 | } 355 | 356 | // inspect will walk the ast and fill a map of names and their struct information 357 | // for use in the generation template. 358 | func (g *Generator) inspect(f *ast.File) map[string]*ast.StructType { 359 | 360 | structs := make(map[string]*ast.StructType) 361 | // Inspect the AST and find all structs. 362 | ast.Inspect(f, func(n ast.Node) bool { 363 | switch x := n.(type) { 364 | case *ast.Ident: 365 | if x.Obj == nil { 366 | return true 367 | } 368 | // Make sure it's a Type Identifier 369 | if x.Obj.Kind != ast.Typ { 370 | return true 371 | } 372 | // Make sure it's a spec (Type Identifiers can be throughout the code) 373 | ts, ok := x.Obj.Decl.(*ast.TypeSpec) 374 | if !ok { 375 | return true 376 | } 377 | // Only store the struct types (we don't do anything for interfaces) 378 | if sts, store := ts.Type.(*ast.StructType); store { 379 | structs[x.Name] = sts 380 | g.knownStructs[x.Name] = sts 381 | } 382 | } 383 | // Return true to continue through the tree 384 | return true 385 | }) 386 | 387 | return structs 388 | } 389 | -------------------------------------------------------------------------------- /internal/example/example_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/abice/gencheck" 10 | "github.com/abice/gencheck/internal/benchmark" 11 | "github.com/google/uuid" 12 | "github.com/pkg/errors" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/suite" 15 | ) 16 | 17 | // ExampleTestSuite: Run this test suite AFTER running gokay against example.go 18 | type ExampleTestSuite struct { 19 | suite.Suite 20 | } 21 | 22 | // TestExampleTestSuite 23 | func TestExampleTestSuite(t *testing.T) { 24 | suite.Run(t, new(ExampleTestSuite)) 25 | } 26 | 27 | // TestValidateTestStruct_NoValues 28 | func (s *ExampleTestSuite) TestValidateTestStruct_FailFast() { 29 | expected := gencheck.ValidationErrors{ 30 | gencheck.NewFieldError("Test", "RequiredString", "required", fmt.Errorf("is required")), 31 | } 32 | 33 | underTest := Test{ 34 | ExternalEmbedded: ExternalEmbedded{EmbeddedString: `abcd`}, 35 | Embedded: Embedded{FieldString: `1234`}, 36 | MaxString: "1234", 37 | MaxNumber: 1113.00001, 38 | MaxMultiple: []string{"", "", "", "", "", "", "", "", ""}, 39 | } 40 | 41 | err := underTest.Validate() 42 | s.Require().IsType(gencheck.ValidationErrors{}, err, "Error returned was not ValidationErrors type") 43 | 44 | ve := err.(gencheck.ValidationErrors) 45 | 46 | s.Require().Equal(len(expected), len(ve), "Validation Errors were of different length") 47 | 48 | for i, fe := range ve { 49 | s.Equal(expected[i], fe) 50 | } 51 | } 52 | 53 | // TestValidateTestStruct_NoValues 54 | func (s *ExampleTestSuite) TestValidateTestStruct_NoValues() { 55 | expected := gencheck.ValidationErrors{ 56 | gencheck.NewFieldError("Test", "FieldString", "required", fmt.Errorf("is required")), 57 | gencheck.NewFieldError("Test", "EmbeddedString", "required", fmt.Errorf("is required")), 58 | gencheck.NewFieldError("Test", "RequiredMultiple", "required", fmt.Errorf("is required")), 59 | gencheck.NewFieldError("Test", "LenString", "len", fmt.Errorf("length mismatch")), 60 | gencheck.NewFieldError("Test", "LenNumber", "len", fmt.Errorf("length mismatch")), 61 | gencheck.NewFieldError("Test", "LenMultiple", "len", fmt.Errorf("length mismatch")), 62 | gencheck.NewFieldError("Test", "MinString", "min", fmt.Errorf("length failed check for min=1")), 63 | gencheck.NewFieldError("Test", "MinNumber", "min", fmt.Errorf("failed check for min=1113.00")), 64 | gencheck.NewFieldError("Test", "MinMultiple", "min", fmt.Errorf("length failed check for min=7")), 65 | gencheck.NewFieldError("Test", "MaxString", "max", fmt.Errorf("length failed check for max=3")), 66 | gencheck.NewFieldError("Test", "MaxNumber", "max", fmt.Errorf("failed check for max=1113.00")), 67 | gencheck.NewFieldError("Test", "MaxMultiple", "max", fmt.Errorf("length failed check for max=7")), 68 | gencheck.NewFieldError("Test", "EqString", "eq", fmt.Errorf("EqString did not equal 3")), 69 | gencheck.NewFieldError("Test", "EqNumber", "eq", fmt.Errorf("EqNumber did not equal 2.33")), 70 | gencheck.NewFieldError("Test", "EqMultiple", "eq", fmt.Errorf("Length of EqMultiple did not equal 7")), 71 | gencheck.NewFieldError("Test", "NeString", "ne", fmt.Errorf("NeString equaled ")), 72 | gencheck.NewFieldError("Test", "NeNumber", "ne", fmt.Errorf("NeNumber equaled 0")), 73 | gencheck.NewFieldError("Test", "NeMultiple", "ne", fmt.Errorf("Length of NeMultiple equaled")), 74 | gencheck.NewFieldError("Test", "LtString", "lt", fmt.Errorf("length failed check for lt=3")), 75 | gencheck.NewFieldError("Test", "LtNumber", "lt", fmt.Errorf("failed check for lt=5.56")), 76 | gencheck.NewFieldError("Test", "LtMultiple", "lt", fmt.Errorf("length failed check for lt=2")), 77 | gencheck.NewFieldError("Test", "LtTime", "lt", fmt.Errorf("is after now")), 78 | gencheck.NewFieldError("Test", "LteString", "lte", fmt.Errorf("length failed check for lte=3")), 79 | gencheck.NewFieldError("Test", "LteNumber", "lte", fmt.Errorf("failed check for lte=5.56")), 80 | gencheck.NewFieldError("Test", "LteMultiple", "lte", fmt.Errorf("length failed check for lte=2")), 81 | gencheck.NewFieldError("Test", "GtString", "gt", fmt.Errorf("length failed check for gt=3")), 82 | gencheck.NewFieldError("Test", "GtNumber", "gt", fmt.Errorf("failed check for gt=5.56")), 83 | gencheck.NewFieldError("Test", "GtMultiple", "gt", fmt.Errorf("length failed check for gt=2")), 84 | gencheck.NewFieldError("Test", "GtTime", "gt", fmt.Errorf("is before now")), 85 | gencheck.NewFieldError("Test", "GteString", "gte", fmt.Errorf("length failed check for gte=3")), 86 | gencheck.NewFieldError("Test", "GteNumber", "gte", fmt.Errorf("failed check for gte=5.56")), 87 | gencheck.NewFieldError("Test", "GteMultiple", "gte", fmt.Errorf("length failed check for gte=2")), 88 | gencheck.NewFieldError("Test", "GteTime", "gte", fmt.Errorf("is before now")), 89 | gencheck.NewFieldError("Test", "GteTimeVal", "gte", fmt.Errorf("is before %s", time.Now().UTC().Add(1*time.Second).Truncate(time.Second).Format("2006-01-02 15:04:05"))), 90 | gencheck.NewFieldError("Test", "GteTimePtr", "gte", fmt.Errorf("is before now")), 91 | gencheck.NewFieldError("Test", "HexadecimalString", "hexadecimal", fmt.Errorf("'' is not a hexadecimal string")), 92 | gencheck.NewFieldError("Test", "Contains", "contains", fmt.Errorf("Contains did not contain purpose")), 93 | gencheck.NewFieldError("Test", "ContainsPtr", "contains", fmt.Errorf("ContainsPtr did not contain purpose")), 94 | gencheck.NewFieldError("Test", "ContainsArray", "contains", fmt.Errorf("ContainsArray did not contain nonsense")), 95 | gencheck.NewFieldError("Test", "ContainsAny", "containsany", fmt.Errorf("ContainsAny did not contain any of !@#$")), 96 | gencheck.NewFieldError("Test", "UUID", "uuid", fmt.Errorf("'' is not a UUID")), 97 | gencheck.NewFieldError("Test", "UUID3", "uuid3", fmt.Errorf("'' is not a UUIDv3")), 98 | gencheck.NewFieldError("Test", "UUID4", "uuid4", fmt.Errorf("'' is not a UUIDv4")), 99 | gencheck.NewFieldError("Test", "UUID5", "uuid5", fmt.Errorf("'' is not a UUIDv5")), 100 | gencheck.NewFieldError("Test", "CIDR", "cidr", fmt.Errorf("invalid CIDR address")), 101 | gencheck.NewFieldError("Test", "CIDRv4", "cidrv4", fmt.Errorf("invalid CIDR address")), 102 | gencheck.NewFieldError("Test", "CIDRv6", "cidrv6", fmt.Errorf("invalid CIDR address")), 103 | gencheck.NewFieldError("Test", "MinIntPtr", "required", fmt.Errorf("is required")), 104 | gencheck.NewFieldError("Test", "InnerDive", "dive", fmt.Errorf("validation: field validation failed for 'Inner.EqCSFieldString': is required")), 105 | gencheck.NewFieldError("Test", "InnerDivePtr", "dive", fmt.Errorf("validation: field validation failed for 'Inner.EqCSFieldString': is required")), 106 | gencheck.NewFieldError("Test", "InnerDiveSlice[0]", "dive", fmt.Errorf("validation: field validation failed for 'Inner.EqCSFieldString': is required")), 107 | gencheck.NewFieldError("Test", "InnerDiveSlicePtr[0]", "dive", fmt.Errorf("validation: field validation failed for 'Inner.EqCSFieldString': is required")), 108 | gencheck.NewFieldError("Test", "InnerDiveMap[test]", "dive", fmt.Errorf("validation: field validation failed for 'Inner.EqCSFieldString': is required")), 109 | gencheck.NewFieldError("Test", "InnerDiveMapPtr[test]", "dive", fmt.Errorf("validation: field validation failed for 'Inner.EqCSFieldString': is required")), 110 | gencheck.NewFieldError("Test", "OtherFileDive", "dive", fmt.Errorf("validation: field validation failed for 'OtherFile.EqCSFieldString': is required")), 111 | gencheck.NewFieldError("Test", "MapContains", "contains", fmt.Errorf("MapContains did not contain key")), 112 | gencheck.NewFieldError("Test", "TestString", "dive", fmt.Errorf(`validation: field validation failed for 'TestString.Required': is required`)), 113 | } 114 | testTime := time.Now().UTC() 115 | notPurpose := "notPurpose" 116 | underTest := Test{ 117 | RequiredString: "Here I am", // Put in required string to prevent fast failure 118 | MaxString: "1234", 119 | MaxNumber: 1113.00001, 120 | MaxMultiple: []string{"", "", "", "", "", "", "", "", ""}, 121 | LtString: "1234", 122 | LtNumber: 5.56000001, 123 | LtMultiple: []string{"", "", ""}, 124 | LtTime: time.Now().UTC().Add(1 * time.Millisecond), 125 | LteString: "1234", 126 | LteNumber: 5.56000001, 127 | LteMultiple: []string{"", "", ""}, 128 | GteTimePtr: &testTime, 129 | ContainsPtr: ¬Purpose, 130 | InnerDive: Inner{}, 131 | InnerDivePtr: &Inner{}, 132 | InnerDiveSlice: []Inner{{}}, 133 | InnerDiveSlicePtr: []*Inner{{}}, 134 | InnerDiveMap: map[string]Inner{"test": {}}, 135 | InnerDiveMapPtr: map[string]*Inner{"test": {}}, 136 | } 137 | 138 | err := underTest.Validate() 139 | s.Error(err, "Error should not be nil") 140 | s.Require().IsType(gencheck.ValidationErrors{}, err, "Error returned was not ValidationErrors type") 141 | 142 | ve := err.(gencheck.ValidationErrors) 143 | 144 | s.Require().Equal(len(expected), len(ve), "Validation Errors were of different length") 145 | 146 | for i, fe := range ve { 147 | s.Contains(fe.Error(), strings.TrimSuffix(expected[i].Error(), "'"), "Error string mismatch") 148 | s.Equal(fe.Tag(), expected[i].Tag(), "Tag mismatch") 149 | } 150 | } 151 | 152 | // TestValidateTestStruct_Values 153 | func (s *ExampleTestSuite) TestValidateTestStruct_Values() { 154 | i := int64(2000) 155 | underTest := Test{ 156 | ExternalEmbedded: ExternalEmbedded{EmbeddedString: `abcd`}, 157 | Embedded: Embedded{FieldString: `1234`}, 158 | LenMultiple: []string{"", "", "", "", "", "", ""}, 159 | LenNumber: 1113, 160 | LenString: "a", 161 | EqMultiple: []string{"", "", "", "", "", "", ""}, 162 | EqNumber: 2.33, 163 | EqString: "3", 164 | NeMultiple: []string{"", "", "", "", "", "", ""}, 165 | NeNumber: 2.33, 166 | NeString: "3", 167 | RequiredMultiple: []string{}, 168 | RequiredString: "b", 169 | MinString: "1234567", 170 | MinNumber: 1113.000001, 171 | MinMultiple: []string{"", "", "", "", "", "", "", ""}, 172 | UUID: "7112EE37-3219-4A26-BA01-1D230BC9257B", 173 | UUID3: uuid.NewMD5(uuid.NameSpaceX500, []byte("test")).String(), 174 | UUID4: strings.ToUpper(uuid.New().String()), 175 | UUID5: uuid.NewSHA1(uuid.NameSpaceDNS, []byte("test")).String(), 176 | MinIntPtr: &i, 177 | GteString: "1234", 178 | GteNumber: 5.5600001, 179 | GteMultiple: []string{"", ""}, 180 | LteTime: time.Now().Add(-5 * time.Second), 181 | GtTime: time.Now().Add(1 * time.Millisecond), 182 | GteTime: time.Now().Add(5 * time.Second), 183 | GteTimeVal: time.Now().Add(5 * time.Second), 184 | GtNumber: 5.5600001, 185 | GtMultiple: []string{"", "", ""}, 186 | GtString: "1234", 187 | Contains: "purpose Of this test", 188 | ContainsArray: []string{"test", "last", "purpose", "nonsense"}, 189 | ContainsAny: "This is a test string!", 190 | InnerDive: Inner{EqCSFieldString: "test"}, 191 | InnerDivePtr: &Inner{EqCSFieldString: "something"}, 192 | InnerDiveSlice: []Inner{{EqCSFieldString: "something"}}, 193 | InnerDiveSlicePtr: []*Inner{{EqCSFieldString: "something"}}, 194 | InnerDiveMap: map[string]Inner{"test": {EqCSFieldString: "something"}}, 195 | InnerDiveMapPtr: map[string]*Inner{"test": {EqCSFieldString: "something"}}, 196 | MapContains: map[string]interface{}{"key": "x"}, 197 | HexadecimalString: "0x0F007BA11", 198 | CIDR: "0.0.0.0/24", 199 | CIDRv4: "0.0.0.0/24", 200 | CIDRv6: "2620:0:2d0:200::7/32", 201 | URL: "http://test.com", 202 | URI: "scp://test.com/123", 203 | OtherFileDive: OtherFile{EqCSFieldString: "test"}, 204 | OtherFileDivePtr: &OtherFile{EqCSFieldString: "something"}, 205 | TestString: benchmark.TestString{Required: "x", Len: "xxxxxxxxxx", Min: "xxxxx", Max: "x"}, 206 | TestStringPtr: &benchmark.TestString{Required: "x", Len: "xxxxxxxxxx", Min: "xxxxx", Max: "x"}, 207 | } 208 | 209 | err := underTest.Validate() 210 | s.NoError(err, "Valid Struct should not have had an error") 211 | 212 | } 213 | 214 | // TestValidateTestStruct_Values 215 | func (s *ExampleTestSuite) TestValidateTestStruct_MinPtrFailure() { 216 | i := int64(1233) 217 | underTest := Test{ 218 | ExternalEmbedded: ExternalEmbedded{EmbeddedString: `abcd`}, 219 | Embedded: Embedded{FieldString: `1234`}, 220 | LenMultiple: []string{"", "", "", "", "", "", ""}, 221 | LenNumber: 1113, 222 | LenString: "a", 223 | RequiredMultiple: []string{}, 224 | RequiredString: "b", 225 | MinString: "1234567", 226 | MinNumber: 1113.000001, 227 | MinMultiple: []string{"", "", "", "", "", "", "", ""}, 228 | UUID: "7112EE37-3219-4A26-BA01-1D230BC9257B", 229 | UUID3: uuid.NewMD5(uuid.New(), []byte("test")).String(), 230 | UUID4: uuid.New().String(), 231 | UUID5: uuid.NewSHA1(uuid.New(), []byte("test")).String(), 232 | GteString: "1234", 233 | GteNumber: 5.5600001, 234 | GteMultiple: []string{"", ""}, 235 | LteTime: time.Now().Add(-5 * time.Second), 236 | GtTime: time.Now().Add(1 * time.Millisecond), 237 | GteTime: time.Now().Add(5 * time.Second), 238 | GteTimeVal: time.Now().Add(5 * time.Second), 239 | GtNumber: 5.5600001, 240 | GtMultiple: []string{"", "", ""}, 241 | GtString: "1234", 242 | MinIntPtr: &i, 243 | Contains: "purpose Of this test", 244 | ContainsArray: []string{"test", "last", "purpose", "nonsense"}, 245 | ContainsAny: "This is a test string!", 246 | InnerDive: Inner{EqCSFieldString: "test"}, 247 | InnerDivePtr: &Inner{EqCSFieldString: "something"}, 248 | MapContains: map[string]interface{}{"key": "x"}, 249 | HexadecimalString: "0x0F007BA11", 250 | CIDR: "0.0.0.0/24", 251 | CIDRv4: "0.0.0.0/24", 252 | CIDRv6: "2620:0:2d0:200::7/32", 253 | EqMultiple: []string{"", "", "", "", "", "", ""}, 254 | EqNumber: 2.33, 255 | EqString: "3", 256 | NeMultiple: []string{"", "", "", "", "", "", ""}, 257 | NeNumber: 2.33, 258 | NeString: "3", 259 | URL: "http://test.com", 260 | URI: "scp://test.com", 261 | OtherFileDive: OtherFile{EqCSFieldString: "test"}, 262 | OtherFileDivePtr: &OtherFile{EqCSFieldString: "something"}, 263 | TestString: benchmark.TestString{Required: "x", Len: "xxxxxxxxxx", Min: "xxxxx", Max: "x"}, 264 | TestStringPtr: &benchmark.TestString{Required: "x", Len: "xxxxxxxxxx", Min: "xxxxx", Max: "x"}, 265 | } 266 | 267 | err := underTest.Validate() 268 | s.Error(err, "Valid Struct should not have had an error") 269 | s.Require().IsType(gencheck.ValidationErrors{}, err, "Error returned was not ValidationErrors type") 270 | 271 | ve := err.(gencheck.ValidationErrors) 272 | s.Require().Len(ve, 1, "Should only have 1 validation error") 273 | 274 | s.Require().EqualValues(ve[0], gencheck.NewFieldError("Test", "MinIntPtr", "min", fmt.Errorf("failed check for min=1234")), "Error should be min error") 275 | } 276 | 277 | // TestValidateExample_NilMap 278 | func (s *ExampleTestSuite) TestValidateExample_NilMap() { 279 | expected := gencheck.ValidationErrors{ 280 | gencheck.NewFieldError("Example", "MapOfInterfaces", "notnil", fmt.Errorf("is Nil")), 281 | } 282 | 283 | underTest := Example{} 284 | 285 | err := underTest.Validate() 286 | s.EqualValues(expected, err) 287 | } 288 | 289 | // TestValidateNotNil_Map 290 | func (s *ExampleTestSuite) TestValidateExample_Happy() { 291 | 292 | underTest := Example{ 293 | MapOfInterfaces: make(map[string]interface{}), 294 | } 295 | 296 | err := underTest.Validate() 297 | s.Nil(err, "Valid Struct should not have had an error") 298 | } 299 | 300 | // TestValidateTestStruct_Values 301 | func (s *ExampleTestSuite) TestValidateTestStruct_LteTime() { 302 | i := int64(1234) 303 | underTest := Test{ 304 | ExternalEmbedded: ExternalEmbedded{EmbeddedString: `abcd`}, 305 | Embedded: Embedded{FieldString: `1234`}, 306 | LenMultiple: []string{"", "", "", "", "", "", ""}, 307 | LenNumber: 1113, 308 | LenString: "a", 309 | RequiredMultiple: []string{}, 310 | RequiredString: "b", 311 | MinString: "1234567", 312 | MinNumber: 1113.000001, 313 | MinMultiple: []string{"", "", "", "", "", "", "", ""}, 314 | UUID: "7112EE37-3219-4A26-BA01-1D230BC9257B", 315 | UUID3: uuid.NewMD5(uuid.New(), []byte("test")).String(), 316 | UUID4: uuid.New().String(), 317 | UUID5: uuid.NewSHA1(uuid.New(), []byte("test")).String(), 318 | GteString: "1234", 319 | GteNumber: 5.5600001, 320 | GteMultiple: []string{"", ""}, 321 | LteTime: time.Now().Add(1 * time.Second), 322 | GtTime: time.Now().Add(1 * time.Millisecond), 323 | GteTime: time.Now().Add(5 * time.Second), 324 | GteTimeVal: time.Now().Add(5 * time.Second), 325 | GtNumber: 5.5600001, 326 | GtMultiple: []string{"", "", ""}, 327 | GtString: "1234", 328 | MinIntPtr: &i, 329 | Contains: "purpose Of this test", 330 | ContainsArray: []string{"test", "last", "purpose", "nonsense"}, 331 | ContainsAny: "This is a test string!", 332 | InnerDive: Inner{EqCSFieldString: "test"}, 333 | InnerDivePtr: &Inner{EqCSFieldString: "something"}, 334 | MapContains: map[string]interface{}{"key": "x"}, 335 | HexadecimalString: "0x0F007BA11", 336 | CIDR: "0.0.0.0/24", 337 | CIDRv4: "0.0.0.0/24", 338 | CIDRv6: "2620:0:2d0:200::7/32", 339 | EqMultiple: []string{"", "", "", "", "", "", ""}, 340 | EqNumber: 2.33, 341 | EqString: "3", 342 | NeMultiple: []string{"", "", "", "", "", "", ""}, 343 | NeNumber: 2.33, 344 | NeString: "3", 345 | URL: "http://test.com", 346 | URI: "scp://test.com", 347 | OtherFileDive: OtherFile{EqCSFieldString: "test"}, 348 | OtherFileDivePtr: &OtherFile{EqCSFieldString: "something"}, 349 | TestString: benchmark.TestString{Required: "x", Len: "xxxxxxxxxx", Min: "xxxxx", Max: "x"}, 350 | TestStringPtr: &benchmark.TestString{Required: "x", Len: "xxxxxxxxxx", Min: "xxxxx", Max: "x"}, 351 | } 352 | 353 | err := underTest.Validate() 354 | s.Error(err, "Valid Struct should have had an error") 355 | s.Require().IsType(gencheck.ValidationErrors{}, err, "Error returned was not ValidationErrors type") 356 | 357 | ve := err.(gencheck.ValidationErrors) 358 | s.Require().Len(ve, 1, "Should only have 1 validation error") 359 | 360 | s.Require().EqualValues(ve[0], gencheck.NewFieldError("Test", "LteTime", "lte", fmt.Errorf("is after now")), "Error should be lte time error") 361 | } 362 | 363 | // TestValidateTestStruct_Values 364 | func TestValidateTestStruct_Individual(t *testing.T) { 365 | tests := map[string]struct { 366 | uut Test 367 | field string 368 | expected gencheck.FieldError 369 | }{ 370 | "URL": { 371 | field: "URL", 372 | expected: gencheck.NewFieldError("Test", "URL", "url", errors.New("parse x: invalid URI for request")), 373 | uut: Test{ 374 | RequiredString: "x", 375 | URL: "x", 376 | }, 377 | }, 378 | "URLNoScheme": { 379 | field: "URL", 380 | expected: gencheck.NewFieldError("Test", "URL", "url", errors.New("URL is missing a scheme")), 381 | uut: Test{ 382 | RequiredString: "x", 383 | URL: "/x/123", 384 | }, 385 | }, 386 | "URI": { 387 | field: "URI", 388 | expected: gencheck.NewFieldError("Test", "URI", "uri", errors.New("parse x: invalid URI for request")), 389 | uut: Test{ 390 | RequiredString: "x", 391 | URI: "x", 392 | }, 393 | }, 394 | } 395 | 396 | for name, test := range tests { 397 | t.Run(name, func(tt *testing.T) { 398 | err := test.uut.Validate() 399 | assert.Error(tt, err, "Valid Struct should have had an error") 400 | 401 | assert.IsType(tt, gencheck.ValidationErrors{}, err, "Error returned was not ValidationErrors type") 402 | 403 | ve := err.(gencheck.ValidationErrors) 404 | found := false 405 | 406 | for _, e := range ve { 407 | if e.Field() == test.field { 408 | found = true 409 | assert.EqualValues(tt, test.expected, e, "Error did not match expected") 410 | } 411 | } 412 | assert.True(tt, found, `Did not find expected error for field '%s'`, test.field) 413 | }) 414 | } 415 | 416 | } 417 | --------------------------------------------------------------------------------