fix(config): make iteration deterministic
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
6dd94aff4c
commit
d6f7c05838
3 changed files with 78 additions and 5 deletions
21
config.go
21
config.go
|
|
@ -13,9 +13,9 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -200,11 +200,26 @@ func (c *Config) Commit() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// All returns an iterator over all configuration values (including environment variables).
|
||||
// All returns an iterator over all configuration values in lexical key order
|
||||
// (including environment variables).
|
||||
func (c *Config) All() iter.Seq2[string, any] {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return maps.All(c.v.AllSettings())
|
||||
|
||||
settings := c.v.AllSettings()
|
||||
keys := make([]string, 0, len(settings))
|
||||
for key := range settings {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return func(yield func(string, any) bool) {
|
||||
for _, key := range keys {
|
||||
if !yield(key, settings[key]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Path returns the path to the configuration file.
|
||||
|
|
|
|||
|
|
@ -90,6 +90,23 @@ func TestConfig_All_Good(t *testing.T) {
|
|||
assert.Equal(t, "val2", all["key2"])
|
||||
}
|
||||
|
||||
func TestConfig_All_Order_Good(t *testing.T) {
|
||||
m := coreio.NewMockMedium()
|
||||
|
||||
cfg, err := New(WithMedium(m), WithPath("/tmp/test/config.yaml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_ = cfg.Set("zulu", "last")
|
||||
_ = cfg.Set("alpha", "first")
|
||||
|
||||
var keys []string
|
||||
for key, _ := range cfg.All() {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"alpha", "zulu"}, keys)
|
||||
}
|
||||
|
||||
func TestConfig_Path_Good(t *testing.T) {
|
||||
m := coreio.NewMockMedium()
|
||||
|
||||
|
|
@ -199,6 +216,21 @@ func TestLoadEnv_Good(t *testing.T) {
|
|||
assert.Equal(t, "value", result["simple"])
|
||||
}
|
||||
|
||||
func TestLoadEnv_PrefixNormalisation_Good(t *testing.T) {
|
||||
t.Setenv("MYAPP_SETTING", "secret")
|
||||
t.Setenv("MYAPP_ALPHA", "first")
|
||||
|
||||
keys := make([]string, 0, 2)
|
||||
values := make([]string, 0, 2)
|
||||
for key, value := range Env("MYAPP") {
|
||||
keys = append(keys, key)
|
||||
values = append(values, value.(string))
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"alpha", "setting"}, keys)
|
||||
assert.Equal(t, []string{"first", "secret"}, values)
|
||||
}
|
||||
|
||||
func TestLoad_Bad(t *testing.T) {
|
||||
m := coreio.NewMockMedium()
|
||||
|
||||
|
|
|
|||
30
env.go
30
env.go
|
|
@ -3,17 +3,36 @@ package config
|
|||
import (
|
||||
"iter"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func normaliseEnvPrefix(prefix string) string {
|
||||
if prefix == "" || strings.HasSuffix(prefix, "_") {
|
||||
return prefix
|
||||
}
|
||||
return prefix + "_"
|
||||
}
|
||||
|
||||
// Env returns an iterator over environment variables with the given prefix,
|
||||
// providing them as dot-notation keys and values.
|
||||
//
|
||||
// The prefix may be supplied with or without a trailing underscore.
|
||||
//
|
||||
// For example, with prefix "CORE_CONFIG_":
|
||||
//
|
||||
// CORE_CONFIG_FOO_BAR=baz -> yields ("foo.bar", "baz")
|
||||
func Env(prefix string) iter.Seq2[string, any] {
|
||||
return func(yield func(string, any) bool) {
|
||||
prefix = normaliseEnvPrefix(prefix)
|
||||
|
||||
type entry struct {
|
||||
key string
|
||||
value any
|
||||
}
|
||||
|
||||
var entries []entry
|
||||
|
||||
for _, env := range os.Environ() {
|
||||
if !strings.HasPrefix(env, prefix) {
|
||||
continue
|
||||
|
|
@ -27,12 +46,19 @@ func Env(prefix string) iter.Seq2[string, any] {
|
|||
name := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
// Strip prefix and convert to dot notation
|
||||
key := strings.TrimPrefix(name, prefix)
|
||||
key = strings.ToLower(key)
|
||||
key = strings.ReplaceAll(key, "_", ".")
|
||||
|
||||
if !yield(key, value) {
|
||||
entries = append(entries, entry{key: key, value: value})
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].key < entries[j].key
|
||||
})
|
||||
|
||||
for _, entry := range entries {
|
||||
if !yield(entry.key, entry.value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue