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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lang/lang_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ func (suite *LangTestSuite) TestLoad() {
},
validation: validationLines{
rules: map[string]string{
"override": "rule override",
"required.array": "The :field values are required.",
"override": "rule override",
"required.array": "The :field values are required.",
"messageOverride": "Custom error message: placeholders work: ':field', :min - :max",
},
fields: map[string]string{
"email": "email address",
Expand Down
2 changes: 1 addition & 1 deletion middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ func TestLanguageMiddleware(t *testing.T) {
}

type testValidator struct {
validation.BaseValidator
validateFunc func(c *testValidator, ctx *validation.Context) bool
validation.BaseValidator
}

func (v *testValidator) Validate(ctx *validation.Context) bool {
Expand Down
3 changes: 2 additions & 1 deletion resources/lang/en-US/rules.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"override": "rule override",
"required.array": "The :field values are required."
"required.array": "The :field values are required.",
"messageOverride": "Custom error message: placeholders work: ':field', :min - :max"
}
2 changes: 1 addition & 1 deletion validation/comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (
// - Compare the number of keys in an object with a numeric field
// - Compare a file (or multifile) size with a numeric field. The number of KiB of each file is rounded up (ceil).
type ComparisonValidator struct {
BaseValidator
Path *walk.Path
BaseValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
4 changes: 2 additions & 2 deletions validation/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ func Date(acceptedFormats ...string) *DateValidator {

// DateComparisonValidator factorized date comparison validator for static dates (before, after, etc.)
type DateComparisonValidator struct {
BaseValidator
Date time.Time
BaseValidator
}

func (v *DateComparisonValidator) validate(ctx *Context, comparisonFunc func(time.Time, time.Time) bool) bool {
Expand All @@ -85,8 +85,8 @@ func (v *DateComparisonValidator) MessagePlaceholders(_ *Context) []string {

// DateFieldComparisonValidator factorized date comparison validator for field dates (before field, after field, etc.)
type DateFieldComparisonValidator struct {
BaseValidator
Path *walk.Path
BaseValidator
}

func (v *DateFieldComparisonValidator) validate(ctx *Context, comparisonFunc func(time.Time, time.Time) bool) bool {
Expand Down
2 changes: 1 addition & 1 deletion validation/different.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
// For numbers, make sure the two compared numbers have the same type. A `uint` with value `1` will be considered
// different from an `int` with value `1`.
type DifferentValidator struct {
BaseValidator
Path *walk.Path
BaseValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
2 changes: 1 addition & 1 deletion validation/in.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ func NotIn[T comparable](values []T) *NotInValidator[T] {
// InFieldValidator validates the field under validation must be in at least one
// of the arrays matched by the specified path.
type InFieldValidator[T comparable] struct {
BaseValidator
Path *walk.Path
BaseValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
2 changes: 1 addition & 1 deletion validation/regex.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import "regexp"
// RegexValidator the field under validation must be a string matching
// the specified `*regexp.Regexp`.
type RegexValidator struct {
BaseValidator
Regexp *regexp.Regexp
BaseValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
2 changes: 1 addition & 1 deletion validation/required.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func Required() *RequiredValidator {
// RequiredIfValidator is the same as `RequiredValidator` but only applies the behavior
// described if the specified `Condition` function returns true.
type RequiredIfValidator struct {
RequiredValidator
Condition func(*Context) bool
RequiredValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
20 changes: 20 additions & 0 deletions validation/ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ type Validator interface {
// This is use to generate the validation error message. An empty slice can be returned.
// See `lang.Language.Get()` for more details.
MessagePlaceholders(ctx *Context) []string

overrideMessage(langEntry string)
getMessageOverride() string
}

// BaseValidator composable structure that implements the basic functions required to
// satisfy the `Validator` interface.
type BaseValidator struct {
component
messageOverride string
}

func (v *BaseValidator) init(options *Options) {
Expand All @@ -86,6 +90,22 @@ func (v *BaseValidator) IsType() bool { return false }
// MessagePlaceholders returns an empty slice (no placeholders)
func (v *BaseValidator) MessagePlaceholders(_ *Context) []string { return []string{} }

func (v *BaseValidator) overrideMessage(langEntry string) {
v.messageOverride = langEntry
}

func (v *BaseValidator) getMessageOverride() string {
return v.messageOverride
}

// WithMessage set a custom language entry for the error message of a Validator.
// Original placeholders returned by the validator are still used to render the message.
// Type-dependent and "element" suffixes are not added when the message is overridden.
func WithMessage[V Validator](v V, langEntry string) V {
v.overrideMessage(langEntry)
return v
}

// FieldRulesConverter types implementing this interface define their behavior
// when converting a `FieldRules` to `Rules`. This enables rule sets composition.
type FieldRulesConverter interface {
Expand Down
2 changes: 1 addition & 1 deletion validation/same.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
// For numbers, make sure the two compared numbers have the same type. A `uint` with value `1` will be considered
// different from an `int` with value `1`.
type SameValidator struct {
BaseValidator
Path *walk.Path
BaseValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
2 changes: 1 addition & 1 deletion validation/unique.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ var clickhouseTypes = map[reflect.Type]string{
// UniqueValidator validates the field under validation must have a unique value in database
// according to the provided database scope. Uniqueness is checked using a COUNT query.
type UniqueValidator struct {
BaseValidator
Scope func(db *gorm.DB, val any) *gorm.DB // TODO v6: change val to validation.Context
BaseValidator
}

// Validate checks the field under validation satisfies this validator's criteria.
Expand Down
4 changes: 4 additions & 0 deletions validation/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,10 @@ func (v *validator) processAddedErrors(ctx *Context, parentPath *walk.Path, c *w
}

func (v *validator) getLangEntry(ctx *Context, validator Validator) string {
override := validator.getMessageOverride()
if override != "" {
return override
}
langEntry := "validation.rules." + validator.Name()
if validator.IsTypeDependent() {
typeValidator := v.findTypeValidator(ctx.Field.Validators)
Expand Down
34 changes: 31 additions & 3 deletions validation/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import (
"goyave.dev/goyave/v5/slog"
"goyave.dev/goyave/v5/util/errors"
"goyave.dev/goyave/v5/util/fsutil"
"goyave.dev/goyave/v5/util/fsutil/osfs"
"goyave.dev/goyave/v5/util/walk"
)

type extraKey struct{}

type testValidator struct {
placeholders func(ctx *Context) []string
validateFunc func(c component, ctx *Context) bool
BaseValidator
placeholders func(ctx *Context) []string
validateFunc func(c component, ctx *Context) bool
isType bool
isTypeDependent bool
}
Expand Down Expand Up @@ -1110,9 +1111,9 @@ func TestValidateWithContext(t *testing.T) {
type testCtxKey struct{}

type ctxValidator struct {
BaseValidator
t *testing.T
expect func(*testing.T, context.Context) bool
BaseValidator
}

func (ctxValidator) Name() string {
Expand All @@ -1122,3 +1123,30 @@ func (ctxValidator) Name() string {
func (v ctxValidator) Validate(ctx *Context) bool {
return v.expect(v.t, ctx.Context)
}

func TestValidateMessageOverride(t *testing.T) {
lang := lang.New()
require.NoError(t, lang.Load(osfs.New("."), "en-US", "../resources/lang/en-US"))
opts := &Options{
Data: map[string]any{
"field": "str",
},
Language: lang.GetDefault(),
Rules: RuleSet{
{
Path: "field",
Rules: List{WithMessage(Between(123, 456), "validation.rules.messageOverride")},
},
},
}

validationErrors, errs := Validate(opts)
require.Nil(t, errs)

want := &Errors{
Fields: FieldsErrors{
"field": {Errors: []string{"Custom error message: placeholders work: 'field', 123 - 456"}},
},
}
assert.Equal(t, want, validationErrors)
}
Loading