This change enhances the API to support gjson path syntax for accessing nested configuration values. Users can now use paths like 'server.ssl.enabled' or 'database.replicas.0.host' to directly access nested values without manual navigation. Key changes: - All typed getters now support gjson path syntax - Backward compatibility maintained for top-level keys - Proper error handling for null values and non-existent paths - Special float values (Infinity, NaN) handled correctly - Comprehensive test coverage for edge cases This makes the API much more intuitive and reduces boilerplate code when working with nested configuration structures.
293 lines
6.5 KiB
Go
293 lines
6.5 KiB
Go
package smartconfig
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestGjsonPathEdgeCasesRefactored(t *testing.T) {
|
|
config := loadConfig(t, `
|
|
empty_string: ""
|
|
null_value: null
|
|
zero: 0
|
|
false_bool: false
|
|
empty_map: {}
|
|
empty_array: []
|
|
|
|
special_keys:
|
|
"key.with.dots": "dotted key value"
|
|
"key with spaces": "spaced key value"
|
|
"key[with]brackets": "bracketed key value"
|
|
"123": "numeric key"
|
|
"": "empty key"
|
|
|
|
deeply:
|
|
nested:
|
|
structure:
|
|
with:
|
|
many:
|
|
levels:
|
|
value: "deep value"
|
|
|
|
arrays:
|
|
nested:
|
|
- - - value: "triple nested array"
|
|
mixed:
|
|
- name: "first"
|
|
items:
|
|
- id: 1
|
|
- id: 2
|
|
- name: "second"
|
|
items:
|
|
- id: 3
|
|
- id: 4
|
|
|
|
types:
|
|
very_large_number: 9223372036854775807
|
|
very_small_number: -9223372036854775808
|
|
|
|
special_values:
|
|
inf: .inf
|
|
neg_inf: -.inf
|
|
nan: .nan
|
|
`)
|
|
|
|
t.Run("Empty and null values", func(t *testing.T) {
|
|
// Empty string
|
|
val, err := config.GetString("empty_string")
|
|
assertNoError(t, err)
|
|
assertString(t, val, "")
|
|
|
|
// Zero value
|
|
zero, err := config.GetInt("zero")
|
|
assertNoError(t, err)
|
|
assertInt(t, zero, 0)
|
|
|
|
// False boolean
|
|
falseBool, err := config.GetBool("false_bool")
|
|
assertNoError(t, err)
|
|
assertBool(t, falseBool, false)
|
|
|
|
// Null value should fail for typed getters
|
|
_, err = config.GetString("null_value")
|
|
assertError(t, err)
|
|
|
|
_, err = config.GetInt("null_value")
|
|
assertError(t, err)
|
|
|
|
_, err = config.GetBool("null_value")
|
|
assertError(t, err)
|
|
})
|
|
|
|
t.Run("Empty collections", func(t *testing.T) {
|
|
// Empty map
|
|
emptyMap, exists := config.Get("empty_map")
|
|
assertExists(t, exists)
|
|
m := assertType[map[string]interface{}](t, emptyMap)
|
|
if len(m) != 0 {
|
|
t.Errorf("empty_map should have 0 elements, has %d", len(m))
|
|
}
|
|
|
|
// Empty array
|
|
emptyArray, exists := config.Get("empty_array")
|
|
assertExists(t, exists)
|
|
a := assertType[[]interface{}](t, emptyArray)
|
|
if len(a) != 0 {
|
|
t.Errorf("empty_array should have 0 elements, has %d", len(a))
|
|
}
|
|
})
|
|
|
|
t.Run("Deep nesting", func(t *testing.T) {
|
|
deepValue, err := config.GetString("deeply.nested.structure.with.many.levels.value")
|
|
assertNoError(t, err)
|
|
assertString(t, deepValue, "deep value")
|
|
})
|
|
|
|
t.Run("Complex arrays", func(t *testing.T) {
|
|
// Triple nested array
|
|
val, err := config.GetString("arrays.nested.0.0.0.value")
|
|
assertNoError(t, err)
|
|
assertString(t, val, "triple nested array")
|
|
|
|
// Mixed structure navigation
|
|
id1, err := config.GetInt("arrays.mixed.0.items.0.id")
|
|
assertNoError(t, err)
|
|
assertInt(t, id1, 1)
|
|
|
|
id4, err := config.GetInt("arrays.mixed.1.items.1.id")
|
|
assertNoError(t, err)
|
|
assertInt(t, id4, 4)
|
|
})
|
|
|
|
t.Run("Large numbers", func(t *testing.T) {
|
|
largeNum, err := config.GetInt("types.very_large_number")
|
|
assertNoError(t, err)
|
|
assertInt(t, largeNum, 9223372036854775807)
|
|
|
|
smallNum, err := config.GetInt("types.very_small_number")
|
|
assertNoError(t, err)
|
|
assertInt(t, smallNum, -9223372036854775808)
|
|
})
|
|
|
|
t.Run("Special float values", func(t *testing.T) {
|
|
// These special float values should be converted to strings
|
|
inf, err := config.GetString("special_values.inf")
|
|
assertNoError(t, err)
|
|
assertString(t, inf, "Infinity")
|
|
|
|
negInf, err := config.GetString("special_values.neg_inf")
|
|
assertNoError(t, err)
|
|
assertString(t, negInf, "-Infinity")
|
|
|
|
nan, err := config.GetString("special_values.nan")
|
|
assertNoError(t, err)
|
|
assertString(t, nan, "NaN")
|
|
})
|
|
|
|
t.Run("Non-existent paths", func(t *testing.T) {
|
|
paths := []string{
|
|
"nonexistent",
|
|
"nonexistent.nested.key",
|
|
"deeply.nonexistent",
|
|
"arrays.mixed.999.name",
|
|
"arrays.mixed.-1.name",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
t.Run(path, func(t *testing.T) {
|
|
_, err := config.GetString(path)
|
|
assertError(t, err)
|
|
|
|
_, err = config.GetInt(path)
|
|
assertError(t, err)
|
|
|
|
_, exists := config.Get(path)
|
|
assertNotExists(t, exists)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid path syntax", func(t *testing.T) {
|
|
// These paths have invalid syntax
|
|
paths := []string{
|
|
"", // Empty path
|
|
"...", // Only dots
|
|
"key.", // Trailing dot
|
|
".key", // Leading dot
|
|
"key..value", // Double dots
|
|
}
|
|
|
|
for _, path := range paths {
|
|
t.Run(path, func(t *testing.T) {
|
|
_, err := config.GetString(path)
|
|
assertError(t, err)
|
|
|
|
_, exists := config.Get(path)
|
|
assertNotExists(t, exists)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTypeConversions(t *testing.T) {
|
|
config := loadConfig(t, `
|
|
string_number: "123"
|
|
string_bool: "true"
|
|
string_float: "45.67"
|
|
string_bytes: "1GB"
|
|
not_a_number: "abc"
|
|
not_a_bool: "yes"
|
|
partial_number: "123abc"
|
|
object:
|
|
key: value
|
|
array:
|
|
- item1
|
|
- item2
|
|
`)
|
|
|
|
t.Run("Valid conversions", func(t *testing.T) {
|
|
// String to int
|
|
num, err := config.GetInt("string_number")
|
|
assertNoError(t, err)
|
|
assertInt(t, num, 123)
|
|
|
|
// String to bool
|
|
b, err := config.GetBool("string_bool")
|
|
assertNoError(t, err)
|
|
assertBool(t, b, true)
|
|
|
|
// String to float
|
|
f, err := config.GetFloat("string_float")
|
|
assertNoError(t, err)
|
|
assertFloat(t, f, 45.67)
|
|
|
|
// String to bytes
|
|
bytes, err := config.GetBytes("string_bytes")
|
|
assertNoError(t, err)
|
|
assertUint64(t, bytes, 1000000000) // 1GB in decimal
|
|
})
|
|
|
|
t.Run("Invalid conversions", func(t *testing.T) {
|
|
// Invalid string to int
|
|
_, err := config.GetInt("not_a_number")
|
|
assertError(t, err)
|
|
|
|
// Invalid string to bool
|
|
_, err = config.GetBool("not_a_bool")
|
|
assertError(t, err)
|
|
|
|
// Partial number
|
|
_, err = config.GetInt("partial_number")
|
|
assertError(t, err)
|
|
|
|
// Object as string (should fail)
|
|
_, err = config.GetString("object")
|
|
assertError(t, err)
|
|
|
|
// Array as int (should fail)
|
|
_, err = config.GetInt("array")
|
|
assertError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
config := loadConfig(t, `
|
|
concurrent:
|
|
value1: "test1"
|
|
value2: "test2"
|
|
nested:
|
|
value3: "test3"
|
|
`)
|
|
|
|
// Run multiple goroutines accessing the config
|
|
done := make(chan bool, 10)
|
|
for i := 0; i < 10; i++ {
|
|
go func(id int) {
|
|
for j := 0; j < 100; j++ {
|
|
switch id % 3 {
|
|
case 0:
|
|
val, err := config.GetString("concurrent.value1")
|
|
if err != nil || val != "test1" {
|
|
t.Errorf("concurrent access failed: %v, %v", val, err)
|
|
}
|
|
case 1:
|
|
val, err := config.GetString("concurrent.value2")
|
|
if err != nil || val != "test2" {
|
|
t.Errorf("concurrent access failed: %v, %v", val, err)
|
|
}
|
|
case 2:
|
|
val, err := config.GetString("concurrent.nested.value3")
|
|
if err != nil || val != "test3" {
|
|
t.Errorf("concurrent access failed: %v, %v", val, err)
|
|
}
|
|
}
|
|
}
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
}
|