Add Filter logic

pull/262/head v9.2.0
Dean Karn 8 years ago
parent 0c0ae405d3
commit 506cc5da56
  1. 102
      README.md
  2. 105
      benchmarks_test.go
  3. 2
      errors.go
  4. 11
      validator.go
  5. 46
      validator_instance.go
  6. 171
      validator_test.go

@ -2,7 +2,7 @@ Package validator
================
<img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v9/logo.png">
[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![Project status](https://img.shields.io/badge/version-9.1.3-green.svg)
![Project status](https://img.shields.io/badge/version-9.2.0-green.svg)
[![Build Status](https://semaphoreci.com/api/v1/joeybloggs/validator/branches/v9/badge.svg)](https://semaphoreci.com/joeybloggs/validator)
[![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=v9&service=github)](https://coveralls.io/github/go-playground/validator?branch=v9)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)
@ -68,54 +68,58 @@ Benchmarks
------
###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 using Go version go1.7 darwin/amd64
```go
BenchmarkFieldSuccess-8 20000000 105 ns/op 0 B/op 0 allocs/op
BenchmarkFieldSuccessParallel-8 50000000 35.1 ns/op 0 B/op 0 allocs/op
BenchmarkFieldFailure-8 5000000 337 ns/op 208 B/op 4 allocs/op
BenchmarkFieldFailureParallel-8 20000000 120 ns/op 208 B/op 4 allocs/op
BenchmarkFieldDiveSuccess-8 2000000 716 ns/op 201 B/op 11 allocs/op
BenchmarkFieldDiveSuccessParallel-8 10000000 253 ns/op 201 B/op 11 allocs/op
BenchmarkFieldDiveFailure-8 1000000 1060 ns/op 412 B/op 16 allocs/op
BenchmarkFieldDiveFailureParallel-8 5000000 360 ns/op 413 B/op 16 allocs/op
BenchmarkFieldCustomTypeSuccess-8 5000000 299 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeSuccessParallel-8 20000000 86.0 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeFailure-8 5000000 341 ns/op 208 B/op 4 allocs/op
BenchmarkFieldCustomTypeFailureParallel-8 20000000 140 ns/op 208 B/op 4 allocs/op
BenchmarkFieldOrTagSuccess-8 2000000 893 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagSuccessParallel-8 5000000 431 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagFailure-8 2000000 563 ns/op 224 B/op 5 allocs/op
BenchmarkFieldOrTagFailureParallel-8 5000000 417 ns/op 224 B/op 5 allocs/op
BenchmarkStructLevelValidationSuccess-8 5000000 339 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationSuccessParallel-8 20000000 114 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationFailure-8 2000000 630 ns/op 304 B/op 8 allocs/op
BenchmarkStructLevelValidationFailureParallel-8 5000000 291 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCustomTypeSuccess-8 3000000 540 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeSuccessParallel-8 10000000 176 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeFailure-8 2000000 821 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleCustomTypeFailureParallel-8 5000000 336 ns/op 440 B/op 10 allocs/op
BenchmarkStructPartialSuccess-8 2000000 686 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialSuccessParallel-8 5000000 282 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialFailure-8 2000000 931 ns/op 480 B/op 11 allocs/op
BenchmarkStructPartialFailureParallel-8 5000000 394 ns/op 480 B/op 11 allocs/op
BenchmarkStructExceptSuccess-8 1000000 1017 ns/op 496 B/op 12 allocs/op
BenchmarkStructExceptSuccessParallel-8 10000000 233 ns/op 240 B/op 5 allocs/op
BenchmarkStructExceptFailure-8 2000000 864 ns/op 464 B/op 10 allocs/op
BenchmarkStructExceptFailureParallel-8 5000000 393 ns/op 464 B/op 10 allocs/op
BenchmarkStructSimpleCrossFieldSuccess-8 3000000 552 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldSuccessParallel-8 10000000 202 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldFailure-8 2000000 798 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossFieldFailureParallel-8 5000000 356 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 2000000 825 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 5000000 300 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailure-8 2000000 1103 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 3000000 433 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleSuccess-8 5000000 360 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleSuccessParallel-8 20000000 110 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleFailure-8 2000000 783 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleFailureParallel-8 5000000 358 ns/op 424 B/op 9 allocs/op
BenchmarkStructComplexSuccess-8 1000000 2120 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexSuccessParallel-8 2000000 659 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexFailure-8 300000 5126 ns/op 3041 B/op 53 allocs/op
BenchmarkStructComplexFailureParallel-8 1000000 2261 ns/op 3041 B/op 53 allocs/op
BenchmarkFieldSuccess-8 20000000 104 ns/op 0 B/op 0 allocs/op
BenchmarkFieldSuccessParallel-8 50000000 34.5 ns/op 0 B/op 0 allocs/op
BenchmarkFieldFailure-8 5000000 335 ns/op 208 B/op 4 allocs/op
BenchmarkFieldFailureParallel-8 20000000 118 ns/op 208 B/op 4 allocs/op
BenchmarkFieldDiveSuccess-8 2000000 718 ns/op 201 B/op 11 allocs/op
BenchmarkFieldDiveSuccessParallel-8 10000000 234 ns/op 201 B/op 11 allocs/op
BenchmarkFieldDiveFailure-8 2000000 971 ns/op 412 B/op 16 allocs/op
BenchmarkFieldDiveFailureParallel-8 5000000 341 ns/op 413 B/op 16 allocs/op
BenchmarkFieldCustomTypeSuccess-8 5000000 268 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeSuccessParallel-8 20000000 82.3 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeFailure-8 5000000 331 ns/op 208 B/op 4 allocs/op
BenchmarkFieldCustomTypeFailureParallel-8 20000000 116 ns/op 208 B/op 4 allocs/op
BenchmarkFieldOrTagSuccess-8 2000000 872 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagSuccessParallel-8 5000000 389 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagFailure-8 3000000 569 ns/op 224 B/op 5 allocs/op
BenchmarkFieldOrTagFailureParallel-8 5000000 397 ns/op 224 B/op 5 allocs/op
BenchmarkStructLevelValidationSuccess-8 5000000 334 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationSuccessParallel-8 20000000 111 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationFailure-8 2000000 622 ns/op 304 B/op 8 allocs/op
BenchmarkStructLevelValidationFailureParallel-8 10000000 274 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCustomTypeSuccess-8 3000000 525 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeSuccessParallel-8 10000000 165 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeFailure-8 2000000 826 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleCustomTypeFailureParallel-8 5000000 378 ns/op 440 B/op 10 allocs/op
BenchmarkStructFilteredSuccess-8 2000000 734 ns/op 288 B/op 9 allocs/op
BenchmarkStructFilteredSuccessParallel-8 5000000 313 ns/op 288 B/op 9 allocs/op
BenchmarkStructFilteredFailure-8 2000000 592 ns/op 256 B/op 7 allocs/op
BenchmarkStructFilteredFailureParallel-8 10000000 272 ns/op 256 B/op 7 allocs/op
BenchmarkStructPartialSuccess-8 2000000 682 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialSuccessParallel-8 10000000 279 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialFailure-8 2000000 938 ns/op 480 B/op 11 allocs/op
BenchmarkStructPartialFailureParallel-8 5000000 398 ns/op 480 B/op 11 allocs/op
BenchmarkStructExceptSuccess-8 1000000 1088 ns/op 496 B/op 12 allocs/op
BenchmarkStructExceptSuccessParallel-8 10000000 257 ns/op 240 B/op 5 allocs/op
BenchmarkStructExceptFailure-8 2000000 897 ns/op 464 B/op 10 allocs/op
BenchmarkStructExceptFailureParallel-8 5000000 394 ns/op 464 B/op 10 allocs/op
BenchmarkStructSimpleCrossFieldSuccess-8 3000000 535 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldSuccessParallel-8 10000000 184 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldFailure-8 2000000 789 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossFieldFailureParallel-8 5000000 386 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 2000000 793 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 5000000 287 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailure-8 1000000 1065 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 3000000 417 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleSuccess-8 5000000 364 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleSuccessParallel-8 20000000 112 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleFailure-8 2000000 785 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleFailureParallel-8 5000000 339 ns/op 424 B/op 9 allocs/op
BenchmarkStructComplexSuccess-8 1000000 2136 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexSuccessParallel-8 2000000 755 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexFailure-8 300000 5248 ns/op 3041 B/op 53 allocs/op
BenchmarkStructComplexFailureParallel-8 1000000 2363 ns/op 3041 B/op 53 allocs/op
```
Complimentary Software

@ -1,6 +1,7 @@
package validator
import (
"bytes"
sql "database/sql/driver"
"testing"
"time"
@ -375,6 +376,110 @@ func BenchmarkStructSimpleCustomTypeFailureParallel(b *testing.B) {
})
}
func BenchmarkStructFilteredSuccess(b *testing.B) {
validate := New()
type Test struct {
Name string `validate:"required"`
NickName string `validate:"required"`
}
test := &Test{
Name: "Joey Bloggs",
}
byts := []byte("Name")
fn := func(ns []byte) bool {
return !bytes.HasSuffix(ns, byts)
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
validate.StructFiltered(test, fn)
}
}
func BenchmarkStructFilteredSuccessParallel(b *testing.B) {
validate := New()
type Test struct {
Name string `validate:"required"`
NickName string `validate:"required"`
}
test := &Test{
Name: "Joey Bloggs",
}
byts := []byte("Name")
fn := func(ns []byte) bool {
return !bytes.HasSuffix(ns, byts)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
validate.StructFiltered(test, fn)
}
})
}
func BenchmarkStructFilteredFailure(b *testing.B) {
validate := New()
type Test struct {
Name string `validate:"required"`
NickName string `validate:"required"`
}
test := &Test{
Name: "Joey Bloggs",
}
byts := []byte("NickName")
fn := func(ns []byte) bool {
return !bytes.HasSuffix(ns, byts)
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
validate.StructFiltered(test, fn)
}
}
func BenchmarkStructFilteredFailureParallel(b *testing.B) {
validate := New()
type Test struct {
Name string `validate:"required"`
NickName string `validate:"required"`
}
test := &Test{
Name: "Joey Bloggs",
}
byts := []byte("NickName")
fn := func(ns []byte) bool {
return !bytes.HasSuffix(ns, byts)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
validate.StructFiltered(test, fn)
}
})
}
func BenchmarkStructPartialSuccess(b *testing.B) {
validate := New()

@ -13,6 +13,7 @@ const (
fieldErrMsg = "Key: '%s' Error:Field validation for '%s' failed on the '%s' tag"
)
// ValidationErrorsTranslations is the translation return type
type ValidationErrorsTranslations map[string]string
// InvalidValidationError describes an invalid argument passed to
@ -55,6 +56,7 @@ func (ve ValidationErrors) Error() string {
return strings.TrimSpace(buff.String())
}
// Translate translates all of the ValidationErrors
func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {
trans := make(ValidationErrorsTranslations)

@ -17,6 +17,8 @@ type validate struct {
hasExcludes bool
includeExclude map[string]struct{} // reset only if StructPartial or StructExcept are called, no need otherwise
ffn FilterFunc
// StructLevel & FieldLevel fields
slflParent reflect.Value
slCurrent reflect.Value
@ -54,12 +56,21 @@ func (v *validate) validateStruct(parent reflect.Value, current reflect.Value, t
if v.isPartial {
if v.ffn != nil {
// used with StructFiltered
if v.ffn(append(structNs, f.name...)) {
continue
}
} else {
// used with StructPartial & StructExcept
_, ok = v.includeExclude[string(append(structNs, f.name...))]
if (ok && v.hasExcludes) || (!ok && !v.hasExcludes) {
continue
}
}
}
v.traverseField(parent, current.Field(f.idx), ns, structNs, f, f.cTags)
}

@ -36,6 +36,12 @@ var (
defaultCField = &cField{namesEqual: true}
)
// FilterFunc is the type used to filter fields using
// StructFiltered(...) function.
// returning true results in the field being filtered/skiped from
// validation
type FilterFunc func(ns []byte) bool
// CustomTypeFunc allows for overriding or adding custom field type handler functions
// field = field value of the type to return a value to be validated
// example Valuer from sql drive see https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29
@ -192,6 +198,7 @@ func (v *Validate) RegisterCustomTypeFunc(fn CustomTypeFunc, types ...interface{
v.hasCustomFuncs = true
}
// RegisterTranslation registers translations against the provided tag.
func (v *Validate) RegisterTranslation(tag string, trans ut.Translator, registerFn RegisterTranslationsFunc, translationFn TranslationFunc) (err error) {
if v.transTagFunc == nil {
@ -248,6 +255,43 @@ func (v *Validate) Struct(s interface{}) (err error) {
return
}
// StructFiltered validates a structs exposed fields, that pass the FilterFunc check and automatically validates
// nested structs, unless otherwise specified.
//
// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise.
// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors.
func (v *Validate) StructFiltered(s interface{}, fn FilterFunc) (err error) {
val := reflect.ValueOf(s)
top := val
if val.Kind() == reflect.Ptr && !val.IsNil() {
val = val.Elem()
}
if val.Kind() != reflect.Struct || val.Type() == timeType {
return &InvalidValidationError{Type: reflect.TypeOf(s)}
}
// good to validate
vd := v.pool.Get().(*validate)
vd.top = top
vd.isPartial = true
vd.ffn = fn
// vd.hasExcludes = false // only need to reset in StructPartial and StructExcept
vd.validateStruct(top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)
if len(vd.errs) > 0 {
err = vd.errs
vd.errs = nil
}
v.pool.Put(vd)
return
}
// StructPartial validates the fields passed in only, ignoring all others.
// Fields may be provided in a namespaced fashion relative to the struct provided
// eg. NestedStruct.Field or NestedArrayField[0].Struct.Name
@ -271,6 +315,7 @@ func (v *Validate) StructPartial(s interface{}, fields ...string) (err error) {
vd := v.pool.Get().(*validate)
vd.top = top
vd.isPartial = true
vd.ffn = nil
vd.hasExcludes = false
vd.includeExclude = make(map[string]struct{})
@ -349,6 +394,7 @@ func (v *Validate) StructExcept(s interface{}, fields ...string) (err error) {
vd := v.pool.Get().(*validate)
vd.top = top
vd.isPartial = true
vd.ffn = nil
vd.hasExcludes = true
vd.includeExclude = make(map[string]struct{})

@ -1,6 +1,7 @@
package validator
import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/json"
@ -6560,3 +6561,173 @@ func TestTranslationErrors(t *testing.T) {
NotEqual(t, err, nil)
Equal(t, err.Error(), "error: conflicting key 'required' rule 'Unknown' with text '{0} is a required field', value being ignored")
}
func TestStructFiltered(t *testing.T) {
p1 := func(ns []byte) bool {
if bytes.HasSuffix(ns, []byte("NoTag")) || bytes.HasSuffix(ns, []byte("Required")) {
return false
}
return true
}
p2 := func(ns []byte) bool {
if bytes.HasSuffix(ns, []byte("SubSlice[0].Test")) ||
bytes.HasSuffix(ns, []byte("SubSlice[0]")) ||
bytes.HasSuffix(ns, []byte("SubSlice")) ||
bytes.HasSuffix(ns, []byte("Sub")) ||
bytes.HasSuffix(ns, []byte("SubIgnore")) ||
bytes.HasSuffix(ns, []byte("Anonymous")) ||
bytes.HasSuffix(ns, []byte("Anonymous.A")) {
return false
}
return true
}
p3 := func(ns []byte) bool {
if bytes.HasSuffix(ns, []byte("SubTest.Test")) {
return false
}
return true
}
// p4 := []string{
// "A",
// }
tPartial := &TestPartial{
NoTag: "NoTag",
Required: "Required",
SubSlice: []*SubTest{
{
Test: "Required",
},
{
Test: "Required",
},
},
Sub: &SubTest{
Test: "1",
},
SubIgnore: &SubTest{
Test: "",
},
Anonymous: struct {
A string `validate:"required"`
ASubSlice []*SubTest `validate:"required,dive"`
SubAnonStruct []struct {
Test string `validate:"required"`
OtherTest string `validate:"required"`
} `validate:"required,dive"`
}{
A: "1",
ASubSlice: []*SubTest{
{
Test: "Required",
},
{
Test: "Required",
},
},
SubAnonStruct: []struct {
Test string `validate:"required"`
OtherTest string `validate:"required"`
}{
{"Required", "RequiredOther"},
{"Required", "RequiredOther"},
},
},
}
validate := New()
// the following should all return no errors as everything is valid in
// the default state
errs := validate.StructFiltered(tPartial, p1)
Equal(t, errs, nil)
errs = validate.StructFiltered(tPartial, p2)
Equal(t, errs, nil)
// this isn't really a robust test, but is ment to illustrate the ANON CASE below
errs = validate.StructFiltered(tPartial.SubSlice[0], p3)
Equal(t, errs, nil)
// mod tParial for required feild and re-test making sure invalid fields are NOT required:
tPartial.Required = ""
// inversion and retesting Partial to generate failures:
errs = validate.StructFiltered(tPartial, p1)
NotEqual(t, errs, nil)
AssertError(t, errs, "TestPartial.Required", "TestPartial.Required", "Required", "Required", "required")
// reset Required field, and set nested struct
tPartial.Required = "Required"
tPartial.Anonymous.A = ""
// will pass as unset feilds is not going to be tested
errs = validate.StructFiltered(tPartial, p1)
Equal(t, errs, nil)
// will fail as unset feild is tested
errs = validate.StructFiltered(tPartial, p2)
NotEqual(t, errs, nil)
AssertError(t, errs, "TestPartial.Anonymous.A", "TestPartial.Anonymous.A", "A", "A", "required")
// reset nested struct and unset struct in slice
tPartial.Anonymous.A = "Required"
tPartial.SubSlice[0].Test = ""
// these will pass as unset item is NOT tested
errs = validate.StructFiltered(tPartial, p1)
Equal(t, errs, nil)
errs = validate.StructFiltered(tPartial, p2)
NotEqual(t, errs, nil)
AssertError(t, errs, "TestPartial.SubSlice[0].Test", "TestPartial.SubSlice[0].Test", "Test", "Test", "required")
Equal(t, len(errs.(ValidationErrors)), 1)
// Unset second slice member concurrently to test dive behavior:
tPartial.SubSlice[1].Test = ""
errs = validate.StructFiltered(tPartial, p1)
Equal(t, errs, nil)
errs = validate.StructFiltered(tPartial, p2)
NotEqual(t, errs, nil)
Equal(t, len(errs.(ValidationErrors)), 1)
AssertError(t, errs, "TestPartial.SubSlice[0].Test", "TestPartial.SubSlice[0].Test", "Test", "Test", "required")
// reset struct in slice, and unset struct in slice in unset posistion
tPartial.SubSlice[0].Test = "Required"
// these will pass as the unset item is NOT tested
errs = validate.StructFiltered(tPartial, p1)
Equal(t, errs, nil)
errs = validate.StructFiltered(tPartial, p2)
Equal(t, errs, nil)
tPartial.SubSlice[1].Test = "Required"
tPartial.Anonymous.SubAnonStruct[0].Test = ""
// these will pass as the unset item is NOT tested
errs = validate.StructFiltered(tPartial, p1)
Equal(t, errs, nil)
errs = validate.StructFiltered(tPartial, p2)
Equal(t, errs, nil)
dt := time.Now()
err := validate.StructFiltered(&dt, func(ns []byte) bool { return true })
NotEqual(t, err, nil)
Equal(t, err.Error(), "validator: (nil *time.Time)")
}

Loading…
Cancel
Save