github.com/volatiletech/authboss@v2.4.1+incompatible/defaults/rules.go (about) 1 package defaults 2 3 import ( 4 "fmt" 5 "regexp" 6 "unicode" 7 8 "github.com/pkg/errors" 9 "github.com/volatiletech/authboss" 10 ) 11 12 var blankRegex = regexp.MustCompile(`^\s*$`) 13 14 // Rules defines a ruleset by which a string can be validated. 15 // The errors it produces are english only, with some basic pluralization. 16 type Rules struct { 17 // FieldName is the name of the field this is intended to validate. 18 FieldName string 19 20 // MatchError describes the MustMatch regexp to a user. 21 Required bool 22 MatchError string 23 MustMatch *regexp.Regexp 24 MinLength, MaxLength int 25 MinLetters int 26 MinLower, MinUpper int 27 MinNumeric int 28 MinSymbols int 29 AllowWhitespace bool 30 } 31 32 // Errors returns an array of errors for each validation error that 33 // is present in the given string. Returns nil if there are no errors. 34 func (r Rules) Errors(toValidate string) authboss.ErrorList { 35 errs := make(authboss.ErrorList, 0) 36 37 ln := len(toValidate) 38 if r.Required && (ln == 0 || blankRegex.MatchString(toValidate)) { 39 return append(errs, FieldError{r.FieldName, errors.New("Cannot be blank")}) 40 } 41 42 if r.MustMatch != nil { 43 if !r.MustMatch.MatchString(toValidate) { 44 errs = append(errs, FieldError{r.FieldName, errors.New(r.MatchError)}) 45 } 46 } 47 48 if (r.MinLength > 0 && ln < r.MinLength) || (r.MaxLength > 0 && ln > r.MaxLength) { 49 errs = append(errs, FieldError{r.FieldName, errors.New(r.lengthErr())}) 50 } 51 52 upper, lower, numeric, symbols, whitespace := tallyCharacters(toValidate) 53 if upper+lower < r.MinLetters { 54 errs = append(errs, FieldError{r.FieldName, errors.New(r.charErr())}) 55 } 56 if upper < r.MinUpper { 57 errs = append(errs, FieldError{r.FieldName, errors.New(r.upperErr())}) 58 } 59 if lower < r.MinLower { 60 errs = append(errs, FieldError{r.FieldName, errors.New(r.lowerErr())}) 61 } 62 if numeric < r.MinNumeric { 63 errs = append(errs, FieldError{r.FieldName, errors.New(r.numericErr())}) 64 } 65 if symbols < r.MinSymbols { 66 errs = append(errs, FieldError{r.FieldName, errors.New(r.symbolErr())}) 67 } 68 if !r.AllowWhitespace && whitespace > 0 { 69 errs = append(errs, FieldError{r.FieldName, errors.New("No whitespace permitted")}) 70 } 71 72 if len(errs) == 0 { 73 return nil 74 } 75 76 return errs 77 } 78 79 // IsValid checks toValidate to make sure it's valid according to the rules. 80 func (r Rules) IsValid(toValidate string) bool { 81 return nil == r.Errors(toValidate) 82 } 83 84 // Rules returns an array of strings describing the rules. 85 func (r Rules) Rules() []string { 86 var rules []string 87 88 if r.MustMatch != nil { 89 rules = append(rules, r.MatchError) 90 } 91 92 if e := r.lengthErr(); len(e) > 0 { 93 rules = append(rules, e) 94 } 95 if e := r.charErr(); len(e) > 0 { 96 rules = append(rules, e) 97 } 98 if e := r.upperErr(); len(e) > 0 { 99 rules = append(rules, e) 100 } 101 if e := r.lowerErr(); len(e) > 0 { 102 rules = append(rules, e) 103 } 104 if e := r.numericErr(); len(e) > 0 { 105 rules = append(rules, e) 106 } 107 if e := r.symbolErr(); len(e) > 0 { 108 rules = append(rules, e) 109 } 110 111 return rules 112 } 113 114 func (r Rules) lengthErr() (err string) { 115 switch { 116 case r.MinLength > 0 && r.MaxLength > 0: 117 err = fmt.Sprintf("Must be between %d and %d characters", r.MinLength, r.MaxLength) 118 case r.MinLength > 0: 119 err = fmt.Sprintf("Must be at least %d character", r.MinLength) 120 if r.MinLength > 1 { 121 err += "s" 122 } 123 case r.MaxLength > 0: 124 err = fmt.Sprintf("Must be at most %d character", r.MaxLength) 125 if r.MaxLength > 1 { 126 err += "s" 127 } 128 } 129 130 return err 131 } 132 133 func (r Rules) charErr() (err string) { 134 if r.MinLetters > 0 { 135 err = fmt.Sprintf("Must contain at least %d letter", r.MinLetters) 136 if r.MinLetters > 1 { 137 err += "s" 138 } 139 } 140 return err 141 } 142 143 func (r Rules) upperErr() (err string) { 144 if r.MinUpper > 0 { 145 err = fmt.Sprintf("Must contain at least %d uppercase letter", r.MinUpper) 146 if r.MinUpper > 1 { 147 err += "s" 148 } 149 } 150 return err 151 } 152 153 func (r Rules) lowerErr() (err string) { 154 if r.MinLower > 0 { 155 err = fmt.Sprintf("Must contain at least %d lowercase letter", r.MinLower) 156 if r.MinLower > 1 { 157 err += "s" 158 } 159 } 160 return err 161 } 162 163 func (r Rules) numericErr() (err string) { 164 if r.MinNumeric > 0 { 165 err = fmt.Sprintf("Must contain at least %d number", r.MinNumeric) 166 if r.MinNumeric > 1 { 167 err += "s" 168 } 169 } 170 return err 171 } 172 173 func (r Rules) symbolErr() (err string) { 174 if r.MinSymbols > 0 { 175 err = fmt.Sprintf("Must contain at least %d symbol", r.MinSymbols) 176 if r.MinSymbols > 1 { 177 err += "s" 178 } 179 } 180 return err 181 } 182 183 func tallyCharacters(s string) (upper, lower, numeric, symbols, whitespace int) { 184 for _, c := range s { 185 switch { 186 case unicode.IsLetter(c): 187 if unicode.IsUpper(c) { 188 upper++ 189 } else { 190 lower++ 191 } 192 case unicode.IsDigit(c): 193 numeric++ 194 case unicode.IsSpace(c): 195 whitespace++ 196 default: 197 symbols++ 198 } 199 } 200 201 return upper, lower, numeric, symbols, whitespace 202 }