Closed pi-kei closed 8 months ago
I ran into a similar case with mo.Option
1. I want to validate that if a value is Some (present) it isn't an empty string, while also allowing the option to be None (absent).
I found one solution (not sure if it is the best one). If I change ValidateValuer
function to this:
var nilValue *bool
// ValidateValuer implements validator.CustomTypeFunc
func ValidateValuer(field reflect.Value) interface{} {
if valuer, ok := field.Interface().(driver.Valuer); ok {
val, err := valuer.Value()
if err == nil {
if val == nil {
return nilValue
}
return val
}
}
return nil
}
Notice return nilValue
when val == nil
.
In this case omitnil
works as expected.
@pi-kei your solution worked for me, with a slight modification (thanks!!):
func init() {
Validate = validator.New(validator.WithRequiredStructEnabled())
_ = Validate.RegisterValidation("notblank", validators.NotBlank)
Validate.RegisterCustomTypeFunc(OptionValuer,
mo.Option[string]{},
mo.Option[bool]{},
mo.Option[[]byte]{},
mo.Option[[]int]{},
mo.Option[time.Time]{},
)
}
// ...
// OptionValuer implements validator.CustomTypeFunc for samber/mo.Option
func OptionValuer(field reflect.Value) interface{} {
if valuer, ok := field.Interface().(driver.Valuer); ok {
if val, err := valuer.Value(); err == nil {
// return pointer here, so omitnil checks work
// for validator.
return &val
}
}
return nil
}
// later example struct using validation
type ExampleOptionValues struct {
StartTime mo.Option[time.Time] `validate:"omitnil"`
Name mo.Option[string] `validate:"omitnil,notblank"`
Description mo.Option[string] `validate:"omitnil,notblank"`
Tz mo.Option[string] `validate:"omitnil,timezone"`
IntList mo.Option[[]int] `validate:"omitnil,gt=0"`
}
This allows me to skip mo.None
values (via omitnil), while still ensuring notblank works (whereas omitempty was not working with notblank as it would skip a present but empty string mo.Some[string]("")
for Name, Description, etc.
@dropwhile Have you tried to compile with escape analyzer? I'm just wondering if it says that val
has moved to heap in OptionValuer
function from your example.
@pi-kei Yes, it looks like val
in this line does move to the heap:
val, err := valuer.Value()
escape(val escapes to heap)optimizer details
validate.go(51, 11): escflow: flow: ~r0 = &val:
validate.go(51, 11): escflow: from &val (address-of)
validate.go(51, 11): escflow: from &val (interface-converted)
validate.go(51, 4): escflow: from return &val (return)
This is the solution to work with null package
validate.RegisterCustomTypeFunc(validateNullType, null.Bool{}, null.String{}, null.Int{}, null.Float{}, null.Time{})
type NullType interface {
IsZero() bool
driver.Valuer
}
var nilString *string
func validateNullType(field reflect.Value) interface{} {
if nullValue, ok := field.Interface().(NullType); ok {
if nullValue.IsZero() {
return nilString
}
if val, err := nullValue.Value(); err == nil {
return val
}
}
return nil
}
Test cases
func TestValidateNull(t *testing.T) {
type Form struct {
Name null.String `json:"name" validate:"omitnil,required"`
OK null.Bool `json:"ok" validate:"omitnil"`
TagID null.String `json:"tag_id" validate:"omitnil,ulid"`
}
testCases := []struct {
name string
form Form
error bool
}{
{
name: "empty form",
form: Form{},
error: false,
},
{
name: "name is empty",
form: Form{
Name: null.StringFrom(""),
},
error: true,
},
{
name: "name is not empty",
form: Form{
Name: null.StringFrom("name"),
},
error: false,
},
{
name: "tag is empty",
form: Form{
TagID: null.StringFrom(""),
},
error: true,
},
{
name: "tag is not ULID",
form: Form{
TagID: null.StringFrom("tag"),
},
error: true,
},
{
name: "tag is ULID",
form: Form{
TagID: null.NewString("01J95KAAMRW1MNPR39SA3SV99K", true),
},
error: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.error {
assert.Error(t, validate.Struct(tc.form))
} else {
assert.NoError(t, validate.Struct(tc.form))
}
})
}
}
Package version eg. v9, v10:
v10.16.0
Issue, Question or Enhancement:
There's an example for custom field types (e.g. null.String or sql.NullString) here But it doesn't cover the case to allow null value and deny empty string value. So I've tried to implement it by myself but turnes out that it is not possible and I need help.
Code sample, to showcase or reproduce:
We have only two options for conditional validation:
omitempty
andomitnil
.omitempty
is not for our case because it turns off validation on empty string. Here are some tests:TestNullNoOmitnil
proves that we need some conditional validation.TestNull
is failing.omitnil
is not doing it's job.TestZero
proves thatgte
is not allowing zero values.TestValid
proves that valid values are passing validation.