feat(12-01): add Laravel request validation to lagoon
- lagoon.ValidateRequest ports Laravel 9 request validation: wildcard expansion, implicit-rule stop, bail, sometimes/nullable/blank skipping, size messages split by type and character-counted string lengths - ParseRules, In, CustomRule, UploadedFile and ErrorKeys for rule tables - pl/en lagoon::validation catalogs ported verbatim from WinterCMS - lagoon.Validate answers a numeric range failure with the bound that failed (min, max or numeric between) instead of always max
This commit is contained in:
@@ -146,8 +146,8 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
||||
}
|
||||
return []string{validateMessage(ctx, tr, ruleName, field, nil)}, nil
|
||||
}
|
||||
if !moneyInRange(s, rangeMin, rangeMax) {
|
||||
return []string{validateMessage(ctx, tr, "max", field, map[string]string{"max": rangeMax, "min": rangeMin})}, nil
|
||||
if msg, failed := numericRangeMessage(ctx, tr, field, s, rangeMin, rangeMax, minArg == "" && betweenMin != "", maxArg == "" && betweenMax != ""); failed {
|
||||
return []string{msg}, nil
|
||||
}
|
||||
tags = withoutTag(tags, "numeric")
|
||||
}
|
||||
@@ -202,6 +202,38 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// numericRangeMessage checks a numeric value against its bounds and answers
|
||||
// a failure with the message of the bound that failed: min below the lower
|
||||
// bound, max above the upper, and Laravel's numeric between message when
|
||||
// that bound came from between.
|
||||
func numericRangeMessage(ctx context.Context, tr *phrasebook.Translator, field, val, lo, hi string, loFromBetween, hiFromBetween bool) (string, bool) {
|
||||
r := new(big.Rat)
|
||||
if _, ok := r.SetString(val); !ok {
|
||||
return validateMessage(ctx, tr, "max", field, map[string]string{"max": hi, "min": lo}), true
|
||||
}
|
||||
below, above := false, false
|
||||
if lo != "" {
|
||||
if m, ok := new(big.Rat).SetString(lo); ok && r.Cmp(m) < 0 {
|
||||
below = true
|
||||
}
|
||||
}
|
||||
if hi != "" {
|
||||
if m, ok := new(big.Rat).SetString(hi); ok && r.Cmp(m) > 0 {
|
||||
above = true
|
||||
}
|
||||
}
|
||||
params := map[string]string{"min": lo, "max": hi}
|
||||
switch {
|
||||
case (below && loFromBetween) || (above && hiFromBetween):
|
||||
return validateMessage(ctx, tr, "between.numeric", field, params), true
|
||||
case below:
|
||||
return validateMessage(ctx, tr, "min", field, params), true
|
||||
case above:
|
||||
return validateMessage(ctx, tr, "max", field, params), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func splitRule(rule string) []string {
|
||||
var out []string
|
||||
for _, p := range strings.Split(rule, "|") {
|
||||
@@ -307,33 +339,6 @@ func numericString(val any) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func moneyInRange(val any, min, max string) bool {
|
||||
s, ok := numericString(val)
|
||||
if !ok {
|
||||
s = strings.TrimSpace(fmt.Sprint(val))
|
||||
if s == "" || s == "<nil>" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
r := new(big.Rat)
|
||||
if _, ok := r.SetString(s); !ok {
|
||||
return false
|
||||
}
|
||||
if min != "" {
|
||||
m := new(big.Rat)
|
||||
if _, ok := m.SetString(min); ok && r.Cmp(m) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if max != "" {
|
||||
m := new(big.Rat)
|
||||
if _, ok := m.SetString(max); ok && r.Cmp(m) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func uniqueOK(tx *gorm.DB, model any, table, column string, val any) (bool, error) {
|
||||
if tx == nil {
|
||||
return false, fmt.Errorf("lagoon: unique:%s requires a database handle", table)
|
||||
@@ -405,6 +410,9 @@ func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field
|
||||
}
|
||||
params["attribute"] = laravelAttribute(field)
|
||||
key := "lagoon::validate." + rule
|
||||
if rule == "between.numeric" {
|
||||
key = "lagoon::validation.between.numeric"
|
||||
}
|
||||
if tr != nil {
|
||||
s := tr.Get(ctx, key, params)
|
||||
if s != "" && s != key {
|
||||
@@ -437,6 +445,8 @@ func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field
|
||||
return "The " + attr + " must be a file of the allowed types."
|
||||
case "between":
|
||||
return "The " + attr + " must be between " + params["min"] + " and " + params["max"] + " characters."
|
||||
case "between.numeric":
|
||||
return "The " + attr + " must be between " + params["min"] + " and " + params["max"] + "."
|
||||
case "boolean":
|
||||
return "The " + attr + " field must be true or false."
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user