/
validation.go
283 lines (253 loc) · 10.4 KB
/
validation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// This package handles data validation
package validation
import (
"encoding/json"
"fmt"
"log"
"reflect"
"regexp"
"strings"
"github.com/grebett/tools"
"gopkg.in/mgo.v2/bson"
)
//***********************************************************************************
// CONSTANTS
//***********************************************************************************
const (
UNAUTHENTICATED = iota
USER
OWNER
ADMIN
NONE
)
const (
INIT = iota
GET
SET
)
//***********************************************************************************
// STRUCTURES
//***********************************************************************************
// DataErrors are detailed errors when receiving or manipulating data
type DataError struct {
Type string `json:"type"`
Reason string `json:"reason"`
Field string `json:"field,omitempty"`
Value interface{} `json:"value,omitempty"`
}
// This struct hosts the Validate fn secondary parameters
type Options struct {
Usage int // INIT, SET, GET
UserRights int // UNAUTHENTICATED to ADMIN
Args interface{} // custom args to be used with Default fn
}
// Error stringer for DataErrors
func (e *DataError) Error() string {
return fmt.Sprintf("%s for %s = %v: %s", e.Type, e.Field, e.Value, e.Reason)
}
// This struct contains information about a specifical fields – could be a separated package later
type Validator struct {
Type string // the string representation of the expected type
Field string // the key the validator is about
Regexp string // if a string, the pattern the valus has to match
Rights [3]int // INIT, GET, SET minimal value to equal to act on the field value
Boundaries Boundaries // if a number, the min and max boundaries for the value
IsRequired bool // is the field required
Default func(interface{}) interface{} // this function is called to replace the optional nil value with default one - the arg interface{} value is usually a map[string]interface{} -- should I change it?
CustomTest func(interface{}) (bool, *DataError) // this function enables user custom testing
}
// This inner struct sets the boundaries for an int value - see above
type Boundaries struct {
Min float64
Max float64
}
//***********************************************************************************
// METHODS - why no private?
//***********************************************************************************
// This method create a regexp from the pattern defined in the Validation struct and test it for the provided string
func (v *Validator) ExecRegexp(str string) (bool, error) {
validate, err := regexp.Compile(v.Regexp)
if err != nil {
return false, err
}
return validate.MatchString(str), nil
}
// This method test if the provided int fits in the validator boundaries
func (v *Validator) CheckBoundaries(value float64) bool {
return value >= v.Boundaries.Min && value <= v.Boundaries.Max
}
// This method checks if the user has the rights for the specified usage
func (v *Validator) CheckRights(userRights int, usage int) bool {
return userRights >= usage
}
//***********************************************************************************
// FUNCTIONS
//***********************************************************************************
// Requirements
// ------------
// *errors = append(*errors, &DataError{Type: "Validation error", Reason: "Required", Field: field})
// Data validation
// ---------------
// This public function runs the provided validators against the provided data
// The usage int is an enum for INIT, GET or SET value
// the checkValue flag enables a more complex validation -- is it still needed?
func Validate(validators map[string]*Validator, _map map[string]interface{}, opt Options) (map[string]interface{}, []*DataError) {
errors := make([]*DataError, 0)
dest := make(map[string]interface{})
// browse the validators and get the path they are written for
for path, validator := range validators {
// get the value
value, err := tools.ReadDeep(_map, path)
if err != nil {
panic(err)
} else {
// if the value is nil or is a slice with len == 0
if value == nil || (reflect.ValueOf(value).Kind() == reflect.Slice && len(value.([]interface{})) == 0) {
// for INIT only, if value does not exist, check in the validators if it is required and apply defaults accordingly
if opt.Usage == INIT {
// does not check for now if the slice is not nil but has nil values in it...
if validator.IsRequired {
errors = append(errors, &DataError{Type: "Validation error", Reason: "Required", Field: path})
} else if validator.Default != nil {
err := tools.WriteDeep(dest, path, validator.Default(opt.Args))
if err != nil {
panic(err)
}
}
}
// else the field is simply ignored
continue
} else {
// copy value to dest (differs based on usage: mongoDB need dot notation for update --> https://docs.mongodb.org/manual/reference/glossary/#term-dot-notation)
if opt.Usage == SET {
dest[path] = value
} else {
err := tools.WriteDeep(dest, path, value)
if err != nil {
panic(err)
}
}
// check type
if checkType(validator, value, &errors) == false {
continue
}
// check requirements
if checkValue(validator, value, &errors) == false {
continue
}
// check rights
if checkRights(validator, opt.Usage, opt.UserRights, &errors) == false {
continue // not useful, but for consistancy
}
}
}
}
return dest, errors
}
// this private function runs the rights validator
// -----------------------------------------------
// the rights property of the validator is of type [3]int
// the three indexes of the array correspond to :
// 0 => initialization rights: the rights needed to set the property when creating the document
// 1 => get rights: can the user get this property?
// 2 => set rights: can the user update the property value?
// rights values are, in order: UNAUTHENTICATED, USER, OWNER, ADMIN, NONE
// returns true if everything is ok, false otherelse (could be the contrary)
func checkRights(validator *Validator, usage int, userRights int, errors *[]*DataError) bool {
if ok := validator.CheckRights(userRights, validator.Rights[usage]); !ok {
*errors = append(*errors, &DataError{Type: "Validation error", Reason: "Insufficient rights", Field: validator.Field})
return false
}
return true
}
// this private function runs the validator according to the provided field if existing
// the validator checks different conditions, some based on value type
// returns true if everything is ok, false otherelse (could be the contrary)
func checkValue(validator *Validator, valueToTest interface{}, errors *[]*DataError) bool {
// test based on value's type
switch value := valueToTest.(type) {
case string:
if validator.Regexp != "" {
ok, err := validator.ExecRegexp(value)
if err != nil {
log.Panic(err) // if the regexp is false, panic!
} else {
if !ok {
*errors = append(*errors, &DataError{"Validation error", "Regex not match", validator.Field, value})
return false
}
}
}
case json.Number:
n, _ := value.Float64()
if ok := validator.CheckBoundaries(n); !ok {
*errors = append(*errors, &DataError{"Validation error", "Out of boundaries", validator.Field, value})
return false
}
}
// user's custom test
if validator.CustomTest != nil {
ok, err := validator.CustomTest(valueToTest)
if !ok {
*errors = append(*errors, err)
return false
}
}
return true
}
// This function check if the real type behind the interface value is the one wished by the validators
func checkType(validator *Validator, valueToTest interface{}, errors *[]*DataError) bool {
kind := reflect.ValueOf(valueToTest).Kind()
switch kind {
case reflect.Slice:
array := validator.Type[0:2] // indeed, the type representation string begins with []
_type := validator.Type[2:] // here we have the type after []
if array != "[]" {
*errors = append(*errors, &DataError{Type: "Validation error", Reason: "Type mismatch", Field: validator.Field, Value: reflect.TypeOf(valueToTest).String()})
return false
} else {
for _, value := range valueToTest.([]interface{}) {
vtype := reflect.TypeOf(value).String()
if vtype != _type {
// _type can be bson.ObjectId... which is basicly a string. So the condition above may fail but the type is in fact correct. Let's check:
if stringValue, ok := value.(string); ok && _type == "bson.ObjectId" && bson.IsObjectIdHex(stringValue) {
return true
} else {
*errors = append(*errors, &DataError{Type: "Validation error", Reason: "Type mismatch", Field: validator.Field, Value: "[] contains " + reflect.TypeOf(value).String()})
return false
}
}
}
}
case reflect.Map:
// json maps are map[string]interface{}, but we could test for more...
parts := strings.SplitAfter(validator.Type, "]")
for key, value := range valueToTest.(map[string]interface{}) {
vtype := reflect.TypeOf(value)
// such as is the string key a correct ObjectId ?
if parts[0] == "map[bson.ObjectId]" {
if !bson.IsObjectIdHex(key) {
*errors = append(*errors, &DataError{Type: "Validation error", Reason: "Type mismatch", Field: validator.Field, Value: "one of the indexes at least is not valid ObjectId: " + key})
return false
}
}
// or test the real value behind interface{}
if vtype.String() != parts[1] {
*errors = append(*errors, &DataError{Type: "Validation error", Reason: "Type mismatch", Field: validator.Field, Value: "one of the map values is of type: " + vtype.String()})
return false
}
}
default:
if _type := reflect.TypeOf(valueToTest); _type != nil && _type.String() != validator.Type {
// bson.ObjectId is match as a string, let's try to save them off the error pireflect.TypeOf(valueToTest)reflect.TypeOf(valueToTest)t
if _type.String() == "string" && bson.IsObjectIdHex(valueToTest.(string)) && validator.Type == "bson.ObjectId" {
return true
} else {
// ok, let'em fall
*errors = append(*errors, &DataError{Type: "Validation error", Reason: "Type mismatch", Field: validator.Field, Value: _type.String()})
return false
}
}
}
return true
}