Skip to content

Commit 8b6d704

Browse files
committed
feat: add ConfigBuilder for layered configuration
- ConfigBuilder with fluent API for composing multiple sources - Sources: MemorySource, FileSource, OptionalFileSource, EnvSource - DeepMerge for recursive map merging - AddDefaults(), AddFile(), AddOptionalFile(), AddEnv() methods - Environment variables support nesting via separator (MYAPP_DB_HOST -> db.host) - Build() returns a ConfigMap with all sources merged Example: config, _ := NewConfigBuilder(). AddDefaults(map[string]interface{}{"port": 8080}). AddFile("config.json"). AddOptionalFile("config.local.json"). AddEnv("MYAPP"). Build()
1 parent b0b6212 commit 8b6d704

2 files changed

Lines changed: 534 additions & 0 deletions

File tree

builder.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package prefer
2+
3+
import (
4+
"os"
5+
"strings"
6+
)
7+
8+
// Source represents a configuration source for the ConfigBuilder.
9+
type Source interface {
10+
// Load returns configuration data as a map.
11+
Load() (map[string]interface{}, error)
12+
}
13+
14+
// DeepMerge merges override into base, returning a new map.
15+
// Nested maps are merged recursively; other values are overwritten.
16+
func DeepMerge(base, override map[string]interface{}) map[string]interface{} {
17+
result := make(map[string]interface{})
18+
19+
// Copy base
20+
for k, v := range base {
21+
result[k] = v
22+
}
23+
24+
// Merge override
25+
for k, v := range override {
26+
if baseVal, exists := result[k]; exists {
27+
baseMap, baseIsMap := baseVal.(map[string]interface{})
28+
overrideMap, overrideIsMap := v.(map[string]interface{})
29+
if baseIsMap && overrideIsMap {
30+
result[k] = DeepMerge(baseMap, overrideMap)
31+
continue
32+
}
33+
}
34+
result[k] = v
35+
}
36+
37+
return result
38+
}
39+
40+
// ConfigBuilder builds configuration from multiple layered sources.
41+
// Sources are applied in order, with later sources overriding earlier ones.
42+
type ConfigBuilder struct {
43+
sources []Source
44+
}
45+
46+
// NewConfigBuilder creates a new ConfigBuilder.
47+
func NewConfigBuilder() *ConfigBuilder {
48+
return &ConfigBuilder{
49+
sources: make([]Source, 0),
50+
}
51+
}
52+
53+
// AddSource adds a custom source to the builder.
54+
func (b *ConfigBuilder) AddSource(source Source) *ConfigBuilder {
55+
b.sources = append(b.sources, source)
56+
return b
57+
}
58+
59+
// AddDefaults adds in-memory default values.
60+
func (b *ConfigBuilder) AddDefaults(defaults map[string]interface{}) *ConfigBuilder {
61+
return b.AddSource(&MemorySource{data: defaults})
62+
}
63+
64+
// AddFile adds a required configuration file.
65+
func (b *ConfigBuilder) AddFile(identifier string) *ConfigBuilder {
66+
return b.AddSource(&FileSource{identifier: identifier, required: true})
67+
}
68+
69+
// AddOptionalFile adds an optional configuration file.
70+
// If the file doesn't exist, it's silently skipped.
71+
func (b *ConfigBuilder) AddOptionalFile(identifier string) *ConfigBuilder {
72+
return b.AddSource(&FileSource{identifier: identifier, required: false})
73+
}
74+
75+
// AddEnv adds environment variables with the given prefix.
76+
// Variables are converted to nested structure using the separator.
77+
// Example: MYAPP_DATABASE_HOST with prefix "MYAPP" becomes database.host
78+
func (b *ConfigBuilder) AddEnv(prefix string) *ConfigBuilder {
79+
return b.AddSource(&EnvSource{prefix: prefix, separator: "_"})
80+
}
81+
82+
// AddEnvWithSeparator adds environment variables with a custom separator.
83+
func (b *ConfigBuilder) AddEnvWithSeparator(prefix, separator string) *ConfigBuilder {
84+
return b.AddSource(&EnvSource{prefix: prefix, separator: separator})
85+
}
86+
87+
// Build loads and merges all sources, returning a ConfigMap.
88+
func (b *ConfigBuilder) Build() (*ConfigMap, error) {
89+
merged := make(map[string]interface{})
90+
91+
for _, source := range b.sources {
92+
data, err := source.Load()
93+
if err != nil {
94+
return nil, err
95+
}
96+
merged = DeepMerge(merged, data)
97+
}
98+
99+
return &ConfigMap{data: merged}, nil
100+
}
101+
102+
// MemorySource provides configuration from an in-memory map.
103+
type MemorySource struct {
104+
data map[string]interface{}
105+
}
106+
107+
// NewMemorySource creates a new MemorySource.
108+
func NewMemorySource(data map[string]interface{}) *MemorySource {
109+
return &MemorySource{data: data}
110+
}
111+
112+
func (s *MemorySource) Load() (map[string]interface{}, error) {
113+
// Return a copy to prevent mutation
114+
result := make(map[string]interface{})
115+
for k, v := range s.data {
116+
result[k] = v
117+
}
118+
return result, nil
119+
}
120+
121+
// FileSource loads configuration from a file.
122+
type FileSource struct {
123+
identifier string
124+
required bool
125+
}
126+
127+
// NewFileSource creates a required file source.
128+
func NewFileSource(identifier string) *FileSource {
129+
return &FileSource{identifier: identifier, required: true}
130+
}
131+
132+
// NewOptionalFileSource creates an optional file source.
133+
func NewOptionalFileSource(identifier string) *FileSource {
134+
return &FileSource{identifier: identifier, required: false}
135+
}
136+
137+
func (s *FileSource) Load() (map[string]interface{}, error) {
138+
var result map[string]interface{}
139+
_, err := Load(s.identifier, &result)
140+
if err != nil {
141+
if !s.required {
142+
// Return empty map for optional files that don't exist
143+
return make(map[string]interface{}), nil
144+
}
145+
return nil, err
146+
}
147+
return result, nil
148+
}
149+
150+
// EnvSource loads configuration from environment variables.
151+
type EnvSource struct {
152+
prefix string
153+
separator string
154+
}
155+
156+
// NewEnvSource creates a new EnvSource with the default separator "_".
157+
func NewEnvSource(prefix string) *EnvSource {
158+
return &EnvSource{prefix: prefix, separator: "_"}
159+
}
160+
161+
// NewEnvSourceWithSeparator creates an EnvSource with a custom separator.
162+
func NewEnvSourceWithSeparator(prefix, separator string) *EnvSource {
163+
return &EnvSource{prefix: prefix, separator: separator}
164+
}
165+
166+
func (s *EnvSource) Load() (map[string]interface{}, error) {
167+
result := make(map[string]interface{})
168+
prefix := s.prefix + s.separator
169+
170+
for _, env := range os.Environ() {
171+
parts := strings.SplitN(env, "=", 2)
172+
if len(parts) != 2 {
173+
continue
174+
}
175+
key, value := parts[0], parts[1]
176+
177+
if !strings.HasPrefix(key, prefix) {
178+
continue
179+
}
180+
181+
// Remove prefix and convert to nested structure
182+
key = strings.TrimPrefix(key, prefix)
183+
key = strings.ToLower(key)
184+
keyParts := strings.Split(key, s.separator)
185+
186+
setNested(result, keyParts, value)
187+
}
188+
189+
return result, nil
190+
}
191+
192+
// setNested sets a value in a nested map structure.
193+
func setNested(data map[string]interface{}, parts []string, value interface{}) {
194+
current := data
195+
for i, part := range parts {
196+
if i == len(parts)-1 {
197+
current[part] = value
198+
} else {
199+
if _, exists := current[part]; !exists {
200+
current[part] = make(map[string]interface{})
201+
}
202+
if nested, ok := current[part].(map[string]interface{}); ok {
203+
current = nested
204+
} else {
205+
// Overwrite non-map value with a new map
206+
newMap := make(map[string]interface{})
207+
current[part] = newMap
208+
current = newMap
209+
}
210+
}
211+
}
212+
}

0 commit comments

Comments
 (0)