mirror of
https://github.com/taigrr/jety.git
synced 2026-04-02 03:19:03 -07:00
Compare commits
5 Commits
v0.3.0
...
cd/ci-upda
| Author | SHA1 | Date | |
|---|---|---|---|
| c8cbb72ed7 | |||
| 8b154b58ba | |||
| dd7e2e3ecb | |||
| 60253426ae | |||
| f338b2c662 |
21
.github/workflows/codeql-analysis.yml
vendored
21
.github/workflows/codeql-analysis.yml
vendored
@@ -36,31 +36,20 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ 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
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- 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
|
runs-on: ubuntu-latest
|
||||||
name: Run govulncheck
|
name: Run govulncheck
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- id: govulncheck
|
- id: govulncheck
|
||||||
uses: golang/govulncheck-action@v1
|
uses: golang/govulncheck-action@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -9,14 +9,11 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go-version: ["1.24", "1.26"]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: "1.26"
|
||||||
- run: go test -race -count=1 ./...
|
- run: go test -race -count=1 ./...
|
||||||
- run: go vet ./...
|
- run: go vet ./...
|
||||||
- run: go install honnef.co/go/tools/cmd/staticcheck@latest && staticcheck ./...
|
- run: go install honnef.co/go/tools/cmd/staticcheck@latest && staticcheck ./...
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -1,5 +1,8 @@
|
|||||||
# JETY
|
# JETY
|
||||||
|
|
||||||
|
[](https://github.com/taigrr/jety/actions/workflows/test.yml)
|
||||||
|
[](https://github.com/taigrr/jety/actions/workflows/govulncheck.yml)
|
||||||
|
|
||||||
JSON, ENV, TOML, YAML
|
JSON, ENV, TOML, YAML
|
||||||
|
|
||||||
A lightweight Go configuration management library supporting JSON, ENV, TOML, and YAML formats.
|
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
|
go get github.com/taigrr/jety
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires Go 1.25.5 or later.
|
Requires Go 1.26.1 or later.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ func main() {
|
|||||||
// handle error
|
// handle error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get values (config file > env > default)
|
// Get values (Set > env > config file > default)
|
||||||
port := jety.GetInt("port")
|
port := jety.GetInt("port")
|
||||||
host := jety.GetString("host")
|
host := jety.GetString("host")
|
||||||
}
|
}
|
||||||
@@ -50,7 +53,7 @@ func main() {
|
|||||||
- **Case-insensitive keys**: Keys normalized to lowercase
|
- **Case-insensitive keys**: Keys normalized to lowercase
|
||||||
- **Type coercion**: Getters handle type conversion gracefully
|
- **Type coercion**: Getters handle type conversion gracefully
|
||||||
- **Thread-safe**: Safe for concurrent access
|
- **Thread-safe**: Safe for concurrent access
|
||||||
- **Config precedence**: config file > environment > defaults
|
- **Config precedence**: Set() > environment > config file > defaults
|
||||||
|
|
||||||
## Nested Configuration
|
## Nested Configuration
|
||||||
|
|
||||||
@@ -65,16 +68,25 @@ timeout = "30s"
|
|||||||
client_id = "abc123"
|
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
|
```go
|
||||||
services := jety.GetStringMap("services")
|
services := jety.GetStringMap("services")
|
||||||
cloud := services["cloud"].(map[string]any)
|
cloud := services["cloud"].(map[string]any)
|
||||||
varValue := cloud["var"].(string) // "xyz"
|
varValue := cloud["var"].(string) // "xyz"
|
||||||
|
|
||||||
// For deeper nesting
|
|
||||||
auth := cloud["auth"].(map[string]any)
|
|
||||||
clientID := auth["client_id"].(string) // "abc123"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variable Overrides
|
### Environment Variable Overrides
|
||||||
@@ -100,37 +112,7 @@ export MYAPP_PORT=9000
|
|||||||
export MYAPP_SERVICES_CLOUD_VAR=override_value
|
export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: Environment variables override defaults but config files take highest precedence.
|
**Note**: Environment variables override both defaults and config file values for registered keys (keys that appear in defaults or the config file).
|
||||||
|
|
||||||
## 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 `=`)
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -151,14 +133,21 @@ export MYAPP_SERVICES_CLOUD_VAR=override_value
|
|||||||
| ------------------------ | ------------------------ |
|
| ------------------------ | ------------------------ |
|
||||||
| `Set(key, value)` | Set a value |
|
| `Set(key, value)` | Set a value |
|
||||||
| `SetDefault(key, value)` | Set a default value |
|
| `SetDefault(key, value)` | Set a default value |
|
||||||
|
| `Delete(key)` | Remove a key |
|
||||||
|
| `Sub(key)` | Get scoped sub-config |
|
||||||
| `Get(key)` | Get raw value |
|
| `Get(key)` | Get raw value |
|
||||||
| `GetString(key)` | Get as string |
|
| `GetString(key)` | Get as string |
|
||||||
| `GetInt(key)` | Get as int |
|
| `GetInt(key)` | Get as int |
|
||||||
|
| `GetInt64(key)` | Get as int64 |
|
||||||
|
| `GetFloat64(key)` | Get as float64 |
|
||||||
| `GetBool(key)` | Get as bool |
|
| `GetBool(key)` | Get as bool |
|
||||||
| `GetDuration(key)` | Get as time.Duration |
|
| `GetDuration(key)` | Get as time.Duration |
|
||||||
| `GetStringSlice(key)` | Get as []string |
|
| `GetStringSlice(key)` | Get as []string |
|
||||||
| `GetIntSlice(key)` | Get as []int |
|
| `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
|
### Environment
|
||||||
|
|
||||||
|
|||||||
16
default.go
16
default.go
@@ -20,6 +20,14 @@ func SetConfigName(name string) {
|
|||||||
defaultConfigManager.SetConfigName(name)
|
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 {
|
func GetInt(key string) int {
|
||||||
return defaultConfigManager.GetInt(key)
|
return defaultConfigManager.GetInt(key)
|
||||||
}
|
}
|
||||||
@@ -80,6 +88,14 @@ func SetString(key string, value string) {
|
|||||||
defaultConfigManager.SetString(key, value)
|
defaultConfigManager.SetString(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Delete(key string) {
|
||||||
|
defaultConfigManager.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sub(key string) *ConfigManager {
|
||||||
|
return defaultConfigManager.Sub(key)
|
||||||
|
}
|
||||||
|
|
||||||
func SetConfigDir(path string) {
|
func SetConfigDir(path string) {
|
||||||
defaultConfigManager.SetConfigDir(path)
|
defaultConfigManager.SetConfigDir(path)
|
||||||
}
|
}
|
||||||
|
|||||||
58
getters.go
58
getters.go
@@ -146,6 +146,64 @@ 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 {
|
func (c *ConfigManager) GetInt(key string) int {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/taigrr/jety
|
module github.com/taigrr/jety
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
|
|||||||
25
jety.go
25
jety.go
@@ -308,6 +308,31 @@ func readFile(filename string, fileType configType) (map[string]any, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (c *ConfigManager) SetConfigDir(path string) {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|||||||
255
jety_test.go
255
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) {
|
func TestSetAndGetBool(t *testing.T) {
|
||||||
cm := NewConfigManager()
|
cm := NewConfigManager()
|
||||||
|
|
||||||
@@ -1466,3 +1538,186 @@ func TestPackageLevelGet(t *testing.T) {
|
|||||||
t.Error("Get(key) failed")
|
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 TestDelete(t *testing.T) {
|
||||||
|
cm := NewConfigManager()
|
||||||
|
cm.SetDefault("keep", "yes")
|
||||||
|
cm.SetDefault("remove", "default")
|
||||||
|
cm.Set("remove", "override")
|
||||||
|
|
||||||
|
if !cm.IsSet("remove") {
|
||||||
|
t.Fatal("remove should be set before delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Delete("remove")
|
||||||
|
|
||||||
|
if cm.IsSet("remove") {
|
||||||
|
t.Error("remove should not be set after delete")
|
||||||
|
}
|
||||||
|
if cm.Get("remove") != nil {
|
||||||
|
t.Error("Get(remove) should return nil after delete")
|
||||||
|
}
|
||||||
|
if cm.GetString("keep") != "yes" {
|
||||||
|
t.Error("keep should be unaffected by deleting remove")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteFromAllLayers(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configFile := filepath.Join(dir, "config.toml")
|
||||||
|
if err := os.WriteFile(configFile, []byte(`filekey = "fromfile"`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := NewConfigManager()
|
||||||
|
cm.SetConfigFile(configFile)
|
||||||
|
if err := cm.SetConfigType("toml"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cm.SetDefault("filekey", "default")
|
||||||
|
if err := cm.ReadInConfig(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cm.Set("filekey", "override")
|
||||||
|
|
||||||
|
if cm.GetString("filekey") != "override" {
|
||||||
|
t.Fatal("expected override value before delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Delete("filekey")
|
||||||
|
|
||||||
|
if cm.IsSet("filekey") {
|
||||||
|
t.Error("filekey should not be set after delete from all layers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCaseInsensitive(t *testing.T) {
|
||||||
|
cm := NewConfigManager()
|
||||||
|
cm.Set("MyKey", "value")
|
||||||
|
|
||||||
|
cm.Delete("MYKEY")
|
||||||
|
|
||||||
|
if cm.IsSet("mykey") {
|
||||||
|
t.Error("delete should be case-insensitive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageLevelDelete(t *testing.T) {
|
||||||
|
defaultConfigManager = NewConfigManager()
|
||||||
|
Set("temp", "value")
|
||||||
|
if !IsSet("temp") {
|
||||||
|
t.Fatal("temp should be set")
|
||||||
|
}
|
||||||
|
Delete("temp")
|
||||||
|
if IsSet("temp") {
|
||||||
|
t.Error("temp should not be set after package-level Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
setters.go
13
setters.go
@@ -28,6 +28,19 @@ func (c *ConfigManager) Set(key string, value any) {
|
|||||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete removes a key from all configuration layers (overrides, file,
|
||||||
|
// defaults) and rebuilds the combined configuration. Environment variables
|
||||||
|
// are not affected since they are loaded from the process environment.
|
||||||
|
func (c *ConfigManager) Delete(key string) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
lower := strings.ToLower(key)
|
||||||
|
delete(c.overrideConfig, lower)
|
||||||
|
delete(c.fileConfig, lower)
|
||||||
|
delete(c.defaultConfig, lower)
|
||||||
|
delete(c.combinedConfig, lower)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) SetDefault(key string, value any) {
|
func (c *ConfigManager) SetDefault(key string, value any) {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|||||||
Reference in New Issue
Block a user