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,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, ", ")
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestURL_OK(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("hello"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
u := URL{Raw: srv.URL}
|
||||
rc, err := u.Open(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
b, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "hello" {
|
||||
t.Fatalf("got %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestURL_StatusError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
u := URL{Raw: srv.URL}
|
||||
_, err := u.Open(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user