mirror of
https://github.com/taigrr/jety.git
synced 2026-04-02 11:29:05 -07:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ec3d79700 | |||
| a81a2027ae | |||
| ff8a3444f1 | |||
| c8cbb72ed7 | |||
| 8b154b58ba | |||
| dd7e2e3ecb | |||
| 60253426ae | |||
| f338b2c662 | |||
| e5f7cc7fae | |||
| 91b69246fa | |||
| b16df4e1a9 | |||
| 5aadc84d50 | |||
| 7335ecd39c | |||
| 4c8d8960be | |||
| 6852ffbebb | |||
| 9ff1fdc5ee | |||
| 7f2320f204 | |||
| 94b97a5825 | |||
| 21ea264b79 | |||
| c4c05732f5 | |||
|
59b8a9078f
|
|||
|
550537be3b
|
|||
|
95bdc4d109
|
|||
|
64d37d936f
|
|||
|
62dd32f61b
|
|||
|
9c5923bd4e
|
|||
|
5643d4d262
|
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:
|
||||||
|
|||||||
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 ./...
|
||||||
158
README.md
158
README.md
@@ -1,9 +1,161 @@
|
|||||||
# 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
|
||||||
|
|
||||||
This is a package for collapsing multiple configuration stores (env+json, env+yaml, env+toml) and writing them back to a centralized config.
|
A lightweight Go configuration management library supporting JSON, ENV, TOML, and YAML formats.
|
||||||
|
It provides viper-like `AutomaticEnv` functionality with fewer dependencies.
|
||||||
|
Originally built to support [grlx](http://github.com/gogrlx/grlx).
|
||||||
|
|
||||||
It should behave similarly to the AutomaticEnv functionality of viper, but without some of the extra heft of the depedendencies it carries.
|
## Installation
|
||||||
|
|
||||||
The inital purpose of this repo is to support the configuration requirements of [grlx](http://github.com/gogrlx/grlx), but development may continue to expand until more viper use cases and functionality are covered.
|
```bash
|
||||||
|
go get github.com/taigrr/jety
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Go 1.26.1 or later.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/taigrr/jety"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Set defaults
|
||||||
|
jety.SetDefault("port", 8080)
|
||||||
|
jety.SetDefault("host", "localhost")
|
||||||
|
|
||||||
|
// Environment variables are loaded automatically
|
||||||
|
// e.g., PORT=9000 overrides the default
|
||||||
|
|
||||||
|
// Read from config file
|
||||||
|
jety.SetConfigFile("config.toml")
|
||||||
|
jety.SetConfigType("toml")
|
||||||
|
if err := jety.ReadInConfig(); err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get values (Set > env > config file > default)
|
||||||
|
port := jety.GetInt("port")
|
||||||
|
host := jety.GetString("host")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple formats**: JSON, TOML, YAML
|
||||||
|
- **Automatic env loading**: Environment variables loaded on init
|
||||||
|
- **Prefix filtering**: Filter env vars by prefix (e.g., `MYAPP_`)
|
||||||
|
- **Case-insensitive keys**: Keys normalized to lowercase
|
||||||
|
- **Type coercion**: Getters handle type conversion gracefully
|
||||||
|
- **Thread-safe**: Safe for concurrent access
|
||||||
|
- **Config precedence**: Set() > environment > config file > defaults
|
||||||
|
|
||||||
|
## Nested Configuration
|
||||||
|
|
||||||
|
For nested config structures like:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[services.cloud]
|
||||||
|
var = "xyz"
|
||||||
|
timeout = "30s"
|
||||||
|
|
||||||
|
[services.cloud.auth]
|
||||||
|
client_id = "abc123"
|
||||||
|
```
|
||||||
|
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variable Overrides
|
||||||
|
|
||||||
|
Environment variables use uppercase keys. For nested config, the env var name is the key in uppercase:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Override top-level key
|
||||||
|
export PORT=9000
|
||||||
|
|
||||||
|
# For nested keys, use the full key name in uppercase
|
||||||
|
export SERVICES_CLOUD_VAR=override_value
|
||||||
|
```
|
||||||
|
|
||||||
|
With a prefix:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cm := jety.NewConfigManager().WithEnvPrefix("MYAPP_")
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MYAPP_PORT=9000
|
||||||
|
export MYAPP_SERVICES_CLOUD_VAR=override_value
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Environment variables override both defaults and config file values for registered keys (keys that appear in defaults or the config file).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
| --------------------- | --------------------------------------------- |
|
||||||
|
| `SetConfigFile(path)` | Set config file path |
|
||||||
|
| `SetConfigDir(dir)` | Set config directory |
|
||||||
|
| `SetConfigName(name)` | Set config file name (without extension) |
|
||||||
|
| `SetConfigType(type)` | Set config type: `"toml"`, `"yaml"`, `"json"` |
|
||||||
|
| `ReadInConfig()` | Read config file |
|
||||||
|
| `WriteConfig()` | Write config to file |
|
||||||
|
|
||||||
|
### Values
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
| ------------------------ | ------------------------ |
|
||||||
|
| `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]any |
|
||||||
|
| `IsSet(key)` | Check if key has a value |
|
||||||
|
| `AllKeys()` | List all known keys |
|
||||||
|
| `AllSettings()` | Get all values as a map |
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
| ----------------------- | --------------------------------------------------- |
|
||||||
|
| `WithEnvPrefix(prefix)` | Filter env vars by prefix (strips prefix from keys) |
|
||||||
|
| `SetEnvPrefix(prefix)` | Set prefix for env var lookups |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [LICENSE](LICENSE) file.
|
||||||
|
|||||||
50
default.go
50
default.go
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package jety
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -40,8 +48,8 @@ func Set(key string, value any) {
|
|||||||
defaultConfigManager.Set(key, value)
|
defaultConfigManager.Set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteConfig() {
|
func WriteConfig() error {
|
||||||
defaultConfigManager.WriteConfig()
|
return defaultConfigManager.WriteConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigFileUsed() string {
|
func ConfigFileUsed() string {
|
||||||
@@ -67,3 +75,39 @@ func GetStringMap(key string) map[string]any {
|
|||||||
func GetStringSlice(key string) []string {
|
func GetStringSlice(key string) []string {
|
||||||
return defaultConfigManager.GetStringSlice(key)
|
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()
|
||||||
|
}
|
||||||
|
|||||||
31
doc.go
Normal file
31
doc.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Package jety provides configuration management supporting JSON, ENV, TOML, and YAML formats.
|
||||||
|
//
|
||||||
|
// It offers viper-like AutomaticEnv functionality with minimal dependencies, allowing
|
||||||
|
// configuration to be loaded from files and environment variables with automatic merging.
|
||||||
|
//
|
||||||
|
// Configuration sources are layered with the following precedence (highest to lowest):
|
||||||
|
// - Values set via Set() or SetString()/SetBool()
|
||||||
|
// - Environment variables (optionally filtered by prefix)
|
||||||
|
// - Values from config file via ReadInConfig()
|
||||||
|
// - Default values set via SetDefault()
|
||||||
|
//
|
||||||
|
// Basic usage:
|
||||||
|
//
|
||||||
|
// jety.SetConfigFile("/etc/myapp/config.yaml")
|
||||||
|
// jety.SetConfigType("yaml")
|
||||||
|
// jety.SetEnvPrefix("MYAPP_")
|
||||||
|
// jety.SetDefault("port", 8080)
|
||||||
|
//
|
||||||
|
// if err := jety.ReadInConfig(); err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// port := jety.GetInt("port")
|
||||||
|
//
|
||||||
|
// For multiple independent configurations, create separate ConfigManager instances:
|
||||||
|
//
|
||||||
|
// cm := jety.NewConfigManager()
|
||||||
|
// cm.SetConfigFile("/etc/myapp/config.toml")
|
||||||
|
// cm.SetConfigType("toml")
|
||||||
|
// cm.ReadInConfig()
|
||||||
|
package jety
|
||||||
253
getters.go
253
getters.go
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package jety
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -7,15 +7,78 @@ import (
|
|||||||
"time"
|
"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 {
|
func (c *ConfigManager) Get(key string) any {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return v.Value
|
return v.Value
|
||||||
}
|
}
|
||||||
@@ -23,53 +86,37 @@ func (c *ConfigManager) Get(key string) any {
|
|||||||
func (c *ConfigManager) GetBool(key string) bool {
|
func (c *ConfigManager) GetBool(key string) bool {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return false
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val := v.Value
|
val := v.Value
|
||||||
switch val := val.(type) {
|
switch val := val.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
return val
|
return val
|
||||||
case string:
|
case string:
|
||||||
if strings.ToLower(val) == "true" {
|
return strings.EqualFold(val, "true")
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
case int:
|
case int:
|
||||||
if val == 0 {
|
return val != 0
|
||||||
return false
|
case float32:
|
||||||
}
|
return val != 0
|
||||||
return true
|
case float64:
|
||||||
case float32, float64:
|
return val != 0
|
||||||
if val == 0 {
|
case time.Duration:
|
||||||
return false
|
return val > 0
|
||||||
}
|
|
||||||
return true
|
|
||||||
case nil:
|
case nil:
|
||||||
return false
|
return false
|
||||||
case time.Duration:
|
|
||||||
if val == 0 || val < 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
default:
|
default:
|
||||||
return val.(bool)
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) GetDuration(key string) time.Duration {
|
func (c *ConfigManager) GetDuration(key string) time.Duration {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return 0
|
||||||
if !ok {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val := v.Value
|
val := v.Value
|
||||||
switch val := val.(type) {
|
switch val := val.(type) {
|
||||||
@@ -83,6 +130,8 @@ func (c *ConfigManager) GetDuration(key string) time.Duration {
|
|||||||
return d
|
return d
|
||||||
case int:
|
case int:
|
||||||
return time.Duration(val)
|
return time.Duration(val)
|
||||||
|
case int64:
|
||||||
|
return time.Duration(val)
|
||||||
case float32:
|
case float32:
|
||||||
return time.Duration(val)
|
return time.Duration(val)
|
||||||
case float64:
|
case float64:
|
||||||
@@ -90,20 +139,16 @@ func (c *ConfigManager) GetDuration(key string) time.Duration {
|
|||||||
case nil:
|
case nil:
|
||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
return val.(time.Duration)
|
return 0
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) GetString(key string) string {
|
func (c *ConfigManager) GetString(key string) string {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return ""
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
@@ -117,12 +162,9 @@ func (c *ConfigManager) GetString(key string) string {
|
|||||||
func (c *ConfigManager) GetStringMap(key string) map[string]any {
|
func (c *ConfigManager) GetStringMap(key string) map[string]any {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
@@ -135,34 +177,99 @@ func (c *ConfigManager) GetStringMap(key string) map[string]any {
|
|||||||
func (c *ConfigManager) GetStringSlice(key string) []string {
|
func (c *ConfigManager) GetStringSlice(key string) []string {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case []string:
|
case []string:
|
||||||
return val
|
return val
|
||||||
|
case []any:
|
||||||
|
var ret []string
|
||||||
|
for _, v := range val {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case string:
|
||||||
|
ret = append(ret, v)
|
||||||
|
default:
|
||||||
|
ret = append(ret, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return 0
|
||||||
if !ok {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case int:
|
case int:
|
||||||
return val
|
return val
|
||||||
|
case int64:
|
||||||
|
return int(val)
|
||||||
case string:
|
case string:
|
||||||
i, err := strconv.Atoi(val)
|
i, err := strconv.Atoi(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,16 +290,38 @@ func (c *ConfigManager) GetInt(key string) int {
|
|||||||
func (c *ConfigManager) GetIntSlice(key string) []int {
|
func (c *ConfigManager) GetIntSlice(key string) []int {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
v, ok := c.combinedConfig[strings.ToLower(key)]
|
v, ok := c.resolve(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
v, ok = c.envConfig[strings.ToLower(c.envPrefix+key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case []int:
|
case []int:
|
||||||
return val
|
return val
|
||||||
|
case []any:
|
||||||
|
var ret []int
|
||||||
|
for _, v := range val {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case int:
|
||||||
|
ret = append(ret, v)
|
||||||
|
case int64:
|
||||||
|
ret = append(ret, int(v))
|
||||||
|
case string:
|
||||||
|
i, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, i)
|
||||||
|
case float32:
|
||||||
|
ret = append(ret, int(v))
|
||||||
|
case float64:
|
||||||
|
ret = append(ret, int(v))
|
||||||
|
case nil:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -1,8 +1,8 @@
|
|||||||
module github.com/taigrr/jety
|
module github.com/taigrr/jety
|
||||||
|
|
||||||
go 1.21.3
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.6.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,5 +1,5 @@
|
|||||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
268
jety.go
268
jety.go
@@ -1,10 +1,11 @@
|
|||||||
package config
|
package jety
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -21,58 +22,74 @@ const (
|
|||||||
type (
|
type (
|
||||||
configType string
|
configType string
|
||||||
|
|
||||||
|
// ConfigMap holds a configuration entry with its original key name and value.
|
||||||
ConfigMap struct {
|
ConfigMap struct {
|
||||||
Key string
|
Key string
|
||||||
Value any
|
Value any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigManager manages layered configuration from defaults, files,
|
||||||
|
// environment variables, and programmatic overrides.
|
||||||
ConfigManager struct {
|
ConfigManager struct {
|
||||||
configName string
|
configName string
|
||||||
configPath string
|
configPath string
|
||||||
configFileUsed string
|
configFileUsed string
|
||||||
configType configType
|
configType configType
|
||||||
envPrefix string
|
overrideConfig map[string]ConfigMap
|
||||||
mapConfig map[string]ConfigMap
|
fileConfig map[string]ConfigMap
|
||||||
defaultConfig map[string]ConfigMap
|
defaultConfig map[string]ConfigMap
|
||||||
envConfig map[string]ConfigMap
|
envConfig map[string]ConfigMap
|
||||||
combinedConfig map[string]ConfigMap
|
combinedConfig map[string]ConfigMap
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
explicitDefaults bool
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrConfigFileNotFound = errors.New("config File Not Found")
|
var (
|
||||||
|
ErrConfigFileNotFound = errors.New("config file not found")
|
||||||
|
ErrConfigFileEmpty = errors.New("config file is empty")
|
||||||
|
)
|
||||||
|
|
||||||
func NewConfigManager() *ConfigManager {
|
// parseEnv reads environment variables, optionally filtering by prefix,
|
||||||
cm := ConfigManager{}
|
// and returns a map keyed by lowercased (and prefix-stripped) variable names.
|
||||||
cm.envConfig = make(map[string]ConfigMap)
|
func parseEnv(prefix string) map[string]ConfigMap {
|
||||||
cm.mapConfig = make(map[string]ConfigMap)
|
result := make(map[string]ConfigMap)
|
||||||
cm.defaultConfig = make(map[string]ConfigMap)
|
for _, env := range os.Environ() {
|
||||||
cm.combinedConfig = make(map[string]ConfigMap)
|
key, value, found := strings.Cut(env, "=")
|
||||||
cm.envPrefix = ""
|
if !found {
|
||||||
envSet := os.Environ()
|
continue
|
||||||
for _, env := range envSet {
|
}
|
||||||
kv := strings.Split(env, "=")
|
if prefix != "" {
|
||||||
lower := strings.ToLower(kv[0])
|
stripped, ok := strings.CutPrefix(key, prefix)
|
||||||
cm.envConfig[lower] = ConfigMap{Key: kv[0], Value: kv[1]}
|
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 {
|
func (c *ConfigManager) WithEnvPrefix(prefix string) *ConfigManager {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
c.envPrefix = prefix
|
c.envConfig = parseEnv(prefix)
|
||||||
envSet := os.Environ()
|
|
||||||
c.envConfig = make(map[string]ConfigMap)
|
|
||||||
for _, env := range envSet {
|
|
||||||
kv := strings.Split(env, "=")
|
|
||||||
if strings.HasPrefix(kv[0], prefix) {
|
|
||||||
withoutPrefix := strings.TrimPrefix(kv[0], prefix)
|
|
||||||
lower := strings.ToLower(withoutPrefix)
|
|
||||||
c.envConfig[lower] = ConfigMap{Key: withoutPrefix, Value: kv[1]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,23 +99,69 @@ func (c *ConfigManager) ConfigFileUsed() string {
|
|||||||
return c.configFileUsed
|
return c.configFileUsed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) UseExplicitDefaults(enable bool) {
|
// IsSet checks whether a key has been set in any configuration source.
|
||||||
c.mutex.Lock()
|
func (c *ConfigManager) IsSet(key string) bool {
|
||||||
defer c.mutex.Unlock()
|
c.mutex.RLock()
|
||||||
c.explicitDefaults = enable
|
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() {
|
func (c *ConfigManager) collapse() {
|
||||||
c.mutex.RLock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.Unlock()
|
||||||
ccm := make(map[string]ConfigMap)
|
ccm := make(map[string]ConfigMap)
|
||||||
|
// Precedence (highest to lowest): overrides (Set) > env > file > defaults
|
||||||
for k, v := range c.defaultConfig {
|
for k, v := range c.defaultConfig {
|
||||||
ccm[k] = v
|
ccm[k] = v
|
||||||
if _, ok := c.envConfig[k]; ok {
|
}
|
||||||
ccm[k] = c.envConfig[k]
|
for k, v := range c.fileConfig {
|
||||||
|
ccm[k] = v
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, v := range c.mapConfig {
|
for k, v := range c.overrideConfig {
|
||||||
ccm[k] = v
|
ccm[k] = v
|
||||||
}
|
}
|
||||||
c.combinedConfig = ccm
|
c.combinedConfig = ccm
|
||||||
@@ -107,6 +170,9 @@ func (c *ConfigManager) collapse() {
|
|||||||
func (c *ConfigManager) WriteConfig() error {
|
func (c *ConfigManager) WriteConfig() error {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
if c.configFileUsed == "" {
|
||||||
|
return errors.New("no config file specified")
|
||||||
|
}
|
||||||
flattenedConfig := make(map[string]any)
|
flattenedConfig := make(map[string]any)
|
||||||
for _, v := range c.combinedConfig {
|
for _, v := range c.combinedConfig {
|
||||||
flattenedConfig[v.Key] = v.Value
|
flattenedConfig[v.Key] = v.Value
|
||||||
@@ -128,8 +194,10 @@ func (c *ConfigManager) WriteConfig() error {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
enc := yaml.NewEncoder(f)
|
enc := yaml.NewEncoder(f)
|
||||||
err = enc.Encode(flattenedConfig)
|
if err = enc.Encode(flattenedConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return enc.Close()
|
||||||
case ConfigTypeJSON:
|
case ConfigTypeJSON:
|
||||||
f, err := os.Create(c.configFileUsed)
|
f, err := os.Create(c.configFileUsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -143,8 +211,10 @@ func (c *ConfigManager) WriteConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) SetConfigType(configType string) error {
|
func (c *ConfigManager) SetConfigType(ct string) error {
|
||||||
switch configType {
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
switch ct {
|
||||||
case "toml":
|
case "toml":
|
||||||
c.configType = ConfigTypeTOML
|
c.configType = ConfigTypeTOML
|
||||||
case "yaml":
|
case "yaml":
|
||||||
@@ -152,20 +222,42 @@ func (c *ConfigManager) SetConfigType(configType string) error {
|
|||||||
case "json":
|
case "json":
|
||||||
c.configType = ConfigTypeJSON
|
c.configType = ConfigTypeJSON
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("config type %s not supported", configType)
|
return fmt.Errorf("config type %s not supported", ct)
|
||||||
}
|
}
|
||||||
return nil
|
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) {
|
func (c *ConfigManager) SetEnvPrefix(prefix string) {
|
||||||
c.envPrefix = prefix
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
c.envConfig = parseEnv(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) ReadInConfig() error {
|
func (c *ConfigManager) ReadInConfig() error {
|
||||||
c.mutex.Lock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.Unlock()
|
configFile := c.configFileUsed
|
||||||
// assume config = map[string]any
|
if configFile == "" && c.configPath != "" && c.configName != "" {
|
||||||
confFileData, err := readFile(c.configFileUsed, c.configType)
|
ext := ""
|
||||||
|
switch c.configType {
|
||||||
|
case ConfigTypeTOML:
|
||||||
|
ext = ".toml"
|
||||||
|
case ConfigTypeYAML:
|
||||||
|
ext = ".yaml"
|
||||||
|
case ConfigTypeJSON:
|
||||||
|
ext = ".json"
|
||||||
|
}
|
||||||
|
configFile = filepath.Join(c.configPath, c.configName+ext)
|
||||||
|
}
|
||||||
|
configType := c.configType
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
|
||||||
|
if configFile == "" {
|
||||||
|
return errors.New("no config file specified: use SetConfigFile or SetConfigDir + SetConfigName")
|
||||||
|
}
|
||||||
|
|
||||||
|
confFileData, err := readFile(configFile, configType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -174,39 +266,73 @@ func (c *ConfigManager) ReadInConfig() error {
|
|||||||
lower := strings.ToLower(k)
|
lower := strings.ToLower(k)
|
||||||
conf[lower] = ConfigMap{Key: k, Value: v}
|
conf[lower] = ConfigMap{Key: k, Value: v}
|
||||||
}
|
}
|
||||||
c.mapConfig = conf
|
c.mutex.Lock()
|
||||||
|
c.fileConfig = conf
|
||||||
|
c.configFileUsed = configFile
|
||||||
|
c.mutex.Unlock()
|
||||||
c.collapse()
|
c.collapse()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFile(filename string, fileType configType) (map[string]any, error) {
|
func readFile(filename string, fileType configType) (map[string]any, error) {
|
||||||
|
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)
|
fileData := make(map[string]any)
|
||||||
switch fileType {
|
switch fileType {
|
||||||
case ConfigTypeTOML:
|
case ConfigTypeTOML:
|
||||||
_, err := toml.DecodeFile(filename, &fileData)
|
_, err := toml.NewDecoder(f).Decode(&fileData)
|
||||||
return fileData, err
|
return fileData, err
|
||||||
case ConfigTypeYAML:
|
case ConfigTypeYAML:
|
||||||
f, err := os.Open(filename)
|
err := yaml.NewDecoder(f).Decode(&fileData)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
d := yaml.NewDecoder(f)
|
|
||||||
err = d.Decode(&fileData)
|
|
||||||
return fileData, err
|
return fileData, err
|
||||||
case ConfigTypeJSON:
|
case ConfigTypeJSON:
|
||||||
f, err := os.Open(filename)
|
err := json.NewDecoder(f).Decode(&fileData)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
err = json.NewDecoder(f).Decode(&fileData)
|
|
||||||
return fileData, err
|
return fileData, err
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("config type %s not supported", fileType)
|
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) {
|
func (c *ConfigManager) SetConfigDir(path string) {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|||||||
1798
jety_test.go
Normal file
1798
jety_test.go
Normal file
File diff suppressed because it is too large
Load Diff
24
setters.go
24
setters.go
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package jety
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -8,7 +8,7 @@ func (c *ConfigManager) SetBool(key string, value bool) {
|
|||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
lower := strings.ToLower(key)
|
lower := strings.ToLower(key)
|
||||||
c.mapConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.overrideConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ func (c *ConfigManager) SetString(key string, value string) {
|
|||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
lower := strings.ToLower(key)
|
lower := strings.ToLower(key)
|
||||||
c.mapConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.overrideConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ func (c *ConfigManager) Set(key string, value any) {
|
|||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
lower := strings.ToLower(key)
|
lower := strings.ToLower(key)
|
||||||
c.mapConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.overrideConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,14 +33,14 @@ func (c *ConfigManager) SetDefault(key string, value any) {
|
|||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
lower := strings.ToLower(key)
|
lower := strings.ToLower(key)
|
||||||
c.defaultConfig[lower] = ConfigMap{Key: key, Value: value}
|
c.defaultConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
if _, ok := c.mapConfig[lower]; !ok {
|
// Update combinedConfig respecting precedence: override > env > file > default
|
||||||
if envVal, ok := c.envConfig[lower]; ok {
|
if v, ok := c.overrideConfig[lower]; ok {
|
||||||
c.mapConfig[lower] = envVal
|
c.combinedConfig[lower] = v
|
||||||
c.combinedConfig[lower] = envVal
|
} else if v, ok := c.envConfig[lower]; ok {
|
||||||
} else {
|
c.combinedConfig[lower] = ConfigMap{Key: key, Value: v.Value}
|
||||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
} else if v, ok := c.fileConfig[lower]; ok {
|
||||||
}
|
c.combinedConfig[lower] = v
|
||||||
} else {
|
} else {
|
||||||
c.combinedConfig[lower] = c.mapConfig[lower]
|
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user