feat(lagoon): check min and max as length on every non-numeric field (quick-261104-fx1)
- min and max compare the characters of a string or a number's decimal form and the element count of a slice or map on any field without numeric or integer (Laravel's getSize), with the min.array and max.array messages for lists - blank values, booleans, dates and byte slices are not measured; a malformed bound is an error on every non-numeric field - reverse the 261104-f4x pin, prove it through the cabana settings save sequence, and update the README, docs and the storePassword comment
This commit is contained in:
@@ -119,10 +119,11 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
||||
return nil, fmt.Errorf("lagoon: unrecognized validation rule %q", tok)
|
||||
}
|
||||
}
|
||||
// On a string or url field min and max are a length in characters. A
|
||||
// malformed bound fails loudly whatever the value, so a typo in Rules()
|
||||
// On a field that is not numeric or integer, min and max are a length
|
||||
// (Laravel's getSize): characters of a string or number, items of a list.
|
||||
// A malformed bound fails loudly whatever the value, so a typo in Rules()
|
||||
// cannot disable the limit unnoticed.
|
||||
lengthRange := (hasString || hasURL) && !hasNumeric && !hasInteger
|
||||
lengthRange := !hasNumeric && !hasInteger
|
||||
minLen, maxLen := -1, -1
|
||||
if lengthRange {
|
||||
var err error
|
||||
@@ -148,16 +149,10 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
|
||||
if maxArg != "" {
|
||||
rangeMax = maxArg
|
||||
}
|
||||
} else {
|
||||
if betweenMin != "" {
|
||||
tags = append(tags, "min="+betweenMin, "max="+betweenMax)
|
||||
}
|
||||
if minArg != "" {
|
||||
tags = append(tags, "min="+minArg)
|
||||
}
|
||||
if maxArg != "" {
|
||||
tags = append(tags, "max="+maxArg)
|
||||
}
|
||||
} else if betweenMin != "" {
|
||||
// The tags keep the between length check below running; the tags
|
||||
// loop itself skips min and max.
|
||||
tags = append(tags, "min="+betweenMin, "max="+betweenMax)
|
||||
}
|
||||
if numericRange && (rangeMin != "" || rangeMax != "") {
|
||||
s, ok := numericString(val)
|
||||
@@ -176,31 +171,30 @@ 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 lengthRange && isStr {
|
||||
n := utf8.RuneCountInString(s)
|
||||
// Like Laravel, string, url, min and max 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 lengthRange && (minLen >= 0 || maxLen >= 0) {
|
||||
if n, kind, ok := ruleLength(val); ok {
|
||||
params := map[string]string{"min": minArg, "max": maxArg}
|
||||
if minLen >= 0 && n < minLen {
|
||||
msgs = append(msgs, validateMessage(ctx, tr, "min.string", field, params))
|
||||
msgs = append(msgs, validateMessage(ctx, tr, "min."+kind, field, params))
|
||||
}
|
||||
if maxLen >= 0 && n > maxLen {
|
||||
msgs = append(msgs, validateMessage(ctx, tr, "max.string", field, params))
|
||||
msgs = append(msgs, validateMessage(ctx, tr, "max."+kind, field, params))
|
||||
}
|
||||
}
|
||||
if len(msgs) > 0 {
|
||||
return msgs, nil
|
||||
}
|
||||
}
|
||||
if len(msgs) > 0 {
|
||||
return msgs, nil
|
||||
}
|
||||
}
|
||||
usedBetween := betweenMin != "" && betweenMax != ""
|
||||
@@ -408,6 +402,44 @@ func ruleString(val any) (string, bool) {
|
||||
return rv.String(), true
|
||||
}
|
||||
|
||||
// ruleLength is Laravel's getSize without a numeric rule: the characters
|
||||
// (runes) of a string, a named string type or a number's decimal form, with
|
||||
// kind "string", and the element count of a slice or map, with kind "array".
|
||||
// Pointers and interfaces are dereferenced (a nil one is not measured).
|
||||
// Booleans, structs (time.Time, Date, TimeOfDay), fixed-size arrays and byte
|
||||
// slices are not measured: ok is false.
|
||||
func ruleLength(val any) (n int, kind string, ok bool) {
|
||||
if val == nil {
|
||||
return 0, "", false
|
||||
}
|
||||
rv := reflect.ValueOf(val)
|
||||
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
|
||||
if rv.IsNil() {
|
||||
return 0, "", false
|
||||
}
|
||||
rv = rv.Elem()
|
||||
}
|
||||
switch rv.Kind() {
|
||||
case reflect.String:
|
||||
return utf8.RuneCountInString(rv.String()), "string", true
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return utf8.RuneCountInString(strconv.FormatInt(rv.Int(), 10)), "string", true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return utf8.RuneCountInString(strconv.FormatUint(rv.Uint(), 10)), "string", true
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return utf8.RuneCountInString(strconv.FormatFloat(rv.Float(), 'f', -1, 64)), "string", true
|
||||
case reflect.Slice:
|
||||
if rv.Type().Elem().Kind() == reflect.Uint8 {
|
||||
return 0, "", false
|
||||
}
|
||||
return rv.Len(), "array", true
|
||||
case reflect.Map:
|
||||
return rv.Len(), "array", true
|
||||
default:
|
||||
return 0, "", false
|
||||
}
|
||||
}
|
||||
|
||||
func numericString(val any) (string, bool) {
|
||||
if val == nil {
|
||||
return "", false
|
||||
@@ -509,7 +541,7 @@ func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field
|
||||
params["attribute"] = laravelAttribute(field)
|
||||
key := "lagoon::validate." + rule
|
||||
switch rule {
|
||||
case "between.numeric", "min.string", "max.string":
|
||||
case "between.numeric", "min.string", "max.string", "min.array", "max.array":
|
||||
key = "lagoon::validation." + rule
|
||||
}
|
||||
if tr != nil {
|
||||
@@ -556,6 +588,10 @@ func validateMessage(ctx context.Context, tr *phrasebook.Translator, rule, field
|
||||
return "The " + attr + " must be at least " + params["min"] + " characters."
|
||||
case "max.string":
|
||||
return "The " + attr + " may not be greater than " + params["max"] + " characters."
|
||||
case "min.array":
|
||||
return "The " + attr + " must have at least " + params["min"] + " items."
|
||||
case "max.array":
|
||||
return "The " + attr + " may not have more than " + params["max"] + " items."
|
||||
default:
|
||||
return "The " + attr + " is invalid."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user