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  }