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.
326 lines
7.7 KiB
Go
326 lines
7.7 KiB
Go
package smartconfig
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGjsonPathSupport(t *testing.T) {
|
|
yamlContent := `
|
|
server:
|
|
host: localhost
|
|
port: 8080
|
|
ssl:
|
|
enabled: true
|
|
cert: /etc/ssl/cert.pem
|
|
|
|
database:
|
|
primary:
|
|
host: db1.example.com
|
|
port: 5432
|
|
credentials:
|
|
username: admin
|
|
password: secret123
|
|
replicas:
|
|
- host: db2.example.com
|
|
port: 5433
|
|
- host: db3.example.com
|
|
port: 5434
|
|
|
|
features:
|
|
new_ui: true
|
|
rate_limiting: false
|
|
max_connections: 100
|
|
timeout_seconds: 30.5
|
|
max_file_size: 10MB
|
|
|
|
metadata:
|
|
version: 1.2.3
|
|
build: 12345
|
|
`
|
|
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
t.Run("GetString with gjson paths", func(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected string
|
|
}{
|
|
{"server.host", "localhost"},
|
|
{"server.ssl.cert", "/etc/ssl/cert.pem"},
|
|
{"database.primary.host", "db1.example.com"},
|
|
{"database.primary.credentials.username", "admin"},
|
|
{"database.replicas.0.host", "db2.example.com"},
|
|
{"database.replicas.1.host", "db3.example.com"},
|
|
{"metadata.version", "1.2.3"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
result, err := config.GetString(tt.path)
|
|
if err != nil {
|
|
t.Errorf("GetString(%q) failed: %v", tt.path, err)
|
|
return
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("GetString(%q) = %q, want %q", tt.path, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetInt with gjson paths", func(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected int
|
|
}{
|
|
{"server.port", 8080},
|
|
{"database.primary.port", 5432},
|
|
{"database.replicas.0.port", 5433},
|
|
{"database.replicas.1.port", 5434},
|
|
{"features.max_connections", 100},
|
|
{"metadata.build", 12345},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
result, err := config.GetInt(tt.path)
|
|
if err != nil {
|
|
t.Errorf("GetInt(%q) failed: %v", tt.path, err)
|
|
return
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("GetInt(%q) = %d, want %d", tt.path, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetBool with gjson paths", func(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected bool
|
|
}{
|
|
{"server.ssl.enabled", true},
|
|
{"features.new_ui", true},
|
|
{"features.rate_limiting", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
result, err := config.GetBool(tt.path)
|
|
if err != nil {
|
|
t.Errorf("GetBool(%q) failed: %v", tt.path, err)
|
|
return
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("GetBool(%q) = %v, want %v", tt.path, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetFloat with gjson paths", func(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected float64
|
|
}{
|
|
{"features.timeout_seconds", 30.5},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
result, err := config.GetFloat(tt.path)
|
|
if err != nil {
|
|
t.Errorf("GetFloat(%q) failed: %v", tt.path, err)
|
|
return
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("GetFloat(%q) = %f, want %f", tt.path, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetBytes with gjson paths", func(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected uint64
|
|
}{
|
|
{"features.max_file_size", 10000000}, // 10MB (decimal, as parsed by humanize)
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
result, err := config.GetBytes(tt.path)
|
|
if err != nil {
|
|
t.Errorf("GetBytes(%q) failed: %v", tt.path, err)
|
|
return
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("GetBytes(%q) = %d, want %d", tt.path, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Get with gjson paths", func(t *testing.T) {
|
|
// Test array access
|
|
value, exists := config.Get("database.replicas")
|
|
if !exists {
|
|
t.Error("Get(database.replicas) should exist")
|
|
return
|
|
}
|
|
replicas, ok := value.([]interface{})
|
|
if !ok {
|
|
t.Errorf("Get(database.replicas) should return []interface{}, got %T", value)
|
|
return
|
|
}
|
|
if len(replicas) != 2 {
|
|
t.Errorf("Expected 2 replicas, got %d", len(replicas))
|
|
}
|
|
|
|
// Test nested object access
|
|
value, exists = config.Get("database.primary.credentials")
|
|
if !exists {
|
|
t.Error("Get(database.primary.credentials) should exist")
|
|
return
|
|
}
|
|
creds, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("Get(database.primary.credentials) should return map[string]interface{}, got %T", value)
|
|
return
|
|
}
|
|
if creds["username"] != "admin" {
|
|
t.Errorf("Expected username=admin, got %v", creds["username"])
|
|
}
|
|
})
|
|
|
|
t.Run("Non-existent paths", func(t *testing.T) {
|
|
_, err := config.GetString("server.nonexistent")
|
|
if err == nil {
|
|
t.Error("GetString(server.nonexistent) should return error")
|
|
}
|
|
|
|
_, err = config.GetInt("database.nonexistent.port")
|
|
if err == nil {
|
|
t.Error("GetInt(database.nonexistent.port) should return error")
|
|
}
|
|
|
|
_, exists := config.Get("features.nonexistent")
|
|
if exists {
|
|
t.Error("Get(features.nonexistent) should not exist")
|
|
}
|
|
})
|
|
|
|
t.Run("Backward compatibility", func(t *testing.T) {
|
|
// Test that top-level keys still work
|
|
yamlContent := `
|
|
app_name: TestApp
|
|
port: 9090
|
|
debug: true
|
|
timeout: 45.5
|
|
max_size: 50MB
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Test direct top-level access
|
|
name, err := config.GetString("app_name")
|
|
if err != nil || name != "TestApp" {
|
|
t.Errorf("GetString(app_name) = %q, %v; want TestApp, nil", name, err)
|
|
}
|
|
|
|
port, err := config.GetInt("port")
|
|
if err != nil || port != 9090 {
|
|
t.Errorf("GetInt(port) = %d, %v; want 9090, nil", port, err)
|
|
}
|
|
|
|
debug, err := config.GetBool("debug")
|
|
if err != nil || !debug {
|
|
t.Errorf("GetBool(debug) = %v, %v; want true, nil", debug, err)
|
|
}
|
|
|
|
timeout, err := config.GetFloat("timeout")
|
|
if err != nil || timeout != 45.5 {
|
|
t.Errorf("GetFloat(timeout) = %f, %v; want 45.5, nil", timeout, err)
|
|
}
|
|
|
|
maxSize, err := config.GetBytes("max_size")
|
|
expectedSize := uint64(50000000) // 50MB (decimal, as parsed by humanize)
|
|
if err != nil || maxSize != expectedSize {
|
|
t.Errorf("GetBytes(max_size) = %d, %v; want %d, nil", maxSize, err, expectedSize)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGjsonAdvancedPaths(t *testing.T) {
|
|
yamlContent := `
|
|
users:
|
|
- name: Alice
|
|
age: 30
|
|
roles:
|
|
- admin
|
|
- developer
|
|
- name: Bob
|
|
age: 25
|
|
roles:
|
|
- developer
|
|
- name: Charlie
|
|
age: 35
|
|
roles:
|
|
- manager
|
|
- admin
|
|
|
|
settings:
|
|
features:
|
|
ui:
|
|
theme: dark
|
|
language: en
|
|
api:
|
|
rate_limit: 1000
|
|
timeout: 30
|
|
`
|
|
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
t.Run("Array element access", func(t *testing.T) {
|
|
// Access specific user
|
|
name, err := config.GetString("users.1.name")
|
|
if err != nil || name != "Bob" {
|
|
t.Errorf("GetString(users.1.name) = %q, %v; want Bob, nil", name, err)
|
|
}
|
|
|
|
age, err := config.GetInt("users.2.age")
|
|
if err != nil || age != 35 {
|
|
t.Errorf("GetInt(users.2.age) = %d, %v; want 35, nil", age, err)
|
|
}
|
|
|
|
// Access role in array
|
|
role, err := config.GetString("users.0.roles.0")
|
|
if err != nil || role != "admin" {
|
|
t.Errorf("GetString(users.0.roles.0) = %q, %v; want admin, nil", role, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Deep nested access", func(t *testing.T) {
|
|
theme, err := config.GetString("settings.features.ui.theme")
|
|
if err != nil || theme != "dark" {
|
|
t.Errorf("GetString(settings.features.ui.theme) = %q, %v; want dark, nil", theme, err)
|
|
}
|
|
|
|
rateLimit, err := config.GetInt("settings.features.api.rate_limit")
|
|
if err != nil || rateLimit != 1000 {
|
|
t.Errorf("GetInt(settings.features.api.rate_limit) = %d, %v; want 1000, nil", rateLimit, err)
|
|
}
|
|
})
|
|
}
|