Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ linters:
- exhaustruct
- gochecknoglobals # we want some globals for now
- mnd
- noinlineerr # we want inline errs, why not?
- nonamedreturns # we want named returns
- paralleltest # TODO
- testpackage
- wsl # deprecated
settings:
cyclop:
package-average: 4
Expand All @@ -35,6 +37,12 @@ linters:
- switch
- map
default-signifies-exhaustive: true
forbidigo:
forbid:
- pattern: "context.Background"
msg: "Use t.Context() in tests instead of context.Background()"
- pattern: "context.TODO"
msg: "Use t.Context() in tests instead of context.TODO()"
funlen:
ignore-comments: true
gocritic:
Expand Down Expand Up @@ -87,7 +95,7 @@ linters:
- stdlib
- generic
lll:
line-length: 120
line-length: 128
tab-width: 1
makezero:
always: false
Expand Down Expand Up @@ -160,6 +168,11 @@ linters:
exclusions:
warn-unused: true
generated: lax
# rules:
# - linters:
# - forbidigo
# text: Use t.Context
# path-except: _test\.go
issues:
max-issues-per-linter: 0
max-same-issues: 0
Expand All @@ -168,7 +181,6 @@ issues:
formatters:
enable:
- gofmt
- gofumpt
- goimports
settings:
gofmt:
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
all: fmt vulncheck lint test

fmt:
@find -name "*.go"|xargs go tool -modfile=tools/go.mod gofumpt -extra -w
@find -name "*.go"|xargs go tool -modfile=tools/go.mod goimports -w
@go fmt ./...
@go tool -modfile=tools/go.mod goimports -l -w .
@go run mvdan.cc/gofumpt@v0.8.0 -l -w -extra .

vulncheck:
@go tool -modfile=tools/go.mod govulncheck ./...

lint:
@go tool -modfile=tools/go.mod golangci-lint config verify
@go tool -modfile=tools/go.mod golangci-lint run
@go tool -modfile tools/go.mod modernize -test ./...

test:
@go test -vet=all -cover -covermode=atomic -coverprofile=unit.cov .
@go tool -modfile=tools/go.mod stampli -quiet -coverage=$$(go tool cover -func=unit.cov|tail -n1|tr -s "\t"|cut -f3|tr -d "%")

clean:
@rm -rf unit.cov unit.svg
57 changes: 8 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Vali, Yet Another **Vali**dator

[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Build and Test](https://github.com/alexaandru/vali/actions/workflows/ci.yml/badge.svg)](https://github.com/alexaandru/vali/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/alexaandru/vali/badge.svg)](https://coveralls.io/github/alexaandru/vali)
[![Test](https://github.com/alexaandru/vali/actions/workflows/ci.yml/badge.svg)](https://github.com/alexaandru/vali/actions/workflows/ci.yml)
![Coverage](coverage-badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/alexaandru/vali)](https://goreportcard.com/report/github.com/alexaandru/vali)
[![Go Reference](https://pkg.go.dev/badge/github.com/alexaandru/vali.svg)](https://pkg.go.dev/github.com/alexaandru/vali)
[![Socket.dev](https://socket.dev/api/badge/go/package/github.com/alexaandru/vali)](https://socket.dev/go/package/github.com/alexaandru/vali)
Expand Down Expand Up @@ -30,17 +30,18 @@ Foo *string `validate:"required"`

passes if `*Foo != ""` NOT if `Foo != nil`.

It only validates public/exported fields and (by default)will error
out if validation tags are found on private fields. This helps catch
mistakes early. You can control this behavior using the `ErrorOnPrivate`
option on the `Validator` instance.
It validates both public and private fields, as long as they have
the validation tags.

Non-goals:

- `slice`/`map` dive;
- cross field checks;
- anything that needs a 3rd party dep.

**Why?** Complex validation reads better when is expressed as Go code,
rather than in struct tags "perlisms".

## Available Checks

| Check | Description | Domain |
Expand Down Expand Up @@ -97,56 +98,14 @@ s := struct {
Foo struct {
Bar string `validate:"required, one_of:foo|bar|baz"`
}
email string `validate:"required,email"`
}{}

if err := vali.Validate(s); err != nil {
fmt.Println("oh noes!...")
}
```

<details>
<summary><strong>Working with Private Fields</strong></summary>

By default, Vali will error out if it finds validation tags on private fields:

```Go
type User struct {
Name string `validate:"required"`
email string `validate:"email"` // This will cause an error
}

err := vali.Validate(User{})
// err will contain ErrPrivateField
```

If you need to disable this behavior (for backward compatibility), you have two options:

1. Create a custom validator with `ErrorOnPrivate` set to false:

```Go
v := vali.New()
v.ErrorOnPrivate = false

type User struct {
Name string `validate:"required"`
email string `validate:"email"` // This will be silently ignored
}

err := v.Validate(User{}) // No error about the private field
```

2. Disable it globally on the default validator:

```Go
// Disable ErrorOnPrivate globally for all validation calls using the DefaultValidator
vali.DefaultValidator.ErrorOnPrivate = false

// Now all calls to the package-level Validate function will ignore private field tags
err := vali.Validate(User{}) // No error about the private field
```

</details>

## Documentation

- this README;
Expand Down
50 changes: 26 additions & 24 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ var (
ErrRequired = errors.New("value missing")
ErrInvalidChecker = errors.New("invalid checker")
ErrInvalidCmp = errors.New("invalid comparison")
ErrPrivateField = errors.New("private field")
)

//nolint:errcheck,lll // well covered with tests
Expand All @@ -59,7 +58,7 @@ var expLabel = map[expOutcome]string{
}

func email(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
if _, err = mail.ParseAddress(s); err != nil {
return fmt.Errorf("%q is not a valid email address", s)
}
Expand All @@ -68,7 +67,7 @@ func email(v reflect.Value) (err error) {
}

func urL(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))

u, err := url.Parse(s)
if err != nil {
Expand All @@ -83,15 +82,15 @@ func urL(v reflect.Value) (err error) {
}

func ip(v reflect.Value) (err error) {
if s := fmt.Sprint(v.Interface()); net.ParseIP(s) == nil {
if s := fmt.Sprint(Interface(v)); net.ParseIP(s) == nil {
return fmt.Errorf("%q is not a valid IP address", s)
}

return
}

func ipv4(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
if ip := net.ParseIP(s); ip == nil || ip.To4() == nil {
return fmt.Errorf("%q is not a valid IPv4 address", s)
}
Expand All @@ -100,7 +99,7 @@ func ipv4(v reflect.Value) (err error) {
}

func ipv6(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
if ip := net.ParseIP(s); ip == nil || ip.To4() != nil {
return fmt.Errorf("%q is not a valid IPv6 address", s)
}
Expand All @@ -109,7 +108,7 @@ func ipv6(v reflect.Value) (err error) {
}

func mac(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
if _, err = net.ParseMAC(s); err != nil {
return fmt.Errorf("%q is not a valid MAC address", s)
}
Expand All @@ -118,7 +117,7 @@ func mac(v reflect.Value) (err error) {
}

func isbn(v reflect.Value) (err error) {
switch s := strings.ReplaceAll(fmt.Sprint(v.Interface()), "-", ""); len(s) {
switch s := strings.ReplaceAll(fmt.Sprint(Interface(v)), "-", ""); len(s) {
case 10:
return validateISBN10(s)
case 13:
Expand Down Expand Up @@ -187,7 +186,7 @@ func validateISBN13(s string) (err error) {
}

func boolean(v reflect.Value) (err error) {
switch s := fmt.Sprint(v.Interface()); strings.ToLower(s) {
switch s := fmt.Sprint(Interface(v)); strings.ToLower(s) {
case "1", "t", "true", "yes", "y", "on":
return
case "0", "f", "false", "no", "n", "off":
Expand All @@ -198,7 +197,7 @@ func boolean(v reflect.Value) (err error) {
}

func creditCard(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
s = strings.ReplaceAll(s, " ", "")
s = strings.ReplaceAll(s, "-", "")

Expand All @@ -217,7 +216,7 @@ func creditCard(v reflect.Value) (err error) {

func jsoN(v reflect.Value) (err error) {
var (
s = fmt.Sprint(v.Interface())
s = fmt.Sprint(Interface(v))
js any
)

Expand All @@ -229,7 +228,7 @@ func jsoN(v reflect.Value) (err error) {
}

func ascii(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
for i, r := range s {
if r > unicode.MaxASCII {
return fmt.Errorf("%q contains non-ASCII character %q at position %d", s, r, i)
Expand All @@ -240,7 +239,7 @@ func ascii(v reflect.Value) (err error) {
}

func lowercase(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
for i, r := range s {
if unicode.IsUpper(r) {
return fmt.Errorf("%q contains uppercase character %q at position %d", s, r, i)
Expand All @@ -251,7 +250,7 @@ func lowercase(v reflect.Value) (err error) {
}

func uppercase(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
for i, r := range s {
if unicode.IsLower(r) {
return fmt.Errorf("%q contains lowercase character %q at position %d", s, r, i)
Expand All @@ -270,11 +269,12 @@ func luhn(v reflect.Value) (err error) {
case reflect.Float32, reflect.Float64:
// Format float without scientific notation and remove decimal point.
s = strings.ReplaceAll(fmt.Sprintf("%.0f", v.Float()), ".", "")
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
s = fmt.Sprintf("%d", v.Interface())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s = strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
s = strconv.FormatUint(v.Uint(), 10)
default:
s = fmt.Sprint(v.Interface())
s = fmt.Sprint(Interface(v))
}

s = strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "-", "")
Expand Down Expand Up @@ -310,7 +310,7 @@ func luhn(v reflect.Value) (err error) {

// NPI validates if a string is a valid National Provider Identifier.
func npi(v reflect.Value) (err error) {
s := fmt.Sprint(v.Interface())
s := fmt.Sprint(Interface(v))
if !npiRx.MatchString(s) {
return fmt.Errorf("%q is not a valid NPI", s)
}
Expand All @@ -335,7 +335,7 @@ func Regex(arg string) (c Checker, err error) {
}

return func(v reflect.Value) (err error) {
act := fmt.Sprint(v.Interface())
act := fmt.Sprint(Interface(v))
if rx.MatchString(act) {
return
}
Expand Down Expand Up @@ -407,20 +407,22 @@ func sizeCmp(arg string, exp expOutcome) (c Checker, err error) {
case v.CanFloat():
var x float64

switch vv := v.Interface().(type) {
case float32:
switch v.Kind() { //nolint:exhaustive // only floats can float
case reflect.Float32:
if x, err = strconv.ParseFloat(arg, 32); err != nil {
return
}

vv := float32(v.Float())
if cmp2(vv, float32(x), exp) {
return fmt.Errorf("%.0f is %s %.0f", vv, label, x)
}
case float64:
case reflect.Float64:
if x, err = strconv.ParseFloat(arg, 64); err != nil {
return
}

vv := v.Float()
if cmp2(vv, x, exp) {
return fmt.Errorf("%.0f is %s %.0f", vv, label, x)
}
Expand All @@ -432,7 +434,7 @@ func sizeCmp(arg string, exp expOutcome) (c Checker, err error) {
return
}

for v.Kind() == reflect.Ptr {
for v.Kind() == reflect.Pointer {
if v.IsNil() {
return
}
Expand Down
21 changes: 21 additions & 0 deletions coverage-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading