Files
go-config/env/options.go
eSlider 719fccf712 feat(env): add nestedUnderJQPath for extracting nested subtrees by jq-style paths
Introduce `nestedUnderJQPath`, enabling subtree extraction using jq-style path parsing. Add associated utility functions (`splitJQPath`, `peelRedundantPathLeaf`, `buildNestedTreeAtPath`) and test coverage. Extend `.env Codec` to support path-specific subtree merges. Update `WithFile` to accept an optional jqPath for targeted merges.
2026-05-12 13:54:27 +01:00

117 lines
3.5 KiB
Go

package env
import (
"context"
"io"
"net/http"
"os"
"github.com/eslider/go-config/internal/keymap"
"github.com/eslider/go-config/internal/merge"
"github.com/eslider/go-config/internal/source"
"github.com/go-viper/mapstructure/v2"
)
// Option configures a Codec.
type Option func(*Codec)
// WithCurrentEnvironment appends the process environment as a source (read at Map time).
func WithCurrentEnvironment() Option {
return func(c *Codec) {
c.layers = append(c.layers, loadedLayer{
load: func(_ context.Context) (map[string]string, error) {
return flatFromEnviron(os.Environ()), nil
},
})
}
}
// WithFile appends a dotenv file path as a source.
// Optional jqPath selects only that subtree before merging (jq-style path, e.g. ".service"):
// only the object at that path is merged into the config at the same path (its fields,
// not a scalar binding for the whole branch). Keys outside that path in the file are
// ignored. A redundant single-key wrapper repeating the path leaf (e.g. service.service.*)
// is flattened so fields merge directly under service.
func WithFile(path string, jqPath ...string) Option {
jp := ""
if len(jqPath) > 0 {
jp = jqPath[0]
}
return withSourceAtJQ(source.File{Path: path}, path, jp)
}
// WithBytes appends raw dotenv bytes as a source.
func WithBytes(b []byte) Option {
return withSource(source.Bytes{Data: b, Name: "bytes"}, "bytes")
}
// WithReader appends dotenv content from r (read fully on each Map call).
func WithReader(r io.Reader) Option {
return withSource(source.Reader{R: r, Name: "reader"}, "reader")
}
// WithURL appends an HTTP(S) URL returning dotenv content. Use WithHTTPHeader /
// WithHTTPClient before WithURL so they apply to this request.
func WithURL(raw string) Option {
return func(c *Codec) {
var hdr http.Header
if c.urlHeader != nil {
hdr = c.urlHeader.Clone()
}
s := source.URL{Raw: raw, Header: hdr, HTTPClient: c.httpClient}
withSource(s, raw)(c)
}
}
// WithHTTPClient sets the HTTP client used by subsequent WithURL sources.
func WithHTTPClient(client *http.Client) Option {
return func(c *Codec) { c.httpClient = client }
}
// WithHTTPHeader adds a header for subsequent WithURL sources.
func WithHTTPHeader(k, v string) Option {
return func(c *Codec) {
if c.urlHeader == nil {
c.urlHeader = make(http.Header)
}
c.urlHeader.Add(k, v)
}
}
// WithPrefix strips prefix from variable names before path splitting.
func WithPrefix(prefix string) Option {
return func(c *Codec) { c.prefix = prefix }
}
// WithKeyNormalizer sets key normalizer after merge (nil disables).
func WithKeyNormalizer(n keymap.Normalizer) Option {
return func(c *Codec) { c.normalizer = n }
}
// WithSliceMerge sets a slice merge strategy when merging sources.
func WithSliceMerge(s merge.SliceStrategy) Option {
return func(c *Codec) { c.sliceStrat = s }
}
// WithTrim toggles struct string trim via mapstructure hook.
func WithTrim(enable bool) Option {
return func(c *Codec) { c.structOpts.Trim = enable }
}
// WithWeaklyTyped toggles weak typing for struct decode.
func WithWeaklyTyped(enable bool) Option {
return func(c *Codec) { c.structOpts.WeaklyTyped = enable }
}
// WithTagName sets the struct tag name for mapstructure.
func WithTagName(name string) Option {
return func(c *Codec) { c.structOpts.TagName = name }
}
// WithDecodeHook appends a decode hook.
func WithDecodeHook(h mapstructure.DecodeHookFunc) Option {
return func(c *Codec) {
c.structOpts.ExtraHooks = append(c.structOpts.ExtraHooks, h)
}
}