mirror of
https://github.com/taigrr/jety.git
synced 2026-04-02 03:19:03 -07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5f7cc7fae | |||
| 91b69246fa | |||
| b16df4e1a9 | |||
| 5aadc84d50 | |||
| 7335ecd39c | |||
| 4c8d8960be |
22
.github/workflows/test.yml
vendored
Normal file
22
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: ["1.24", "1.26"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- run: go test -race -count=1 ./...
|
||||||
|
- run: go vet ./...
|
||||||
|
- run: go install honnef.co/go/tools/cmd/staticcheck@latest && staticcheck ./...
|
||||||
32
default.go
32
default.go
@@ -67,3 +67,35 @@ 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 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()
|
||||||
|
}
|
||||||
|
|||||||
68
getters.go
68
getters.go
@@ -7,15 +7,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// resolve looks up a key in combinedConfig, falling back to envConfig.
|
||||||
|
func (c *ConfigManager) resolve(key string) (ConfigMap, bool) {
|
||||||
|
lower := strings.ToLower(key)
|
||||||
|
if v, ok := c.combinedConfig[lower]; ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
if v, ok := c.envConfig[lower]; ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
return ConfigMap{}, false
|
||||||
|
}
|
||||||
|
|
||||||
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(key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return v.Value
|
return v.Value
|
||||||
}
|
}
|
||||||
@@ -23,12 +32,9 @@ 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(key)]
|
return false
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val := v.Value
|
val := v.Value
|
||||||
switch val := val.(type) {
|
switch val := val.(type) {
|
||||||
@@ -54,12 +60,9 @@ func (c *ConfigManager) GetBool(key string) bool {
|
|||||||
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(key)]
|
return 0
|
||||||
if !ok {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val := v.Value
|
val := v.Value
|
||||||
switch val := val.(type) {
|
switch val := val.(type) {
|
||||||
@@ -89,12 +92,9 @@ func (c *ConfigManager) GetDuration(key string) time.Duration {
|
|||||||
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(key)]
|
return ""
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
@@ -108,12 +108,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(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:
|
||||||
@@ -126,12 +123,9 @@ 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(key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case []string:
|
case []string:
|
||||||
@@ -155,12 +149,9 @@ func (c *ConfigManager) GetStringSlice(key string) []string {
|
|||||||
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(key)]
|
return 0
|
||||||
if !ok {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case int:
|
case int:
|
||||||
@@ -187,12 +178,9 @@ 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(key)]
|
return nil
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch val := v.Value.(type) {
|
switch val := v.Value.(type) {
|
||||||
case []int:
|
case []int:
|
||||||
|
|||||||
211
jety.go
211
jety.go
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -23,22 +22,25 @@ 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
|
||||||
mapConfig map[string]ConfigMap
|
overrideConfig map[string]ConfigMap
|
||||||
defaultConfig map[string]ConfigMap
|
fileConfig map[string]ConfigMap
|
||||||
envConfig map[string]ConfigMap
|
defaultConfig map[string]ConfigMap
|
||||||
combinedConfig map[string]ConfigMap
|
envConfig map[string]ConfigMap
|
||||||
mutex sync.RWMutex
|
combinedConfig map[string]ConfigMap
|
||||||
explicitDefaults bool
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,39 +49,47 @@ var (
|
|||||||
ErrConfigFileEmpty = errors.New("config file is empty")
|
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)
|
|
||||||
envSet := os.Environ()
|
|
||||||
for _, env := range envSet {
|
|
||||||
key, value, found := strings.Cut(env, "=")
|
key, value, found := strings.Cut(env, "=")
|
||||||
if !found {
|
if !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lower := strings.ToLower(key)
|
if prefix != "" {
|
||||||
cm.envConfig[lower] = ConfigMap{Key: key, Value: value}
|
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 {
|
func (c *ConfigManager) WithEnvPrefix(prefix string) *ConfigManager {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
envSet := os.Environ()
|
c.envConfig = parseEnv(prefix)
|
||||||
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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,29 +99,80 @@ 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.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maps.Copy(ccm, c.mapConfig)
|
for k, v := range c.overrideConfig {
|
||||||
|
ccm[k] = v
|
||||||
|
}
|
||||||
c.combinedConfig = ccm
|
c.combinedConfig = ccm
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -133,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 {
|
||||||
@@ -148,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()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
switch configType {
|
switch ct {
|
||||||
case "toml":
|
case "toml":
|
||||||
c.configType = ConfigTypeTOML
|
c.configType = ConfigTypeTOML
|
||||||
case "yaml":
|
case "yaml":
|
||||||
@@ -159,29 +222,17 @@ 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.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
// Re-read environment variables, stripping the prefix from matching keys.
|
c.envConfig = parseEnv(prefix)
|
||||||
// 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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigManager) ReadInConfig() error {
|
func (c *ConfigManager) ReadInConfig() error {
|
||||||
@@ -216,7 +267,7 @@ func (c *ConfigManager) ReadInConfig() error {
|
|||||||
conf[lower] = ConfigMap{Key: k, Value: v}
|
conf[lower] = ConfigMap{Key: k, Value: v}
|
||||||
}
|
}
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
c.mapConfig = conf
|
c.fileConfig = conf
|
||||||
c.configFileUsed = configFile
|
c.configFileUsed = configFile
|
||||||
c.mutex.Unlock()
|
c.mutex.Unlock()
|
||||||
c.collapse()
|
c.collapse()
|
||||||
@@ -224,33 +275,33 @@ func (c *ConfigManager) ReadInConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readFile(filename string, fileType configType) (map[string]any, error) {
|
func readFile(filename string, fileType configType) (map[string]any, error) {
|
||||||
fileData := make(map[string]any)
|
f, err := os.Open(filename)
|
||||||
if d, err := os.Stat(filename); os.IsNotExist(err) {
|
if err != nil {
|
||||||
return nil, ErrConfigFileNotFound
|
if os.IsNotExist(err) {
|
||||||
} else if d.Size() == 0 {
|
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
|
return nil, ErrConfigFileEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
151
jety_test.go
151
jety_test.go
@@ -66,7 +66,7 @@ func TestNewConfigManager(t *testing.T) {
|
|||||||
if cm.envConfig == nil {
|
if cm.envConfig == nil {
|
||||||
t.Error("envConfig not initialized")
|
t.Error("envConfig not initialized")
|
||||||
}
|
}
|
||||||
if cm.mapConfig == nil {
|
if cm.overrideConfig == nil {
|
||||||
t.Error("mapConfig not initialized")
|
t.Error("mapConfig not initialized")
|
||||||
}
|
}
|
||||||
if cm.defaultConfig == nil {
|
if cm.defaultConfig == nil {
|
||||||
@@ -554,7 +554,7 @@ func TestEnvOverridesDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigFileOverridesEnv(t *testing.T) {
|
func TestEnvOverridesConfigFile(t *testing.T) {
|
||||||
os.Setenv("PORT", "5000")
|
os.Setenv("PORT", "5000")
|
||||||
defer os.Unsetenv("PORT")
|
defer os.Unsetenv("PORT")
|
||||||
|
|
||||||
@@ -575,9 +575,9 @@ func TestConfigFileOverridesEnv(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config file should override env and default
|
// Env should override config file (env > file > defaults)
|
||||||
if got := cm.GetInt("port"); got != 9000 {
|
if got := cm.GetInt("port"); got != 5000 {
|
||||||
t.Errorf("GetInt(port) = %d, want 9000 (from file)", got)
|
t.Errorf("GetInt(port) = %d, want 5000 (env overrides file)", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,17 +760,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) {
|
func TestSetString(t *testing.T) {
|
||||||
cm := NewConfigManager()
|
cm := NewConfigManager()
|
||||||
cm.SetString("name", "test")
|
cm.SetString("name", "test")
|
||||||
@@ -1347,3 +1336,133 @@ func TestPackageLevelSetEnvPrefixOverrides(t *testing.T) {
|
|||||||
t.Fatalf("subprocess failed: %v\n%s", err, out)
|
t.Fatalf("subprocess failed: %v\n%s", err, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrecedenceChain(t *testing.T) {
|
||||||
|
// Verify: Set > env > file > defaults
|
||||||
|
os.Setenv("PORT", "5000")
|
||||||
|
os.Setenv("HOST", "envhost")
|
||||||
|
os.Setenv("LOG", "envlog")
|
||||||
|
defer os.Unsetenv("PORT")
|
||||||
|
defer os.Unsetenv("HOST")
|
||||||
|
defer os.Unsetenv("LOG")
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
configFile := filepath.Join(dir, "config.yaml")
|
||||||
|
if err := os.WriteFile(configFile, []byte("port: 9000\nhost: filehost\nlog: filelog\nname: filename"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := NewConfigManager()
|
||||||
|
cm.SetDefault("port", 8080)
|
||||||
|
cm.SetDefault("host", "defaulthost")
|
||||||
|
cm.SetDefault("log", "defaultlog")
|
||||||
|
cm.SetDefault("name", "defaultname")
|
||||||
|
cm.SetDefault("extra", "defaultextra")
|
||||||
|
|
||||||
|
cm.SetConfigFile(configFile)
|
||||||
|
if err := cm.SetConfigType("yaml"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cm.ReadInConfig(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Set("port", 1111) // Set overrides everything
|
||||||
|
|
||||||
|
// port: Set(1111) > env(5000) > file(9000) > default(8080) → 1111
|
||||||
|
if got := cm.GetInt("port"); got != 1111 {
|
||||||
|
t.Errorf("port: got %d, want 1111 (Set overrides all)", got)
|
||||||
|
}
|
||||||
|
// host: env(envhost) > file(filehost) > default(defaulthost) → envhost
|
||||||
|
if got := cm.GetString("host"); got != "envhost" {
|
||||||
|
t.Errorf("host: got %q, want envhost (env overrides file)", got)
|
||||||
|
}
|
||||||
|
// name: file(filename) > default(defaultname) → filename
|
||||||
|
if got := cm.GetString("name"); got != "filename" {
|
||||||
|
t.Errorf("name: got %q, want filename (file overrides default)", got)
|
||||||
|
}
|
||||||
|
// extra: only default → defaultextra
|
||||||
|
if got := cm.GetString("extra"); got != "defaultextra" {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
22
setters.go
22
setters.go
@@ -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] = ConfigMap{Key: key, Value: envVal.Value}
|
c.combinedConfig[lower] = v
|
||||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: envVal.Value}
|
} 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