├── LICENSE ├── README.md ├── decoder.go ├── decoder_test.go ├── doc.go ├── error.go ├── error_test.go ├── examples_test.go ├── form.go ├── form_test.go ├── legit.go ├── legit_test.go ├── numbers.go ├── numbers_test.go ├── patterns.go ├── patterns_test.go ├── strings.go └── strings_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, James Cunningham 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LEGIT 2 | 3 | [![GoDoc](https://godoc.org/github.com/jamescun/legit?status.png)](https://godoc.org/github.com/jamescun/legit) 4 | 5 | Legit is an input validation framework for Go. Legit differs from existing frameworks by constructing validation from types and interfaces, preferring custom validators to complex struct tags. 6 | 7 | go get -u github.com/jamescun/legit 8 | 9 | Included validators: 10 | 11 | - Email 12 | - UUID 13 | - UUID3 14 | - UUID4 15 | - UUID5 16 | - Credit Card 17 | - Lowercase 18 | - Uppercase 19 | - No whitespace 20 | - Printable characters 21 | - Alpha 22 | - Alphanumeric 23 | - Numeric 24 | - ASCII 25 | - Positive number 26 | - Negative number 27 | 28 | 29 | Example 30 | ------- 31 | ```go 32 | package main 33 | 34 | import ( 35 | "fmt" 36 | "net/http" 37 | 38 | "github.com/jamescun/legit" 39 | ) 40 | 41 | type User struct { 42 | Email legit.Email `json:"email"` 43 | Age legit.Positive `json:"age"` 44 | } 45 | 46 | func Handler(w http.ResponseWriter, r *http.Request) { 47 | var user User 48 | err := legit.ParseRequestAndValidate(r, &user) 49 | if err != nil { 50 | fmt.Fprintln(w, "invalid user:", err) 51 | return 52 | } 53 | } 54 | ``` 55 | 56 | 57 | Custom Example 58 | -------------- 59 | 60 | ```go 61 | package main 62 | 63 | import ( 64 | "fmt" 65 | "regexp" 66 | "errors" 67 | "encoding/json" 68 | 69 | "github.com/jamescun/legit" 70 | ) 71 | 72 | type Name string 73 | 74 | // very simplistic regexp for human name validation 75 | var expName = regexp.MustCompile(`[a-zA-Z\ ]{1,64}`) 76 | 77 | // attach Validate() method to our custom name type, satisfying the legit.Object interface, 78 | // defining our custom name validation. 79 | func (n Name) Validate() error { 80 | if !expName.MatchString(string(n)) { 81 | return errors.New("invalid name") 82 | } 83 | 84 | return nil 85 | } 86 | 87 | type User struct { 88 | Email legit.Email `json:"email"` 89 | Name Name `json:"name"` 90 | } 91 | 92 | func main() { 93 | body := []byte(`{"email": "test@example.org", "name": "John Doe"}`) 94 | 95 | var user User 96 | json.Unmarshal(body, &user) 97 | 98 | err := legit.Validate(user) 99 | if err != nil { 100 | fmt.Println("invalid user!", err) 101 | } 102 | } 103 | 104 | ``` 105 | 106 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | // Decoder is used to decode a request body to a type 11 | type Decoder interface { 12 | // return true if decoder can understand the given MIME type 13 | Match(mime string) bool 14 | 15 | // return nil if data from reader was unmarshaled into dst 16 | Decode(r io.Reader, dst interface{}) error 17 | } 18 | 19 | // Decoders contains multiple decoders for matching 20 | type Decoders []Decoder 21 | 22 | // return the first matching decoder for a MIME type, or nil if no match 23 | func (d Decoders) Match(mime string) Decoder { 24 | for _, dec := range d { 25 | if dec.Match(mime) { 26 | return dec 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // JSON decoder can decode any JSON body with the MIME type "application/json" 34 | type JSON struct{} 35 | 36 | func (j JSON) Match(mime string) bool { 37 | return strings.HasPrefix(mime, "application/json") 38 | } 39 | 40 | func (j JSON) Decode(r io.Reader, dst interface{}) error { 41 | return json.NewDecoder(r).Decode(dst) 42 | } 43 | 44 | type XML struct{} 45 | 46 | func (x XML) Match(mime string) bool { 47 | return strings.HasPrefix(mime, "application/xml") 48 | } 49 | 50 | func (x XML) Decode(r io.Reader, dst interface{}) error { 51 | return xml.NewDecoder(r).Decode(dst) 52 | } 53 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDecoders_Match(t *testing.T) { 12 | d := Decoders{JSON{}} 13 | assert.Equal(t, JSON{}, d.Match("application/json; charset=utf-8")) 14 | assert.Equal(t, nil, d.Match("application/xml")) 15 | } 16 | 17 | func TestJSON_Match(t *testing.T) { 18 | j := JSON{} 19 | assert.True(t, j.Match("application/json")) 20 | assert.True(t, j.Match("application/json; charset=utf-8")) 21 | assert.False(t, j.Match("application/xml")) 22 | } 23 | 24 | func TestJSON_Decode(t *testing.T) { 25 | j := JSON{} 26 | r := bytes.NewReader([]byte(`"hello world"`)) 27 | 28 | var body string 29 | err := j.Decode(r, &body) 30 | if assert.NoError(t, err) { 31 | assert.Equal(t, "hello world", body) 32 | } 33 | } 34 | 35 | func TestXML_Match(t *testing.T) { 36 | x := XML{} 37 | 38 | assert.True(t, x.Match("application/xml")) 39 | assert.True(t, x.Match("application/xml; charset=utf-8")) 40 | assert.False(t, x.Match("application/json")) 41 | } 42 | 43 | func TestXML_Decode(t *testing.T) { 44 | x := XML{} 45 | r := bytes.NewReader([]byte(`Hello World`)) 46 | 47 | var body struct { 48 | XMLName xml.Name `xml:"Test"` 49 | 50 | Value string `xml:"Value"` 51 | } 52 | err := x.Decode(r, &body) 53 | if assert.NoError(t, err) { 54 | assert.Equal(t, "Hello World", body.Value) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Legit is an input validation framework for Go. Legit differs from existing 2 | // frameworks by constructing validation from types and interfaces, preferring 3 | // custom validators to complex struct tags. 4 | package legit 5 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Errors contains one or more validate errors 8 | type Errors []error 9 | 10 | // returns string representation of the first validation error encountered 11 | func (e Errors) Error() string { 12 | if len(e) > 0 { 13 | return e[0].Error() 14 | } 15 | 16 | return "" 17 | } 18 | 19 | // StructError contains the field name and message of a failed validation 20 | type StructError struct { 21 | Field string `json:"field"` 22 | Message error `json:"message"` 23 | } 24 | 25 | // returns the string representation of the field and failed validation 26 | func (se StructError) Error() string { 27 | return fmt.Sprintf("%s: %s", se.Field, se.Message) 28 | } 29 | 30 | // SliceError contains the index and message of a failed validation 31 | type SliceError struct { 32 | Index int `json:"index"` 33 | Message error `json:"message"` 34 | } 35 | 36 | // returns the string representation of the index and failed validation 37 | func (se SliceError) Error() string { 38 | return fmt.Sprintf("%d: %s", se.Index, se.Message) 39 | } 40 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestErrors_Error(t *testing.T) { 11 | assert.Equal(t, "foo", Errors{errors.New("foo"), errors.New("bar")}.Error()) 12 | assert.Equal(t, "", Errors{}.Error()) 13 | } 14 | 15 | func TestStructError_Error(t *testing.T) { 16 | assert.Equal(t, "foo: bar", StructError{Field: "foo", Message: errors.New("bar")}.Error()) 17 | } 18 | 19 | func TestSliceError_Error(t *testing.T) { 20 | assert.Equal(t, "1: bar", SliceError{Index: 1, Message: errors.New("bar")}.Error()) 21 | } 22 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type User struct { 9 | Email Email 10 | } 11 | 12 | func ExampleValidate() { 13 | body := []byte(`{"email": "not an email"}`) 14 | 15 | var user User 16 | json.Unmarshal(body, &user) 17 | 18 | err := Validate(user) 19 | if err != nil { 20 | fmt.Println("invalid user!", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /form.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | var ( 10 | // ErrEncoding is returned when a matching decoder is not found for 11 | // the encoding. 12 | ErrEncoding = errors.New("unknown encoding") 13 | ) 14 | 15 | // Form implements the decoding and validation of user data from readers 16 | // and HTTP requests 17 | type Form struct { 18 | Legit Legit 19 | Decoders Decoders 20 | } 21 | 22 | var form = NewForm() 23 | 24 | // NewForm returns a Form assignment with the default Legit configuration and 25 | // a JSON decoder 26 | func NewForm() Form { 27 | return Form{ 28 | Legit: New(), 29 | Decoders: Decoders{JSON{}}, 30 | } 31 | } 32 | 33 | // ParseAndValidate first decodes a reader using the first decoder matching the 34 | // given mime type, then applies validation to the collected input 35 | func ParseAndValidate(r io.Reader, mime string, dst interface{}) error { 36 | return form.ParseAndValidate(r, mime, dst) 37 | } 38 | 39 | // ParseAndValidate first decodes a reader using the first decoder matching the 40 | // given mime type, then applies validation to the collected input 41 | func (f Form) ParseAndValidate(r io.Reader, mime string, dst interface{}) error { 42 | dec := f.Decoders.Match(mime) 43 | if dec == nil { 44 | return ErrEncoding 45 | } 46 | 47 | err := dec.Decode(r, dst) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | err = f.Legit.Validate(dst) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // ParseRequestAndValidate is the same as ParseAndValidate accepting a HTTP 61 | // request for the reader and using the "Content-Type" header for the MIME type 62 | func ParseRequestAndValidate(r *http.Request, dst interface{}) error { 63 | return form.ParseRequestAndValidate(r, dst) 64 | } 65 | 66 | // ParseRequestAndValidate is the same as ParseAndValidate accepting a HTTP 67 | // request for the reader and using the "Content-Type" header for the MIME type 68 | func (f Form) ParseRequestAndValidate(r *http.Request, dst interface{}) error { 69 | return f.ParseAndValidate(r.Body, r.Header.Get("Content-Type"), dst) 70 | } 71 | -------------------------------------------------------------------------------- /form_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestForm_NewForm(t *testing.T) { 13 | f := NewForm() 14 | assert.False(t, f.Legit.Strict) 15 | assert.Equal(t, Decoders{JSON{}}, f.Decoders) 16 | } 17 | 18 | func TestForm_ParseAndValidate(t *testing.T) { 19 | r := bytes.NewReader([]byte(`"foo"`)) 20 | 21 | var body Lower 22 | err := form.ParseAndValidate(r, "application/json", &body) 23 | assert.NoError(t, err) 24 | assert.Equal(t, Lower("foo"), body) 25 | } 26 | 27 | func TestForm_ParseAndValidate_noDecoder(t *testing.T) { 28 | err := form.ParseAndValidate(nil, "application/xml", nil) 29 | assert.Equal(t, ErrEncoding, err) 30 | } 31 | 32 | func TestForm_ParseAndValidate_invalidBody(t *testing.T) { 33 | r := bytes.NewReader([]byte(`foo`)) 34 | 35 | var body string 36 | err := form.ParseAndValidate(r, "application/json", &body) 37 | assert.NotNil(t, err) 38 | } 39 | 40 | func TestForm_ParseAndValidate_validation(t *testing.T) { 41 | r := bytes.NewReader([]byte(`"FOO"`)) 42 | 43 | var body Lower 44 | err := form.ParseAndValidate(r, "application/json", &body) 45 | if assert.NotNil(t, err) { 46 | assert.Equal(t, errLower, err) 47 | } 48 | } 49 | 50 | func TestForm_ParseRequestAndValidate(t *testing.T) { 51 | r := &http.Request{ 52 | Body: ioutil.NopCloser(bytes.NewReader([]byte(`"foo"`))), 53 | Header: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}}, 54 | } 55 | 56 | var body Lower 57 | err := form.ParseRequestAndValidate(r, &body) 58 | assert.NoError(t, err) 59 | assert.Equal(t, Lower("foo"), body) 60 | } 61 | -------------------------------------------------------------------------------- /legit.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | var ( 9 | // ErrNotStruct is returned when a struct validation method is not given 10 | // a struct. 11 | ErrNotStruct = errors.New("object is not a struct") 12 | 13 | // ErrNotSlice is returned when a slice validation method is not given 14 | // a slice. 15 | ErrNotSlice = errors.New("object is not a slice") 16 | 17 | // ErrStrict is returned when strict validation mode is enabled and a 18 | // field does not satisfy the Validator interface. 19 | ErrStrict = errors.New("field is not a validator") 20 | ) 21 | 22 | // Validator is a type that can be validated 23 | type Validator interface { 24 | // returns nil if object is valid 25 | Validate() error 26 | } 27 | 28 | var validator = reflect.TypeOf((*Validator)(nil)).Elem() 29 | 30 | var legit = New() 31 | 32 | // Legit implements validation of types implementing the Validator interface, 33 | // structs and slices. 34 | type Legit struct { 35 | // Strict mode requires that all fields in a struct be validatable 36 | Strict bool 37 | } 38 | 39 | // New return a Legit assignment without strict validation 40 | func New() Legit { 41 | return Legit{ 42 | Strict: false, 43 | } 44 | } 45 | 46 | func Validate(src interface{}) error { 47 | return legit.Validate(src) 48 | } 49 | 50 | func (l Legit) Validate(src interface{}) error { 51 | // prevent Validation methods being called on nil pointers 52 | if src == nil { 53 | return nil 54 | } 55 | 56 | // skip reflection if src implements custom Validator interface 57 | if obj, ok := src.(Validator); ok { 58 | return obj.Validate() 59 | } 60 | 61 | objv := resolvePointer(reflect.ValueOf(src)) 62 | objt := objv.Type() 63 | 64 | return l.validate(objv, objt) 65 | } 66 | 67 | func (l Legit) validate(objv reflect.Value, objt reflect.Type) error { 68 | // don't attempt to validate pointers (optional fields) 69 | if objv.Kind() == reflect.Ptr && objv.IsNil() { 70 | return nil 71 | } 72 | 73 | if objt.Implements(validator) { 74 | return objv.Interface().(Validator).Validate() 75 | } 76 | 77 | switch objv.Kind() { 78 | case reflect.Struct: 79 | return l.validateStruct(objv, objt) 80 | case reflect.Slice: 81 | return l.validateSlice(objv, objt) 82 | } 83 | 84 | if l.Strict { 85 | return ErrStrict 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func ValidateStruct(src interface{}) error { 92 | return legit.ValidateStruct(src) 93 | } 94 | 95 | func (l Legit) ValidateStruct(src interface{}) error { 96 | objv := resolvePointer(reflect.ValueOf(src)) 97 | if objv.Kind() != reflect.Struct { 98 | return ErrNotStruct 99 | } 100 | objt := objv.Type() 101 | 102 | return l.validateStruct(objv, objt) 103 | } 104 | 105 | func (l Legit) validateStruct(objv reflect.Value, objt reflect.Type) error { 106 | var errors Errors 107 | 108 | for i := 0; i < objt.NumField(); i++ { 109 | ft := objt.Field(i) 110 | 111 | // see reflect StructField.PkgPath for determining if field is exported 112 | // TODO: is there a better way to determine if a field is exported? 113 | if len(ft.PkgPath) < 1 { 114 | fv := objv.Field(i) 115 | 116 | err := l.validate(fv, fv.Type()) 117 | if err != nil { 118 | errors = append(errors, StructError{Field: ft.Name, Message: err}) 119 | } 120 | } 121 | } 122 | 123 | if len(errors) > 0 { 124 | return errors 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func ValidateSlice(src interface{}) error { 131 | return legit.ValidateSlice(src) 132 | } 133 | 134 | func (l Legit) ValidateSlice(src interface{}) error { 135 | objv := resolvePointer(reflect.ValueOf(src)) 136 | if objv.Kind() != reflect.Slice { 137 | return ErrNotSlice 138 | } 139 | objt := objv.Type() 140 | 141 | return l.validateSlice(objv, objt) 142 | } 143 | 144 | func (l Legit) validateSlice(objv reflect.Value, objt reflect.Type) error { 145 | if objv.Len() < 1 { 146 | return nil 147 | } 148 | 149 | var errors Errors 150 | 151 | for i := 0; i < objv.Len(); i++ { 152 | iv := objv.Index(i) 153 | err := l.validate(iv, iv.Type()) 154 | if err != nil { 155 | errors = append(errors, SliceError{Index: i, Message: err}) 156 | } 157 | } 158 | 159 | if len(errors) > 0 { 160 | return errors 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // return concrete type from arbitrary pointer depth 167 | func resolvePointer(objv reflect.Value) reflect.Value { 168 | for { 169 | if objv.Kind() == reflect.Ptr { 170 | objv = objv.Elem() 171 | } else { 172 | break 173 | } 174 | } 175 | 176 | return objv 177 | } 178 | -------------------------------------------------------------------------------- /legit_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLegit_New(t *testing.T) { 11 | v := New() 12 | assert.False(t, v.Strict) 13 | } 14 | 15 | func TestValidate(t *testing.T) { 16 | err := Validate(struct { 17 | Name Lower 18 | }{"FOO"}) 19 | if assert.NotNil(t, err) { 20 | assert.Equal(t, Errors{StructError{Field: "Name", Message: errLower}}, err) 21 | } 22 | } 23 | 24 | func TestLegit_Validate(t *testing.T) { 25 | err := legit.Validate([]Lower{"FOO"}) 26 | if assert.NotNil(t, err) { 27 | assert.Equal(t, Errors{SliceError{Index: 0, Message: errLower}}, err) 28 | } 29 | } 30 | 31 | func TestLegit_Validate_customValidator(t *testing.T) { 32 | err := legit.Validate(Lower("foo")) 33 | assert.NoError(t, err) 34 | 35 | err = legit.Validate(Lower("FOO")) 36 | if assert.NotNil(t, err) { 37 | assert.Equal(t, errLower, err) 38 | } 39 | } 40 | 41 | func TestLegit_validate_struct(t *testing.T) { 42 | err := legit.validate(reflected(struct { 43 | Name Lower 44 | }{"FOO"})) 45 | if assert.NotNil(t, err) { 46 | assert.Equal(t, Errors{StructError{Field: "Name", Message: errLower}}, err) 47 | } 48 | } 49 | 50 | func TestLegit_validate_slice(t *testing.T) { 51 | err := legit.validate(reflected([]Lower{"FOO"})) 52 | if assert.NotNil(t, err) { 53 | assert.Equal(t, Errors{SliceError{Index: 0, Message: errLower}}, err) 54 | } 55 | } 56 | 57 | func TestLegit_validate_customValidator(t *testing.T) { 58 | err := legit.validate(reflected(Lower("foo"))) 59 | assert.NoError(t, err) 60 | 61 | err = legit.validate(reflected(Lower("FOO"))) 62 | if assert.NotNil(t, err) { 63 | assert.Equal(t, errLower, err) 64 | } 65 | } 66 | 67 | func TestLegit_validate_strict(t *testing.T) { 68 | l := Legit{Strict: true} 69 | err := l.validate(reflected("foo")) 70 | assert.Equal(t, ErrStrict, err) 71 | } 72 | 73 | func TestLegit_validate_unknown(t *testing.T) { 74 | err := legit.validate(reflected("foo")) 75 | assert.NoError(t, err) 76 | } 77 | 78 | func TestValidateStruct(t *testing.T) { 79 | err := ValidateStruct(struct { 80 | Name Lower 81 | }{"FOO"}) 82 | if assert.NotNil(t, err) { 83 | assert.Equal(t, Errors{StructError{Field: "Name", Message: errLower}}, err) 84 | } 85 | 86 | } 87 | 88 | func TestLegit_ValidateStruct(t *testing.T) { 89 | err := legit.ValidateStruct(Lower("foo")) 90 | assert.Equal(t, ErrNotStruct, err) 91 | 92 | err = legit.ValidateStruct(struct { 93 | Name Lower 94 | }{"FOO"}) 95 | if assert.NotNil(t, err) { 96 | assert.Equal(t, Errors{StructError{Field: "Name", Message: errLower}}, err) 97 | } 98 | } 99 | 100 | func TestLegit_validateStruct(t *testing.T) { 101 | err := legit.validateStruct(reflected(struct { 102 | Name Lower 103 | name Lower // unexported fields should NOT be validated 104 | }{"foo", "FOO"})) 105 | assert.NoError(t, err) 106 | 107 | err = legit.validateStruct(reflected(struct { 108 | First Lower 109 | Last Lower 110 | }{"foo", "FOO"})) 111 | if assert.NotNil(t, err) { 112 | assert.Equal(t, Errors{StructError{Field: "Last", Message: errLower}}, err) 113 | } 114 | } 115 | 116 | func TestValidateSlice(t *testing.T) { 117 | err := ValidateSlice([]Lower{"FOO"}) 118 | if assert.NotNil(t, err) { 119 | assert.Equal(t, Errors{SliceError{Index: 0, Message: errLower}}, err) 120 | } 121 | } 122 | 123 | func TestLegit_ValidateSlice(t *testing.T) { 124 | err := legit.ValidateSlice(Lower("foo")) 125 | assert.Equal(t, ErrNotSlice, err) 126 | 127 | err = legit.ValidateSlice([]Lower{"FOO"}) 128 | if assert.NotNil(t, err) { 129 | assert.Equal(t, Errors{SliceError{Index: 0, Message: errLower}}, err) 130 | } 131 | } 132 | 133 | func TestLegit_validateSlice(t *testing.T) { 134 | err := legit.validateSlice(reflected([]Lower{})) 135 | assert.NoError(t, err) 136 | 137 | err = legit.validateSlice(reflected([]Lower{"foo"})) 138 | assert.NoError(t, err) 139 | 140 | err = legit.validateSlice(reflected([]Lower{"foo", "FOO"})) 141 | if assert.NotNil(t, err) { 142 | assert.Equal(t, Errors{SliceError{Index: 1, Message: errLower}}, err) 143 | } 144 | } 145 | 146 | func TestResolvePointer(t *testing.T) { 147 | v := resolvePointer(reflect.ValueOf(&struct{ Foo string }{"bar"})) 148 | assert.Equal(t, reflect.Struct, v.Kind()) 149 | } 150 | 151 | func reflected(src interface{}) (objv reflect.Value, objt reflect.Type) { 152 | objv = reflect.ValueOf(src) 153 | objt = objv.Type() 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /numbers.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Positive validates any integer that contains a value above (and including) zero. 8 | type Positive int 9 | 10 | var errPositive = errors.New("number is not positive") 11 | 12 | func (p Positive) Validate() error { 13 | if p < 0 { 14 | return errPositive 15 | } 16 | 17 | return nil 18 | } 19 | 20 | // Negative validates any integer that contains a value below zero. 21 | type Negative int 22 | 23 | var errNegative = errors.New("number is not negative") 24 | 25 | func (n Negative) Validate() error { 26 | if n > -1 { 27 | return errNegative 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /numbers_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPositive(t *testing.T) { 10 | testNumber(t, Positive(100), Positive(-100), errPositive) 11 | } 12 | 13 | func TestNegative(t *testing.T) { 14 | testNumber(t, Negative(-100), Negative(100), errNegative) 15 | } 16 | 17 | func testNumber(t *testing.T, pass, fail Validator, failErr error) { 18 | assert.NoError(t, pass.Validate()) 19 | 20 | err := fail.Validate() 21 | if assert.NotNil(t, err) { 22 | assert.Equal(t, failErr, err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /patterns.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | ) 7 | 8 | // Email validates any string matching a RFC 5322 email address 9 | type Email string 10 | 11 | // RFC 5322 "official" email regexp 12 | var expEmail = regexp.MustCompile(`(?:[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])`) 13 | 14 | var errEmail = errors.New("invalid email") 15 | 16 | func (e Email) Validate() error { 17 | if !expEmail.MatchString(string(e)) { 18 | return errEmail 19 | } 20 | 21 | return nil 22 | } 23 | 24 | // CreditCard validates any string matching a credit card number 25 | type CreditCard string 26 | 27 | var expCreditCard = regexp.MustCompile(`^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$`) 28 | 29 | var errCreditCard = errors.New("invalid credit card") 30 | 31 | func (c CreditCard) Validate() error { 32 | if !expCreditCard.MatchString(string(c)) { 33 | return errCreditCard 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // UUID validates any string matching a UUID of any version 40 | type UUID string 41 | 42 | var expUUID = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) 43 | 44 | var errUUID = errors.New("invalid uuid") 45 | 46 | func (u UUID) Validate() error { 47 | if !expUUID.MatchString(string(u)) { 48 | return errUUID 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // UUID3 validates any string matching a version 3 UUID 55 | type UUID3 string 56 | 57 | var expUUID3 = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$`) 58 | 59 | var errUUID3 = errors.New("invalid uuid3") 60 | 61 | func (u UUID3) Validate() error { 62 | if !expUUID3.MatchString(string(u)) { 63 | return errUUID3 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // UUID4 validates any string matching a version 4 UUID 70 | type UUID4 string 71 | 72 | var expUUID4 = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`) 73 | 74 | var errUUID4 = errors.New("invalid uuid4") 75 | 76 | func (u UUID4) Validate() error { 77 | if !expUUID4.MatchString(string(u)) { 78 | return errUUID4 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // UUID5 validates any string matching a version 5 UUID 85 | type UUID5 string 86 | 87 | var expUUID5 = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`) 88 | 89 | var errUUID5 = errors.New("invalid uuid5") 90 | 91 | func (u UUID5) Validate() error { 92 | if !expUUID5.MatchString(string(u)) { 93 | return errUUID5 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /patterns_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEmail(t *testing.T) { 8 | testString(t, Email("foo@example.org"), Email("foo@@bar@!"), errEmail) 9 | } 10 | 11 | func TestCreditCard(t *testing.T) { 12 | testString(t, CreditCard("375556917985515"), CreditCard("foo"), errCreditCard) 13 | } 14 | 15 | func TestUUID(t *testing.T) { 16 | testString(t, UUID("a987fbc9-4bed-3078-cf07-9141ba07c9f3"), UUID("xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3"), errUUID) 17 | } 18 | 19 | func TestUUID3(t *testing.T) { 20 | testString(t, UUID3("a987fbc9-4bed-3078-cf07-9141ba07c9f3"), UUID3("xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3"), errUUID3) 21 | } 22 | 23 | func TestUUID4(t *testing.T) { 24 | testString(t, UUID4("625e63f3-58f5-40b7-83a1-a72ad31acffb"), UUID4("xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3"), errUUID4) 25 | } 26 | 27 | func TestUUID5(t *testing.T) { 28 | testString(t, UUID5("987fbc97-4bed-5078-9f07-9141ba07c9f3"), UUID5("xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3"), errUUID5) 29 | } 30 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | var errUnsupportedScan = errors.New("unsupported scan") 11 | 12 | // Lower validates any string not containing any uppercase characters. 13 | type Lower string 14 | 15 | func (l *Lower) Scan(src interface{}) error { 16 | if s, ok := src.(string); ok { 17 | *l = Lower(s) 18 | return nil 19 | } 20 | 21 | return errUnsupportedScan 22 | } 23 | 24 | func (l Lower) Value() (driver.Value, error) { 25 | return string(l), nil 26 | } 27 | 28 | var errLower = errors.New("string is not lowercase") 29 | 30 | func (l Lower) Validate() error { 31 | for _, r := range l { 32 | if !unicode.IsLower(r) { 33 | return errLower 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // Upper validates any string not containing any lowercase characters. 41 | type Upper string 42 | 43 | func (u *Upper) Scan(src interface{}) error { 44 | if s, ok := src.(string); ok { 45 | *u = Upper(s) 46 | return nil 47 | } 48 | 49 | return errUnsupportedScan 50 | } 51 | 52 | func (u Upper) Value() (driver.Value, error) { 53 | return string(u), nil 54 | } 55 | 56 | var errUpper = errors.New("string is not uppercase") 57 | 58 | func (u Upper) Validate() error { 59 | for _, r := range u { 60 | if !unicode.IsUpper(r) { 61 | return errUpper 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // NoSpace validates any string not containing any whitespace characters. 69 | type NoSpace string 70 | 71 | func (ns *NoSpace) Scan(src interface{}) error { 72 | if s, ok := src.(string); ok { 73 | *ns = NoSpace(s) 74 | return nil 75 | } 76 | 77 | return errUnsupportedScan 78 | } 79 | 80 | func (ns NoSpace) Value() (driver.Value, error) { 81 | return string(ns), nil 82 | } 83 | 84 | var errNoSpace = errors.New("string contains whitespace") 85 | 86 | func (ns NoSpace) Validate() error { 87 | for _, r := range ns { 88 | if unicode.IsSpace(r) { 89 | return errNoSpace 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // Printable validates any string not containing any non-printing characters. 97 | type Printable string 98 | 99 | func (p *Printable) Scan(src interface{}) error { 100 | if s, ok := src.(string); ok { 101 | *p = Printable(s) 102 | return nil 103 | } 104 | 105 | return errUnsupportedScan 106 | } 107 | 108 | func (p Printable) Value() (driver.Value, error) { 109 | return string(p), nil 110 | } 111 | 112 | var errPrintable = errors.New("string contains non-printing characters") 113 | 114 | func (p Printable) Validate() error { 115 | for _, r := range p { 116 | if !unicode.IsPrint(r) { 117 | return errPrintable 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // Alpha validates any string containing only letters. 125 | type Alpha string 126 | 127 | func (a *Alpha) Scan(src interface{}) error { 128 | if s, ok := src.(string); ok { 129 | *a = Alpha(s) 130 | return nil 131 | } 132 | 133 | return errUnsupportedScan 134 | } 135 | 136 | func (a Alpha) Value() (driver.Value, error) { 137 | return string(a), nil 138 | } 139 | 140 | var errAlpha = errors.New("string contains non-alpha characters") 141 | 142 | func (a Alpha) Validate() error { 143 | for _, r := range a { 144 | if !unicode.IsLetter(r) { 145 | return errAlpha 146 | } 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // Number validates any string containing only numeric characters. 153 | type Number string 154 | 155 | func (n *Number) Scan(src interface{}) error { 156 | if s, ok := src.(string); ok { 157 | *n = Number(s) 158 | return nil 159 | } 160 | 161 | return errUnsupportedScan 162 | } 163 | 164 | func (n Number) Value() (driver.Value, error) { 165 | return string(n), nil 166 | } 167 | 168 | var errNumber = errors.New("string contains non-numeric characters") 169 | 170 | func (n Number) Validate() error { 171 | for _, r := range n { 172 | if !unicode.IsNumber(r) { 173 | return errNumber 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // Float validates any string containing numbers, including an initial minus 181 | // and a single decimal point. 182 | type Float string 183 | 184 | func (f *Float) Scan(src interface{}) error { 185 | if s, ok := src.(string); ok { 186 | *f = Float(s) 187 | return nil 188 | } 189 | 190 | return errUnsupportedScan 191 | } 192 | 193 | func (f Float) Value() (driver.Value, error) { 194 | return string(f), nil 195 | } 196 | 197 | var errFloat = errors.New("float contains non-numeric characters") 198 | 199 | func (f Float) Validate() error { 200 | if len(f) == 0 { 201 | return errFloat 202 | } 203 | 204 | s := string(f) 205 | if s[0] == '-' { 206 | s = s[1:] 207 | } 208 | 209 | if strings.Count(s, ".") > 1 { 210 | return errFloat 211 | } else if s[0] == '.' || s[len(s)-1] == '.' { 212 | return errFloat 213 | } 214 | 215 | i := strings.IndexFunc(s, func(r rune) bool { 216 | return !((r >= '0' && r <= '9') || r == '.') 217 | }) 218 | if i > -1 { 219 | return errFloat 220 | } 221 | 222 | return nil 223 | } 224 | 225 | // Alphanumeric validates any string containing only letters or numbers. 226 | type Alphanumeric string 227 | 228 | func (a *Alphanumeric) Scan(src interface{}) error { 229 | if s, ok := src.(string); ok { 230 | *a = Alphanumeric(s) 231 | return nil 232 | } 233 | 234 | return errUnsupportedScan 235 | } 236 | 237 | func (a Alphanumeric) Value() (driver.Value, error) { 238 | return string(a), nil 239 | } 240 | 241 | var errAlphanumeric = errors.New("string contains non-alphanumeric characters") 242 | 243 | func (a Alphanumeric) Validate() error { 244 | for _, r := range a { 245 | if !unicode.IsLetter(r) && !unicode.IsNumber(r) { 246 | return errAlphanumeric 247 | } 248 | } 249 | 250 | return nil 251 | } 252 | 253 | // ASCII validates any string containing only ASCII characters. 254 | type ASCII string 255 | 256 | func (a *ASCII) Scan(src interface{}) error { 257 | if s, ok := src.(string); ok { 258 | *a = ASCII(s) 259 | return nil 260 | } 261 | 262 | return errUnsupportedScan 263 | } 264 | 265 | func (a ASCII) Value() (driver.Value, error) { 266 | return string(a), nil 267 | } 268 | 269 | var errASCII = errors.New("string contains non-ASCII characters") 270 | 271 | func (a ASCII) Validate() error { 272 | for _, r := range a { 273 | if r > unicode.MaxASCII { 274 | return errASCII 275 | } 276 | } 277 | 278 | return nil 279 | } 280 | 281 | // Required validates any string that is not empty 282 | type Required string 283 | 284 | func (r *Required) Scan(src interface{}) error { 285 | if s, ok := src.(string); ok { 286 | *r = Required(s) 287 | return nil 288 | } 289 | 290 | return errUnsupportedScan 291 | } 292 | 293 | func (r Required) Value() (driver.Value, error) { 294 | return string(r), nil 295 | } 296 | 297 | var errRequired = errors.New("string is required") 298 | 299 | func (r Required) Validate() error { 300 | if len(r) < 1 { 301 | return errRequired 302 | } 303 | 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /strings_test.go: -------------------------------------------------------------------------------- 1 | package legit 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestLower(t *testing.T) { 12 | testString(t, Lower("foo"), Lower("FOO"), errLower) 13 | } 14 | 15 | func TestUpper(t *testing.T) { 16 | testString(t, Upper("FOO"), Upper("foo"), errUpper) 17 | } 18 | 19 | func TestNoSpace(t *testing.T) { 20 | testString(t, NoSpace("foo"), NoSpace(" foo\t bar "), errNoSpace) 21 | } 22 | 23 | func TestPrintable(t *testing.T) { 24 | testString(t, Printable("foo"), Printable("\x00foo"), errPrintable) 25 | } 26 | 27 | func TestAlpha(t *testing.T) { 28 | testString(t, Alpha("foo"), Alpha("abc123"), errAlpha) 29 | } 30 | 31 | func TestNumber(t *testing.T) { 32 | testString(t, Number("1234"), Number("foo"), errNumber) 33 | } 34 | 35 | func TestFloat(t *testing.T) { 36 | testString(t, Float("-1.23"), Float("foo"), errFloat) 37 | 38 | assert.Error(t, Float("").Validate()) 39 | assert.Error(t, Float("1.2.3").Validate()) 40 | assert.Error(t, Float(".1").Validate()) 41 | assert.Error(t, Float("1.").Validate()) 42 | } 43 | 44 | func TestAlphanumeric(t *testing.T) { 45 | testString(t, Alphanumeric("abc123"), Alphanumeric(" foo! "), errAlphanumeric) 46 | } 47 | 48 | func TestASCII(t *testing.T) { 49 | testString(t, ASCII("abc123"), ASCII("föö"), errASCII) 50 | } 51 | 52 | func TestRequired(t *testing.T) { 53 | testString(t, Required("foo"), Required(""), errRequired) 54 | } 55 | 56 | func testString(t *testing.T, pass, fail Validator, failErr error) { 57 | assert.NoError(t, pass.Validate()) 58 | 59 | err := fail.Validate() 60 | if assert.NotNil(t, err) { 61 | assert.Equal(t, failErr, err) 62 | } 63 | } 64 | 65 | func TestStringScan(t *testing.T) { 66 | tests := []struct { 67 | Name string 68 | Scanner sql.Scanner 69 | Src interface{} 70 | Value sql.Scanner 71 | Error error 72 | }{ 73 | {"Lower/Fail", (*Lower)(strPtr("")), 0, nil, errUnsupportedScan}, 74 | {"Lower/Success", (*Lower)(strPtr("")), "foo", (*Lower)(strPtr("foo")), nil}, 75 | {"Upper/Fail", (*Upper)(strPtr("")), 0, nil, errUnsupportedScan}, 76 | {"Upper/Success", (*Upper)(strPtr("")), "foo", (*Upper)(strPtr("foo")), nil}, 77 | {"NoSpace/Fail", (*NoSpace)(strPtr("")), 0, nil, errUnsupportedScan}, 78 | {"NoSpace/Success", (*NoSpace)(strPtr("")), "foo", (*NoSpace)(strPtr("foo")), nil}, 79 | {"Printable/Fail", (*Printable)(strPtr("")), 0, nil, errUnsupportedScan}, 80 | {"Printable/Success", (*Printable)(strPtr("")), "foo", (*Printable)(strPtr("foo")), nil}, 81 | {"Alpha/Fail", (*Alpha)(strPtr("")), 0, nil, errUnsupportedScan}, 82 | {"Alpha/Success", (*Alpha)(strPtr("")), "foo", (*Alpha)(strPtr("foo")), nil}, 83 | {"Number/Fail", (*Number)(strPtr("")), 0, nil, errUnsupportedScan}, 84 | {"Number/Success", (*Number)(strPtr("")), "foo", (*Number)(strPtr("foo")), nil}, 85 | {"Float/Fail", (*Float)(strPtr("")), 0, nil, errUnsupportedScan}, 86 | {"Float/Success", (*Float)(strPtr("")), "foo", (*Float)(strPtr("foo")), nil}, 87 | {"Alphanumeric/Fail", (*Alphanumeric)(strPtr("")), 0, nil, errUnsupportedScan}, 88 | {"Alphanumeric/Success", (*Alphanumeric)(strPtr("")), "foo", (*Alphanumeric)(strPtr("foo")), nil}, 89 | {"ASCII/Fail", (*ASCII)(strPtr("")), 0, nil, errUnsupportedScan}, 90 | {"ASCII/Success", (*ASCII)(strPtr("")), "foo", (*ASCII)(strPtr("foo")), nil}, 91 | {"Required/Fail", (*Required)(strPtr("")), 0, nil, errUnsupportedScan}, 92 | {"Required/Success", (*Required)(strPtr("")), "foo", (*Required)(strPtr("foo")), nil}, 93 | } 94 | 95 | for _, test := range tests { 96 | t.Run(test.Name, func(t *testing.T) { 97 | err := test.Scanner.Scan(test.Src) 98 | if test.Error == nil { 99 | if assert.NoError(t, err) { 100 | assert.Equal(t, test.Value, test.Scanner) 101 | } 102 | } else { 103 | assert.Equal(t, test.Error, err) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func strPtr(s string) *string { 110 | return &s 111 | } 112 | 113 | func TestStringValue(t *testing.T) { 114 | tests := []struct { 115 | Name string 116 | Valuer driver.Valuer 117 | Value driver.Value 118 | Error error 119 | }{ 120 | {"Lower", Lower("foo"), "foo", nil}, 121 | {"Upper", Upper("foo"), "foo", nil}, 122 | {"NoSpace", NoSpace("foo"), "foo", nil}, 123 | {"Printable", Printable("foo"), "foo", nil}, 124 | {"Alpha", Alpha("foo"), "foo", nil}, 125 | {"Number", Number("foo"), "foo", nil}, 126 | {"Float", Float("foo"), "foo", nil}, 127 | {"Alphanumeric", Alphanumeric("foo"), "foo", nil}, 128 | {"ASCII", ASCII("foo"), "foo", nil}, 129 | {"Required", Required("foo"), "foo", nil}, 130 | } 131 | 132 | for _, test := range tests { 133 | t.Run(test.Name, func(t *testing.T) { 134 | v, err := test.Valuer.Value() 135 | if test.Error == nil { 136 | if assert.NoError(t, err) { 137 | assert.Equal(t, test.Value, v) 138 | } 139 | } else { 140 | assert.Equal(t, test.Error, err) 141 | } 142 | }) 143 | } 144 | } 145 | --------------------------------------------------------------------------------