feat(go-config): multi-format codecs, envc CLI, fixtures, ASRs
- Module github.com/eslider/go-config with env/yaml/json/ini packages - internal: source, keymap, merge, structconv, bytesutil - cmd/envc: convert, get, merge - docs/asr ASR-0001..0008, README/CHANGELOG/AGENTS refresh - golangci-lint v2 config; tests + fixtures Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// Package merge provides deep map merging with configurable slice behaviour.
|
||||
package merge
|
||||
|
||||
// SliceStrategy controls how two []any values are combined at the same key.
|
||||
type SliceStrategy int
|
||||
|
||||
const (
|
||||
// Replace overwrites the destination slice with the source slice.
|
||||
Replace SliceStrategy = iota
|
||||
// Concat appends source elements to the destination slice.
|
||||
Concat
|
||||
)
|
||||
|
||||
// Option configures DeepMerge.
|
||||
type Option func(*config)
|
||||
|
||||
type config struct {
|
||||
slice SliceStrategy
|
||||
}
|
||||
|
||||
// WithSliceStrategy sets slice merge behaviour (default Replace).
|
||||
func WithSliceStrategy(s SliceStrategy) Option {
|
||||
return func(c *config) { c.slice = s }
|
||||
}
|
||||
|
||||
// DeepMerge folds src into dst in place. Maps recurse; scalar leaves use
|
||||
// last-write-wins (src overwrites dst). For slice values, behaviour follows cfg.slice.
|
||||
func DeepMerge(dst, src map[string]any, opts ...Option) {
|
||||
cfg := config{slice: Replace}
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
}
|
||||
if dst == nil || src == nil {
|
||||
return
|
||||
}
|
||||
for k, sv := range src {
|
||||
dv, ok := dst[k]
|
||||
if !ok {
|
||||
dst[k] = cloneValue(sv)
|
||||
continue
|
||||
}
|
||||
dm, dIsMap := dv.(map[string]any)
|
||||
sm, sIsMap := sv.(map[string]any)
|
||||
if dIsMap && sIsMap {
|
||||
DeepMerge(dm, sm, opts...)
|
||||
dst[k] = dm
|
||||
continue
|
||||
}
|
||||
if dsa, dIsSlice := dv.([]any); dIsSlice {
|
||||
if ssa, sIsSlice := sv.([]any); sIsSlice {
|
||||
switch cfg.slice {
|
||||
case Concat:
|
||||
dst[k] = append(append([]any{}, dsa...), ssa...)
|
||||
default:
|
||||
dst[k] = append([]any{}, ssa...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
dst[k] = cloneValue(sv)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneValue(v any) any {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
out := make(map[string]any, len(t))
|
||||
DeepMerge(out, t)
|
||||
return out
|
||||
case []any:
|
||||
cp := make([]any, len(t))
|
||||
copy(cp, t)
|
||||
return cp
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package merge
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeepMerge_MapRecursion(t *testing.T) {
|
||||
dst := map[string]any{
|
||||
"a": map[string]any{"x": "1"},
|
||||
}
|
||||
src := map[string]any{
|
||||
"a": map[string]any{"y": "2"},
|
||||
}
|
||||
DeepMerge(dst, src)
|
||||
want := map[string]any{
|
||||
"a": map[string]any{"x": "1", "y": "2"},
|
||||
}
|
||||
if !reflect.DeepEqual(dst, want) {
|
||||
t.Fatalf("got %#v want %#v", dst, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepMerge_ScalarLastWins(t *testing.T) {
|
||||
dst := map[string]any{"k": "first"}
|
||||
src := map[string]any{"k": "second"}
|
||||
DeepMerge(dst, src)
|
||||
if dst["k"] != "second" {
|
||||
t.Fatalf("got %v", dst["k"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepMerge_SliceReplace(t *testing.T) {
|
||||
dst := map[string]any{"s": []any{"a", "b"}}
|
||||
src := map[string]any{"s": []any{"c"}}
|
||||
DeepMerge(dst, src, WithSliceStrategy(Replace))
|
||||
got := dst["s"].([]any)
|
||||
if len(got) != 1 || got[0] != "c" {
|
||||
t.Fatalf("got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepMerge_SliceConcat(t *testing.T) {
|
||||
dst := map[string]any{"s": []any{"a", "b"}}
|
||||
src := map[string]any{"s": []any{"c"}}
|
||||
DeepMerge(dst, src, WithSliceStrategy(Concat))
|
||||
got := dst["s"].([]any)
|
||||
want := []any{"a", "b", "c"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user