github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/valid/rules.go (about) 1 package valid 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "strings" 8 ) 9 10 // QUESTIONS: 11 // Should a rule ever change a field? Like can we make a "trim whitespace" or "convert to int" rule? 12 // I'm leaning in the direction of yeah we should have that, but not certain it won't add too much 13 // complexity 14 // Field names should probably all be in JSON format, even here in the Go code. And the various functions 15 // that actually touch the fields can deal with the conversion but at least everything outside this 16 // package will consistently use the external (JSON) representation whenever there's a string with 17 // a field name in it. 18 19 // Rule is a validation rule which can be applied to an object. 20 // A Rule is responsible for knowing which field(s) it applies to and handling 21 // various types of objects including structs and maps - although that task 22 // should generally be delegated to ReadField() and WriteField(). 23 // A Messages should be returned as the error if validation 24 // errors are found, or nil if validatin passes. Other errors may be returned 25 // to address edge cases of invalid configuration but all validation problems 26 // must be expressed as a Messages. 27 type Rule interface { 28 Apply(obj interface{}) error 29 } 30 31 type RuleFunc func(obj interface{}) error 32 33 func (f RuleFunc) Apply(obj interface{}) error { 34 return f(obj) 35 } 36 37 type Rules []Rule 38 39 func (rl Rules) Apply(obj interface{}) error { 40 var retmsgs Messages 41 for _, r := range rl { 42 err := r.Apply(obj) 43 if err != nil { 44 vml, ok := err.(Messages) 45 if !ok { 46 // other errors are just returned immediately 47 return err 48 } 49 // validation messages are put together in one list 50 retmsgs = append(retmsgs, vml...) 51 } 52 } 53 if len(retmsgs) == 0 { 54 return nil 55 } 56 // return message (or will be nil if non generated) 57 return retmsgs 58 } 59 60 func DefaultFieldMessageName(fieldName string) string { 61 62 var outParts []string 63 64 parts := strings.Split(fieldName, "_") 65 for _, p := range parts { 66 if p == "" { 67 continue 68 } 69 70 var p1, p2 string 71 p1 = p[:1] 72 p2 = p[1:] 73 74 p = strings.ToUpper(p1) + p2 75 76 outParts = append(outParts, p) 77 78 } 79 80 return strings.Join(outParts, " ") 81 82 } 83 84 // NewNotNilRule returns a rule that ensures a field is not nil. 85 func NewNotNilRule(fieldName string) Rule { 86 return RuleFunc(func(obj interface{}) error { 87 88 v, err := ReadField(obj, fieldName) 89 if err != nil { 90 return err 91 } 92 if v == nil { 93 return Messages{Message{ 94 FieldName: fieldName, 95 Code: "notnil", 96 Message: fmt.Sprintf("%s is requried", DefaultFieldMessageName(fieldName)), 97 }} 98 } 99 100 return nil 101 102 }) 103 } 104 105 // NewMinLenRule returns a rule that ensures the string representation of a 106 // field value is greater than or equal the specified number of bytes. 107 // This rule has no effect if the field value is nil. 108 func NewMinLenRule(fieldName string, minLen int) Rule { 109 // log.Printf("NewMinLenRule(%q, %v)", fieldName, minLen) 110 return RuleFunc(func(obj interface{}) error { 111 v, err := ReadField(obj, fieldName) 112 if err != nil { 113 // log.Printf("NewMinLenRule, ReadField(%#v, %q) returned err %v", obj, fieldName, err) 114 return err 115 } 116 if v == nil { 117 return nil 118 } 119 120 var theErr error 121 theErr = Messages{Message{ 122 FieldName: fieldName, 123 Code: "minlen", 124 Message: fmt.Sprintf("The minimum length for %s is %d", DefaultFieldMessageName(fieldName), minLen), 125 Data: map[string]interface{}{"value": minLen}, 126 }} 127 128 vlen := len(fmt.Sprintf("%v", v)) 129 if vlen < minLen { 130 return theErr 131 } 132 // validation succeeded 133 return nil 134 }) 135 } 136 137 // NewMaxLenRule returns a rule that ensures the string representation of a 138 // field value is less than or equal the specified number of bytes. 139 // This rule has no effect if the field value is nil. 140 func NewMaxLenRule(fieldName string, minLen int) Rule { 141 return RuleFunc(func(obj interface{}) error { 142 v, err := ReadField(obj, fieldName) 143 if err != nil { 144 return err 145 } 146 if v == nil { 147 return nil 148 } 149 150 var theErr error 151 theErr = Messages{Message{ 152 FieldName: fieldName, 153 Code: "maxlen", 154 Message: fmt.Sprintf("The maximum length for %s is %d", DefaultFieldMessageName(fieldName), minLen), 155 Data: map[string]interface{}{"value": minLen}, 156 }} 157 158 vlen := len(fmt.Sprintf("%v", v)) 159 if vlen > minLen { 160 return theErr 161 } 162 // validation succeeded 163 return nil 164 }) 165 } 166 167 // NewRegexpRule returns a rule that ensures the string representation of a 168 // field value matches the specified regexp pattern. 169 // This rule has no effect if the field value is nil. 170 func NewRegexpRule(fieldName string, pattern *regexp.Regexp) Rule { 171 return RuleFunc(func(obj interface{}) error { 172 v, err := ReadField(obj, fieldName) 173 if err != nil { 174 return err 175 } 176 if v == nil { 177 return nil 178 } 179 180 vstr := fmt.Sprintf("%v", v) 181 if !pattern.MatchString(vstr) { 182 return Messages{Message{ 183 FieldName: fieldName, 184 Code: "regexp", 185 Message: fmt.Sprintf("%s does not match required pattern", DefaultFieldMessageName(fieldName)), 186 Data: map[string]interface{}{"regexp_string": pattern.String()}, 187 }} 188 } 189 190 // validation succeeded 191 return nil 192 }) 193 } 194 195 const ( 196 EMAIL_REGEXP_PATTERN = `(?i)^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$` 197 ) 198 199 // NewEmailRule returns a rule that ensures the string representation of a 200 // field value looks like an email address according to the pattern EMAIL_REGEXP_PATTERN. 201 // This rule has no effect if the field value is nil. 202 func NewEmailRule(fieldName string) Rule { 203 204 // borrowed from: https://www.regular-expressions.info/email.html; I'm open to suggestions but looking 205 // for something simple 206 pattern := regexp.MustCompile(EMAIL_REGEXP_PATTERN) 207 208 return RuleFunc(func(obj interface{}) error { 209 v, err := ReadField(obj, fieldName) 210 if err != nil { 211 return err 212 } 213 if v == nil { 214 return nil 215 } 216 217 vstr := fmt.Sprintf("%v", v) 218 if !pattern.MatchString(vstr) { 219 return Messages{Message{ 220 FieldName: fieldName, 221 Code: "email", 222 Message: fmt.Sprintf("%s does not appear to be a valid email address", DefaultFieldMessageName(fieldName)), 223 }} 224 } 225 226 // validation succeeded 227 return nil 228 }) 229 } 230 231 // NewMinValRule returns a rule that ensures the field value is equal to or greater than 232 // the value you specify. 233 // This rule has no effect if the field value is nil or if it is not numeric (integer or floating point). 234 func NewMinValRule(fieldName string, minval float64) Rule { 235 236 // FIXME: should we also do something with NaN and infinity here? 237 238 return RuleFunc(func(obj interface{}) error { 239 v, err := ReadField(obj, fieldName) 240 if err != nil { 241 return err 242 } 243 if v == nil { 244 return nil 245 } 246 247 var vfl float64 248 249 vtype := reflect.TypeOf(v) 250 // if it can't be converted, we just ignore the check 251 if !vtype.ConvertibleTo(reflect.TypeOf(vfl)) { 252 return nil 253 } 254 255 vval := reflect.ValueOf(v) 256 reflect.ValueOf(&vfl).Elem().Set(vval.Convert(reflect.TypeOf(vfl))) 257 258 if vfl < minval { 259 return Messages{Message{ 260 FieldName: fieldName, 261 Code: "minval", 262 Message: fmt.Sprintf("%s is below the minimum value (%v)", DefaultFieldMessageName(fieldName), minval), 263 }} 264 } 265 266 // validation succeeded 267 return nil 268 }) 269 } 270 271 // NewMaxValRule returns a rule that ensures the field value is equal to or greater than 272 // the value you specify. 273 // This rule has no effect if the field value is nil or if it is not numeric (integer or floating point). 274 func NewMaxValRule(fieldName string, maxval float64) Rule { 275 276 return RuleFunc(func(obj interface{}) error { 277 v, err := ReadField(obj, fieldName) 278 if err != nil { 279 return err 280 } 281 if v == nil { 282 return nil 283 } 284 285 var vfl float64 286 287 vtype := reflect.TypeOf(v) 288 // if it can't be converted, we just ignore the check 289 if !vtype.ConvertibleTo(reflect.TypeOf(vfl)) { 290 return nil 291 } 292 293 vval := reflect.ValueOf(v) 294 reflect.ValueOf(&vfl).Elem().Set(vval.Convert(reflect.TypeOf(vfl))) 295 296 if vfl > maxval { 297 return Messages{Message{ 298 FieldName: fieldName, 299 Code: "maxval", 300 Message: fmt.Sprintf("%s is below the minimum value (%v)", DefaultFieldMessageName(fieldName), maxval), 301 }} 302 } 303 304 // validation succeeded 305 return nil 306 }) 307 }