github.com/blend/go-sdk@v1.20240719.1/validate/string.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package validate
     9  
    10  import (
    11  	"net"
    12  	"net/mail"
    13  	"net/url"
    14  	"regexp"
    15  	"strings"
    16  	"unicode"
    17  
    18  	"github.com/blend/go-sdk/ex"
    19  	"github.com/blend/go-sdk/uuid"
    20  )
    21  
    22  // String errors
    23  const (
    24  	ErrStringRequired  ex.Class = "string should be set"
    25  	ErrStringForbidden ex.Class = "string should not be set"
    26  	ErrStringLength    ex.Class = "string should be a given length"
    27  	ErrStringLengthMin ex.Class = "string should be a minimum length"
    28  	ErrStringLengthMax ex.Class = "string should be a maximum length"
    29  	ErrStringMatches   ex.Class = "string should match regular expression"
    30  	ErrStringIsUpper   ex.Class = "string should be uppercase"
    31  	ErrStringIsLower   ex.Class = "string should be lowercase"
    32  	ErrStringIsTitle   ex.Class = "string should be titlecase"
    33  	ErrStringIsUUID    ex.Class = "string should be a uuid"
    34  	ErrStringIsEmail   ex.Class = "string should be a valid email address"
    35  	ErrStringIsURI     ex.Class = "string should be a valid uri"
    36  	ErrStringIsIP      ex.Class = "string should be a valid ip address"
    37  	ErrStringIsSlug    ex.Class = "string should be a valid slug (i.e. matching [0-9,a-z,A-Z,_,-])"
    38  	ErrStringIsOneOf   ex.Class = "string should be one of a set of values"
    39  )
    40  
    41  // String contains helpers for string validation.
    42  func String(value *string) StringValidators {
    43  	return StringValidators{value, false}
    44  }
    45  
    46  // SensitiveString contains helpers for sensitive string validation, which
    47  // avoids including sensitive data in error messages
    48  func SensitiveString(value *string) StringValidators {
    49  	return StringValidators{value, true}
    50  }
    51  
    52  // StringValidators returns string validators.
    53  type StringValidators struct {
    54  	Value     *string
    55  	Sensitive bool
    56  }
    57  
    58  func (s StringValidators) maybeCensoredValue() interface{} {
    59  	if s.Value == nil {
    60  		return nil
    61  	}
    62  
    63  	if s.Sensitive {
    64  		return "<sensitive>"
    65  	}
    66  
    67  	return *s.Value
    68  }
    69  
    70  // Required returns a validator that a string is set and not zero length.
    71  func (s StringValidators) Required() Validator {
    72  	return func() error {
    73  		if s.Value == nil {
    74  			return Error(ErrStringRequired, nil)
    75  		}
    76  		if len(*s.Value) == 0 {
    77  			return Error(ErrStringRequired, nil)
    78  		}
    79  		return nil
    80  	}
    81  }
    82  
    83  // Forbidden returns a validator that a string is not set.
    84  func (s StringValidators) Forbidden() Validator {
    85  	return func() error {
    86  		if err := s.Required()(); err == nil {
    87  			return Error(ErrStringForbidden, nil)
    88  		}
    89  		return nil
    90  	}
    91  }
    92  
    93  // MinLen returns a validator that a string is a minimum length.
    94  // If the string is unset (nil) it will fail.
    95  func (s StringValidators) MinLen(length int) Validator {
    96  	return func() error {
    97  		if s.Value == nil {
    98  			return Errorf(ErrStringLengthMin, nil, "length: %d", length)
    99  		}
   100  		if len(*s.Value) < length { //if it's unset, it should fail the minimum check.
   101  			return Errorf(ErrStringLengthMin, s.maybeCensoredValue(), "length: %d", length)
   102  		}
   103  		return nil
   104  	}
   105  }
   106  
   107  // MaxLen returns a validator that a string is a minimum length.
   108  // It will pass if the string is unset (nil).
   109  func (s StringValidators) MaxLen(length int) Validator {
   110  	return func() error {
   111  		if s.Value == nil {
   112  			return nil
   113  		}
   114  		if len(*s.Value) > length {
   115  			return Errorf(ErrStringLengthMax, s.maybeCensoredValue(), "length: %d", length)
   116  		}
   117  		return nil
   118  	}
   119  }
   120  
   121  // Length returns a validator that a string is a minimum length.
   122  // It will error if the string is unset (nil).
   123  func (s StringValidators) Length(length int) Validator {
   124  	return func() error {
   125  		if s.Value == nil {
   126  			return Errorf(ErrStringLength, nil, "length: %d", length)
   127  		}
   128  		if len(*s.Value) != length {
   129  			return Errorf(ErrStringLength, s.maybeCensoredValue(), "length: %d", length)
   130  		}
   131  		return nil
   132  	}
   133  }
   134  
   135  // BetweenLen returns a validator that a string is a between a minimum and maximum length.
   136  // It will error if the string is unset (nil).
   137  func (s StringValidators) BetweenLen(min, max int) Validator {
   138  	return func() error {
   139  		if s.Value == nil {
   140  			return Errorf(ErrStringLengthMin, nil, "length: %d", min)
   141  		}
   142  		if len(*s.Value) < min {
   143  			return Errorf(ErrStringLengthMin, s.maybeCensoredValue(), "length: %d", min)
   144  		}
   145  		if len(*s.Value) > max {
   146  			return Errorf(ErrStringLengthMax, s.maybeCensoredValue(), "length: %d", max)
   147  		}
   148  		return nil
   149  	}
   150  }
   151  
   152  // Matches returns a validator that a string matches a given regex.
   153  // It will error if the string is unset (nil).
   154  func (s StringValidators) Matches(expression string) Validator {
   155  	exp, err := regexp.Compile(expression)
   156  	return func() error {
   157  		if err != nil {
   158  			return ex.New(err)
   159  		}
   160  		if s.Value == nil {
   161  			return Errorf(ErrStringMatches, nil, "expression: %s", expression)
   162  		}
   163  		if !exp.MatchString(string(*s.Value)) {
   164  			return Errorf(ErrStringMatches, s.maybeCensoredValue(), "expression: %s", expression)
   165  		}
   166  		return nil
   167  	}
   168  }
   169  
   170  // IsUpper returns a validator if a string is all uppercase.
   171  // It will error if the string is unset (nil).
   172  func (s StringValidators) IsUpper() Validator {
   173  	return func() error {
   174  		if s.Value == nil {
   175  			return Error(ErrStringIsUpper, nil)
   176  		}
   177  		for _, r := range *s.Value {
   178  			if !unicode.IsUpper(r) {
   179  				return Error(ErrStringIsUpper, s.maybeCensoredValue())
   180  			}
   181  		}
   182  		return nil
   183  	}
   184  }
   185  
   186  // IsLower returns a validator if a string is all lowercase.
   187  // It will error if the string is unset (nil).
   188  func (s StringValidators) IsLower() Validator {
   189  	return func() error {
   190  		if s.Value == nil {
   191  			return Error(ErrStringIsLower, nil)
   192  		}
   193  		for _, r := range *s.Value {
   194  			if !unicode.IsLower(r) {
   195  				return Error(ErrStringIsLower, s.maybeCensoredValue())
   196  			}
   197  		}
   198  		return nil
   199  	}
   200  }
   201  
   202  // IsTitle returns a validator if a string is titlecase.
   203  // Titlecase is defined as the output of strings.ToTitle(s).
   204  // It will error if the string is unset (nil).
   205  func (s StringValidators) IsTitle() Validator {
   206  	return func() error {
   207  		if s.Value == nil {
   208  			return Error(ErrStringIsTitle, nil)
   209  		}
   210  		if strings.ToTitle(string(*s.Value)) == string(*s.Value) {
   211  			return nil
   212  		}
   213  		return Error(ErrStringIsTitle, s.maybeCensoredValue())
   214  	}
   215  }
   216  
   217  // IsUUID returns if a string is a valid uuid.
   218  // It will error if the string is unset (nil).
   219  func (s StringValidators) IsUUID() Validator {
   220  	return func() error {
   221  		if s.Value == nil {
   222  			return Error(ErrStringIsUUID, nil)
   223  		}
   224  		if _, err := uuid.Parse(string(*s.Value)); err != nil {
   225  			return Error(ErrStringIsUUID, s.maybeCensoredValue())
   226  		}
   227  		return nil
   228  	}
   229  }
   230  
   231  // IsEmail returns if a string is a valid email address.
   232  func (s StringValidators) IsEmail() Validator {
   233  	return func() error {
   234  		if s.Value == nil {
   235  			return Error(ErrStringIsEmail, nil)
   236  		}
   237  		if _, err := mail.ParseAddress(string(*s.Value)); err != nil {
   238  			return Error(ErrStringIsEmail, s.maybeCensoredValue())
   239  		}
   240  		return nil
   241  	}
   242  }
   243  
   244  // IsURI returns if a string is a valid uri.
   245  // It will error if the string is unset (nil).
   246  func (s StringValidators) IsURI() Validator {
   247  	return func() error {
   248  		if s.Value == nil {
   249  			return Error(ErrStringIsURI, nil)
   250  		}
   251  		if _, err := url.ParseRequestURI(string(*s.Value)); err != nil {
   252  			return Error(ErrStringIsURI, s.maybeCensoredValue())
   253  		}
   254  		return nil
   255  	}
   256  }
   257  
   258  // IsIP returns if a string is a valid ip address.
   259  // It will error if the string is unset (nil).
   260  func (s StringValidators) IsIP() Validator {
   261  	return func() error {
   262  		if s.Value == nil {
   263  			return Error(ErrStringIsIP, nil)
   264  		}
   265  		if addr := net.ParseIP(string(*s.Value)); addr == nil {
   266  			return Error(ErrStringIsIP, s.maybeCensoredValue())
   267  		}
   268  		return nil
   269  	}
   270  }
   271  
   272  // IsSlug returns if a string is a valid slug as defined by the match rule [0-9,a-z,A-Z,_,-].
   273  // It will error if the string is unset (nil).
   274  func (s StringValidators) IsSlug() Validator {
   275  	return func() error {
   276  		if s.Value == nil {
   277  			return Error(ErrStringIsSlug, nil)
   278  		}
   279  		for _, c := range *s.Value {
   280  			if unicode.IsLetter(c) {
   281  				continue
   282  			}
   283  			if unicode.IsDigit(c) {
   284  				continue
   285  			}
   286  			if c == '-' || c == '_' {
   287  				continue
   288  			}
   289  			return Error(ErrStringIsSlug, s.maybeCensoredValue())
   290  		}
   291  		return nil
   292  	}
   293  }
   294  
   295  // IsOneOf validates a string is one of a known set of values.
   296  func (s StringValidators) IsOneOf(values ...string) Validator {
   297  	return func() error {
   298  		if s.Value == nil {
   299  			return Error(ErrStringIsOneOf, nil, strings.Join(values, ", "))
   300  		}
   301  		for _, value := range values {
   302  			if *s.Value == value {
   303  				return nil
   304  			}
   305  		}
   306  		return Error(ErrStringIsOneOf, s.maybeCensoredValue(), strings.Join(values, ", "))
   307  	}
   308  }