567 lines
12 KiB
Go
567 lines
12 KiB
Go
package vouch
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func typeMismatchError(expected, actual reflect.Kind) error {
|
|
return fmt.Errorf("expected type %s, but got %s", Types[expected], Types[actual])
|
|
}
|
|
|
|
func Required() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val := reflect.ValueOf(v)
|
|
|
|
if v == nil {
|
|
return fmt.Errorf("is required")
|
|
}
|
|
|
|
switch kind {
|
|
case reflect.String, reflect.Array, reflect.Slice, reflect.Map:
|
|
if val.Len() == 0 {
|
|
return fmt.Errorf("is required")
|
|
}
|
|
case reflect.Ptr, reflect.Interface:
|
|
if val.IsNil() {
|
|
return fmt.Errorf("is required")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func MaxLen(n int) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
switch kind {
|
|
case reflect.String:
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if len(val) > n {
|
|
return fmt.Errorf("must be at most %d characters long", n)
|
|
}
|
|
case reflect.Array, reflect.Slice, reflect.Map:
|
|
val := reflect.ValueOf(v)
|
|
if val.Kind() != kind {
|
|
return typeMismatchError(kind, val.Kind())
|
|
}
|
|
|
|
if val.Len() > n {
|
|
return fmt.Errorf("must have at most %d elements", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for MaxLen")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func MinLen(n int) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
switch kind {
|
|
case reflect.String:
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if len(val) < n {
|
|
return fmt.Errorf("must be at least %d characters long", n)
|
|
}
|
|
case reflect.Array, reflect.Slice, reflect.Map:
|
|
val := reflect.ValueOf(v)
|
|
if val.Kind() != kind {
|
|
return typeMismatchError(kind, val.Kind())
|
|
}
|
|
|
|
if val.Len() < n {
|
|
return fmt.Errorf("must have at least %d elements", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for MinLen")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ExactLen(n int) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
switch kind {
|
|
case reflect.String:
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if len(val) != n {
|
|
return fmt.Errorf("must be exactly %d characters long", n)
|
|
}
|
|
case reflect.Array, reflect.Slice, reflect.Map:
|
|
val := reflect.ValueOf(v)
|
|
if val.Kind() != kind {
|
|
return typeMismatchError(kind, val.Kind())
|
|
}
|
|
|
|
if val.Len() != n {
|
|
return fmt.Errorf("must have exactly %d elements", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for ExactLen")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func MaxValue(n float64) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val := reflect.ValueOf(v)
|
|
|
|
switch kind {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if val.Int() > int64(n) {
|
|
return fmt.Errorf("must be at most %d", int64(n))
|
|
}
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if val.Uint() > uint64(n) {
|
|
return fmt.Errorf("must be at most %d", uint64(n))
|
|
}
|
|
case reflect.Float32, reflect.Float64:
|
|
if val.Float() > float64(n) {
|
|
return fmt.Errorf("must be at most %f", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for MaxValue")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func MinValue(n float64) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val := reflect.ValueOf(v)
|
|
|
|
switch kind {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if val.Int() < int64(n) {
|
|
return fmt.Errorf("must be at least %d", int64(n))
|
|
}
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if val.Uint() < uint64(n) {
|
|
return fmt.Errorf("must be at least %d", uint64(n))
|
|
}
|
|
case reflect.Float32, reflect.Float64:
|
|
if val.Float() < float64(n) {
|
|
return fmt.Errorf("must be at least %f", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for MinValue")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func GreaterThan(n float64) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val := reflect.ValueOf(v)
|
|
|
|
switch kind {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if val.Int() <= int64(n) {
|
|
return fmt.Errorf("must be greater than %d", int64(n))
|
|
}
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if val.Uint() <= uint64(n) {
|
|
return fmt.Errorf("must be greater than %d", uint64(n))
|
|
}
|
|
case reflect.Float32, reflect.Float64:
|
|
if val.Float() <= float64(n) {
|
|
return fmt.Errorf("must be greater than %f", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for GreaterThan")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func LessThan(n float64) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val := reflect.ValueOf(v)
|
|
|
|
switch kind {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if val.Int() >= int64(n) {
|
|
return fmt.Errorf("must be less than %d", int64(n))
|
|
}
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if val.Uint() >= uint64(n) {
|
|
return fmt.Errorf("must be less than %d", uint64(n))
|
|
}
|
|
case reflect.Float32, reflect.Float64:
|
|
if val.Float() >= float64(n) {
|
|
return fmt.Errorf("must be less than %f", n)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for LessThan")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Between(min, max float64) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val := reflect.ValueOf(v)
|
|
|
|
switch kind {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if val.Int() < int64(min) || val.Int() > int64(max) {
|
|
return fmt.Errorf("must be between %d and %d", int64(min), int64(max))
|
|
}
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
if val.Uint() < uint64(min) || val.Uint() > uint64(max) {
|
|
return fmt.Errorf("must be between %d and %d", uint64(min), uint64(max))
|
|
}
|
|
case reflect.Float32, reflect.Float64:
|
|
if val.Float() < min || val.Float() > max {
|
|
return fmt.Errorf("must be between %f and %f", min, max)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported type for Between")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func RegexMatch(pattern string) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
reg := regexp.MustCompile(pattern)
|
|
if !reg.MatchString(val) {
|
|
return errors.New("is in an invalid format")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Email() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
|
if !emailRegex.MatchString(val) {
|
|
return errors.New("must be a valid email address")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func URL() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if _, err := url.ParseRequestURI(val); err != nil {
|
|
return errors.New("must be a valid url")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Alpha() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
emailRegex := regexp.MustCompile(`^[a-zA-Z]+$`)
|
|
if !emailRegex.MatchString(val) {
|
|
return errors.New("must contain only letters")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Numeric() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
emailRegex := regexp.MustCompile(`^[0-9]+$`)
|
|
if !emailRegex.MatchString(val) {
|
|
return errors.New("must contain only numbers")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Alphanumeric() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9]+$`)
|
|
if !emailRegex.MatchString(val) {
|
|
return errors.New("must contain only letters and numbers")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ContainsUpper() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
upperRegex := regexp.MustCompile(`[A-Z]`)
|
|
if !upperRegex.MatchString(val) {
|
|
return errors.New("must contain at least one uppercase letter")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ContainsLower() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
lowerRegex := regexp.MustCompile(`[a-z]`)
|
|
if !lowerRegex.MatchString(val) {
|
|
return errors.New("must contain at least one lowercase letter")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ContainsDigit() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
digitRegex := regexp.MustCompile(`[0-9]`)
|
|
if !digitRegex.MatchString(val) {
|
|
return errors.New("must contain at least one digit")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func ContainsSpecial() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
specialRegex := regexp.MustCompile(`[^a-zA-Z0-9]`)
|
|
if !specialRegex.MatchString(val) {
|
|
return errors.New("must contain at least one special character")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func StartsWith(prefix string) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if !strings.HasPrefix(val, prefix) {
|
|
return fmt.Errorf("must start with '%s'", prefix)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func EndsWith(suffix string) Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if !strings.HasSuffix(val, suffix) {
|
|
return fmt.Errorf("must end with '%s'", suffix)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func UUID() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
uuidRegex := regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
if !uuidRegex.MatchString(val) {
|
|
return errors.New("must be a valid UUID")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func IP() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
if net.ParseIP(val) == nil {
|
|
return errors.New("must be a valid IP address")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func IPv4() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
ip := net.ParseIP(val)
|
|
if ip == nil || ip.To4() == nil {
|
|
return errors.New("must be a valid IPv4 address")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func IPv6() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
ip := net.ParseIP(val)
|
|
if ip == nil || ip.To4() != nil {
|
|
return errors.New("must be a valid IPv6 address")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Date() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
dateRegex := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
|
if !dateRegex.MatchString(val) {
|
|
return errors.New("must be a valid date in format YYYY-MM-DD")
|
|
}
|
|
|
|
_, err := time.Parse("2006-01-02", val)
|
|
if err != nil {
|
|
return errors.New("must be a valid date")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func Time() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
timeRegex := regexp.MustCompile(`^\d{2}:\d{2}:\d{2}$`)
|
|
if !timeRegex.MatchString(val) {
|
|
return errors.New("must be a valid time in format HH:MM:SS")
|
|
}
|
|
|
|
_, err := time.Parse("15:04:05", val)
|
|
if err != nil {
|
|
return errors.New("must be a valid time")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func DateTime() Rule {
|
|
return func(v any, kind reflect.Kind) error {
|
|
val, ok := v.(string)
|
|
if !ok {
|
|
return typeMismatchError(reflect.String, kind)
|
|
}
|
|
|
|
dateTimeRegex := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$`)
|
|
if !dateTimeRegex.MatchString(val) {
|
|
return errors.New("must be a valid datetime in ISO 8601 format")
|
|
}
|
|
|
|
_, err := time.Parse(time.RFC3339, val)
|
|
if err != nil {
|
|
return errors.New("must be a valid datetime")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|