Skip to content

Commit 3fa5089

Browse files
committed
validate: improve string number parsing
1 parent 1d69c19 commit 3fa5089

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

internal/validate/yaml_schema.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"reflect"
77
"regexp"
8+
"strconv"
89

910
"github.com/saltyorg/sb-go/internal/logging"
1011

@@ -227,6 +228,19 @@ func (s *Schema) validateFieldWithTypeFlexibility(value any, rule *SchemaRule, p
227228
"password": "validate_password_strength",
228229
}
229230

231+
switch rule.Type {
232+
case "number":
233+
logging.DebugBool(verboseMode, "Running built-in number validator for field '%s'", path)
234+
if err := validateNumberValue(value); err != nil {
235+
return fmt.Errorf("field '%s': %w", path, err)
236+
}
237+
case "float":
238+
logging.DebugBool(verboseMode, "Running built-in float validator for field '%s'", path)
239+
if err := validateFloatValue(value); err != nil {
240+
return fmt.Errorf("field '%s': %w", path, err)
241+
}
242+
}
243+
230244
if validatorName, isBuiltIn := builtInValidators[rule.Type]; isBuiltIn {
231245
logging.DebugBool(verboseMode, "Running built-in %s validator for field '%s'", rule.Type, path)
232246
if validator, exists := customValidators[validatorName]; exists {
@@ -447,6 +461,9 @@ func (s *Schema) validateType(value any, rule *SchemaRule, path string) error {
447461
if rule.Type == "number" {
448462
// "number" type accepts strings and integers, but NOT floats (for whole numbers with flexibility)
449463
if valueType == "string" || valueType == "integer" {
464+
if err := validateNumberValue(value); err != nil {
465+
return fmt.Errorf("field '%s': %w", path, err)
466+
}
450467
logging.DebugBool(verboseMode, "validateType - number field accepts string/integer, allowing %s", valueType)
451468
return nil
452469
}
@@ -463,6 +480,9 @@ func (s *Schema) validateType(value any, rule *SchemaRule, path string) error {
463480
if rule.Type == "float" {
464481
// "float" type accepts strings and actual floats, but not integers (to be explicit about decimals)
465482
if valueType == "string" || valueType == "float" {
483+
if err := validateFloatValue(value); err != nil {
484+
return fmt.Errorf("field '%s': %w", path, err)
485+
}
466486
logging.DebugBool(verboseMode, "validateType - float field accepts string/float, allowing %s", valueType)
467487
return nil
468488
}
@@ -661,3 +681,39 @@ func isEmptyValue(value any) bool {
661681

662682
return false
663683
}
684+
685+
func validateNumberValue(value any) error {
686+
switch v := value.(type) {
687+
case string:
688+
if _, err := strconv.Atoi(v); err != nil {
689+
return fmt.Errorf("must be a whole number (integer), got: %s", v)
690+
}
691+
return nil
692+
case int, int8, int16, int32, int64:
693+
return nil
694+
case uint, uint8, uint16, uint32, uint64:
695+
return nil
696+
case float32, float64:
697+
return fmt.Errorf("must be a whole number (integer), got: %v", v)
698+
default:
699+
return fmt.Errorf("must be a whole number (integer), got: %T", value)
700+
}
701+
}
702+
703+
func validateFloatValue(value any) error {
704+
switch v := value.(type) {
705+
case string:
706+
if _, err := strconv.ParseFloat(v, 64); err != nil {
707+
return fmt.Errorf("must be a float, got: %s", v)
708+
}
709+
return nil
710+
case float32, float64:
711+
return nil
712+
case int, int8, int16, int32, int64:
713+
return fmt.Errorf("must be a float, got: %v", v)
714+
case uint, uint8, uint16, uint32, uint64:
715+
return fmt.Errorf("must be a float, got: %v", v)
716+
default:
717+
return fmt.Errorf("must be a float, got: %T", value)
718+
}
719+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package validate
2+
3+
import "testing"
4+
5+
func TestSchemaValidateWithTypeFlexibilityNumber(t *testing.T) {
6+
schema := &Schema{
7+
Rules: map[string]*SchemaRule{
8+
"value": {
9+
Type: "number",
10+
Required: true,
11+
},
12+
},
13+
}
14+
15+
tests := []struct {
16+
name string
17+
value any
18+
wantErr bool
19+
}{
20+
{name: "string number", value: "8080", wantErr: false},
21+
{name: "int number", value: 8080, wantErr: false},
22+
{name: "invalid string", value: "abc", wantErr: true},
23+
{name: "float value", value: 1.5, wantErr: true},
24+
}
25+
26+
for _, tt := range tests {
27+
t.Run(tt.name, func(t *testing.T) {
28+
err := schema.ValidateWithTypeFlexibility(map[string]any{"value": tt.value})
29+
if tt.wantErr && err == nil {
30+
t.Fatalf("expected error for value %v, got none", tt.value)
31+
}
32+
if !tt.wantErr && err != nil {
33+
t.Fatalf("expected no error for value %v, got: %v", tt.value, err)
34+
}
35+
})
36+
}
37+
}
38+
39+
func TestSchemaValidateWithTypeFlexibilityFloat(t *testing.T) {
40+
schema := &Schema{
41+
Rules: map[string]*SchemaRule{
42+
"value": {
43+
Type: "float",
44+
Required: true,
45+
},
46+
},
47+
}
48+
49+
tests := []struct {
50+
name string
51+
value any
52+
wantErr bool
53+
}{
54+
{name: "string float", value: "1.25", wantErr: false},
55+
{name: "float value", value: 1.25, wantErr: false},
56+
{name: "integer value", value: 5, wantErr: true},
57+
{name: "invalid string", value: "abc", wantErr: true},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
err := schema.ValidateWithTypeFlexibility(map[string]any{"value": tt.value})
63+
if tt.wantErr && err == nil {
64+
t.Fatalf("expected error for value %v, got none", tt.value)
65+
}
66+
if !tt.wantErr && err != nil {
67+
t.Fatalf("expected no error for value %v, got: %v", tt.value, err)
68+
}
69+
})
70+
}
71+
}

0 commit comments

Comments
 (0)