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:
2026-05-02 14:04:01 +01:00
co-authored by Cursor
parent 288a5b600c
commit c575341178
75 changed files with 2810 additions and 528 deletions
+56
View File
@@ -0,0 +1,56 @@
// Package keymap normalizes map keys recursively for cross-format matching.
package keymap
import "strings"
// Normalizer transforms a single path segment or key string.
type Normalizer func(string) string
// LowerAlnum lowercases and strips any rune that is not [a-z0-9].
func LowerAlnum(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range strings.ToLower(s) {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
return b.String()
}
// Identity returns s unchanged.
func Identity(s string) string { return s }
// Walk recursively rewrites every map key in place using n. Slices of maps are walked.
func Walk(m map[string]any, n Normalizer) {
if m == nil || n == nil {
return
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
for _, k := range keys {
v := m[k]
delete(m, k)
nk := n(k)
if nk == "" {
nk = k
}
switch vv := v.(type) {
case map[string]any:
Walk(vv, n)
m[nk] = vv
case []any:
for i := range vv {
if sm, ok := vv[i].(map[string]any); ok {
Walk(sm, n)
vv[i] = sm
}
}
m[nk] = vv
default:
m[nk] = v
}
}
}
+25
View File
@@ -0,0 +1,25 @@
package keymap
import (
"reflect"
"testing"
)
func TestLowerAlnum(t *testing.T) {
if got, want := LowerAlnum("sub-Service"), "subservice"; got != want {
t.Fatalf("got %q want %q", got, want)
}
}
func TestWalk(t *testing.T) {
m := map[string]any{
"Sub-Service": map[string]any{"Pool-Size": "10"},
}
Walk(m, LowerAlnum)
want := map[string]any{
"subservice": map[string]any{"poolsize": "10"},
}
if !reflect.DeepEqual(m, want) {
t.Fatalf("got %#v want %#v", m, want)
}
}