fix(05): WR-03 numeric between/min/max without omitempty or length tags

This commit is contained in:
Jakub Zych
2026-09-19 15:33:16 +02:00
parent e54f9d7f10
commit fb12b2237d
2 changed files with 91 additions and 26 deletions

View File

@@ -7,6 +7,7 @@ import (
"math/big"
"reflect"
"regexp"
"strconv"
"strings"
"git.golem15.com/golem15/summercms/phrasebook"
@@ -46,18 +47,19 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
required := false
var tags []string
var uniqueTable string
var moneyMin, moneyMax string
var betweenMin, betweenMax, minArg, maxArg string
hasNumeric := false
hasInteger := false
for _, tok := range tokens {
name, arg, _ := strings.Cut(tok, ":")
switch name {
case "nullable":
nullable = true
tags = append(tags, "omitempty")
case "required":
required = true
tags = append(tags, "required")
case "integer":
hasInteger = true
if !isIntegerValue(val) && !isEmptyValue(val) {
return []string{validateMessage(ctx, tr, "integer", field, nil)}, nil
}
@@ -69,19 +71,11 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
if !ok {
return nil, fmt.Errorf("lagoon: unrecognized validation rule %q", tok)
}
tags = append(tags, "min="+x, "max="+y)
betweenMin, betweenMax = x, y
case "min":
if hasNumeric {
moneyMin = arg
} else {
tags = append(tags, "min="+arg)
}
minArg = arg
case "max":
if hasNumeric {
moneyMax = arg
} else {
tags = append(tags, "max="+arg)
}
maxArg = arg
case "in":
tags = append(tags, oneofTag(strings.Split(arg, ",")))
case "unique":
@@ -95,14 +89,40 @@ func validateField(ctx context.Context, tx *gorm.DB, model any, field, rule stri
if nullable && isEmptyValue(val) && !required {
return nil, nil
}
if hasNumeric && (moneyMin != "" || moneyMax != "") {
s := strings.TrimSpace(fmt.Sprint(val))
r := new(big.Rat)
if _, ok := r.SetString(s); !ok {
return []string{validateMessage(ctx, tr, "numeric", field, nil)}, nil
numericRange := hasNumeric || hasInteger
var rangeMin, rangeMax string
if numericRange {
if betweenMin != "" {
rangeMin, rangeMax = betweenMin, betweenMax
}
if !moneyInRange(val, moneyMin, moneyMax) {
return []string{validateMessage(ctx, tr, "max", field, map[string]string{"max": moneyMax, "min": moneyMin})}, nil
if minArg != "" {
rangeMin = minArg
}
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)
}
}
if numericRange && (rangeMin != "" || rangeMax != "") {
s, ok := numericString(val)
if !ok {
ruleName := "numeric"
if hasInteger {
ruleName = "integer"
}
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
}
tags = withoutTag(tags, "numeric")
}
@@ -204,10 +224,42 @@ func isIntegerValue(val any) bool {
}
}
func numericString(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()
}
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(rv.Int(), 10), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(rv.Uint(), 10), true
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(rv.Float(), 'f', -1, 64), true
case reflect.String:
s := strings.TrimSpace(rv.String())
if s == "" {
return "", false
}
return s, true
default:
return "", false
}
}
func moneyInRange(val any, min, max string) bool {
s := strings.TrimSpace(fmt.Sprint(val))
if s == "" || s == "<nil>" {
return true
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 {