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  }