├── 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 |
2 |
3 | [](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 |
--------------------------------------------------------------------------------