vouch: v1.0.0 - initial release
This commit is contained in:
commit
f89a72c054
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Timo Riegebauer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
67
README.md
Normal file
67
README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Vouch
|
||||
|
||||
`Vouch` is a lightweight, flexible validation library for Go, allowing you to define and enforce validation rules on struct fields with ease.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
go get git.trcreatives.com/triegebauer/go-vouch
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Defining a Schema
|
||||
|
||||
A schema is a map of field names to validation rules.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.trcreatives.com/triegebauer/go-vouch"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
func main() {
|
||||
schema := vouch.Schema{
|
||||
"Name": vouch.Rules(vouch.Required(), vouch.MinLen(3)),
|
||||
"Age": vouch.Rules(vouch.MinValue(18)),
|
||||
"Email": vouch.Rules(vouch.Required(), vouch.Email()),
|
||||
}
|
||||
|
||||
user := User{Name: "", Age: 16, Email: "invalid-email"}
|
||||
|
||||
if err := schema.Validate(user); err != nil {
|
||||
fmt.Println("Validation error:", err)
|
||||
} else {
|
||||
fmt.Println("Validation passed!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Rules
|
||||
|
||||
- `Required()` - Ensures the field is not empty.
|
||||
- `MinLen(n int)` - Ensures the field has at least `n` characters.
|
||||
- `MaxLen(n int)` - Ensures the field has at most `n` characters.
|
||||
- `MinValue(n float64)` - Ensures the number is at least `n`.
|
||||
- `MaxValue(n float64)` - Ensures the number is at most `n`.
|
||||
- `Email()` - Ensures the field is a valid email.
|
||||
- `URL()` - Ensures the field is a valid URL.
|
||||
- `ContainsUpper()` - Ensures the field contains at least one uppercase letter.
|
||||
- `ContainsDigit()` - Ensures the field contains at least one digit.
|
||||
- ...and many more!
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to open issues or submit pull requests to improve `vouch`.
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
566
default_rules.go
Normal file
566
default_rules.go
Normal file
@ -0,0 +1,566 @@
|
||||
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
|
||||
}
|
||||
}
|
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module git.trcreatives.com/Aviary/go-validation
|
||||
|
||||
go 1.22.2
|
9
rule.go
Normal file
9
rule.go
Normal file
@ -0,0 +1,9 @@
|
||||
package vouch
|
||||
|
||||
import "reflect"
|
||||
|
||||
type Rule func(v any, kind reflect.Kind) error
|
||||
|
||||
func Rules(rules ...Rule) []Rule {
|
||||
return rules
|
||||
}
|
36
schema.go
Normal file
36
schema.go
Normal file
@ -0,0 +1,36 @@
|
||||
package vouch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Schema map[string][]Rule
|
||||
|
||||
func (s Schema) Validate(v any) error {
|
||||
val := reflect.ValueOf(v)
|
||||
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
panic("expected a struct")
|
||||
}
|
||||
|
||||
for i := range val.NumField() {
|
||||
field := val.Field(i)
|
||||
fieldType := val.Type().Field(i)
|
||||
fieldName := fieldType.Name
|
||||
|
||||
if rules, exists := s[fieldName]; exists {
|
||||
for _, rule := range rules {
|
||||
if err := rule(field.Interface(), field.Kind()); err != nil {
|
||||
return fmt.Errorf("field '%s' %s", fieldName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
33
types.go
Normal file
33
types.go
Normal file
@ -0,0 +1,33 @@
|
||||
package vouch
|
||||
|
||||
import "reflect"
|
||||
|
||||
var Types = map[reflect.Kind]string{
|
||||
reflect.Invalid: "invalid",
|
||||
reflect.Bool: "bool",
|
||||
reflect.Int: "int",
|
||||
reflect.Int8: "int8",
|
||||
reflect.Int16: "int16",
|
||||
reflect.Int32: "int32",
|
||||
reflect.Int64: "int64",
|
||||
reflect.Uint: "uint",
|
||||
reflect.Uint8: "uint8",
|
||||
reflect.Uint16: "uint16",
|
||||
reflect.Uint32: "uint32",
|
||||
reflect.Uint64: "uint64",
|
||||
reflect.Uintptr: "uintptr",
|
||||
reflect.Float32: "float32",
|
||||
reflect.Float64: "float64",
|
||||
reflect.Complex64: "complex64",
|
||||
reflect.Complex128: "complex128",
|
||||
reflect.Array: "array",
|
||||
reflect.Chan: "chan",
|
||||
reflect.Func: "func",
|
||||
reflect.Interface: "interface",
|
||||
reflect.Map: "map",
|
||||
reflect.Pointer: "pointer",
|
||||
reflect.Slice: "slice",
|
||||
reflect.String: "string",
|
||||
reflect.Struct: "struct",
|
||||
reflect.UnsafePointer: "unsafe_pointer",
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user