mirror of
https://github.com/romdo/go-validate.git
synced 2026-02-18 23:56:41 +00:00
Merge pull request #1 from romdo/initial-implementation
feat(validate): initial implementation
This commit is contained in:
145
.github/workflows/ci.yml
vendored
Normal file
145
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: CI
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.42
|
||||
env:
|
||||
VERBOSE: "true"
|
||||
|
||||
tidy:
|
||||
name: Tidy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Check if mods are tidy
|
||||
run: make check-tidy
|
||||
|
||||
# benchmark:
|
||||
# name: Benchmarks
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.ref != 'refs/heads/main'
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions/setup-go@v2
|
||||
# with:
|
||||
# go-version: 1.15
|
||||
# - uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/go/pkg/mod
|
||||
# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-go-
|
||||
# - name: Run benchmarks
|
||||
# run: make bench | tee output.raw
|
||||
# - name: Fix benchmark names
|
||||
# run: >-
|
||||
# perl -pe 's/^(Benchmark.+?)\/(\S+)(-\d+)(\s+)/\1__\2\4/' output.raw |
|
||||
# tr '-' '_' | tee output.txt
|
||||
# - name: Announce benchmark result
|
||||
# uses: rhysd/github-action-benchmark@v1
|
||||
# with:
|
||||
# tool: "go"
|
||||
# output-file-path: output.txt
|
||||
# fail-on-alert: true
|
||||
# comment-on-alert: true
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# auto-push: false
|
||||
|
||||
# cov:
|
||||
# name: Coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions/setup-go@v2
|
||||
# with:
|
||||
# go-version: 1.15
|
||||
# - uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/go/pkg/mod
|
||||
# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-go-
|
||||
# - name: Publish coverage
|
||||
# uses: paambaati/codeclimate-action@v2.7.4
|
||||
# env:
|
||||
# VERBOSE: "true"
|
||||
# GOMAXPROCS: 4
|
||||
# CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
||||
# with:
|
||||
# coverageCommand: make cov
|
||||
# prefix: github.com/${{ github.repository }}
|
||||
# coverageLocations: |
|
||||
# ${{ github.workspace }}/coverage.out:gocov
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go_version:
|
||||
- "1.15"
|
||||
- "1.16"
|
||||
- "1.17"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.terraform_version }}
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Run tests
|
||||
run: make test
|
||||
env:
|
||||
VERBOSE: "true"
|
||||
# benchmark-store:
|
||||
# name: Store benchmarks
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions/setup-go@v2
|
||||
# with:
|
||||
# go-version: 1.15
|
||||
# - uses: actions/cache@v2
|
||||
# with:
|
||||
# path: ~/go/pkg/mod
|
||||
# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-go-
|
||||
# - name: Run benchmarks
|
||||
# run: make bench | tee output.raw
|
||||
# - name: Fix benchmark names
|
||||
# run: >-
|
||||
# perl -pe 's/^(Benchmark.+?)\/(\S+)(-\d+)(\s+)/\1__\2\4/' output.raw |
|
||||
# tr '-' '_' | tee output.txt
|
||||
# - name: Store benchmark result
|
||||
# uses: rhysd/github-action-benchmark@v1
|
||||
# with:
|
||||
# tool: "go"
|
||||
# output-file-path: output.txt
|
||||
# github-token: ${{ secrets.ROMDOBOT_TOKEN }}
|
||||
# comment-on-alert: true
|
||||
# auto-push: true
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/*.tidy-check
|
||||
/bin/*
|
||||
/coverage.out
|
||||
/output.txt
|
||||
99
.golangci.yml
Normal file
99
.golangci.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
linters-settings:
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 150
|
||||
goconst:
|
||||
min-occurrences: 5
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
golint:
|
||||
min-confidence: 0
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 80
|
||||
tab-width: 4
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gofumpt
|
||||
- goimports
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- tparallel
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
exclude:
|
||||
- Using the variable on range scope `tt` in function literal
|
||||
- Using the variable on range scope `tc` in function literal
|
||||
exclude-rules:
|
||||
- path: "_test\\.go"
|
||||
linters:
|
||||
- funlen
|
||||
- dupl
|
||||
- goconst
|
||||
- source: "^//go:generate "
|
||||
linters:
|
||||
- lll
|
||||
- source: "`json:"
|
||||
linters:
|
||||
- lll
|
||||
- source: "`yaml:"
|
||||
linters:
|
||||
- lll
|
||||
- source: "`form:"
|
||||
linters:
|
||||
- lll
|
||||
|
||||
run:
|
||||
timeout: 2m
|
||||
allow-parallel-runners: true
|
||||
modules-download-mode: readonly
|
||||
192
Makefile
Normal file
192
Makefile
Normal file
@@ -0,0 +1,192 @@
|
||||
GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //')
|
||||
SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \
|
||||
-or -name "Makefile")
|
||||
|
||||
# Verbose output
|
||||
ifdef VERBOSE
|
||||
V = -v
|
||||
endif
|
||||
|
||||
#
|
||||
# Environment
|
||||
#
|
||||
|
||||
BINDIR := bin
|
||||
TOOLDIR := $(BINDIR)/tools
|
||||
|
||||
# Global environment variables for all targets
|
||||
SHELL ?= /bin/bash
|
||||
SHELL := env \
|
||||
GO111MODULE=on \
|
||||
GOBIN=$(CURDIR)/$(TOOLDIR) \
|
||||
CGO_ENABLED=1 \
|
||||
PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \
|
||||
$(SHELL)
|
||||
|
||||
#
|
||||
# Defaults
|
||||
#
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := test
|
||||
|
||||
#
|
||||
# Tools
|
||||
#
|
||||
|
||||
TOOLS += $(TOOLDIR)/gobin
|
||||
$(TOOLDIR)/gobin:
|
||||
GO111MODULE=off go get -u github.com/myitcv/gobin
|
||||
|
||||
# external tool
|
||||
define tool # 1: binary-name, 2: go-import-path
|
||||
TOOLS += $(TOOLDIR)/$(1)
|
||||
|
||||
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
|
||||
gobin $(V) "$(2)"
|
||||
endef
|
||||
|
||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
|
||||
$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports))
|
||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42))
|
||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
|
||||
|
||||
.PHONY: tools
|
||||
tools: $(TOOLS)
|
||||
|
||||
#
|
||||
# Development
|
||||
#
|
||||
|
||||
BENCH ?= .
|
||||
TESTARGS ?=
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f $(TOOLS)
|
||||
rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test $(V) -count=1 -race $(TESTARGS) ./...
|
||||
|
||||
.PHONY: test-deps
|
||||
test-deps:
|
||||
go test all
|
||||
|
||||
.PHONY: lint
|
||||
lint: $(TOOLDIR)/golangci-lint
|
||||
golangci-lint $(V) run
|
||||
|
||||
.PHONY: format
|
||||
format: $(TOOLDIR)/gofumports
|
||||
gofumports -w .
|
||||
|
||||
.SILENT: bench
|
||||
.PHONY: bench
|
||||
bench:
|
||||
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./...
|
||||
|
||||
#
|
||||
# Code Generation
|
||||
#
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
.PHONY: check-generate
|
||||
check-generate:
|
||||
$(eval CHKDIR := $(shell mktemp -d))
|
||||
cp -av . "$(CHKDIR)"
|
||||
make -C "$(CHKDIR)/" generate
|
||||
( diff -rN . "$(CHKDIR)" && rm -rf "$(CHKDIR)" ) || \
|
||||
( rm -rf "$(CHKDIR)" && exit 1 )
|
||||
|
||||
#
|
||||
# Coverage
|
||||
#
|
||||
|
||||
.PHONY: cov
|
||||
cov: coverage.out
|
||||
|
||||
.PHONY: cov-html
|
||||
cov-html: coverage.out
|
||||
go tool cover -html=./coverage.out
|
||||
|
||||
.PHONY: cov-func
|
||||
cov-func: coverage.out
|
||||
go tool cover -func=./coverage.out
|
||||
|
||||
coverage.out: $(SOURCES)
|
||||
go test $(V) -covermode=count -coverprofile=./coverage.out ./...
|
||||
|
||||
#
|
||||
# Dependencies
|
||||
#
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
go mod download
|
||||
|
||||
.PHONY: deps-update
|
||||
deps-update:
|
||||
go get -u -t ./...
|
||||
|
||||
.PHONY: deps-analyze
|
||||
deps-analyze: $(TOOLDIR)/gomod
|
||||
gomod analyze
|
||||
|
||||
.PHONY: tidy
|
||||
tidy:
|
||||
go mod tidy $(V)
|
||||
|
||||
.PHONY: verify
|
||||
verify:
|
||||
go mod verify
|
||||
|
||||
.SILENT: check-tidy
|
||||
.PHONY: check-tidy
|
||||
check-tidy:
|
||||
cp go.mod go.mod.tidy-check
|
||||
cp go.sum go.sum.tidy-check
|
||||
go mod tidy
|
||||
( \
|
||||
diff go.mod go.mod.tidy-check && \
|
||||
diff go.sum go.sum.tidy-check && \
|
||||
rm -f go.mod go.sum && \
|
||||
mv go.mod.tidy-check go.mod && \
|
||||
mv go.sum.tidy-check go.sum \
|
||||
) || ( \
|
||||
rm -f go.mod go.sum && \
|
||||
mv go.mod.tidy-check go.mod && \
|
||||
mv go.sum.tidy-check go.sum; \
|
||||
exit 1 \
|
||||
)
|
||||
|
||||
#
|
||||
# Documentation
|
||||
#
|
||||
|
||||
# Serve docs
|
||||
.PHONY: docs
|
||||
docs: $(TOOLDIR)/godoc
|
||||
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
||||
@godoc -http=127.0.0.1:6060
|
||||
|
||||
#
|
||||
# Release
|
||||
#
|
||||
|
||||
.PHONY: new-version
|
||||
new-version: check-npx
|
||||
npx standard-version
|
||||
|
||||
.PHONY: next-version
|
||||
next-version: check-npx
|
||||
npx standard-version --dry-run
|
||||
|
||||
.PHONY: check-npx
|
||||
check-npx:
|
||||
$(if $(shell which npx),,\
|
||||
$(error No npx found in PATH, please install NodeJS))
|
||||
69
README.md
69
README.md
@@ -4,10 +4,75 @@
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
[WIP] Go package for object validation with a goal of simple and flexible.
|
||||
Yet another Go struct/object validation package, with a focus on simplicity,
|
||||
flexibility, and full control over validation logic.
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
## License
|
||||
---
|
||||
|
||||
Add validation to any type, by simply implementing the `Validatable` interface:
|
||||
|
||||
```go
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
```
|
||||
|
||||
## Import
|
||||
|
||||
```go
|
||||
import "github.com/romdo/go-validate"
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
type Order struct {
|
||||
Books []*Book `json:"books"`
|
||||
}
|
||||
|
||||
type Book struct {
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
func (s *Book) Validate() error {
|
||||
var errs error
|
||||
if s.Title == "" {
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "Title", Msg: "is required",
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to perform the same kind of check as above for Title.
|
||||
errs = validate.Append(errs, validate.RequireField("Author", s.Author))
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func main() {
|
||||
errs := validate.Validate(&Order{Books: []*Book{{Title: ""}}})
|
||||
|
||||
for _, err := range validate.Errors(errs) {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Above example produces the following output:
|
||||
|
||||
```
|
||||
books.0.title: is required
|
||||
books.0.author: is required
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Please see the
|
||||
[Go Reference](https://pkg.go.dev/github.com/romdo/go-validate#section-documentation)
|
||||
for documentation and examples.
|
||||
|
||||
## LICENSE
|
||||
|
||||
[MIT](https://github.com/romdo/go-conventionalcommit/blob/main/LICENSE)
|
||||
|
||||
67
error.go
Normal file
67
error.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
// Error represents validation errors, and implements Go's error type. Field
|
||||
// indicates the struct field the validation error is relevant to, which is the
|
||||
// full nested path relative to the top-level object being validated.
|
||||
type Error struct {
|
||||
Field string
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (s *Error) Error() string {
|
||||
msg := s.Msg
|
||||
if msg == "" && s.Err != nil {
|
||||
msg = s.Err.Error()
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
msg = "unknown error"
|
||||
}
|
||||
|
||||
if s.Field == "" {
|
||||
return msg
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %s", s.Field, msg)
|
||||
}
|
||||
|
||||
func (s *Error) Is(target error) bool {
|
||||
return errors.Is(s.Err, target)
|
||||
}
|
||||
|
||||
func (s *Error) Unwrap() error {
|
||||
return s.Err
|
||||
}
|
||||
|
||||
// Append combines two errors together into a single new error which internally
|
||||
// keeps track of multiple errors via go.uber.org/multierr. If either error is a
|
||||
// previously combined multierr, the returned error will be a flattened list of
|
||||
// all errors.
|
||||
func Append(errs error, err error) error {
|
||||
return multierr.Append(errs, err)
|
||||
}
|
||||
|
||||
// AppendError appends a new *Error type to errs with the Msg field populated
|
||||
// with the provided msg.
|
||||
func AppendError(errs error, msg string) error {
|
||||
return multierr.Append(errs, &Error{Msg: msg})
|
||||
}
|
||||
|
||||
// AppendFieldError appends a new *Error type to errs with Field and Msg
|
||||
// populated with given field and msg values.
|
||||
func AppendFieldError(errs error, field, msg string) error {
|
||||
return multierr.Append(errs, &Error{Field: field, Msg: msg})
|
||||
}
|
||||
|
||||
// Errors returns a slice of all errors appended into the given error.
|
||||
func Errors(err error) []error {
|
||||
return multierr.Errors(err)
|
||||
}
|
||||
456
error_test.go
Normal file
456
error_test.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
func TestError_Error(t *testing.T) {
|
||||
type fields struct {
|
||||
Field string
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
want: "unknown error",
|
||||
},
|
||||
{
|
||||
name: "field only",
|
||||
fields: fields{
|
||||
Field: "spec.images.0.name",
|
||||
},
|
||||
want: "spec.images.0.name: unknown error",
|
||||
},
|
||||
{
|
||||
name: "msg only",
|
||||
fields: fields{
|
||||
Msg: "flux capacitor is missing",
|
||||
},
|
||||
want: "flux capacitor is missing",
|
||||
},
|
||||
{
|
||||
name: "err only",
|
||||
fields: fields{
|
||||
Err: errors.New("flux capacitor is king"),
|
||||
},
|
||||
want: "flux capacitor is king",
|
||||
},
|
||||
{
|
||||
name: "field and msg",
|
||||
fields: fields{
|
||||
Field: "spec.images.0.name",
|
||||
Msg: "is required",
|
||||
},
|
||||
want: "spec.images.0.name: is required",
|
||||
},
|
||||
{
|
||||
name: "field and err",
|
||||
fields: fields{
|
||||
Field: "spec",
|
||||
Err: errors.New("something is wrong"),
|
||||
},
|
||||
want: "spec: something is wrong",
|
||||
},
|
||||
{
|
||||
name: "msg and err",
|
||||
fields: fields{
|
||||
Msg: "flux capacitor is missing",
|
||||
Err: errors.New("flux capacitor is king"),
|
||||
},
|
||||
want: "flux capacitor is missing",
|
||||
},
|
||||
{
|
||||
name: "field, msg, and err",
|
||||
fields: fields{
|
||||
Field: "spec.images.0.name",
|
||||
Msg: "is required",
|
||||
Err: errors.New("something is wrong"),
|
||||
},
|
||||
want: "spec.images.0.name: is required",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{
|
||||
Field: tt.fields.Field,
|
||||
Msg: tt.fields.Msg,
|
||||
Err: tt.fields.Err,
|
||||
}
|
||||
|
||||
got := err.Error()
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_Is(t *testing.T) {
|
||||
errTest1 := errors.New("errtest1")
|
||||
errTest2 := errors.New("errtest2")
|
||||
|
||||
type fields struct {
|
||||
Err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
target error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
target: errTest1,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Err and target match",
|
||||
fields: fields{Err: errTest1},
|
||||
target: errTest1,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Err and target do not match",
|
||||
fields: fields{Err: errTest2},
|
||||
target: errTest1,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{Err: tt.fields.Err}
|
||||
|
||||
got := errors.Is(err, tt.target)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_Unwrap(t *testing.T) {
|
||||
errTest1 := errors.New("errtest1")
|
||||
errTest2 := errors.New("errtest2")
|
||||
|
||||
type fields struct {
|
||||
Err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Err test1",
|
||||
fields: fields{Err: errTest1},
|
||||
want: errTest1,
|
||||
},
|
||||
{
|
||||
name: "Err test2",
|
||||
fields: fields{Err: errTest2},
|
||||
want: errTest2,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{Err: tt.fields.Err}
|
||||
|
||||
got := err.Unwrap()
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
type args struct {
|
||||
errs error
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "append nil to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
err: nil,
|
||||
},
|
||||
want: []error{},
|
||||
},
|
||||
{
|
||||
name: "append nil to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
err: nil,
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append nil to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
err: nil,
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append err to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
err: errors.New("foo"),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append err to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
err: errors.New("bar"),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append err to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
err: errors.New("baz"),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
errors.New("baz"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Append(tt.args.errs, tt.args.err)
|
||||
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
got := multierr.Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendError(t *testing.T) {
|
||||
type args struct {
|
||||
errs error
|
||||
msg string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "append empty msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
msg: "",
|
||||
},
|
||||
want: []error{
|
||||
&Error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
msg: "foo",
|
||||
},
|
||||
want: []error{
|
||||
&Error{Msg: "foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
msg: "bar",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
&Error{Msg: "bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
msg: "baz",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
&Error{Msg: "baz"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := AppendError(tt.args.errs, tt.args.msg)
|
||||
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
got := multierr.Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendFieldError(t *testing.T) {
|
||||
type args struct {
|
||||
errs error
|
||||
field string
|
||||
msg string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "append empty field and msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
field: "",
|
||||
msg: "",
|
||||
},
|
||||
want: []error{
|
||||
&Error{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to nil",
|
||||
args: args{
|
||||
errs: nil,
|
||||
field: "Type",
|
||||
msg: "foo",
|
||||
},
|
||||
want: []error{
|
||||
&Error{Field: "Type", Msg: "foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to err",
|
||||
args: args{
|
||||
errs: errors.New("foo"),
|
||||
field: "Type",
|
||||
msg: "bar",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
&Error{Field: "Type", Msg: "bar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append msg to multi err",
|
||||
args: args{
|
||||
errs: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
field: "Type",
|
||||
msg: "baz",
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
&Error{Field: "Type", Msg: "baz"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := AppendFieldError(tt.args.errs, tt.args.field, tt.args.msg)
|
||||
|
||||
if len(tt.want) == 0 {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
got := multierr.Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{err: nil},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single error",
|
||||
args: args{err: errors.New("foo")},
|
||||
want: []error{errors.New("foo")},
|
||||
},
|
||||
{
|
||||
name: "multi error with one error",
|
||||
args: args{
|
||||
err: multierr.Combine(errors.New("foo")),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi error with two errors",
|
||||
args: args{
|
||||
err: multierr.Combine(errors.New("foo"), errors.New("bar")),
|
||||
},
|
||||
want: []error{
|
||||
errors.New("foo"),
|
||||
errors.New("bar"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Errors(tt.args.err)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
38
examples/basic/basic.go
Normal file
38
examples/basic/basic.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/romdo/go-validate"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
Books []*Book `json:"books"`
|
||||
}
|
||||
|
||||
type Book struct {
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
func (s *Book) Validate() error {
|
||||
var errs error
|
||||
if s.Title == "" {
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "Title", Msg: "is required",
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to perform the same kind of check as above for Title.
|
||||
errs = validate.Append(errs, validate.RequireField("Author", s.Author))
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func main() {
|
||||
errs := validate.Validate(&Order{Books: []*Book{{Title: ""}}})
|
||||
|
||||
for _, err := range validate.Errors(errs) {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
110
examples/complex/complex.go
Normal file
110
examples/complex/complex.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/romdo/go-validate"
|
||||
)
|
||||
|
||||
type Manifest struct {
|
||||
Spec *Spec `json:"spec"`
|
||||
}
|
||||
|
||||
func (s *Manifest) Validate() error {
|
||||
return validate.RequireField("Spec", s.Spec)
|
||||
}
|
||||
|
||||
type Spec struct {
|
||||
Containers []*Container `json:"containers"`
|
||||
Images []*Image `json:"images"`
|
||||
}
|
||||
|
||||
func (s *Spec) Validate() error {
|
||||
var errs error
|
||||
|
||||
if len(s.Containers) == 0 {
|
||||
errs = validate.AppendFieldError(errs,
|
||||
"Containers", "must contain at least one item",
|
||||
)
|
||||
} else {
|
||||
imgs := map[string]bool{}
|
||||
for _, img := range s.Images {
|
||||
if img.Name != "" {
|
||||
imgs[img.Name] = true
|
||||
}
|
||||
}
|
||||
for i, c := range s.Containers {
|
||||
if c.ImageRef != "" && !imgs[c.ImageRef] {
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: fmt.Sprintf("containers.%d.imageRef", i),
|
||||
Msg: fmt.Sprintf(
|
||||
"image with name '%s' not found", c.ImageRef,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Images) == 0 {
|
||||
errs = validate.AppendFieldError(errs,
|
||||
"Images", "must contain at least one item",
|
||||
)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Name string `json:"name"`
|
||||
ImageRef string `json:"imageRef"`
|
||||
}
|
||||
|
||||
func (s *Container) Validate() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, validate.RequireField("Name", s.Name))
|
||||
errs = validate.Append(errs, validate.RequireField("ImageRef", s.ImageRef))
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Name string `json:"name"`
|
||||
URI string `json:"uri"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
func (s *Image) Validate() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, validate.RequireField("Name", s.Name))
|
||||
errs = validate.Append(errs, validate.RequireField("URI", s.URI))
|
||||
errs = validate.Append(errs, validate.RequireField("Tag", s.Tag))
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func main() {
|
||||
manifest := &Manifest{
|
||||
Spec: &Spec{
|
||||
Containers: []*Container{
|
||||
{
|
||||
ImageRef: "server",
|
||||
},
|
||||
{
|
||||
Name: "worker",
|
||||
ImageRef: "myServer",
|
||||
},
|
||||
},
|
||||
Images: []*Image{
|
||||
{
|
||||
Name: "server",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errs := validate.Validate(manifest)
|
||||
|
||||
for _, err := range validate.Errors(errs) {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/romdo/go-validate
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0
|
||||
)
|
||||
19
go.sum
Normal file
19
go.sum
Normal file
@@ -0,0 +1,19 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
36
helpers.go
Normal file
36
helpers.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// RequireField returns a Error type for the given field if provided value is
|
||||
// empty/zero.
|
||||
func RequireField(field string, value interface{}) error {
|
||||
err := &Error{Field: field, Msg: "is required"}
|
||||
v := reflect.ValueOf(value)
|
||||
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return err
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if !v.IsValid() {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v.Kind() { //nolint:exhaustive
|
||||
case reflect.Map, reflect.Slice:
|
||||
if v.Len() == 0 {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if v.IsZero() {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
406
helpers_test.go
Normal file
406
helpers_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestRequireField(t *testing.T) {
|
||||
var nilMapString map[string]string
|
||||
emptyMapString := map[string]string{}
|
||||
mapString := map[string]string{"foo": "bar"}
|
||||
type testStruct struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type args struct {
|
||||
field string
|
||||
value interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: nil,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "nil pointer",
|
||||
args: args{
|
||||
field: "Title",
|
||||
value: &nilMapString,
|
||||
},
|
||||
want: &Error{Field: "Title", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "true boolean",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: true,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "false boolean",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: false,
|
||||
},
|
||||
want: &Error{Field: "Book", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
args: args{
|
||||
field: "Count",
|
||||
value: int(834),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int",
|
||||
args: args{
|
||||
field: "Count",
|
||||
value: int(0),
|
||||
},
|
||||
want: &Error{Field: "Count", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int8(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int8(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int16(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int16(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int32(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int32(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "int64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int64(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero int64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: int64(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "zero uint",
|
||||
args: args{
|
||||
field: "Count",
|
||||
value: uint(0),
|
||||
},
|
||||
want: &Error{Field: "Count", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint8(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint8",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint8(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint16(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint16",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint16(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint32(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint32",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint32(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "uint64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint64(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero uint64",
|
||||
args: args{
|
||||
field: "Ticks",
|
||||
value: uint64(0),
|
||||
},
|
||||
want: &Error{Field: "Ticks", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "complex64",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex64(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero complex64",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex64(0),
|
||||
},
|
||||
want: &Error{Field: "Offset", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "complex128",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex128(3),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "zero complex128",
|
||||
args: args{
|
||||
field: "Offset",
|
||||
value: complex128(0),
|
||||
},
|
||||
want: &Error{Field: "Offset", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: [3]string{"foo", "bar", "baz"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty array",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: [3]string{},
|
||||
},
|
||||
want: &Error{Field: "List", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "chan",
|
||||
args: args{
|
||||
field: "Comms",
|
||||
value: make(chan int),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "func",
|
||||
args: args{
|
||||
field: "Callback",
|
||||
value: func() error { return nil },
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{"foo": "bar"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map pointer",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: &mapString,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: map[string]string{},
|
||||
},
|
||||
want: &Error{Field: "Lookup", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "empty map pointer",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: &emptyMapString,
|
||||
},
|
||||
want: &Error{Field: "Lookup", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
args: args{
|
||||
field: "Lookup",
|
||||
value: nilMapString,
|
||||
},
|
||||
want: &Error{Field: "Lookup", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "slice",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: []string{"foo", "bar", "baz"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty slice",
|
||||
args: args{
|
||||
field: "List",
|
||||
value: []string{},
|
||||
},
|
||||
want: &Error{Field: "List", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: "foo",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "string pointer",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: stringPtr("foo"),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: "",
|
||||
},
|
||||
want: &Error{Field: "Book", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "empty string pointer",
|
||||
args: args{
|
||||
field: "Book",
|
||||
value: stringPtr(""),
|
||||
},
|
||||
want: &Error{Field: "Book", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "struct",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: testStruct{Name: "hi"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "struct pointer",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: &testStruct{Name: "hi"},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty struct",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: testStruct{},
|
||||
},
|
||||
want: &Error{Field: "Thing", Msg: "is required"},
|
||||
},
|
||||
{
|
||||
name: "empty struct pointer",
|
||||
args: args{
|
||||
field: "Thing",
|
||||
value: &testStruct{},
|
||||
},
|
||||
want: &Error{Field: "Thing", Msg: "is required"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := RequireField(tt.args.field, tt.args.value)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
162
validate.go
Normal file
162
validate.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Package validate is yet another Go struct/object validation package, with a
|
||||
// focus on simplicity, flexibility, and full control over validation logic.
|
||||
//
|
||||
// Interface
|
||||
//
|
||||
// To add validation to any type, simply implement the Validatable interface:
|
||||
//
|
||||
// type Validatable interface {
|
||||
// Validate() error
|
||||
// }
|
||||
//
|
||||
// To mark a object as failing validation, the Validate method simply needs to
|
||||
// return a error.
|
||||
//
|
||||
// When validating array, slice, map, and struct types each item and/or field
|
||||
// that implements Validatable will be validated, meaning deeply nested structs
|
||||
// can be fully validated, and the nested path to each object is tracked and
|
||||
// reported back any validation errors.
|
||||
//
|
||||
// Multiple Errors
|
||||
//
|
||||
// Multiple errors can be reported from the Validate method using one of the
|
||||
// available Append helper functions which append errors together. Under the
|
||||
// hood the go.uber.org/multierr package is used to represent multiple errors as
|
||||
// a single error return type, and you can in fact just directly use multierr in
|
||||
// the a type's Validate method.
|
||||
//
|
||||
// Structs and Field-specific Errors
|
||||
//
|
||||
// When validating a struct, you are likely to have multiple errors for multiple
|
||||
// fields. To specify which field on the struct the error relates to, you have
|
||||
// to return a *validate.Error instead of a normal Go error type. For example:
|
||||
//
|
||||
// type Book struct {
|
||||
// Title string
|
||||
// Author string
|
||||
// }
|
||||
//
|
||||
// func (s *Book) Validate() error {
|
||||
// var errs error
|
||||
//
|
||||
// if s.Title == "" {
|
||||
// errs = validate.Append(errs, &validate.Error{
|
||||
// Field: "Title", Msg: "is required",
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// if s.Author == "" {
|
||||
// // Yields the same result as the Title field check above.
|
||||
// errs = validate.AppendFieldError(errs, "Author", "is required")
|
||||
// }
|
||||
//
|
||||
// return errs
|
||||
// }
|
||||
//
|
||||
// With the above example, if you validate a empty *Book:
|
||||
//
|
||||
// err := validate.Validate(&Book{})
|
||||
// for _, e := range validate.Errors(err) {
|
||||
// fmt.Println(e.Error())
|
||||
// }
|
||||
//
|
||||
// The following errors would be printed:
|
||||
//
|
||||
// Title: is required
|
||||
// Kind: is required
|
||||
//
|
||||
// Error type
|
||||
//
|
||||
// All errors will be wrapped in a *Error before being returned, which is used
|
||||
// to keep track of the path and field the error relates to. There are various
|
||||
// helpers available to create Error instances.
|
||||
//
|
||||
// Handling Validation Errors
|
||||
//
|
||||
// As mentioned above, multiple errors are wrapped up into a single error return
|
||||
// value using go.uber.org/multierr. You can access all errors individually with
|
||||
// Errors(), which accepts a single error, and returns []error. The Errors()
|
||||
// function is just wrapper around multierr.Errors(), so you could use that
|
||||
// instead if you prefer.
|
||||
//
|
||||
// Struct Field Tags
|
||||
//
|
||||
// Fields on a struct which customize the name via a json, yaml, or form field
|
||||
// tag, will automatically have the field name converted to the name in the tag
|
||||
// in returned *Error types with a non-empty Field value.
|
||||
//
|
||||
// You can customize the field name conversion logic by creating a custom
|
||||
// Validator instance, and calling FieldNameFunc() on it.
|
||||
//
|
||||
// Nested Validatable Objects
|
||||
//
|
||||
// All items/fields on any structs, maps, slices or arrays which are encountered
|
||||
// will be validated if they implement the Validatable interface. While
|
||||
// traversing nested data structures, a path list tracks the location of the
|
||||
// current object being validation in relation to the top-level object being
|
||||
// validated. This path is used within the field in the final output errors.
|
||||
//
|
||||
// By default path components are joined with a dot, but this can be customized
|
||||
// when using a custom Validator instance and calling FieldJoinFunc() passing in
|
||||
// a custom function to handle path joining.
|
||||
//
|
||||
// As an example, if our Book struct from above is nested within the following
|
||||
// structs:
|
||||
//
|
||||
// type Order struct {
|
||||
// Items []*Item `json:"items"`
|
||||
// }
|
||||
//
|
||||
// type Item struct {
|
||||
// Book *Book `json:"book"`
|
||||
// }
|
||||
//
|
||||
// And we have a Order where the book in the second Item has a empty Author
|
||||
// field:
|
||||
//
|
||||
// err := validate.Validate(&Order{
|
||||
// Items: []*Item{
|
||||
// {Book: &Book{Title: "The Traveler", Author: "John Twelve Hawks"}},
|
||||
// {Book: &Book{Title: "The Firm"}},
|
||||
// },
|
||||
// })
|
||||
// for _, e := range validate.Errors(err) {
|
||||
// fmt.Println(e.Error())
|
||||
// }
|
||||
//
|
||||
// Then we would get the following error:
|
||||
//
|
||||
// items.1.book.Author: is required
|
||||
//
|
||||
// Note how both "items" and "book" are lower cased thanks to the json tags on
|
||||
// the struct fields, while our Book struct does not have a json tag for the
|
||||
// Author field.
|
||||
//
|
||||
// Also note that the error message does not start with "Order". The field path
|
||||
// is relative to the object being validated, hence the top-level object is not
|
||||
// part of the returned field path.
|
||||
package validate
|
||||
|
||||
// global is a private instance of Validator to enable the package root-level
|
||||
// Validate() function.
|
||||
var global = New()
|
||||
|
||||
// Validate will validate the given object. Structs, maps, slices, and arrays
|
||||
// will have each of their fields/items validated, effectively performing a
|
||||
// deep-validation.
|
||||
func Validate(v interface{}) error {
|
||||
return global.Validate(v)
|
||||
}
|
||||
|
||||
// Validatable is the primary interface that a object needs to implement to be
|
||||
// validatable with Validator.
|
||||
//
|
||||
// Validation errors are reported by returning a error from the Validate
|
||||
// method. Multiple errors can be combined into a single error to return with
|
||||
// Append() and related functions, or via go.uber.org/multierr.
|
||||
//
|
||||
// For validatable structs, the field the validation error relates to can be
|
||||
// specified by returning a *Error type with the Field value specified.
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
907
validate_test.go
Normal file
907
validate_test.go
Normal file
@@ -0,0 +1,907 @@
|
||||
package validate_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/romdo/go-validate"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//
|
||||
// Test helper types
|
||||
//
|
||||
|
||||
type validatableString string
|
||||
|
||||
func (s validatableString) Validate() error {
|
||||
if strings.Contains(string(s), " ") {
|
||||
return &validate.Error{Msg: "must not contain space"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type validatableStruct struct {
|
||||
Foo string
|
||||
Bar string `json:"bar"`
|
||||
Foz string `yaml:"foz"`
|
||||
Baz string `form:"baz"`
|
||||
|
||||
f func() error
|
||||
}
|
||||
|
||||
func (s *validatableStruct) Validate() error {
|
||||
if s.f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.f()
|
||||
}
|
||||
|
||||
type nestedStruct struct {
|
||||
OtherField *validatableStruct
|
||||
OtherFieldJSON *validatableStruct `json:"other_field,omitempty"`
|
||||
OtherFieldYAML *validatableStruct `yaml:"otherField,omitempty"`
|
||||
OtherFieldFORM *validatableStruct `form:"other-field,omitempty"`
|
||||
|
||||
skippedField *validatableStruct
|
||||
SkippedFieldJSON *validatableStruct `json:"-,omitempty"`
|
||||
SkippedFieldYAML *validatableStruct `yaml:"-,omitempty"`
|
||||
SkippedFieldFORM *validatableStruct `form:"-,omitempty"`
|
||||
|
||||
OtherArray [5]*validatableStruct
|
||||
OtherArrayJSON [5]*validatableStruct `json:"other_array,omitempty"`
|
||||
OtherArrayYAML [5]*validatableStruct `yaml:"otherArray,omitempty"`
|
||||
OtherArrayFORM [5]*validatableStruct `form:"other-array,omitempty"`
|
||||
|
||||
OtherSlice []*validatableStruct
|
||||
OtherSliceJSON []*validatableStruct `json:"other_slice,omitempty"`
|
||||
OtherSliceYAML []*validatableStruct `yaml:"otherSlice,omitempty"`
|
||||
OtherSliceFORM []*validatableStruct `form:"other-slice,omitempty"`
|
||||
|
||||
OtherStringMap map[string]*validatableStruct
|
||||
OtherStringMapJSON map[string]*validatableStruct `json:"other_string_map,omitempty"`
|
||||
OtherStringMapYAML map[string]*validatableStruct `yaml:"otherStringMap,omitempty"`
|
||||
OtherStringMapFORM map[string]*validatableStruct `form:"other-string-map,omitempty"`
|
||||
|
||||
OtherIntMap map[int]*validatableStruct
|
||||
OtherIntMapJSON map[int]*validatableStruct `json:"other_int_map,omitempty"`
|
||||
OtherIntMapYAML map[int]*validatableStruct `yaml:"otherIntMap,omitempty"`
|
||||
OtherIntMapFORM map[int]*validatableStruct `form:"other-int-map,omitempty"`
|
||||
|
||||
OtherStruct *nestedStruct
|
||||
OtherStructJSON *nestedStruct `json:"other_struct,omitempty"`
|
||||
OtherStructYAML *nestedStruct `yaml:"otherStruct,omitempty"`
|
||||
OtherStructFORM *nestedStruct `form:"other-struct,omitempty"`
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
mixedValidationErrors := &validatableStruct{
|
||||
f: func() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
})
|
||||
errs = validate.Append(errs, errors.New("bar: is missing"))
|
||||
|
||||
return errs
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
obj interface{}
|
||||
wantErrs []error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
obj: nil,
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "no error",
|
||||
obj: &validatableStruct{},
|
||||
wantErrs: nil,
|
||||
},
|
||||
{
|
||||
name: "valid validatable string type",
|
||||
obj: validatableString("hello-world"),
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "invalid validatable string type",
|
||||
obj: validatableString("hello world"),
|
||||
wantErrs: []error{
|
||||
&validate.Error{Msg: "must not contain space"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single Go error",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return errors.New("foo: is required")
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Err: errors.New("foo: is required")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single *validate.Error",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple Go errors",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, errors.New("foo: is required"))
|
||||
errs = validate.Append(errs, errors.New("bar: is missing"))
|
||||
|
||||
return errs
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Err: errors.New("foo: is required")},
|
||||
&validate.Error{Err: errors.New("bar: is missing")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple *validate.Error",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
var errs error
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
})
|
||||
errs = validate.Append(errs, &validate.Error{
|
||||
Field: "bar", Msg: "is required", Err: errors.New("whoops"),
|
||||
})
|
||||
|
||||
return errs
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "foo", Msg: "is required", Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "bar", Msg: "is required", Err: errors.New("whoops"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix of Go error and *validate.Error",
|
||||
obj: mixedValidationErrors,
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "Foo", Msg: "is required", Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{Err: errors.New("bar: is missing")},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Field name conversion
|
||||
//
|
||||
{
|
||||
name: "no json, yaml or form field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Foo", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "Foo", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts field name via json field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Bar", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "bar", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts field name via yaml field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Foz", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "foz", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "converts field name via form field tag",
|
||||
obj: &validatableStruct{f: func() error {
|
||||
return &validate.Error{Field: "Baz", Msg: "is required"}
|
||||
}},
|
||||
wantErrs: []error{
|
||||
&validate.Error{Field: "baz", Msg: "is required"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested with no validation errors",
|
||||
obj: &nestedStruct{
|
||||
OtherField: &validatableStruct{},
|
||||
OtherArray: [5]*validatableStruct{{}, {}, {}, {}},
|
||||
OtherSlice: []*validatableStruct{{}},
|
||||
OtherStringMap: map[string]*validatableStruct{
|
||||
"hi": {},
|
||||
"bye": {},
|
||||
},
|
||||
OtherIntMap: map[int]*validatableStruct{42: {}, 64: {}},
|
||||
OtherStruct: &nestedStruct{
|
||||
OtherField: &validatableStruct{},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
//
|
||||
// Nested in a struct field.
|
||||
//
|
||||
{
|
||||
name: "nested in a struct field",
|
||||
obj: &nestedStruct{
|
||||
OtherField: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherField", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherFieldJSON: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_field", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherFieldYAML: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherField", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherFieldFORM: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-field", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in a unexposed/ignored fields.
|
||||
//
|
||||
{
|
||||
name: "nested in a unexposed field",
|
||||
obj: &nestedStruct{
|
||||
skippedField: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field skipped by json tag",
|
||||
obj: &nestedStruct{
|
||||
SkippedFieldJSON: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field skipped by yaml tag",
|
||||
obj: &nestedStruct{
|
||||
SkippedFieldYAML: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
{
|
||||
name: "nested in a struct field skipped by yaml tag",
|
||||
obj: &nestedStruct{
|
||||
SkippedFieldFORM: mixedValidationErrors,
|
||||
},
|
||||
wantErrs: []error{},
|
||||
},
|
||||
//
|
||||
// Nested in an array.
|
||||
//
|
||||
{
|
||||
name: "nested in an array",
|
||||
obj: &nestedStruct{
|
||||
OtherArray: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherArray.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherArray.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherArray.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherArray.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in an array with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherArrayJSON: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_array.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_array.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_array.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_array.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in an array with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherArrayYAML: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherArray.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherArray.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherArray.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherArray.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in an array with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherArrayFORM: [5]*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-array.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-array.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-array.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-array.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in a slice.
|
||||
//
|
||||
{
|
||||
name: "nested in a slice",
|
||||
obj: &nestedStruct{
|
||||
OtherSlice: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherSlice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a slice with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherSliceJSON: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_slice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_slice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_slice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_slice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a slice with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherSliceYAML: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherSlice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherSlice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherSlice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherSlice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a slice with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherSliceFORM: []*validatableStruct{
|
||||
mixedValidationErrors,
|
||||
mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-slice.0.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-slice.0", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-slice.1.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-slice.1", Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in an string map.
|
||||
//
|
||||
{
|
||||
name: "nested in a string map",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMap: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a string map with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMapJSON: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_string_map.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_string_map.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_string_map.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_string_map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a string map with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMapYAML: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a string map with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStringMapFORM: map[string]*validatableStruct{
|
||||
"hello": mixedValidationErrors,
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-string-map.hello.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-string-map.hello",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-string-map.world.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-string-map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in an int map.
|
||||
//
|
||||
{
|
||||
name: "nested in a int map",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMap: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherIntMap.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMapJSON: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_int_map.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_int_map.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_int_map.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_int_map.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMapYAML: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherIntMap.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherIntMapFORM: map[int]*validatableStruct{
|
||||
42: mixedValidationErrors,
|
||||
64: mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-int-map.42.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-int-map.42",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-int-map.64.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-int-map.64",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
//
|
||||
// Nested in another struct.
|
||||
//
|
||||
{
|
||||
name: "nested in another struct",
|
||||
obj: &nestedStruct{
|
||||
OtherStruct: &nestedStruct{
|
||||
OtherField: mixedValidationErrors,
|
||||
OtherStringMap: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherField",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherStringMap.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "OtherStruct.OtherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with json tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStructJSON: &nestedStruct{
|
||||
OtherFieldJSON: mixedValidationErrors,
|
||||
OtherStringMapJSON: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_field",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_string_map.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other_struct.other_string_map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with yaml tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStructYAML: &nestedStruct{
|
||||
OtherFieldYAML: mixedValidationErrors,
|
||||
OtherStringMapYAML: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherField.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherField",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherStringMap.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "otherStruct.otherStringMap.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested in a int map with form tag",
|
||||
obj: &nestedStruct{
|
||||
OtherStructFORM: &nestedStruct{
|
||||
OtherFieldFORM: mixedValidationErrors,
|
||||
OtherStringMapFORM: map[string]*validatableStruct{
|
||||
"world": mixedValidationErrors,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrs: []error{
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-field.Foo", Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-field",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-string-map.world.Foo",
|
||||
Msg: "is required",
|
||||
Err: errors.New("oops"),
|
||||
},
|
||||
&validate.Error{
|
||||
Field: "other-struct.other-string-map.world",
|
||||
Err: errors.New("bar: is missing"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Validate(tt.obj)
|
||||
|
||||
if len(tt.wantErrs) == 0 {
|
||||
assert.Nil(t, err, "validation error should be nil")
|
||||
}
|
||||
|
||||
got := validate.Errors(err)
|
||||
assert.ElementsMatch(t, tt.wantErrs, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
161
validator.go
Normal file
161
validator.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
// FieldNameFunc is a function which converts a given reflect.StructField to a
|
||||
// string. The default will lookup json, yaml, and form field tags.
|
||||
type FieldNameFunc func(reflect.StructField) string
|
||||
|
||||
// FieldJoinFunc joins a path slice with a given field. Both path and field may
|
||||
// be empty values.
|
||||
type FieldJoinFunc func(path []string, field string) string
|
||||
|
||||
// Validator validates Validatable objects.
|
||||
type Validator struct {
|
||||
fieldName FieldNameFunc
|
||||
fieldJoin FieldJoinFunc
|
||||
}
|
||||
|
||||
// New creates a new Validator.
|
||||
func New() *Validator {
|
||||
return &Validator{}
|
||||
}
|
||||
|
||||
// Validate will validate the given object. Structs, maps, slices, and arrays
|
||||
// will have each of their fields/items validated, effectively performing a
|
||||
// deep-validation.
|
||||
func (s *Validator) Validate(data interface{}) error {
|
||||
if s.fieldName == nil {
|
||||
s.fieldName = DefaultFieldName
|
||||
}
|
||||
|
||||
if s.fieldJoin == nil {
|
||||
s.fieldJoin = DefaultFieldJoin
|
||||
}
|
||||
|
||||
return s.validate(nil, data)
|
||||
}
|
||||
|
||||
// FieldNameFunc allows setting a custom FieldNameFunc method. It receives a
|
||||
// reflect.StructField, and must return a string for the name of that field. If
|
||||
// the returned string is empty, validation will not run against the field's
|
||||
// value, or any nested data within.
|
||||
func (s *Validator) FieldNameFunc(f FieldNameFunc) {
|
||||
s.fieldName = f
|
||||
}
|
||||
|
||||
// FieldJoinFunc allows setting a custom FieldJoinFunc method. It receives a
|
||||
// string slice of parent fields, and a string of the field name the error is
|
||||
// reported against. All parent paths, must be joined with the current.
|
||||
func (s *Validator) FieldJoinFunc(f FieldJoinFunc) {
|
||||
s.fieldJoin = f
|
||||
}
|
||||
|
||||
func (s *Validator) validate(path []string, data interface{}) error {
|
||||
var errs error
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
d := reflect.ValueOf(data)
|
||||
if d.Kind() == reflect.Ptr {
|
||||
if d.IsNil() {
|
||||
return nil
|
||||
}
|
||||
d = d.Elem()
|
||||
}
|
||||
|
||||
if v, ok := data.(Validatable); ok {
|
||||
verrs := v.Validate()
|
||||
for _, err := range multierr.Errors(verrs) {
|
||||
// Create a new Error for all errors returned by Validate function
|
||||
// to correctly resolve field name, and also field path in relation
|
||||
// to parent objects being validated.
|
||||
newErr := &Error{}
|
||||
|
||||
e := &Error{}
|
||||
if ok := errors.As(err, &e); ok {
|
||||
field := e.Field
|
||||
if field != "" {
|
||||
if sf, ok := d.Type().FieldByName(e.Field); ok {
|
||||
field = s.fieldName(sf)
|
||||
}
|
||||
}
|
||||
newErr.Field = s.fieldJoin(path, field)
|
||||
newErr.Msg = e.Msg
|
||||
newErr.Err = e.Err
|
||||
} else {
|
||||
newErr.Field = s.fieldJoin(path, "")
|
||||
newErr.Err = err
|
||||
}
|
||||
|
||||
errs = multierr.Append(errs, newErr)
|
||||
}
|
||||
}
|
||||
|
||||
switch d.Kind() { //nolint:exhaustive
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < d.Len(); i++ {
|
||||
v := d.Index(i)
|
||||
err := s.validate(append(path, strconv.Itoa(i)), v.Interface())
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range d.MapKeys() {
|
||||
v := d.MapIndex(k)
|
||||
err := s.validate(append(path, fmt.Sprintf("%v", k)), v.Interface())
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < d.NumField(); i++ {
|
||||
v := d.Field(i)
|
||||
fldName := s.fieldName(d.Type().Field(i))
|
||||
if v.CanSet() && fldName != "" {
|
||||
err := s.validate(append(path, fldName), v.Interface())
|
||||
errs = multierr.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// DefaultFieldName is the default FieldNameFunc used by Validator.
|
||||
//
|
||||
// Uses json, yaml, and form field tags to lookup field name first.
|
||||
func DefaultFieldName(fld reflect.StructField) string {
|
||||
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
|
||||
|
||||
if name == "" {
|
||||
name = strings.SplitN(fld.Tag.Get("yaml"), ",", 2)[0]
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
name = strings.SplitN(fld.Tag.Get("form"), ",", 2)[0]
|
||||
}
|
||||
|
||||
if name == "-" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return fld.Name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// DefaultFieldJoin is the default FieldJoinFunc used by Validator.
|
||||
func DefaultFieldJoin(path []string, field string) string {
|
||||
if field != "" {
|
||||
path = append(path, field)
|
||||
}
|
||||
|
||||
return strings.Join(path, ".")
|
||||
}
|
||||
86
validator_test.go
Normal file
86
validator_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//
|
||||
// Test helper types
|
||||
//
|
||||
|
||||
type testStruct struct {
|
||||
Foo string `json:"foo"`
|
||||
|
||||
f func() error
|
||||
}
|
||||
|
||||
func (s *testStruct) Validate() error {
|
||||
if s.f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.f()
|
||||
}
|
||||
|
||||
type testNestedStruct struct {
|
||||
OtherField *testStruct `yaml:"other_field"`
|
||||
}
|
||||
|
||||
type MyStruct struct {
|
||||
Name string
|
||||
Kind string
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
got := New()
|
||||
|
||||
assert.IsType(t, &Validator{}, got)
|
||||
}
|
||||
|
||||
func TestValidator_FieldNameFunc(t *testing.T) {
|
||||
v := New()
|
||||
v.FieldNameFunc(func(sf reflect.StructField) string {
|
||||
return "<" + strings.ToUpper(sf.Name) + ">"
|
||||
})
|
||||
err := v.Validate(&testNestedStruct{
|
||||
OtherField: &testStruct{f: func() error {
|
||||
return &Error{Field: "Foo", Msg: "oops"}
|
||||
}},
|
||||
})
|
||||
|
||||
got := Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, []error{
|
||||
&Error{Field: "<OTHERFIELD>.<FOO>", Msg: "oops"},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestValidator_FieldJoinFunc(t *testing.T) {
|
||||
v := New()
|
||||
v.FieldJoinFunc(func(path []string, field string) string {
|
||||
if field != "" {
|
||||
path = append(path, field)
|
||||
}
|
||||
|
||||
return "[" + strings.Join(path, "][") + "]"
|
||||
})
|
||||
err := v.Validate(&testNestedStruct{
|
||||
OtherField: &testStruct{f: func() error {
|
||||
return &Error{Field: "Foo", Msg: "oops"}
|
||||
}},
|
||||
})
|
||||
|
||||
got := Errors(err)
|
||||
|
||||
assert.ElementsMatch(t, []error{
|
||||
&Error{Field: "[other_field][foo]", Msg: "oops"},
|
||||
}, got)
|
||||
}
|
||||
Reference in New Issue
Block a user