Add safety check for numeric type conversions
Ensure that numeric conversions are lossless by verifying the converted value matches the original string when converted back. This prevents: - Loss of leading zeros (0123 stays as string) - Loss of trailing zeros in floats (1.50 stays as string) - Loss of plus signs (+123 stays as string) - Changes in notation (1e10 stays as string) Only convert to numeric types when the string representation would be preserved exactly. This maintains data integrity while still providing convenient type conversions for clean numeric values. Added comprehensive tests to verify the safety check behavior.
This commit is contained in:
parent
e6db26d2c4
commit
e7fa389634
10
README.md
10
README.md
@ -206,9 +206,17 @@ bool_string: "prefix-${ENV:ENABLED}" # Forces string output
|
||||
For standalone interpolations, smartconfig automatically converts:
|
||||
- `"true"` → `true` (boolean)
|
||||
- `"false"` → `false` (boolean)
|
||||
- Numeric strings → numbers (int or float64)
|
||||
- Numeric strings → numbers (int or float64) with safety checks
|
||||
- Everything else → string
|
||||
|
||||
**Safety Check**: Numbers are only converted if the conversion is lossless. This means:
|
||||
- `"123"` → `123` (converts to int)
|
||||
- `"0123"` → `"0123"` (stays string - leading zeros)
|
||||
- `"123.45"` → `123.45` (converts to float)
|
||||
- `"1.50"` → `"1.50"` (stays string - trailing zeros would be lost)
|
||||
- `"+123"` → `"+123"` (stays string - plus sign would be lost)
|
||||
- `"1e10"` → `"1e10"` (stays string - notation would change)
|
||||
|
||||
## Supported Resolvers
|
||||
|
||||
### Local Resolvers
|
||||
|
@ -605,16 +605,26 @@ func (c *Config) convertToType(s string) interface{} {
|
||||
|
||||
// Try integer
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
return i
|
||||
// Verify the conversion is lossless
|
||||
if strconv.Itoa(i) == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
// Try float
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
// Check if it's actually an integer
|
||||
if float64(int(f)) == f {
|
||||
return int(f)
|
||||
// Verify integer conversion is lossless
|
||||
if strconv.Itoa(int(f)) == s {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
// For floats, use FormatFloat to check if conversion is lossless
|
||||
// Using -1 precision means use the smallest number of digits necessary
|
||||
if strconv.FormatFloat(f, 'f', -1, 64) == s {
|
||||
return f
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Default to string
|
||||
|
@ -1,6 +1,7 @@
|
||||
package smartconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -142,3 +143,115 @@ func TestTypedInterpolation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumericConversionSafety(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected interface{}
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
name: "integer with leading zeros stays string",
|
||||
input: "0123",
|
||||
expected: "0123",
|
||||
desc: "Leading zeros should prevent integer conversion",
|
||||
},
|
||||
{
|
||||
name: "float with trailing zeros",
|
||||
input: "1.50",
|
||||
expected: "1.50",
|
||||
desc: "Trailing zeros should prevent float conversion",
|
||||
},
|
||||
{
|
||||
name: "scientific notation stays string",
|
||||
input: "1e10",
|
||||
expected: "1e10",
|
||||
desc: "Scientific notation should stay as string",
|
||||
},
|
||||
{
|
||||
name: "integer within range converts",
|
||||
input: "12345",
|
||||
expected: 12345,
|
||||
desc: "Normal integers should convert",
|
||||
},
|
||||
{
|
||||
name: "float with no trailing zeros converts",
|
||||
input: "123.45",
|
||||
expected: 123.45,
|
||||
desc: "Normal floats should convert",
|
||||
},
|
||||
{
|
||||
name: "negative integer converts",
|
||||
input: "-123",
|
||||
expected: -123,
|
||||
desc: "Negative integers should convert",
|
||||
},
|
||||
{
|
||||
name: "negative float converts",
|
||||
input: "-123.45",
|
||||
expected: -123.45,
|
||||
desc: "Negative floats should convert",
|
||||
},
|
||||
{
|
||||
name: "very large number stays string",
|
||||
input: "999999999999999999999999999999",
|
||||
expected: "999999999999999999999999999999",
|
||||
desc: "Numbers beyond int range should stay as strings",
|
||||
},
|
||||
{
|
||||
name: "float that looks like int converts to int",
|
||||
input: "123.0",
|
||||
expected: "123.0",
|
||||
desc: "Float notation should be preserved even for whole numbers",
|
||||
},
|
||||
{
|
||||
name: "plus sign prevents conversion",
|
||||
input: "+123",
|
||||
expected: "+123",
|
||||
desc: "Plus sign should prevent conversion",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Set up environment variable
|
||||
varName := "TEST_NUMERIC_" + strings.ToUpper(strings.ReplaceAll(tt.name, " ", "_"))
|
||||
_ = os.Setenv(varName, tt.input)
|
||||
defer func() { _ = os.Unsetenv(varName) }()
|
||||
|
||||
yaml := fmt.Sprintf("value: ${ENV:%s}", varName)
|
||||
config, err := NewFromReader(strings.NewReader(yaml))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse YAML: %v", err)
|
||||
}
|
||||
|
||||
val, exists := config.Get("value")
|
||||
if !exists {
|
||||
t.Fatal("Expected value to exist")
|
||||
}
|
||||
|
||||
// Check type and value
|
||||
switch expected := tt.expected.(type) {
|
||||
case int:
|
||||
if i, ok := val.(int); !ok {
|
||||
t.Errorf("%s: Expected int, got %T: %v", tt.desc, val, val)
|
||||
} else if i != expected {
|
||||
t.Errorf("%s: Expected %d, got %d", tt.desc, expected, i)
|
||||
}
|
||||
case float64:
|
||||
if f, ok := val.(float64); !ok {
|
||||
t.Errorf("%s: Expected float64, got %T: %v", tt.desc, val, val)
|
||||
} else if f != expected {
|
||||
t.Errorf("%s: Expected %f, got %f", tt.desc, expected, f)
|
||||
}
|
||||
case string:
|
||||
if s, ok := val.(string); !ok {
|
||||
t.Errorf("%s: Expected string, got %T: %v", tt.desc, val, val)
|
||||
} else if s != expected {
|
||||
t.Errorf("%s: Expected %q, got %q", tt.desc, expected, s)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user