feat(lagoon): accept string and url rules in lagoon.Validate (quick-261104-f4x)

- string requires a string value (pointers and named string types
  included), url a URL matching Laravel's pattern (urlPattern, shared
  with lagoon.ValidateRequest); both skip an empty or blank value
- lagoon::validate.string and .url in the en and pl catalogs, English
  fallbacks for a nil translator
- settings saves whose model rules use nullable|string|max:255 or
  nullable|url|max:500 no longer fail with an unrecognized-rule error
This commit is contained in:
Jakub Zych
2026-11-04 11:08:06 +01:00
parent 617eaef78e
commit d2ce3a2c3b
7 changed files with 270 additions and 2 deletions

View File

@@ -51,6 +51,8 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
var betweenMin, betweenMax, minArg, maxArg string
hasNumeric := false
hasInteger := false
hasString := false
hasURL := false
for _, tok := range tokens {
name, arg, _ := strings.Cut(tok, ":")
switch name {
@@ -87,6 +89,10 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
}
case "email":
tags = append(tags, "email")
case "string":
hasString = true
case "url":
hasURL = true
case "confirmed":
if fmt.Sprint(val) != fmt.Sprint(values[field+"_confirmation"]) {
return []string{validateMessage(ctx, tr, "confirmed", field, nil)}, nil
@@ -155,6 +161,23 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
if required && isEmptyValue(val) {
return []string{validateMessage(ctx, tr, "required", field, nil)}, nil
}
if hasString || hasURL {
// Like Laravel, string and url skip a blank value; required has
// already handled emptiness.
s, isStr := ruleString(val)
if !isEmptyValue(val) && (!isStr || strings.TrimSpace(s) != "") {
if hasString && !isStr {
return []string{validateMessage(ctx, tr, "string", field, nil)}, nil
}
var msgs []string
if hasURL && (!isStr || !urlPattern.MatchString(s)) {
msgs = append(msgs, validateMessage(ctx, tr, "url", field, nil))
}
if len(msgs) > 0 {
return msgs, nil
}
}
}
usedBetween := betweenMin != "" && betweenMax != ""
if len(tags) > 0 {
msgs := make([]string, 0, len(tags))
@@ -327,6 +350,26 @@ func isIntegerValue(val any) bool {
}
}
// ruleString answers the string a string or url rule checks: it
// dereferences pointers and interfaces (a nil one is no string) and accepts
// any value whose kind is string, named string types included.
func ruleString(val any) (string, bool) {
if val == nil {
return "", false
}
rv := reflect.ValueOf(val)
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
if rv.IsNil() {
return "", false
}
rv = rv.Elem()
}
if rv.Kind() != reflect.String {
return "", false
}
return rv.String(), true
}
func numericString(val any) (string, bool) {
if val == nil {
return "", false
@@ -466,6 +509,10 @@ func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field
return "The " + attr + " must be between " + params["min"] + " and " + params["max"] + "."
case "boolean":
return "The " + attr + " field must be true or false."
case "string":
return "The " + attr + " must be a string."
case "url":
return "The " + attr + " format is invalid."
default:
return "The " + attr + " is invalid."
}