mirror of
https://github.com/taigrr/jety.git
synced 2026-04-02 11:29:05 -07:00
Compare commits
13 Commits
cd/fix-pre
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ec3d79700 | |||
| a81a2027ae | |||
| ff8a3444f1 | |||
| c8cbb72ed7 | |||
| 8b154b58ba | |||
| dd7e2e3ecb | |||
| 60253426ae | |||
| f338b2c662 | |||
| e5f7cc7fae | |||
| 91b69246fa | |||
| b16df4e1a9 | |||
| 5aadc84d50 | |||
| 7335ecd39c |
21
.github/workflows/codeql-analysis.yml
vendored
21
.github/workflows/codeql-analysis.yml
vendored
@@ -36,31 +36,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
2
.github/workflows/govulncheck.yml
vendored
2
.github/workflows/govulncheck.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Run govulncheck
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- id: govulncheck
|
||||
uses: golang/govulncheck-action@v1
|
||||
with:
|
||||
|
||||
19
.github/workflows/test.yml
vendored
Normal file
19
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.26"
|
||||
- run: go test -race -count=1 ./...
|
||||
- run: go vet ./...
|
||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest && staticcheck ./...
|
||||
69
README.md
69
README.md
@@ -1,5 +1,8 @@
|
||||
# JETY
|
||||
|
||||
[](https://github.com/taigrr/jety/actions/workflows/test.yml)
|
||||
[](https://github.com/taigrr/jety/actions/workflows/govulncheck.yml)
|
||||
|
||||
JSON, ENV, TOML, YAML
|
||||
|
||||
A lightweight Go configuration management library supporting JSON, ENV, TOML, and YAML formats.
|
||||
@@ -12,7 +15,7 @@ Originally built to support [grlx](http://github.com/gogrlx/grlx).
|
||||
go get github.com/taigrr/jety
|
||||
```
|
||||
|
||||
Requires Go 1.25.5 or later.
|
||||
Requires Go 1.26.1 or later.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -36,7 +39,7 @@ func main() {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// Get values (config file > env > default)
|
||||
// Get values (Set > env > config file > default)
|
||||
port := jety.GetInt("port")
|
||||
host := jety.GetString("host")
|
||||
}
|
||||
@@ -50,7 +53,7 @@ func main() {
|
||||
- **Case-insensitive keys**: Keys normalized to lowercase
|
||||
- **Type coercion**: Getters handle type conversion gracefully
|
||||
- **Thread-safe**: Safe for concurrent access
|
||||
- **Config precedence**: config file > environment > defaults
|
||||
- **Config precedence**: Set() > environment > config file > defaults
|
||||
|
||||
## Nested Configuration
|
||||
|
||||
@@ -65,16 +68,25 @@ timeout = "30s"
|
||||
client_id = "abc123"
|
||||
```
|
||||
|
||||
Access nested values using `GetStringMap` and type assertions:
|
||||
Use `Sub()` to get a scoped ConfigManager for a nested section:
|
||||
|
||||
```go
|
||||
cloud := jety.Sub("services")
|
||||
if cloud != nil {
|
||||
inner := cloud.Sub("cloud")
|
||||
if inner != nil {
|
||||
varValue := inner.GetString("var") // "xyz"
|
||||
timeout := inner.GetDuration("timeout") // 30s
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or access nested values directly with `GetStringMap` and type assertions:
|
||||
|
||||
```go
|
||||
services := jety.GetStringMap("services")
|
||||
cloud := services["cloud"].(map[string]any)
|
||||
varValue := cloud["var"].(string) // "xyz"
|
||||
|
||||
// For deeper nesting
|
||||
auth := cloud["auth"].(map[string]any)
|
||||
clientID := auth["client_id"].(string) // "abc123"
|
||||
```
|
||||
|
||||
### Environment Variable Overrides
|
||||
@@ -100,37 +112,7 @@ export MYAPP_PORT=9000
|
||||
export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||
```
|
||||
|
||||
**Note**: Environment variables override defaults but config files take highest precedence.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From v0.x to v1.x
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
1. **`WriteConfig()` now returns `error`**
|
||||
|
||||
```go
|
||||
// Before
|
||||
jety.WriteConfig()
|
||||
|
||||
// After
|
||||
if err := jety.WriteConfig(); err != nil {
|
||||
// handle error
|
||||
}
|
||||
// Or if you want to ignore the error:
|
||||
_ = jety.WriteConfig()
|
||||
```
|
||||
|
||||
2. **Go 1.25.5 minimum required**
|
||||
|
||||
Update your Go version or pin to an older jety release.
|
||||
|
||||
#### Non-Breaking Improvements
|
||||
|
||||
- Getters (`GetBool`, `GetInt`, `GetDuration`) now return zero values instead of panicking on unknown types
|
||||
- Added `int64` support in `GetInt`, `GetIntSlice`, and `GetDuration`
|
||||
- Improved env var parsing (handles values containing `=`)
|
||||
**Note**: Environment variables override both defaults and config file values for registered keys (keys that appear in defaults or the config file).
|
||||
|
||||
## API
|
||||
|
||||
@@ -151,14 +133,21 @@ export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||
| ------------------------ | ------------------------ |
|
||||
| `Set(key, value)` | Set a value |
|
||||
| `SetDefault(key, value)` | Set a default value |
|
||||
| `Delete(key)` | Remove a key |
|
||||
| `Sub(key)` | Get scoped sub-config |
|
||||
| `Get(key)` | Get raw value |
|
||||
| `GetString(key)` | Get as string |
|
||||
| `GetInt(key)` | Get as int |
|
||||
| `GetInt64(key)` | Get as int64 |
|
||||
| `GetFloat64(key)` | Get as float64 |
|
||||
| `GetBool(key)` | Get as bool |
|
||||
| `GetDuration(key)` | Get as time.Duration |
|
||||
| `GetStringSlice(key)` | Get as []string |
|
||||
| `GetIntSlice(key)` | Get as []int |
|
||||
| `GetStringMap(key)` | Get as map[string]string |
|
||||
| `GetStringMap(key)` | Get as map[string]any |
|
||||
| `IsSet(key)` | Check if key has a value |
|
||||
| `AllKeys()` | List all known keys |
|
||||
| `AllSettings()` | Get all values as a map |
|
||||
|
||||
### Environment
|
||||
|
||||
|
||||
44
default.go
44
default.go
@@ -20,6 +20,14 @@ func SetConfigName(name string) {
|
||||
defaultConfigManager.SetConfigName(name)
|
||||
}
|
||||
|
||||
func GetFloat64(key string) float64 {
|
||||
return defaultConfigManager.GetFloat64(key)
|
||||
}
|
||||
|
||||
func GetInt64(key string) int64 {
|
||||
return defaultConfigManager.GetInt64(key)
|
||||
}
|
||||
|
||||
func GetInt(key string) int {
|
||||
return defaultConfigManager.GetInt(key)
|
||||
}
|
||||
@@ -67,3 +75,39 @@ func GetStringMap(key string) map[string]any {
|
||||
func GetStringSlice(key string) []string {
|
||||
return defaultConfigManager.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func Get(key string) any {
|
||||
return defaultConfigManager.Get(key)
|
||||
}
|
||||
|
||||
func SetBool(key string, value bool) {
|
||||
defaultConfigManager.SetBool(key, value)
|
||||
}
|
||||
|
||||
func SetString(key string, value string) {
|
||||
defaultConfigManager.SetString(key, value)
|
||||
}
|
||||
|
||||
func Sub(key string) *ConfigManager {
|
||||
return defaultConfigManager.Sub(key)
|
||||
}
|
||||
|
||||
func SetConfigDir(path string) {
|
||||
defaultConfigManager.SetConfigDir(path)
|
||||
}
|
||||
|
||||
func WithEnvPrefix(prefix string) *ConfigManager {
|
||||
return defaultConfigManager.WithEnvPrefix(prefix)
|
||||
}
|
||||
|
||||
func IsSet(key string) bool {
|
||||
return defaultConfigManager.IsSet(key)
|
||||
}
|
||||
|
||||
func AllKeys() []string {
|
||||
return defaultConfigManager.AllKeys()
|
||||
}
|
||||
|
||||
func AllSettings() map[string]any {
|
||||
return defaultConfigManager.AllSettings()
|
||||
}
|
||||
|
||||
180
getters.go
180
getters.go
@@ -7,15 +7,78 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// resolve looks up a key in combinedConfig, falling back to envConfig.
|
||||
// It supports dot notation (e.g., "services.mas.server") to traverse nested maps.
|
||||
func (c *ConfigManager) resolve(key string) (ConfigMap, bool) {
|
||||
lower := strings.ToLower(key)
|
||||
|
||||
// First, try direct lookup (for top-level keys or keys without dots)
|
||||
if v, ok := c.combinedConfig[lower]; ok {
|
||||
return v, true
|
||||
}
|
||||
if v, ok := c.envConfig[lower]; ok {
|
||||
return v, true
|
||||
}
|
||||
|
||||
// If key contains dots, try traversing nested maps
|
||||
if strings.Contains(lower, ".") {
|
||||
if v, ok := c.resolveNested(lower, c.combinedConfig); ok {
|
||||
return v, true
|
||||
}
|
||||
if v, ok := c.resolveNested(lower, c.envConfig); ok {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigMap{}, false
|
||||
}
|
||||
|
||||
// resolveNested traverses nested maps using dot-separated key paths.
|
||||
func (c *ConfigManager) resolveNested(key string, config map[string]ConfigMap) (ConfigMap, bool) {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) < 2 {
|
||||
return ConfigMap{}, false
|
||||
}
|
||||
|
||||
// Look up the first part in the config
|
||||
firstPart := parts[0]
|
||||
entry, ok := config[firstPart]
|
||||
if !ok {
|
||||
return ConfigMap{}, false
|
||||
}
|
||||
|
||||
// Traverse the remaining parts through nested maps
|
||||
current := entry.Value
|
||||
for i := 1; i < len(parts); i++ {
|
||||
m, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return ConfigMap{}, false
|
||||
}
|
||||
|
||||
// Try case-insensitive lookup in the nested map
|
||||
part := parts[i]
|
||||
found := false
|
||||
for k, v := range m {
|
||||
if strings.EqualFold(k, part) {
|
||||
current = v
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ConfigMap{}, false
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigMap{Key: key, Value: current}, true
|
||||
}
|
||||
|
||||
func (c *ConfigManager) Get(key string) any {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return v.Value
|
||||
}
|
||||
@@ -23,12 +86,9 @@ func (c *ConfigManager) Get(key string) any {
|
||||
func (c *ConfigManager) GetBool(key string) bool {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
val := v.Value
|
||||
switch val := val.(type) {
|
||||
@@ -54,12 +114,9 @@ func (c *ConfigManager) GetBool(key string) bool {
|
||||
func (c *ConfigManager) GetDuration(key string) time.Duration {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
val := v.Value
|
||||
switch val := val.(type) {
|
||||
@@ -89,12 +146,9 @@ func (c *ConfigManager) GetDuration(key string) time.Duration {
|
||||
func (c *ConfigManager) GetString(key string) string {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
switch val := v.Value.(type) {
|
||||
@@ -108,12 +162,9 @@ func (c *ConfigManager) GetString(key string) string {
|
||||
func (c *ConfigManager) GetStringMap(key string) map[string]any {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch val := v.Value.(type) {
|
||||
case map[string]any:
|
||||
@@ -126,12 +177,9 @@ func (c *ConfigManager) GetStringMap(key string) map[string]any {
|
||||
func (c *ConfigManager) GetStringSlice(key string) []string {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch val := v.Value.(type) {
|
||||
case []string:
|
||||
@@ -152,15 +200,70 @@ func (c *ConfigManager) GetStringSlice(key string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigManager) GetFloat64(key string) float64 {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch val := v.Value.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case float32:
|
||||
return float64(val)
|
||||
case int:
|
||||
return float64(val)
|
||||
case int64:
|
||||
return float64(val)
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
case nil:
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigManager) GetInt64(key string) int64 {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch val := v.Value.(type) {
|
||||
case int64:
|
||||
return val
|
||||
case int:
|
||||
return int64(val)
|
||||
case string:
|
||||
i, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
case float32:
|
||||
return int64(val)
|
||||
case float64:
|
||||
return int64(val)
|
||||
case nil:
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigManager) GetInt(key string) int {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
switch val := v.Value.(type) {
|
||||
case int:
|
||||
@@ -187,12 +290,9 @@ func (c *ConfigManager) GetInt(key string) int {
|
||||
func (c *ConfigManager) GetIntSlice(key string) []int {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
v, ok = c.envConfig[strings.ToLower(key)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch val := v.Value.(type) {
|
||||
case []int:
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/taigrr/jety
|
||||
|
||||
go 1.26.0
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
|
||||
225
jety.go
225
jety.go
@@ -22,23 +22,25 @@ const (
|
||||
type (
|
||||
configType string
|
||||
|
||||
// ConfigMap holds a configuration entry with its original key name and value.
|
||||
ConfigMap struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
// ConfigManager manages layered configuration from defaults, files,
|
||||
// environment variables, and programmatic overrides.
|
||||
ConfigManager struct {
|
||||
configName string
|
||||
configPath string
|
||||
configFileUsed string
|
||||
configType configType
|
||||
overrideConfig map[string]ConfigMap
|
||||
fileConfig map[string]ConfigMap
|
||||
defaultConfig map[string]ConfigMap
|
||||
envConfig map[string]ConfigMap
|
||||
combinedConfig map[string]ConfigMap
|
||||
mutex sync.RWMutex
|
||||
explicitDefaults bool
|
||||
configName string
|
||||
configPath string
|
||||
configFileUsed string
|
||||
configType configType
|
||||
overrideConfig map[string]ConfigMap
|
||||
fileConfig map[string]ConfigMap
|
||||
defaultConfig map[string]ConfigMap
|
||||
envConfig map[string]ConfigMap
|
||||
combinedConfig map[string]ConfigMap
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,40 +49,47 @@ var (
|
||||
ErrConfigFileEmpty = errors.New("config file is empty")
|
||||
)
|
||||
|
||||
func NewConfigManager() *ConfigManager {
|
||||
cm := ConfigManager{}
|
||||
cm.envConfig = make(map[string]ConfigMap)
|
||||
cm.overrideConfig = make(map[string]ConfigMap)
|
||||
cm.fileConfig = make(map[string]ConfigMap)
|
||||
cm.defaultConfig = make(map[string]ConfigMap)
|
||||
cm.combinedConfig = make(map[string]ConfigMap)
|
||||
envSet := os.Environ()
|
||||
for _, env := range envSet {
|
||||
// parseEnv reads environment variables, optionally filtering by prefix,
|
||||
// and returns a map keyed by lowercased (and prefix-stripped) variable names.
|
||||
func parseEnv(prefix string) map[string]ConfigMap {
|
||||
result := make(map[string]ConfigMap)
|
||||
for _, env := range os.Environ() {
|
||||
key, value, found := strings.Cut(env, "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
lower := strings.ToLower(key)
|
||||
cm.envConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||
if prefix != "" {
|
||||
stripped, ok := strings.CutPrefix(key, prefix)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = stripped
|
||||
}
|
||||
result[strings.ToLower(key)] = ConfigMap{Key: key, Value: value}
|
||||
}
|
||||
return &cm
|
||||
return result
|
||||
}
|
||||
|
||||
// NewConfigManager creates a new ConfigManager with all environment
|
||||
// variables loaded. Use [ConfigManager.WithEnvPrefix] or
|
||||
// [ConfigManager.SetEnvPrefix] to filter by prefix.
|
||||
func NewConfigManager() *ConfigManager {
|
||||
return &ConfigManager{
|
||||
envConfig: parseEnv(""),
|
||||
overrideConfig: make(map[string]ConfigMap),
|
||||
fileConfig: make(map[string]ConfigMap),
|
||||
defaultConfig: make(map[string]ConfigMap),
|
||||
combinedConfig: make(map[string]ConfigMap),
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnvPrefix filters environment variables to only those starting with
|
||||
// the given prefix, stripping the prefix from key names. Returns the
|
||||
// ConfigManager for chaining.
|
||||
func (c *ConfigManager) WithEnvPrefix(prefix string) *ConfigManager {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
envSet := os.Environ()
|
||||
c.envConfig = make(map[string]ConfigMap)
|
||||
for _, env := range envSet {
|
||||
key, value, found := strings.Cut(env, "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if withoutPrefix, ok := strings.CutPrefix(key, prefix); ok {
|
||||
lower := strings.ToLower(withoutPrefix)
|
||||
c.envConfig[lower] = ConfigMap{Key: withoutPrefix, Value: value}
|
||||
}
|
||||
}
|
||||
c.envConfig = parseEnv(prefix)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -90,10 +99,48 @@ func (c *ConfigManager) ConfigFileUsed() string {
|
||||
return c.configFileUsed
|
||||
}
|
||||
|
||||
func (c *ConfigManager) UseExplicitDefaults(enable bool) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.explicitDefaults = enable
|
||||
// IsSet checks whether a key has been set in any configuration source.
|
||||
func (c *ConfigManager) IsSet(key string) bool {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
lower := strings.ToLower(key)
|
||||
if _, ok := c.combinedConfig[lower]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := c.envConfig[lower]
|
||||
return ok
|
||||
}
|
||||
|
||||
// AllKeys returns all keys from all configuration sources, deduplicated.
|
||||
func (c *ConfigManager) AllKeys() []string {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
seen := make(map[string]struct{})
|
||||
var keys []string
|
||||
for k := range c.combinedConfig {
|
||||
if _, ok := seen[k]; !ok {
|
||||
seen[k] = struct{}{}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
for k := range c.envConfig {
|
||||
if _, ok := seen[k]; !ok {
|
||||
seen[k] = struct{}{}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// AllSettings returns all settings as a flat map of key to value.
|
||||
func (c *ConfigManager) AllSettings() map[string]any {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
result := make(map[string]any, len(c.combinedConfig))
|
||||
for k, v := range c.combinedConfig {
|
||||
result[k] = v.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *ConfigManager) collapse() {
|
||||
@@ -107,8 +154,10 @@ func (c *ConfigManager) collapse() {
|
||||
for k, v := range c.fileConfig {
|
||||
ccm[k] = v
|
||||
}
|
||||
for k := range c.defaultConfig {
|
||||
if v, ok := c.envConfig[k]; ok {
|
||||
for k, v := range c.envConfig {
|
||||
if _, inDefaults := c.defaultConfig[k]; inDefaults {
|
||||
ccm[k] = v
|
||||
} else if _, inFile := c.fileConfig[k]; inFile {
|
||||
ccm[k] = v
|
||||
}
|
||||
}
|
||||
@@ -121,6 +170,9 @@ func (c *ConfigManager) collapse() {
|
||||
func (c *ConfigManager) WriteConfig() error {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
if c.configFileUsed == "" {
|
||||
return errors.New("no config file specified")
|
||||
}
|
||||
flattenedConfig := make(map[string]any)
|
||||
for _, v := range c.combinedConfig {
|
||||
flattenedConfig[v.Key] = v.Value
|
||||
@@ -142,8 +194,10 @@ func (c *ConfigManager) WriteConfig() error {
|
||||
}
|
||||
defer f.Close()
|
||||
enc := yaml.NewEncoder(f)
|
||||
err = enc.Encode(flattenedConfig)
|
||||
return err
|
||||
if err = enc.Encode(flattenedConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return enc.Close()
|
||||
case ConfigTypeJSON:
|
||||
f, err := os.Create(c.configFileUsed)
|
||||
if err != nil {
|
||||
@@ -157,10 +211,10 @@ func (c *ConfigManager) WriteConfig() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigManager) SetConfigType(configType string) error {
|
||||
func (c *ConfigManager) SetConfigType(ct string) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
switch configType {
|
||||
switch ct {
|
||||
case "toml":
|
||||
c.configType = ConfigTypeTOML
|
||||
case "yaml":
|
||||
@@ -168,29 +222,17 @@ func (c *ConfigManager) SetConfigType(configType string) error {
|
||||
case "json":
|
||||
c.configType = ConfigTypeJSON
|
||||
default:
|
||||
return fmt.Errorf("config type %s not supported", configType)
|
||||
return fmt.Errorf("config type %s not supported", ct)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEnvPrefix filters environment variables to only those starting with
|
||||
// the given prefix, stripping the prefix from key names.
|
||||
func (c *ConfigManager) SetEnvPrefix(prefix string) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
// Re-read environment variables, stripping the prefix from matching keys.
|
||||
// This mirrors WithEnvPrefix behavior so that prefixed env vars are
|
||||
// accessible by their unprefixed key name.
|
||||
envSet := os.Environ()
|
||||
c.envConfig = make(map[string]ConfigMap)
|
||||
for _, env := range envSet {
|
||||
key, value, found := strings.Cut(env, "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if withoutPrefix, ok := strings.CutPrefix(key, prefix); ok {
|
||||
lower := strings.ToLower(withoutPrefix)
|
||||
c.envConfig[lower] = ConfigMap{Key: withoutPrefix, Value: value}
|
||||
}
|
||||
}
|
||||
c.envConfig = parseEnv(prefix)
|
||||
}
|
||||
|
||||
func (c *ConfigManager) ReadInConfig() error {
|
||||
@@ -233,39 +275,64 @@ func (c *ConfigManager) ReadInConfig() error {
|
||||
}
|
||||
|
||||
func readFile(filename string, fileType configType) (map[string]any, error) {
|
||||
fileData := make(map[string]any)
|
||||
if d, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return nil, ErrConfigFileNotFound
|
||||
} else if d.Size() == 0 {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrConfigFileNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
return nil, ErrConfigFileEmpty
|
||||
}
|
||||
|
||||
fileData := make(map[string]any)
|
||||
switch fileType {
|
||||
case ConfigTypeTOML:
|
||||
_, err := toml.DecodeFile(filename, &fileData)
|
||||
_, err := toml.NewDecoder(f).Decode(&fileData)
|
||||
return fileData, err
|
||||
case ConfigTypeYAML:
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
d := yaml.NewDecoder(f)
|
||||
err = d.Decode(&fileData)
|
||||
err := yaml.NewDecoder(f).Decode(&fileData)
|
||||
return fileData, err
|
||||
case ConfigTypeJSON:
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
err = json.NewDecoder(f).Decode(&fileData)
|
||||
err := json.NewDecoder(f).Decode(&fileData)
|
||||
return fileData, err
|
||||
default:
|
||||
return nil, fmt.Errorf("config type %s not supported", fileType)
|
||||
}
|
||||
}
|
||||
|
||||
// Sub returns a new ConfigManager rooted at the given key. The key must
|
||||
// refer to a map value (e.g., from a nested TOML/YAML/JSON section).
|
||||
// The returned ConfigManager has the nested map loaded as its default
|
||||
// config, and does not inherit the parent's environment prefix, overrides,
|
||||
// or config file. Returns nil if the key does not exist or is not a map.
|
||||
func (c *ConfigManager) Sub(key string) *ConfigManager {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
v, ok := c.resolve(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
m, ok := v.Value.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
sub := NewConfigManager()
|
||||
for k, val := range m {
|
||||
lower := strings.ToLower(k)
|
||||
sub.defaultConfig[lower] = ConfigMap{Key: k, Value: val}
|
||||
sub.combinedConfig[lower] = ConfigMap{Key: k, Value: val}
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *ConfigManager) SetConfigDir(path string) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
421
jety_test.go
421
jety_test.go
@@ -120,6 +120,78 @@ func TestSetAndGetInt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndGetInt64(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
want int64
|
||||
}{
|
||||
{"int64", int64(9223372036854775807), 9223372036854775807},
|
||||
{"int", 42, 42},
|
||||
{"string", "123456789012345", 123456789012345},
|
||||
{"float64", 99.9, 99},
|
||||
{"float32", float32(50.5), 50},
|
||||
{"invalid string", "not-a-number", 0},
|
||||
{"nil", nil, 0},
|
||||
{"unknown type", struct{}{}, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm.Set("key", tt.value)
|
||||
got := cm.GetInt64("key")
|
||||
if got != tt.want {
|
||||
t.Errorf("GetInt64() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndGetFloat64(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
want float64
|
||||
}{
|
||||
{"float64", 3.14159, 3.14159},
|
||||
{"float32", float32(2.5), 2.5},
|
||||
{"int", 42, 42.0},
|
||||
{"int64", int64(100), 100.0},
|
||||
{"string", "1.618", 1.618},
|
||||
{"invalid string", "not-a-float", 0},
|
||||
{"nil", nil, 0},
|
||||
{"unknown type", struct{}{}, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cm.Set("key", tt.value)
|
||||
got := cm.GetFloat64("key")
|
||||
if got != tt.want {
|
||||
t.Errorf("GetFloat64() = %f, want %f", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFloat64NotSet(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
if got := cm.GetFloat64("nonexistent"); got != 0 {
|
||||
t.Errorf("GetFloat64(nonexistent) = %f, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInt64NotSet(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
if got := cm.GetInt64("nonexistent"); got != 0 {
|
||||
t.Errorf("GetInt64(nonexistent) = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndGetBool(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
|
||||
@@ -760,17 +832,6 @@ func TestPackageLevelFunctions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseExplicitDefaults(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.UseExplicitDefaults(true)
|
||||
|
||||
// Just verify it doesn't panic and the field is set
|
||||
cm.SetDefault("key", "value")
|
||||
if got := cm.GetString("key"); got != "value" {
|
||||
t.Errorf("GetString(key) = %q, want %q", got, "value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetString(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.SetString("name", "test")
|
||||
@@ -1397,3 +1458,341 @@ func TestPrecedenceChain(t *testing.T) {
|
||||
t.Errorf("extra: got %q, want defaultextra (default)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSet(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("exists", "yes")
|
||||
|
||||
if !cm.IsSet("exists") {
|
||||
t.Error("IsSet(exists) = false, want true")
|
||||
}
|
||||
if cm.IsSet("nope") {
|
||||
t.Error("IsSet(nope) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllKeys(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.SetDefault("a", 1)
|
||||
cm.Set("b", 2)
|
||||
|
||||
keys := cm.AllKeys()
|
||||
found := make(map[string]bool)
|
||||
for _, k := range keys {
|
||||
found[k] = true
|
||||
}
|
||||
if !found["a"] || !found["b"] {
|
||||
t.Errorf("AllKeys() = %v, want to contain a and b", keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllSettings(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("port", 8080)
|
||||
cm.Set("host", "localhost")
|
||||
|
||||
settings := cm.AllSettings()
|
||||
if settings["port"] != 8080 || settings["host"] != "localhost" {
|
||||
t.Errorf("AllSettings() = %v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOverridesFileWithoutDefault(t *testing.T) {
|
||||
// Bug fix: env should override file even when no default is set for that key
|
||||
os.Setenv("HOST", "envhost")
|
||||
defer os.Unsetenv("HOST")
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(configFile, []byte("host: filehost"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("yaml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// No SetDefault("host", ...) was called — env should still win
|
||||
if got := cm.GetString("host"); got != "envhost" {
|
||||
t.Errorf("GetString(host) = %q, want envhost (env overrides file even without default)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelIsSet(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("x", 1)
|
||||
if !IsSet("x") {
|
||||
t.Error("IsSet(x) = false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelGet(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("key", "val")
|
||||
if Get("key") != "val" {
|
||||
t.Error("Get(key) failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelGetFloat64(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("rate", 3.14)
|
||||
if got := GetFloat64("rate"); got != 3.14 {
|
||||
t.Errorf("GetFloat64(rate) = %f, want 3.14", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelGetInt64(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("bignum", int64(9223372036854775807))
|
||||
if got := GetInt64("bignum"); got != 9223372036854775807 {
|
||||
t.Errorf("GetInt64(bignum) = %d, want 9223372036854775807", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.toml")
|
||||
if err := os.WriteFile(configFile, []byte(tomlConfig), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("toml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sub := cm.Sub("database")
|
||||
if sub == nil {
|
||||
t.Fatal("Sub(database) returned nil")
|
||||
}
|
||||
|
||||
host := sub.GetString("host")
|
||||
if host != "db.example.com" {
|
||||
t.Errorf("Sub(database).GetString(host) = %q, want %q", host, "db.example.com")
|
||||
}
|
||||
|
||||
port := sub.GetInt("port")
|
||||
if port != 5432 {
|
||||
t.Errorf("Sub(database).GetInt(port) = %d, want 5432", port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubNonExistentKey(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.SetDefault("simple", "value")
|
||||
|
||||
sub := cm.Sub("nonexistent")
|
||||
if sub != nil {
|
||||
t.Error("Sub(nonexistent) should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubNonMapKey(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("name", "plain-string")
|
||||
|
||||
sub := cm.Sub("name")
|
||||
if sub != nil {
|
||||
t.Error("Sub on a non-map key should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubIsIndependent(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("section", map[string]any{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
})
|
||||
|
||||
sub := cm.Sub("section")
|
||||
if sub == nil {
|
||||
t.Fatal("Sub(section) returned nil")
|
||||
}
|
||||
|
||||
// Modifying sub should not affect parent
|
||||
sub.Set("key1", "modified")
|
||||
if sub.GetString("key1") != "modified" {
|
||||
t.Error("sub should reflect the Set")
|
||||
}
|
||||
|
||||
parentSection := cm.Get("section").(map[string]any)
|
||||
if parentSection["key1"] != "val1" {
|
||||
t.Error("modifying sub should not affect parent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelSub(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("db", map[string]any{"host": "localhost", "port": 5432})
|
||||
|
||||
sub := Sub("db")
|
||||
if sub == nil {
|
||||
t.Fatal("package-level Sub returned nil")
|
||||
}
|
||||
if sub.GetString("host") != "localhost" {
|
||||
t.Errorf("Sub(db).GetString(host) = %q, want %q", sub.GetString("host"), "localhost")
|
||||
}
|
||||
if sub.GetInt("port") != 5432 {
|
||||
t.Errorf("Sub(db).GetInt(port) = %d, want 5432", sub.GetInt("port"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDotNotationAccess(t *testing.T) {
|
||||
const tomlWithNested = `
|
||||
port = 8080
|
||||
|
||||
[services.mas]
|
||||
server = "192.168.1.15"
|
||||
user = "central"
|
||||
disabled = false
|
||||
|
||||
[services.salesforce.prod]
|
||||
host = "sf.example.com"
|
||||
port = 443
|
||||
`
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "nested.toml")
|
||||
if err := os.WriteFile(configFile, []byte(tomlWithNested), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("toml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Top-level key should still work
|
||||
if got := cm.GetInt("port"); got != 8080 {
|
||||
t.Errorf("GetInt(port) = %d, want 8080", got)
|
||||
}
|
||||
|
||||
// Dot notation for nested string
|
||||
if got := cm.GetString("services.mas.server"); got != "192.168.1.15" {
|
||||
t.Errorf("GetString(services.mas.server) = %q, want %q", got, "192.168.1.15")
|
||||
}
|
||||
if got := cm.GetString("services.mas.user"); got != "central" {
|
||||
t.Errorf("GetString(services.mas.user) = %q, want %q", got, "central")
|
||||
}
|
||||
|
||||
// Dot notation for nested bool
|
||||
if got := cm.GetBool("services.mas.disabled"); got != false {
|
||||
t.Errorf("GetBool(services.mas.disabled) = %v, want false", got)
|
||||
}
|
||||
|
||||
// Dot notation for nested map
|
||||
masMap := cm.GetStringMap("services.mas")
|
||||
if masMap == nil {
|
||||
t.Fatal("GetStringMap(services.mas) = nil")
|
||||
}
|
||||
if masMap["server"] != "192.168.1.15" {
|
||||
t.Errorf("services.mas map server = %v, want %q", masMap["server"], "192.168.1.15")
|
||||
}
|
||||
|
||||
// Three levels deep
|
||||
if got := cm.GetString("services.salesforce.prod.host"); got != "sf.example.com" {
|
||||
t.Errorf("GetString(services.salesforce.prod.host) = %q, want %q", got, "sf.example.com")
|
||||
}
|
||||
if got := cm.GetInt("services.salesforce.prod.port"); got != 443 {
|
||||
t.Errorf("GetInt(services.salesforce.prod.port) = %d, want 443", got)
|
||||
}
|
||||
|
||||
// GetStringMap at intermediate level
|
||||
sfProdMap := cm.GetStringMap("services.salesforce.prod")
|
||||
if sfProdMap == nil {
|
||||
t.Fatal("GetStringMap(services.salesforce.prod) = nil")
|
||||
}
|
||||
if sfProdMap["host"] != "sf.example.com" {
|
||||
t.Errorf("salesforce.prod map host = %v, want %q", sfProdMap["host"], "sf.example.com")
|
||||
}
|
||||
|
||||
// Nonexistent nested key should return zero value
|
||||
if got := cm.GetString("services.mas.nonexistent"); got != "" {
|
||||
t.Errorf("GetString(services.mas.nonexistent) = %q, want empty", got)
|
||||
}
|
||||
if got := cm.GetStringMap("services.nonexistent"); got != nil {
|
||||
t.Errorf("GetStringMap(services.nonexistent) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDotNotationCaseInsensitive(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("services", map[string]any{
|
||||
"MAS": map[string]any{
|
||||
"Server": "192.168.1.15",
|
||||
},
|
||||
})
|
||||
|
||||
// Should work regardless of case
|
||||
if got := cm.GetString("services.mas.server"); got != "192.168.1.15" {
|
||||
t.Errorf("GetString(services.mas.server) = %q, want %q", got, "192.168.1.15")
|
||||
}
|
||||
if got := cm.GetString("SERVICES.MAS.SERVER"); got != "192.168.1.15" {
|
||||
t.Errorf("GetString(SERVICES.MAS.SERVER) = %q, want %q", got, "192.168.1.15")
|
||||
}
|
||||
if got := cm.GetString("Services.Mas.Server"); got != "192.168.1.15" {
|
||||
t.Errorf("GetString(Services.Mas.Server) = %q, want %q", got, "192.168.1.15")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDotNotationWithAllGetters(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("config", map[string]any{
|
||||
"nested": map[string]any{
|
||||
"string": "hello",
|
||||
"int": 42,
|
||||
"int64": int64(9223372036854775807),
|
||||
"float": 3.14,
|
||||
"bool": true,
|
||||
"dur": "30s",
|
||||
"strings": []string{"a", "b", "c"},
|
||||
"ints": []any{1, 2, 3},
|
||||
"map": map[string]any{"key": "value"},
|
||||
},
|
||||
})
|
||||
|
||||
if got := cm.Get("config.nested.string"); got != "hello" {
|
||||
t.Errorf("Get(config.nested.string) = %v, want %q", got, "hello")
|
||||
}
|
||||
if got := cm.GetString("config.nested.string"); got != "hello" {
|
||||
t.Errorf("GetString(config.nested.string) = %q, want %q", got, "hello")
|
||||
}
|
||||
if got := cm.GetInt("config.nested.int"); got != 42 {
|
||||
t.Errorf("GetInt(config.nested.int) = %d, want 42", got)
|
||||
}
|
||||
if got := cm.GetInt64("config.nested.int64"); got != 9223372036854775807 {
|
||||
t.Errorf("GetInt64(config.nested.int64) = %d, want 9223372036854775807", got)
|
||||
}
|
||||
if got := cm.GetFloat64("config.nested.float"); got != 3.14 {
|
||||
t.Errorf("GetFloat64(config.nested.float) = %f, want 3.14", got)
|
||||
}
|
||||
if got := cm.GetBool("config.nested.bool"); got != true {
|
||||
t.Errorf("GetBool(config.nested.bool) = %v, want true", got)
|
||||
}
|
||||
if got := cm.GetDuration("config.nested.dur"); got != 30*time.Second {
|
||||
t.Errorf("GetDuration(config.nested.dur) = %v, want 30s", got)
|
||||
}
|
||||
if got := cm.GetStringSlice("config.nested.strings"); len(got) != 3 || got[0] != "a" {
|
||||
t.Errorf("GetStringSlice(config.nested.strings) = %v, want [a b c]", got)
|
||||
}
|
||||
if got := cm.GetIntSlice("config.nested.ints"); len(got) != 3 || got[0] != 1 {
|
||||
t.Errorf("GetIntSlice(config.nested.ints) = %v, want [1 2 3]", got)
|
||||
}
|
||||
if got := cm.GetStringMap("config.nested.map"); got == nil || got["key"] != "value" {
|
||||
t.Errorf("GetStringMap(config.nested.map) = %v, want map[key:value]", got)
|
||||
}
|
||||
}
|
||||
|
||||
16
setters.go
16
setters.go
@@ -33,14 +33,14 @@ func (c *ConfigManager) SetDefault(key string, value any) {
|
||||
defer c.mutex.Unlock()
|
||||
lower := strings.ToLower(key)
|
||||
c.defaultConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||
if _, ok := c.overrideConfig[lower]; !ok {
|
||||
if envVal, ok := c.envConfig[lower]; ok {
|
||||
c.overrideConfig[lower] = ConfigMap{Key: key, Value: envVal.Value}
|
||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: envVal.Value}
|
||||
} else {
|
||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||
}
|
||||
// Update combinedConfig respecting precedence: override > env > file > default
|
||||
if v, ok := c.overrideConfig[lower]; ok {
|
||||
c.combinedConfig[lower] = v
|
||||
} else if v, ok := c.envConfig[lower]; ok {
|
||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: v.Value}
|
||||
} else if v, ok := c.fileConfig[lower]; ok {
|
||||
c.combinedConfig[lower] = v
|
||||
} else {
|
||||
c.combinedConfig[lower] = c.overrideConfig[lower]
|
||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user