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
+129
View File
@@ -0,0 +1,129 @@
// Package source provides pluggable configuration inputs (bytes, files, URLs).
package source
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// Source opens a byte stream for reading. Callers must close the ReadCloser.
type Source interface {
Open(ctx context.Context) (io.ReadCloser, error)
String() string
}
// Bytes holds raw bytes in memory.
type Bytes struct {
Data []byte
Name string
}
// Open returns a ReadCloser over a copy of Data.
func (b Bytes) Open(_ context.Context) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(b.Data)), nil
}
// String returns a label for errors (defaults to "bytes").
func (b Bytes) String() string {
if b.Name != "" {
return b.Name
}
return "bytes"
}
// Reader wraps an io.Reader. The reader is consumed once per Open call.
type Reader struct {
R io.Reader
Name string
}
// Open returns the reader as a ReadCloser when possible, otherwise wraps r.R.
func (r Reader) Open(_ context.Context) (io.ReadCloser, error) {
rc, ok := r.R.(io.ReadCloser)
if ok {
return rc, nil
}
return io.NopCloser(r.R), nil
}
// String returns a label for errors (defaults to "reader").
func (r Reader) String() string {
if r.Name != "" {
return r.Name
}
return "reader"
}
// File opens a path on the local filesystem.
type File struct {
Path string
}
// Open opens the file path.
func (f File) Open(_ context.Context) (io.ReadCloser, error) {
return os.Open(f.Path)
}
// String returns the file path.
func (f File) String() string {
return f.Path
}
// URL fetches remote content over HTTP or HTTPS.
type URL struct {
Raw string
Header http.Header
HTTPClient *http.Client
}
func defaultClient() *http.Client {
return &http.Client{Timeout: 30 * time.Second}
}
// Open performs a GET request and returns the response body.
func (u URL) Open(ctx context.Context) (io.ReadCloser, error) {
client := u.HTTPClient
if client == nil {
client = defaultClient()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.Raw, nil)
if err != nil {
return nil, fmt.Errorf("source url: new request: %w", err)
}
if u.Header != nil {
req.Header = u.Header.Clone()
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("source url %q: %w", u.Raw, err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
_ = resp.Body.Close()
return nil, fmt.Errorf("source url %q: status %s", u.Raw, resp.Status)
}
return resp.Body, nil
}
func (u URL) String() string {
return u.Raw
}
// JoinNames joins source labels for error messages.
func JoinNames(srcs []Source) string {
if len(srcs) == 0 {
return ""
}
parts := make([]string, 0, len(srcs))
for _, s := range srcs {
if s != nil {
parts = append(parts, s.String())
}
}
return strings.Join(parts, ", ")
}