├── .drone.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── verify.go └── verify_test.go /.drone.yml: -------------------------------------------------------------------------------- 1 | workspace: 2 | base: /go 3 | path: src/github.com/codyoss/verify 4 | 5 | pipeline: 6 | test: 7 | image: golang:1.12.4-stretch 8 | secrets: [ CODECOV_TOKEN ] 9 | commands: 10 | - go test -race -coverprofile=coverage.txt -covermode=atomic 11 | - curl -s https://codecov.io/bash > .codecov && chmod +x .codecov && ./.codecov -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Editors 15 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cody Oss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # verify 2 | 3 | Package verify uses struct field tags to verify data. 4 | 5 | [![GoDoc](https://godoc.org/github.com/codyoss/verify?status.svg)](https://godoc.org/github.com/codyoss/verify) 6 | [![Build Status](https://cloud.drone.io/api/badges/codyoss/verify/status.svg)](https://cloud.drone.io/codyoss/verify) 7 | [![codecov](https://codecov.io/gh/codyoss/verify/branch/master/graph/badge.svg)](https://codecov.io/gh/codyoss/verify) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/codyoss/verify)](https://goreportcard.com/report/github.com/codyoss/verify) 9 | 10 | ## Tags supported 11 | 12 | - `minSize` -- specifies the minimum allowable length of a field. This can only be used on the following types: string, 13 | slice, array, or map. 14 | 15 | - `maxSize` -- specifies the maximum allowable length of a field. This can only be used on the following types: string, 16 | slice, array, or map. 17 | 18 | - `min` -- specifies the minimum allowable value of a field. This should only be used on types that can be parsed into 19 | an int64 or float64. 20 | 21 | - `max` -- specifies the maximum allowable value of a field. This should only be used on types that can be parsed into 22 | an int64 or float64. 23 | 24 | - `required` -- specifies the field may not be set to the zero value for the given type. This may be used on any types 25 | except arrays and structs. 26 | 27 | ## Example usage 28 | 29 | Here is an example of the usage of each tag: 30 | 31 | ```golang 32 | type Foo struct { 33 | A []string `verify:"minSize=5"` 34 | B string `verify:"maxSize=10"` 35 | C int8 `verify:"min=3"` 36 | D float32 `verify:"max=1.2"` 37 | E int64 `verify:"min=3,max=7"` 38 | F *bool `verify:"required"` 39 | } 40 | ``` 41 | 42 | ## Limitations 43 | 44 | 1. verify only supports working with flat structures at the moment; it will not work with inner/embedded structs. Also, 45 | 2. Because this package makes use of reflection the tags may only be used on exported fields. 46 | 47 | ## Blog Post 48 | 49 | I wrote a blog post about how to make your own struct field tags. [Check it out here!](https://medium.com/@codyoss/creating-your-own-struct-field-tags-in-go-c6c86727eff) -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codyoss/verify 2 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | // Package verify uses struct field tags to verify data. There are five tags currently supported: 2 | // 3 | // minSize -- specifies the minimum allowable length of a field. This can only be used on the following types: string, 4 | // slice, array, or map. 5 | // 6 | // maxSize -- specifies the maximum allowable length of a field. This can only be used on the following types: string, 7 | // slice, array, or map. 8 | // 9 | // min -- specifies the minimum allowable value of a field. This should only be used on types that can be parsed into 10 | // an int64 or float64. 11 | // 12 | // max -- specifies the maximum allowable value of a field. This should only be used on types that can be parsed into 13 | // an int64 or float64. 14 | // 15 | // required -- specifies the field may not be set to the zero value for the given type. This may be used on any types 16 | // except arrays and structs. 17 | // 18 | // Here is an example of the usage of each tag: 19 | // 20 | // type Foo struct { 21 | // A []string `verify:"minSize=5"` 22 | // B string `verify:"maxSize=10"` 23 | // C int8 `verify:"min=3"` 24 | // D float32 `verify:"max=1.2"` 25 | // E int64 `verify:"min=3,max=7"` 26 | // F *bool `verify:"required"` 27 | // } 28 | // 29 | // There are currently a few limitation with this project. The first is verify only supports working with flat 30 | // structures at the moment; it will not work with inner/embedded structs. Also, because the package makes use of 31 | // reflection the tags may only be used on exported fields. 32 | package verify 33 | 34 | import ( 35 | "errors" 36 | "fmt" 37 | "reflect" 38 | "strconv" 39 | "strings" 40 | ) 41 | 42 | const ( 43 | verifyTagKey = "verify" 44 | tagMinSize = "minSize" 45 | tagMaxSize = "maxSize" 46 | tagMin = "min" 47 | tagMax = "max" 48 | tagRequired = "required" 49 | 50 | parseBase = 10 51 | parseBit = 64 52 | ) 53 | 54 | var ( 55 | errInvalidKind = errors.New("v provided must be a struct, interface, or pointer to a struct") 56 | 57 | errMissingValueMinSize = errors.New("minSize must specify a size") 58 | errMissingValueMaxSize = errors.New("maxSize must specify a size") 59 | errMissingValueMin = errors.New("min must specify a size") 60 | errMissingValueMax = errors.New("max must specify a size") 61 | 62 | errValueTypeMinSize = errors.New("minSize can only be used with types: string, slice, array, or map") 63 | errValueTypeMaxSize = errors.New("maxSize can only be used with types: string, slice, array, or map") 64 | errValueTypeMin = errors.New("min can only be used with types: int, int8, int16, int32, int64, float32, or float64") 65 | errValueTypeMax = errors.New("max can only be used with types: int, int8, int16, int32, int64, float32, or float64") 66 | 67 | errConvertToNumberMinSize = errors.New("minSize value must be an int") 68 | errConvertToNumberMaxSize = errors.New("maxSize value must be an int") 69 | errConvertToNumberMin = errors.New("min value must be an int64 or float64") 70 | errConvertToNumberMax = errors.New("max value must be an int or float64") 71 | ) 72 | 73 | // It takes a struct and uses reflection to verify it based on its struct field tags. An error is returned should any of 74 | // the fields fail their validation. The returned error will describe each field that failed validation. Only interfaces 75 | // a struct, or a pointer to struct should be passed to this function. 76 | func It(v interface{}) error { 77 | rv := reflect.ValueOf(v) 78 | 79 | for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Ptr { 80 | rv = rv.Elem() 81 | } 82 | if rv.Kind() != reflect.Struct { 83 | return errInvalidKind 84 | } 85 | 86 | rt := rv.Type() 87 | // TODO: Append errors 88 | for i := 0; i < rt.NumField(); i++ { 89 | if tags, ok := rt.Field(i).Tag.Lookup(verifyTagKey); ok { 90 | err := verifyField(rv.Field(i), rt.Field(i).Name, tags) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | func verifyField(f reflect.Value, name string, tag string) error { 100 | var tagErrs []string 101 | var tagPrefix string 102 | st := strings.Split(tag, ",") 103 | 104 | // verify each valid sub-tag found 105 | for _, v := range st { 106 | tagPrefix = v 107 | i := strings.IndexByte(v, '=') 108 | if i != -1 { 109 | tagPrefix = v[:i] 110 | } 111 | switch tagPrefix { 112 | case tagMinSize: 113 | if i == -1 { 114 | return errMissingValueMinSize 115 | } 116 | min, err := strconv.Atoi(v[i+1:]) 117 | if err != nil { 118 | return errConvertToNumberMinSize 119 | } 120 | 121 | switch f.Kind() { 122 | case reflect.String, reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: 123 | if f.Len() < min { 124 | tagErrs = append(tagErrs, fmt.Sprintf("%s has a length less than %d", name, min)) 125 | } 126 | default: 127 | return errValueTypeMinSize 128 | } 129 | case tagMaxSize: 130 | if i == -1 { 131 | return errMissingValueMaxSize 132 | } 133 | max, err := strconv.Atoi(v[i+1:]) 134 | if err != nil { 135 | return errConvertToNumberMaxSize 136 | } 137 | 138 | switch f.Kind() { 139 | case reflect.String, reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: 140 | if f.Len() > max { 141 | tagErrs = append(tagErrs, fmt.Sprintf("%s has a length greater than %d", name, max)) 142 | } 143 | default: 144 | return errValueTypeMaxSize 145 | } 146 | case tagMin: 147 | var minI int64 148 | var minF float64 149 | var isMinFloat bool 150 | if i == -1 { 151 | return errMissingValueMin 152 | } 153 | minI, err := strconv.ParseInt(v[i+1:], parseBase, parseBit) 154 | if err != nil { 155 | minF, err = strconv.ParseFloat(v[i+1:], parseBit) 156 | if err != nil { 157 | return errConvertToNumberMin 158 | } 159 | isMinFloat = true 160 | } 161 | switch f.Kind() { 162 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 163 | if isMinFloat { 164 | return fmt.Errorf("%s type is int while min is float", name) 165 | } 166 | if f.Int() < minI { 167 | tagErrs = append(tagErrs, fmt.Sprintf("%s has value less than min %d", name, minI)) 168 | } 169 | case reflect.Float32, reflect.Float64: 170 | if !isMinFloat { 171 | return fmt.Errorf("%s type is float while min is int", name) 172 | } 173 | if f.Float() < minF { 174 | tagErrs = append(tagErrs, fmt.Sprintf("%s has value less than min %f", name, minF)) 175 | } 176 | default: 177 | return errValueTypeMin 178 | } 179 | case tagMax: 180 | var maxI int64 181 | var maxF float64 182 | var isMaxFloat bool 183 | if i == -1 { 184 | return errMissingValueMax 185 | } 186 | maxI, err := strconv.ParseInt(v[i+1:], parseBase, parseBit) 187 | if err != nil { 188 | maxF, err = strconv.ParseFloat(v[i+1:], parseBit) 189 | if err != nil { 190 | return errConvertToNumberMax 191 | } 192 | isMaxFloat = true 193 | } 194 | switch f.Kind() { 195 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 196 | if isMaxFloat { 197 | return fmt.Errorf("%s type is int while max is float", name) 198 | } 199 | if f.Int() > maxI { 200 | tagErrs = append(tagErrs, fmt.Sprintf("%s has value greater than max %d", name, maxI)) 201 | } 202 | case reflect.Float32, reflect.Float64: 203 | if !isMaxFloat { 204 | return fmt.Errorf("%s type is float while max is int", name) 205 | } 206 | if f.Float() > maxF { 207 | tagErrs = append(tagErrs, fmt.Sprintf("%s has value greater than max %f", name, maxF)) 208 | } 209 | default: 210 | return errValueTypeMax 211 | } 212 | case tagRequired: 213 | switch f.Kind() { 214 | case reflect.Func, reflect.Map, reflect.Slice: 215 | if f.IsNil() { 216 | tagErrs = append(tagErrs, fmt.Sprintf("%s is required but is set to zero value", name)) 217 | } 218 | case reflect.Array, reflect.Struct: 219 | default: 220 | if f.Interface() == reflect.Zero(f.Type()).Interface() { 221 | tagErrs = append(tagErrs, fmt.Sprintf("%s is required but is set to zero value", name)) 222 | } 223 | } 224 | } 225 | } 226 | 227 | // collect all errors to return to user 228 | if tagErrs != nil { 229 | var sb strings.Builder 230 | for i, v := range tagErrs { 231 | if i != 0 { 232 | sb.WriteString(", ") 233 | } 234 | sb.WriteString(v) 235 | } 236 | return fmt.Errorf("verify found the following errors: [%s]", sb.String()) 237 | } 238 | 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /verify_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/codyoss/verify" 8 | ) 9 | 10 | func TestItInputTypes(t *testing.T) { 11 | var va Aer = valueAer{} 12 | var pa Aer = &pointerAer{} 13 | 14 | tests := []struct { 15 | name string 16 | input interface{} 17 | wantErr bool 18 | }{ 19 | {"bool input", true, true}, 20 | {"string input", "", true}, 21 | {"int input", 0, true}, 22 | {"float64 input", 1.4, true}, 23 | {"*string input", new(string), true}, 24 | {"struct input", valueAer{}, false}, 25 | {"*struct input", &pointerAer{}, false}, 26 | {"interface struct input", va, false}, 27 | {"interface *struct input", pa, false}, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | got := verify.It(tt.input) 32 | if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { 33 | t.Errorf("wantErr is %v, while got is %v", tt.wantErr, got) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestItMinSize(t *testing.T) { 40 | type A struct { 41 | A bool `verify:"minSize"` 42 | } 43 | type B struct { 44 | A bool `verify:"minSize=abc"` 45 | } 46 | type C struct { 47 | A bool `verify:"minSize=5"` 48 | } 49 | type D struct { 50 | A string `verify:"minSize=5"` 51 | } 52 | 53 | tests := []struct { 54 | name string 55 | input interface{} 56 | wantErr bool 57 | }{ 58 | {"missing value", A{}, true}, 59 | {"can't parse value", B{}, true}, 60 | {"field wrong type", C{}, true}, 61 | {"field too short", D{}, true}, 62 | {"works", D{"12345"}, false}, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | got := verify.It(tt.input) 67 | if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { 68 | t.Errorf("wantErr is %v, while got is %v", tt.wantErr, got) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestItMaxSize(t *testing.T) { 75 | type A struct { 76 | A bool `verify:"maxSize"` 77 | } 78 | type B struct { 79 | A bool `verify:"maxSize=abc"` 80 | } 81 | type C struct { 82 | A bool `verify:"maxSize=5"` 83 | } 84 | type D struct { 85 | A string `verify:"maxSize=5"` 86 | } 87 | 88 | tests := []struct { 89 | name string 90 | input interface{} 91 | wantErr bool 92 | }{ 93 | {"missing value", A{}, true}, 94 | {"can't parse value", B{}, true}, 95 | {"field wrong type", C{}, true}, 96 | {"field too long", D{"123456"}, true}, 97 | {"works", D{"12345"}, false}, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | got := verify.It(tt.input) 102 | if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { 103 | t.Errorf("wantErr is %v, while got is %v", tt.wantErr, got) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestItMin(t *testing.T) { 110 | type A struct { 111 | A bool `verify:"min"` 112 | } 113 | type B struct { 114 | A bool `verify:"min=abc"` 115 | } 116 | type C struct { 117 | A bool `verify:"min=3"` 118 | } 119 | type D struct { 120 | A float64 `verify:"min=2"` 121 | } 122 | type E struct { 123 | A int64 `verify:"min=2.1"` 124 | } 125 | type F struct { 126 | A float64 `verify:"min=2.1"` 127 | } 128 | type G struct { 129 | A int64 `verify:"min=2"` 130 | } 131 | 132 | tests := []struct { 133 | name string 134 | input interface{} 135 | wantErr bool 136 | }{ 137 | {"missing value", A{}, true}, 138 | {"can't parse value", B{}, true}, 139 | {"field wrong type", C{}, true}, 140 | {"field type does not match tag type", D{3.1}, true}, 141 | {"tag type does not match field type", E{3}, true}, 142 | {"too small float", F{1.1}, true}, 143 | {"too small int", G{1}, true}, 144 | {"works float", F{3.1}, false}, 145 | {"works int", G{3}, false}, 146 | } 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | got := verify.It(tt.input) 150 | if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { 151 | t.Errorf("wantErr is %v, while got is %v", tt.wantErr, got) 152 | } 153 | }) 154 | } 155 | } 156 | 157 | func TestItMax(t *testing.T) { 158 | type A struct { 159 | A bool `verify:"max"` 160 | } 161 | type B struct { 162 | A bool `verify:"max=abc"` 163 | } 164 | type C struct { 165 | A bool `verify:"max=3"` 166 | } 167 | type D struct { 168 | A float64 `verify:"max=2"` 169 | } 170 | type E struct { 171 | A int64 `verify:"max=2.1"` 172 | } 173 | type F struct { 174 | A float64 `verify:"max=2.1"` 175 | } 176 | type G struct { 177 | A int64 `verify:"max=2"` 178 | } 179 | 180 | tests := []struct { 181 | name string 182 | input interface{} 183 | wantErr bool 184 | }{ 185 | {"missing value", A{}, true}, 186 | {"can't parse value", B{}, true}, 187 | {"field wrong type", C{}, true}, 188 | {"field type does not match tag type", D{1.1}, true}, 189 | {"tag type does not match field type", E{1}, true}, 190 | {"too large float", F{3.1}, true}, 191 | {"too large int", G{3}, true}, 192 | {"works float", F{1.1}, false}, 193 | {"works int", G{1}, false}, 194 | } 195 | for _, tt := range tests { 196 | t.Run(tt.name, func(t *testing.T) { 197 | got := verify.It(tt.input) 198 | if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { 199 | t.Errorf("wantErr is %v, while got is %v", tt.wantErr, got) 200 | } 201 | }) 202 | } 203 | } 204 | 205 | func TestItRequired(t *testing.T) { 206 | 207 | type Zero struct { 208 | A string 209 | } 210 | type A struct { 211 | A string `verify:"required"` 212 | } 213 | type B struct { 214 | A bool `verify:"required"` 215 | } 216 | type C struct { 217 | A int `verify:"required"` 218 | } 219 | type D struct { 220 | A float64 `verify:"required"` 221 | } 222 | type E struct { 223 | A Zero `verify:"required"` 224 | } 225 | type F struct { 226 | A *Zero `verify:"required"` 227 | } 228 | type G struct { 229 | A []int `verify:"required"` 230 | } 231 | 232 | tests := []struct { 233 | name string 234 | input interface{} 235 | wantErr bool 236 | }{ 237 | {"deafult string", A{}, true}, 238 | {"deafult bool", B{}, true}, 239 | {"deafult int", C{}, true}, 240 | {"deafult float64", D{}, true}, 241 | {"deafult struct", E{}, false}, 242 | {"deafult *struct", F{}, true}, 243 | {"deafult slice", G{}, true}, 244 | {"non-deafult string", A{"a"}, false}, 245 | {"non-deafult bool", B{true}, false}, 246 | {"non-deafult int", C{1}, false}, 247 | {"non-deafult float64", D{1.1}, false}, 248 | {"non-deafult struct", E{Zero{"a"}}, false}, 249 | {"non-deafult *struct", F{&Zero{"a"}}, false}, 250 | {"non-deafult slice", G{[]int{1}}, false}, 251 | } 252 | for _, tt := range tests { 253 | t.Run(tt.name, func(t *testing.T) { 254 | got := verify.It(tt.input) 255 | if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { 256 | t.Errorf("wantErr is %v, while got is %v", tt.wantErr, got) 257 | } 258 | }) 259 | } 260 | } 261 | 262 | func TestItMultipleValidationsFail(t *testing.T) { 263 | type A struct { 264 | A int `verify:"required,max=-1"` 265 | } 266 | 267 | err := verify.It(A{}) 268 | if err == nil || !strings.Contains(err.Error(), "zero value") || !strings.Contains(err.Error(), "greater than max") { 269 | t.Error("expected err two contain two messages") 270 | } 271 | 272 | } 273 | 274 | type Aer interface { 275 | A() 276 | } 277 | 278 | type valueAer struct{} 279 | 280 | func (a valueAer) A() {} 281 | 282 | type pointerAer struct{} 283 | 284 | func (a *pointerAer) A() {} 285 | --------------------------------------------------------------------------------