Skip to content

Commit cdb1a42

Browse files
committed
Implementations for yaml based configuration management.
1 parent bce6917 commit cdb1a42

File tree

6 files changed

+210
-12
lines changed

6 files changed

+210
-12
lines changed

README.md

+36-11
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ func TestMapRunner(t *testing.T) {
9696
floatingStrings := []string{"0.1", "0.2", "22", "22.1"}
9797

9898
res, err := NewTransformer[string, float64](floatingStrings).
99-
Map(MapIt[string, float64](func(item string) (float64, error) { return strconv.ParseFloat(item, 64) })).
100-
Map(MapIt[float64, float64](func(item float64) (float64, error) { return item * 10, nil })).
99+
Transform(MapIt[string, float64](func(item string) (float64, error) { return strconv.ParseFloat(item, 64) })).
100+
Transform(MapIt[float64, float64](func(item float64) (float64, error) { return item * 10, nil })).
101101
Result()
102102
if err != nil {
103103
t.Errorf("Testcase failed with error : %v", err)
@@ -126,10 +126,10 @@ func TestFilterIt(t *testing.T) {
126126
floatingStrings := []string{"0.1", "0.2", "22", "22.1"}
127127

128128
res, err := NewTransformer[string, int64](floatingStrings).
129-
Map(MapIt[string, float64](func(item string) (float64, error) {return strconv.ParseFloat(item, 64)})).
130-
Map(MapIt[float64, float64](func(item float64) (float64, error) { return item * 10, nil })).
131-
Map(MapIt[float64, int64](func(item float64) (int64, error) { return int64(item), nil })).
132-
Map(FilterIt[int64](func(item int64) (bool, error) { return item%2 == 0, nil })).
129+
Transform(MapIt[string, float64](func(item string) (float64, error) {return strconv.ParseFloat(item, 64)})).
130+
Transform(MapIt[float64, float64](func(item float64) (float64, error) { return item * 10, nil })).
131+
Transform(MapIt[float64, int64](func(item float64) (int64, error) { return int64(item), nil })).
132+
Transform(FilterIt[int64](func(item int64) (bool, error) { return item%2 == 0, nil })).
133133
Result()
134134
if err != nil {
135135
t.Errorf("Testcase failed with error : %v", err)
@@ -171,10 +171,10 @@ func TestMapRunnerLib(t *testing.T) {
171171
floatingStrings := []string{"0.1", "0.2", "22", "22.1"}
172172

173173
res, err := streams.NewTransformer[string, int64](floatingStrings).
174-
Map(streams.MapIt[string, float64](func(item string) (float64, error) { return strconv.ParseFloat(item, 64) })).
175-
Map(streams.MapIt[float64, float64](func(item float64) (float64, error) { return item * 10, nil })).
176-
Map(streams.MapIt[float64, int64](func(item float64) (int64, error) { return int64(item), nil })).
177-
Map(streams.FilterIt[int64](func(item int64) (bool, error) { return item%2 == 0, nil })).
174+
Transform(streams.MapIt[string, float64](func(item string) (float64, error) { return strconv.ParseFloat(item, 64) })).
175+
Transform(streams.MapIt[float64, float64](func(item float64) (float64, error) { return item * 10, nil })).
176+
Transform(streams.MapIt[float64, int64](func(item float64) (int64, error) { return int64(item), nil })).
177+
Transform(streams.FilterIt[int64](func(item int64) (bool, error) { return item%2 == 0, nil })).
178178
Result()
179179
if err != nil {
180180
t.Errorf("Testcase failed with error : %v", err)
@@ -217,4 +217,29 @@ func TestSqlWriteExec_CreateOrderTxn(t *testing.T) {
217217
},
218218
)
219219
}
220-
```
220+
```
221+
222+
## Yaml Configs
223+
224+
```go
225+
func ExampleLoadConfigWithOverrides() {
226+
// Load configs in order of precedence
227+
_, err := yaml_configs.LoadConfigWithSuffix(
228+
"./test_data/env",
229+
"local",
230+
)
231+
}
232+
```
233+
234+
This will load the configs from the files in the order of precedence.
235+
Values from later files override earlier ones.
236+
237+
```go
238+
func ExampleGet() {
239+
// Get a value from the config
240+
value := yaml_configs.Get[string]("database.host")
241+
fmt.Println(value)
242+
}
243+
```
244+
245+

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ require (
88
github.com/DATA-DOG/go-sqlmock v1.5.2
99
github.com/davecgh/go-spew v1.1.1 // indirect
1010
github.com/pmezard/go-difflib v1.0.0 // indirect
11-
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
gopkg.in/yaml.v3 v3.0.1
1212
)

yaml_configs/test_data/env.local.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
database:
2+
host: localhost
3+
port: 5430
4+
user: postgres
5+
password: postgres
6+
dbname: postgres

yaml_configs/test_data/env.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
database:
2+
host: localhost
3+
port: 5432
4+
user: postgres
5+
password: postgres
6+
dbname: postgres
7+

yaml_configs/yaml_configs.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package yaml_configs
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"sync"
8+
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
var (
13+
configDoOnce sync.Once
14+
config *Config
15+
)
16+
17+
type Config struct {
18+
configMap map[string]any
19+
configFlatMap map[string]any
20+
}
21+
22+
// LoadConfigWithSuffix loads a config file with a suffix, and overrides the config with the suffix file
23+
// file path is path.suffix.yaml
24+
// provide path without .yaml
25+
func LoadConfigWithSuffix(path string, suffix string) (*Config, error) {
26+
return LoadConfigWithOverrides(
27+
fmt.Sprintf("%s.yaml", path),
28+
fmt.Sprintf("%s.%s.yaml", path, suffix),
29+
)
30+
}
31+
32+
// LoadConfigWithOverrides loads configs in order, with later files overriding earlier ones
33+
func LoadConfigWithOverrides(paths ...string) (*Config, error) {
34+
var loadErr error
35+
36+
configDoOnce.Do(func() {
37+
config = &Config{
38+
configMap: make(map[string]any),
39+
configFlatMap: make(map[string]any),
40+
}
41+
42+
// Load each config file in order
43+
for _, path := range paths {
44+
if err := loadAndMerge(path, config); err != nil {
45+
loadErr = err
46+
return
47+
}
48+
}
49+
})
50+
51+
if loadErr != nil {
52+
return nil, loadErr
53+
}
54+
return config, nil
55+
}
56+
57+
func loadAndMerge(path string, cfg *Config) error {
58+
yamlFile, err := os.Open(path)
59+
if err != nil {
60+
if os.IsNotExist(err) {
61+
// Skip if file doesn't exist
62+
fmt.Printf("File %s does not exist, skipping\n", path)
63+
return nil
64+
}
65+
return err
66+
}
67+
defer yamlFile.Close()
68+
69+
var newConfig map[string]any
70+
if err := yaml.NewDecoder(yamlFile).Decode(&newConfig); err != nil {
71+
return err
72+
}
73+
74+
// Merge new config into existing
75+
mergeMap(cfg.configMap, newConfig)
76+
77+
// Rebuild flat map
78+
cfg.configFlatMap = make(map[string]any)
79+
flattenConfig(cfg.configMap, "", cfg.configFlatMap)
80+
81+
return nil
82+
}
83+
84+
// mergeMap recursively merges src into dst
85+
func mergeMap(dst, src map[string]any) {
86+
for key, srcVal := range src {
87+
if dstVal, exists := dst[key]; exists {
88+
// If both are maps, merge recursively
89+
if dstMap, ok := dstVal.(map[string]any); ok {
90+
if srcMap, ok := srcVal.(map[string]any); ok {
91+
mergeMap(dstMap, srcMap)
92+
continue
93+
}
94+
}
95+
}
96+
// Otherwise override the value
97+
dst[key] = srcVal
98+
}
99+
}
100+
101+
func flattenConfig(configMap map[string]any, prefix string, flatMap map[string]any) {
102+
for key, value := range configMap {
103+
var newKey string
104+
if prefix == "" {
105+
newKey = key
106+
} else {
107+
newKey = fmt.Sprintf("%s.%s", prefix, key)
108+
}
109+
if nestedMap, ok := value.(map[string]any); ok {
110+
flattenConfig(nestedMap, newKey, flatMap)
111+
} else {
112+
113+
flatMap[newKey] = value
114+
flatMap[strings.ToUpper(newKey)] = value
115+
flatMap[strings.ToLower(newKey)] = value
116+
flatMap[strings.ReplaceAll(newKey, ".", "_")] = value
117+
}
118+
}
119+
}
120+
121+
func (c *Config) Get(key string) any {
122+
return c.configFlatMap[key]
123+
}
124+
125+
func Get[T any](key string) T {
126+
value, ok := config.configFlatMap[key]
127+
if !ok {
128+
return *new(T)
129+
}
130+
return value.(T)
131+
}

yaml_configs/yaml_configs_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package yaml_configs
2+
3+
import (
4+
"fmt"
5+
"log"
6+
)
7+
8+
func ExampleLoadConfigWithOverrides() {
9+
// Load configs in order of precedence
10+
_, err := LoadConfigWithSuffix(
11+
"./test_data/env",
12+
"local",
13+
)
14+
if err != nil {
15+
log.Fatal(err)
16+
}
17+
18+
// Values from later files override earlier ones
19+
fmt.Println(Get[string]("database.host"))
20+
fmt.Println(Get[int]("database.port"))
21+
fmt.Println(Get[string]("database.dbname"))
22+
fmt.Println(Get[string]("database.user"))
23+
fmt.Println(Get[string]("database.password"))
24+
// Output: localhost
25+
// 5430
26+
// postgres
27+
// postgres
28+
// postgres
29+
}

0 commit comments

Comments
 (0)