Documentation
¶
Overview ¶
Package recon is the entry point for runtime data — environment variables, .env files, configuration files (YAML / TOML / JSONC / JSON), command-line flags, standard input, in-memory buffers, programmatic defaults and overrides, and remote configuration stores (etcd / consul / vault / awsssm / k8s via separate adapter modules).
The name is a deliberate triple meaning, each accurate:
- reconnaissance: surveys the runtime environment on load and every reload.
- reconciliation: reconciles values across sources via the documented precedence chain and per-key aliases on every Get and Bind.
- reconfiguration: mutates the live registry (Set, AddSource, Reload, hot-watch) without re-instantiating.
Design ¶
- No global state. Every operation is on an explicit *Registry.
- Generics-first: Get, [Bind], Live, PerSourceFor.
- Live by default. Live gives a typed atomic-snapshot view that re-binds on every successful reload.
- Open seams. Codec, SchemaValidator, WatcherFactory, FlagAdapter, RemoteBackend, and Source are interfaces; one option replaces any default.
See the README at https://github.com/go-rotini/recon for runnable examples.
Index ¶
- Constants
- Variables
- func DetectFormat(path string) (string, bool)
- func DotTransform(p Path) string
- func FormatError(reg *Registry, err error, color ...bool) string
- func FormatErrorColor(reg *Registry, err error) string
- func Get[T any](r *Registry, key string) (T, bool, error)
- func IdentityTransform(p Path) string
- func KebabTransform(p Path) string
- func MustGet[T any](r *Registry, key string) T
- func SnakeUpperTransform(p Path) string
- type AliasCycleError
- type BackendWatcher
- type BufferOption
- type BufferSource
- type Codec
- type Codecs
- type CoercionError
- type Configs
- func (c *Configs) Close() error
- func (c *Configs) Events() <-chan NamedEvent
- func (c *Configs) Get(name string) (*Registry, bool)
- func (c *Configs) MustGet(name string) *Registry
- func (c *Configs) Names() []string
- func (c *Configs) Register(name string, r *Registry) error
- func (c *Configs) Remove(name string) error
- type DecodeOption
- func WithCustomDecoder[T any](fn func(Value) (T, error)) DecodeOption
- func WithDecodeContext(ctx context.Context) DecodeOption
- func WithDecodeErrorBehavior(b ErrorBehavior) DecodeOption
- func WithDecodeLenient() DecodeOption
- func WithDecodeStrict() DecodeOption
- func WithDecodeTag(name string) DecodeOption
- type DeprecationWarning
- type Description
- type EmptyValueError
- type EnvFamily
- type EnvOption
- type ErrorBehavior
- type Event
- type FSWatcher
- type FSWatcherOption
- type FieldTag
- type FileOption
- type FileSource
- type FileSourceFS
- type FlagAdapter
- type FlagOption
- type FlagSource
- type ImmutableChangedError
- type JSONSchemaValidator
- func NewJSONSchemaValidator(schemaJSON []byte) (*JSONSchemaValidator, error)
- func NewJSONSchemaValidatorFromSchema(s *jsonschema.Schema) *JSONSchemaValidator
- func NewJSONSchemaValidatorJSONC(schemaJSONC []byte) (*JSONSchemaValidator, error)
- func NewJSONSchemaValidatorTOML(schemaTOML []byte) (*JSONSchemaValidator, error)
- func NewJSONSchemaValidatorYAML(schemaYAML []byte) (*JSONSchemaValidator, error)
- type KeyDescription
- type KeyTransform
- type Live
- type MapSource
- type MemoryBackend
- func (m *MemoryBackend) Close() error
- func (m *MemoryBackend) Delete(key string)
- func (m *MemoryBackend) Get(_ context.Context, key string) (string, bool, error)
- func (m *MemoryBackend) List(_ context.Context, prefix string) ([]string, error)
- func (m *MemoryBackend) Put(key, value string)
- func (m *MemoryBackend) PutAll(kv map[string]string)
- func (m *MemoryBackend) Snapshot() map[string]string
- func (m *MemoryBackend) Watch(ctx context.Context) (<-chan struct{}, error)
- type MergeStrategy
- type MissingRequiredError
- type MultiError
- type NamedEvent
- type OSEnvSource
- type Option
- func WithCodec(c Codec) Option
- func WithCodecs(cs *Codecs) Option
- func WithErrorBehavior(b ErrorBehavior) Option
- func WithEventBufferSize(n int) Option
- func WithLenient() Option
- func WithLogger(l *slog.Logger) Option
- func WithMerge(strategy MergeStrategy) Option
- func WithPoll(interval time.Duration) Option
- func WithPrecedence(order ...string) Option
- func WithReloadDebounce(d time.Duration) Option
- func WithSchema(schemaJSON []byte) Option
- func WithSecretRedactor(fn func(string) string) Option
- func WithSource(s Source) Option
- func WithSources(s ...Source) Option
- func WithStrict() Option
- func WithValidator(v SchemaValidator) Option
- func WithWatcher(w WatcherFactory) Option
- func WithoutCodec(name string) Option
- type ParseError
- type Path
- type PerSource
- type PollWatcher
- type Position
- type RawValue
- type Registry
- func (r *Registry) AddSource(s Source) error
- func (r *Registry) AllKeys() []string
- func (r *Registry) Bind(target any, opts ...DecodeOption) error
- func (r *Registry) BindContext(ctx context.Context, target any, opts ...DecodeOption) error
- func (r *Registry) Close() error
- func (r *Registry) Describe() Description
- func (r *Registry) DescribeKey(key string) (KeyDescription, bool)
- func (r *Registry) DrainWarnings() []DeprecationWarning
- func (r *Registry) Events() <-chan Event
- func (r *Registry) GenerateTemplate(format string, opts ...SaveOption) ([]byte, error)
- func (r *Registry) Get(key string) (Value, bool, error)
- func (r *Registry) GetAny(key string) (any, bool, error)
- func (r *Registry) GetBool(key string) (bool, bool, error)
- func (r *Registry) GetDuration(key string) (time.Duration, bool, error)
- func (r *Registry) GetFloat(key string) (float64, bool, error)
- func (r *Registry) GetInt(key string) (int, bool, error)
- func (r *Registry) GetInt64(key string) (int64, bool, error)
- func (r *Registry) GetPath(p Path) (Value, bool, error)
- func (r *Registry) GetString(key string) (string, bool, error)
- func (r *Registry) GetStringMap(key string) (map[string]string, bool, error)
- func (r *Registry) GetStringSlice(key string) ([]string, bool, error)
- func (r *Registry) GetTime(key string) (time.Time, bool, error)
- func (r *Registry) InsertSource(at int, s Source) error
- func (r *Registry) IsImmutable(key string) bool
- func (r *Registry) IsSecret(key string) bool
- func (r *Registry) IsSet(key string) bool
- func (r *Registry) MarkSecret(key string)
- func (r *Registry) PinSource(key, sourceName string) error
- func (r *Registry) Prefix() Path
- func (r *Registry) RegisterAlias(alias, canonical string) error
- func (r *Registry) Reload() error
- func (r *Registry) ReloadContext(ctx context.Context) error
- func (r *Registry) RemoveSource(name string) error
- func (r *Registry) Save(w io.Writer, opts ...SaveOption) error
- func (r *Registry) SaveString(opts ...SaveOption) (string, error)
- func (r *Registry) SaveTo(path string, opts ...SaveOption) error
- func (r *Registry) Set(key string, value any) error
- func (r *Registry) SetDefault(key string, value any) error
- func (r *Registry) Snapshot() *Snapshot
- func (r *Registry) Sources() []string
- func (r *Registry) Sub(prefix string) *Registry
- func (r *Registry) TemplateKeys(opts ...SaveOption) []Path
- func (r *Registry) Unmarshal(target any, opts ...DecodeOption) error
- func (r *Registry) UnmarshalKey(key string, target any, opts ...DecodeOption) error
- func (r *Registry) Unpin(key string) error
- func (r *Registry) Unset(key string) error
- func (r *Registry) Validate() error
- func (r *Registry) Validator() SchemaValidator
- type RemoteBackend
- type RemoteOption
- type RemoteSource
- func (s *RemoteSource) Close() error
- func (s *RemoteSource) Get(path Path) (Value, bool, error)
- func (s *RemoteSource) Keys() []Path
- func (s *RemoteSource) Name() string
- func (s *RemoteSource) Refresh(ctx context.Context) error
- func (s *RemoteSource) Watch(ctx context.Context) (<-chan SourceChange, error)
- type SaveOption
- type SchemaValidator
- type Secret
- type Snapshot
- type Source
- func NewBufferSource(name, format string, data []byte, opts ...BufferOption) (Source, error)
- func NewDotenvSource(path string, opts ...FileOption) (Source, error)
- func NewEnvFamilySource(name string, families ...EnvFamily) Source
- func NewFileSource(path string, opts ...FileOption) (Source, error)
- func NewFileSourceFS(name string, fsys fs.FS, path string, opts ...FileOption) (Source, error)
- func NewJSONCSource(path string, opts ...FileOption) (Source, error)
- func NewJSONSource(path string, opts ...FileOption) (Source, error)
- func NewStdinSource(format string, opts ...StdinOption) (Source, error)
- func NewTOMLSource(path string, opts ...FileOption) (Source, error)
- func NewYAMLSource(path string, opts ...FileOption) (Source, error)
- type SourceChange
- type SourceError
- type StdinOption
- type UnknownKeyError
- type Unmarshaler
- type ValidationError
- type Validator
- type ValidatorContext
- type Value
- func (v Value) Any() any
- func (v Value) AsBool() (bool, error)
- func (v Value) AsDuration() (time.Duration, error)
- func (v Value) AsFloat64() (float64, error)
- func (v Value) AsInt64() (int64, error)
- func (v Value) AsMap() (map[string]Value, error)
- func (v Value) AsRaw() (RawValue, error)
- func (v Value) AsSlice() ([]Value, error)
- func (v Value) AsString() (string, error)
- func (v Value) AsTime() (time.Time, error)
- func (v Value) IsZero() bool
- func (v Value) Kind() ValueKind
- func (v Value) Source() string
- func (v Value) String() string
- type ValueKind
- type ValueSource
- type Watcher
- type WatcherFactory
Examples ¶
Constants ¶
const ( FormatYAML = "yaml" FormatTOML = "toml" FormatJSON = "json" FormatJSONC = "jsonc" FormatDotenv = "dotenv" )
Canonical codec names. A Codec.Name implementation should use one of these strings when implementing a well-known format so a custom codec shadows the bundled default by name.
const DefaultDelimiter = "."
DefaultDelimiter is the path-segment delimiter ParsePath consumes and Path.String emits.
const TagName = "recon"
TagName is the default struct tag the decoder reads. Override per-call via WithDecodeTag. When the primary tag is absent on a field, the decoder falls back through env / json / yaml / toml in that order so structs from those ecosystems bind without re-tagging.
Variables ¶
var ( // ErrKeyNotFound is returned when a lookup finds no value and no // default applies. ErrKeyNotFound = errors.New("recon: key not found") // ErrTypeMismatch is returned when a [Value]'s wire kind does not // match the type a caller requested via an As* accessor. ErrTypeMismatch = errors.New("recon: type mismatch") // ErrMissingRequired is returned when a key tagged required has no // value supplied by any source and no default. ErrMissingRequired = errors.New("recon: missing required value") // ErrEmptyValue is returned when a key tagged notEmpty resolved to // the empty string. ErrEmptyValue = errors.New("recon: empty value") // ErrUnknownKey is returned in strict mode when a source supplies // a key the bind target does not declare. ErrUnknownKey = errors.New("recon: unknown key (strict mode)") // ErrImmutableChanged is returned when a reload would change a // key tagged immutable. ErrImmutableChanged = errors.New("recon: immutable key changed") // ErrCoercion is returned when a wire value cannot be converted // to the requested Go type. ErrCoercion = errors.New("recon: coercion failed") // ErrReadOnlySource is returned when a write is attempted against // a read-only source. ErrReadOnlySource = errors.New("recon: source is read-only") // ErrAliasCycle is returned when [Registry.RegisterAlias] would // create a cycle. ErrAliasCycle = errors.New("recon: alias cycle") // ErrInvalidPath is returned when a path argument is malformed. ErrInvalidPath = errors.New("recon: invalid path") // ErrUnsupportedFormat is returned when no registered codec // matches a requested format or file extension. ErrUnsupportedFormat = errors.New("recon: unsupported format") // ErrValidation is returned when a [SchemaValidator] reports // failure. ErrValidation = errors.New("recon: validation failed") // ErrSourceConflict is returned when [Registry.AddSource] would // introduce a duplicate name. ErrSourceConflict = errors.New("recon: source name conflict") // ErrRegistryClosed is returned by operations on a Closed // registry. ErrRegistryClosed = errors.New("recon: registry closed") // ErrNilContext is returned when a context-taking call receives // nil. ErrNilContext = errors.New("recon: nil context") // ErrSchemaInvalid is returned when a supplied schema fails to // compile. ErrSchemaInvalid = errors.New("recon: schema invalid") )
Sentinel errors. Concrete error types in this package wrap one of these so callers can use errors.Is for classification.
Functions ¶
func DetectFormat ¶
DetectFormat returns the canonical codec name for path's extension. Case-insensitive; only the trailing extension is examined (so "config.local.yaml" resolves to FormatYAML).
func DotTransform ¶
DotTransform projects Path{"server","port"} to "server.port" — the identity projection for file sources whose storage key already matches the recon path.
func FormatError ¶
FormatError renders err into a multi-line, human-readable summary. Entries in a *MultiError become separate lines; typed errors that carry a Path are formatted with their path leading and source-provenance trailing in parentheses.
reg is optional. When non-nil, FormatError consults its current snapshot to surface the precedence chain alongside each failing path. Returns "" when err is nil so it composes cleanly with log.Println.
Pass color=true to opt into ANSI colorization. Callers that always want color can use FormatErrorColor.
Example ¶
ExampleFormatError renders a multi-error from Bind into a single human-readable summary suitable for direct printing.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
type Config struct {
Port int `recon:"port,required"`
Name string `recon:"name,required"`
}
r, _ := recon.New()
defer func() { _ = r.Close() }()
var c Config
err := r.Bind(&c)
// FormatError prints one bullet per error in the order Bind
// surfaced them — Bind walks fields in declaration order, which
// is stable but uninteresting for the example. Use the unordered
// directive so the comparison sorts lines first.
fmt.Println(recon.FormatError(r, err))
}
Output: recon: 2 errors: • name: missing required value • port: missing required value
func FormatErrorColor ¶
FormatErrorColor is FormatError with ANSI colorization always on.
func Get ¶
Get is the generic typed accessor. Supported T: string, bool, int, int64, float64, time.Time, time.Duration, []string, Value. Unsupported types return wrapped ErrTypeMismatch; callers wanting struct binding should use Registry.Bind.
func IdentityTransform ¶
IdentityTransform is an alias for DotTransform, named to make "this source's keys are already recon-shaped" explicit at call sites.
func KebabTransform ¶
KebabTransform projects Path{"server","port"} to "server-port". The default for CLI-flag sources.
func MustGet ¶
MustGet panics on error or not-set. Useful in main() when a missing key is a programmer error.
func SnakeUpperTransform ¶
SnakeUpperTransform projects Path{"server","port"} to "SERVER_PORT". The default for env-backed sources. Path segments keep their internal characters; only the separator is replaced with "_" and the whole result is uppercased.
Types ¶
type AliasCycleError ¶
type AliasCycleError struct {
Chain []Path
}
AliasCycleError reports that Registry.RegisterAlias would create a cycle. Chain lists the offending alias chain in walk order.
func (*AliasCycleError) Error ¶
func (e *AliasCycleError) Error() string
func (*AliasCycleError) Is ¶
func (e *AliasCycleError) Is(target error) bool
type BackendWatcher ¶
BackendWatcher is the optional RemoteBackend capability for push-style notification. Pull-only backends omit it and the wrapping RemoteSource polls instead. The channel signals "something changed"; backends may coalesce. Watch must close the channel when ctx cancels.
type BufferOption ¶
type BufferOption func(*bufferOptions)
BufferOption configures NewBufferSource.
func WithBufferCodec ¶
func WithBufferCodec(c Codec) BufferOption
WithBufferCodec pins the codec for NewBufferSource.
type BufferSource ¶
type BufferSource struct {
*MapSource
// contains filtered or unexported fields
}
BufferSource is a Source backed by bytes plus a Codec. The bytes are decoded once at construction; subsequent Get / Keys calls read from the decoded map. Useful for tests, in-process bytes, and the "stdin was piped as YAML" pattern.
func (*BufferSource) Codec ¶
func (s *BufferSource) Codec() Codec
Codec returns the codec used to decode this source's bytes.
func (*BufferSource) Format ¶
func (s *BufferSource) Format() string
Format returns the format hint passed at construction.
type Codec ¶
type Codec interface {
Name() string
Extensions() []string
Decode(data []byte) (map[string]any, error)
Encode(v map[string]any) ([]byte, error)
}
Codec parses and serializes a single file format. The bundled YAML / TOML / JSONC / JSON / Dotenv values implement it; users plug in third-party parsers by implementing the same shape.
Decode returns a nested map[string]any whose leaves are limited to string, bool, int64, float64, time.Time, []any, map[string]any, or nil. Codecs are responsible for widening native numeric types and for converting implementation-specific types (yaml.Node, etc.) at the boundary.
Encode is the inverse and should be byte-stable for the same input (deterministic key ordering, no whitespace drift) so round-trips reproduce.
Name is the canonical lowercase identifier used by Codecs.Register and WithFileFormat. Extensions is the lowercased set (including the leading dot) consulted by Codecs.ByExtension.
var Dotenv Codec = dotenvCodec{}
Dotenv is the Codec for `.env` files. Registered in the default codec set by New.
var JSON Codec = jsonCodec{}
JSON is the Codec for application/json. Registered in the default codec set by New.
var JSONC Codec = jsoncCodec{}
JSONC is the Codec for JSONC / JSON5 documents. Registered in the default codec set by New.
type Codecs ¶
type Codecs struct {
// contains filtered or unexported fields
}
Codecs is a registry of Codec values. The zero value is unusable; construct with NewCodecs. Safe for concurrent use.
Registration is keyed by Codec.Name: a Register with a duplicate name replaces the prior entry, letting a user-supplied codec shadow a bundled default.
func DefaultCodecs ¶
func DefaultCodecs() *Codecs
DefaultCodecs returns a fresh Codecs registry populated with the bundled format codecs (YAML, TOML, JSONC, JSON, Dotenv). Each call returns an independent registry; callers that need shared state should obtain one and pass it around.
func NewCodecs ¶
NewCodecs returns a Codecs pre-populated with initial. Later entries with the same Name shadow earlier ones.
func (*Codecs) ByExtension ¶
ByExtension returns the codec whose Codec.Extensions includes ext (case-insensitive, ext should include the leading dot). On miss, the package-wide DetectFormat fallback is consulted.
func (*Codecs) Clone ¶
Clone returns an independent shallow copy. The codecs themselves are shared (stateless by contract); the lookup map is fresh.
func (*Codecs) Unregister ¶
Unregister removes the codec named name. Unknown names are ignored.
type CoercionError ¶
type CoercionError struct {
Path Path
Source string
WireType string
Target string
Cause error
Secret bool
}
CoercionError reports that a wire value could not be converted to the target Go type. When Secret is true, Cause is suppressed from the rendered output so the offending value never leaves the registry.
func (*CoercionError) Error ¶
func (e *CoercionError) Error() string
func (*CoercionError) Is ¶
func (e *CoercionError) Is(target error) bool
func (*CoercionError) Unwrap ¶
func (e *CoercionError) Unwrap() error
type Configs ¶
type Configs struct {
// contains filtered or unexported fields
}
Configs is a set of named Registry instances. Use when an application has multiple independent configuration namespaces with their own precedence, schema, or watch policy.
Safe for concurrent use. Closing a Configs closes every contained Registry.
func (*Configs) Close ¶
Close closes every contained registry. Idempotent. Returns a *MultiError aggregating per-registry Close errors.
The multiplex engine is stopped before registries close so forwarders observe ctx cancellation rather than racing closed source channels.
func (*Configs) Events ¶
func (c *Configs) Events() <-chan NamedEvent
Events returns a multiplexed channel carrying every contained registry's events, tagged by name. The channel is created on the first call and reused on subsequent calls.
Registries added via [Register] after Events is called are folded in automatically; those removed via [Remove] or closed externally have their forwarder shut down. Closed by [Close]. Returns nil on a closed Configs.
func (*Configs) Register ¶
Register attaches r under name. Returns wrapped ErrSourceConflict when the name is taken or empty, ErrInvalidPath when r is nil.
If [Events] has already been called, the new registry is folded into the multiplexed stream automatically.
type DecodeOption ¶
type DecodeOption func(*decodeOptions)
DecodeOption configures one Registry.Bind / Registry.Unmarshal call. Registry-level options supply the defaults; DecodeOption is for per-call overrides.
func WithCustomDecoder ¶
func WithCustomDecoder[T any](fn func(Value) (T, error)) DecodeOption
WithCustomDecoder registers a per-call decoder for type T. When a bound field has Go type T, fn receives the resolved Value in place of the built-in coercion.
func WithDecodeContext ¶
func WithDecodeContext(ctx context.Context) DecodeOption
WithDecodeContext threads ctx through the decode pass, available to ValidatorContext hooks the bind target may implement.
func WithDecodeErrorBehavior ¶
func WithDecodeErrorBehavior(b ErrorBehavior) DecodeOption
WithDecodeErrorBehavior overrides the registry's error-accumulation behavior for this call.
func WithDecodeLenient ¶
func WithDecodeLenient() DecodeOption
WithDecodeLenient turns off strict decoding for this call.
func WithDecodeStrict ¶
func WithDecodeStrict() DecodeOption
WithDecodeStrict turns on strict decoding for this call.
func WithDecodeTag ¶
func WithDecodeTag(name string) DecodeOption
WithDecodeTag changes which struct tag the decoder inspects. Default "recon"; the decoder falls back through env / json / yaml / toml when the primary tag is absent.
type DeprecationWarning ¶
DeprecationWarning is a non-fatal notice that a `deprecated`-tagged key was read. Delivered on Event.Warnings and via Registry.DrainWarnings. Replacement is empty unless the `deprecated=` tag option named one.
func (DeprecationWarning) String ¶
func (w DeprecationWarning) String() string
type Description ¶
type Description struct {
Keys []KeyDescription
}
Description is the structured snapshot view Registry.Describe returns. Keys are sorted by canonical path; alias paths are not listed as separate rows but appear on their target's Aliases field.
type EmptyValueError ¶
EmptyValueError reports that a notEmpty key resolved to "".
func (*EmptyValueError) Error ¶
func (e *EmptyValueError) Error() string
func (*EmptyValueError) Is ¶
func (e *EmptyValueError) Is(target error) bool
type EnvFamily ¶ added in v1.0.2
type EnvFamily struct {
// Target is the recon path the assembled family binds to.
Target Path
// Base is the variable-name prefix that introduces the family.
Base string
// Separator joins Base to each nested segment (and the segments to one
// another). An empty Separator disables the family.
Separator string
}
EnvFamily describes one nested group of environment variables. Every variable named Base+Separator+K1[+Separator+K2...] is collected into a nested map[string]any rooted at Target: with Base "ACME_HTTP" and Separator "__", ACME_HTTP__RETRY__MAX=9 contributes {retry: {max: "9"}}. Segments are lowercased; values stay strings.
type EnvOption ¶
type EnvOption func(*envOptions)
EnvOption configures NewOSEnvSource.
func WithEnvKeyParser ¶
WithEnvKeyParser overrides the env-var-name → Path projection used by OSEnvSource.Keys. The parser receives the name with any configured prefix already stripped; returning an empty Path skips the variable. Nil is silently ignored.
func WithEnvPrefix ¶
WithEnvPrefix limits an OSEnvSource to env vars whose name starts with prefix. The default transform then projects "server.port" to "<prefix>SERVER_PORT".
func WithEnvTransform ¶
func WithEnvTransform(fn KeyTransform) EnvOption
WithEnvTransform overrides the default path → env-var-name projection. Pair with WithEnvKeyParser for the inverse used by OSEnvSource.Keys. Nil is silently ignored.
func WithEnvVars ¶ added in v1.0.2
WithEnvVars pins explicit environment-variable names for specific recon paths, in both directions: a pinned path projects to exactly the given variable name (exempt from any WithEnvPrefix) and that variable parses back to the path during OSEnvSource.Keys enumeration. Non-pinned paths keep the default snake-upper (prefix-aware) projection.
vars maps a canonical recon path string (see Path.String) to a variable name, e.g. {"token": "WIDGET_TOKEN"}. A nil or empty map is ignored. Repeated calls merge.
type ErrorBehavior ¶
type ErrorBehavior int
ErrorBehavior controls how Registry.Bind / Registry.Unmarshal accumulates per-field errors. FailCollect (the default) surfaces every problem at once; FailFast stops at the first.
const ( // FailCollect aggregates every per-field error into a [*MultiError]. FailCollect ErrorBehavior = iota // FailFast stops decoding at the first per-field error. FailFast )
type Event ¶
type Event struct {
// Time is the wall-clock time the reload attempt completed.
Time time.Time
// Source is the name of the source whose change triggered the
// reload. Empty for manual reloads via [Registry.Reload].
Source string
// Changed lists paths whose resolved value differs from the previous
// snapshot, sorted by canonical path string. Empty on failure.
Changed []Path
// Err is non-nil when the reload failed. Readers continue to observe
// the previous snapshot until the next successful reload.
Err error
// Warnings carries non-fatal notices (deprecations, dropped events)
// that should not invalidate the snapshot.
Warnings []DeprecationWarning
}
Event is delivered on the channel returned by Registry.Events. One Event corresponds to one reload attempt — successful or failed.
On success Err is nil and Changed lists the paths whose resolved value differs from the previous snapshot. On failure Err is set, Changed is empty, and the previous snapshot is retained.
type FSWatcher ¶
type FSWatcher struct {
// contains filtered or unexported fields
}
FSWatcher is the WatcherFactory backed by rotinifs.Watcher. It is the default factory installed by New. The zero value is unusable; construct via NewFSWatcher.
func NewFSWatcher ¶
func NewFSWatcher(opts ...FSWatcherOption) *FSWatcher
NewFSWatcher returns an FSWatcher configured by opts.
func (*FSWatcher) Watch ¶
Watch implements WatcherFactory. The returned channel emits a SourceChange for every observed fs event until ctx is canceled. Errors surface as a SourceChange with non-nil Err.
type FSWatcherOption ¶
type FSWatcherOption func(*FSWatcher)
FSWatcherOption configures an FSWatcher.
func WithFSWatcherDebounce ¶
func WithFSWatcherDebounce(d time.Duration) FSWatcherOption
WithFSWatcherDebounce sets the per-path debounce window applied by the underlying fs.Watcher.
func WithFSWatcherLogger ¶
func WithFSWatcherLogger(l *slog.Logger) FSWatcherOption
WithFSWatcherLogger threads a logger into the underlying fs.Watcher.
func WithFSWatcherPollInterval ¶
func WithFSWatcherPollInterval(d time.Duration) FSWatcherOption
WithFSWatcherPollInterval forces the fs.Watcher's polling backend at the supplied interval — useful when kernel notifications are unreliable or test timing must stay predictable.
type FieldTag ¶
type FieldTag struct {
// Name is the canonical key; empty means "use the field name."
Name string
// Skip is true when the tag is exactly "-".
Skip bool
// Path overrides the inferred path. Set via `path=server.port`.
Path string
// Source pins this field to a specific source by name. Set via
// `source=env`.
Source string
// Format hints that the field's raw value is a sub-document to
// decode. Set via `format=json`.
Format string
// Aliases lists additional paths that resolve to this field. Set
// via `aliases=a;b;c`.
Aliases []string
// Transform names a key-spelling transform: snake / kebab / camel
// / upper / lower. Set via `transform=`.
Transform string
// Inline, on an embedded struct, flattens the field-name prefix
// out of the path.
Inline bool
// Required: an absent value is an error.
Required bool
// NotEmpty: the resolved value must be non-empty.
NotEmpty bool
// HasDefault, DefaultValue: a `default=` option was supplied.
HasDefault bool
DefaultValue string
// Secret redacts in [Describe] / [Snapshot.String] / errors.
Secret bool
// Immutable: a reload must not change this field.
Immutable bool
// Expand applies ${VAR} expansion to the resolved value.
Expand bool
// FromFile: the resolved value is a path; load the file contents
// as the actual value.
FromFile bool
// Unset clears the source value after the field is read.
Unset bool
// Deprecated and DeprecationMessage: emit a [DeprecationWarning]
// on read.
Deprecated bool
DeprecationMessage string
// Validate carries a free-form expression for future
// CEL / struct-validator integration.
Validate string
// Layout is the time.Time parse layout. Set via `layout=`.
Layout string
// Base64 / Hex: byte-encoding decoders. Mutually exclusive.
Base64 bool
Hex bool
// Separator / KVSeparator govern string → slice / string → map
// splits. Empty fields fall back to "," and "=".
Separator string
KVSeparator string
}
FieldTag is the parsed form of a single struct-tag value. The grammar follows encoding/json:
recon:"name,opt1,opt2=value,opt3"
An empty Name means "use the Go field name".
type FileOption ¶
type FileOption func(*fileOptions)
FileOption configures FileSource / FileSourceFS and the format-named constructors.
func WithFileCodec ¶
func WithFileCodec(c Codec) FileOption
WithFileCodec pins the codec, bypassing extension-based resolution.
func WithFileFormat ¶
func WithFileFormat(name string) FileOption
WithFileFormat selects the codec by registered name. Equivalent to WithFileCodec but useful when the codec value is not in scope.
func WithFileWatcher ¶
func WithFileWatcher(w WatcherFactory) FileOption
WithFileWatcher overrides the registry-wide WatcherFactory for this source only.
func WithOptional ¶
func WithOptional(optional bool) FileOption
WithOptional treats a missing file as a no-op rather than an error. Useful for `.env.local` / `.config.local.yaml` overrides.
func WithPathExpansion ¶
func WithPathExpansion(enabled bool) FileOption
WithPathExpansion controls POSIX-shell expansion of paths (~, $VAR, ${VAR:-default}, ${VAR:?msg}, ${VAR:+alt}). Default true.
func WithSearchPaths ¶
func WithSearchPaths(dirs ...string) FileOption
WithSearchPaths makes FileSource look for the filename in each directory in order; first existing file wins. The constructor's primary path argument supplies the filename.
type FileSource ¶
type FileSource struct {
*MapSource
// contains filtered or unexported fields
}
FileSource is a codec-driven Source backed by a single file on the local filesystem. The file is read at construction and decoded; later Get calls read from the decoded map.
Pair with WithFileCodec / WithSearchPaths / WithPathExpansion / WithOptional to express "look in N directories, expand ~, treat missing as no-op" in one constructor.
FileSource implements Watcher when a WatcherFactory is available — either set per-source via WithFileWatcher or injected by the registry from WithWatcher.
func (*FileSource) Format ¶
func (s *FileSource) Format() string
Format returns the canonical codec name driving this source's decode.
func (*FileSource) Path ¶
func (s *FileSource) Path() string
Path returns the absolute, expanded path the source reads from.
func (*FileSource) Reload ¶
func (s *FileSource) Reload() error
Reload re-reads the file and atomically swaps the underlying map. On a missing-file outcome with WithOptional, the source is emptied without error. Decode failures retain the existing contents.
func (*FileSource) SetWatcher ¶
func (s *FileSource) SetWatcher(w WatcherFactory)
SetWatcher attaches a WatcherFactory after construction. Used by the registry to inject its registry-wide factory. Has no effect on a running subscription.
func (*FileSource) Watch ¶
func (s *FileSource) Watch(ctx context.Context) (<-chan SourceChange, error)
Watch returns a SourceChange channel for the underlying file. A nil factory yields a closed channel so the optional-watch contract on Source stays satisfiable.
Each upstream notification triggers a [Reload], then forwards a SourceChange downstream.
type FileSourceFS ¶
type FileSourceFS struct {
*MapSource
// contains filtered or unexported fields
}
FileSourceFS is the io/fs.FS-backed twin of FileSource. Useful for shipping a default config embedded in the binary and overlaying a user-supplied file on top via precedence.
Read-only: no [Reload], no Watcher — embedded files don't change.
func (*FileSourceFS) Format ¶
func (s *FileSourceFS) Format() string
Format returns the canonical codec name driving this source's decode.
func (*FileSourceFS) Path ¶
func (s *FileSourceFS) Path() string
Path returns the in-fs.FS path the source reads from.
type FlagAdapter ¶
type FlagAdapter interface {
// Names returns the names of flags the user explicitly set, in
// any order. The returned slice may alias the adapter's storage;
// [FlagSource] never mutates it.
Names() []string
// Lookup returns the value associated with the named flag. set is
// false for flags whose value came from a compile-time default.
Lookup(name string) (value any, set bool)
}
FlagAdapter is the seam between recon and a command-line-flag parser. recon does not pick a parser — the stdlib `flag` package or any third-party library can satisfy the interface from the caller's side.
An adapter must distinguish flags the user explicitly set from those still holding compile-time defaults. Flags occupy the second-highest precedence layer, so reporting an unset flag would shadow lower-precedence sources unconditionally.
Example ¶
ExampleFlagAdapter demonstrates implementing the FlagAdapter interface against a tiny argv-parser shim. Real callers wrap their library of choice — stdlib flag, pflag, kong, rotini — in this same shape.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
parsed := exampleFlags{set: map[string]any{"port": 9000}}
flags, err := recon.NewFlagSource(parsed)
if err != nil {
panic(err)
}
r, _ := recon.New(recon.WithSource(flags))
defer func() { _ = r.Close() }()
v, _, _ := r.GetInt("port")
fmt.Println("port:", v)
}
// exampleFlags is the tiny FlagAdapter shim ExampleFlagAdapter
// drives. A real adapter would query its parser library for the
// "was this flag set?" signal.
type exampleFlags struct{ set map[string]any }
func (e exampleFlags) Names() []string {
out := make([]string, 0, len(e.set))
for k := range e.set {
out = append(out, k)
}
return out
}
func (e exampleFlags) Lookup(name string) (any, bool) {
v, ok := e.set[name]
return v, ok
}
Output: port: 9000
type FlagOption ¶
type FlagOption func(*flagOptions)
FlagOption configures NewFlagSource.
func WithFlagName ¶
func WithFlagName(name string) FlagOption
WithFlagName overrides the default "flags" source name. Useful when composing multiple flag adapters into one registry.
func WithFlagPathTransform ¶
func WithFlagPathTransform(fn func(flagName string) Path) FlagOption
WithFlagPathTransform replaces the default flag-name → Path transform. Useful when "--server-port" should resolve to the path "server.port" rather than the single-segment "server-port".
type FlagSource ¶
type FlagSource struct {
// contains filtered or unexported fields
}
FlagSource is a Source backed by a FlagAdapter. Pair with WithFlagPathTransform to map flag names like "--server-port" onto recon paths like "server.port".
func NewFlagSource ¶
func NewFlagSource(adapter FlagAdapter, opts ...FlagOption) (*FlagSource, error)
NewFlagSource constructs a FlagSource backed by adapter. Default name is "flags"; override via WithFlagName when composing multiple adapters into one registry. Returns wrapped ErrInvalidPath when adapter is nil.
func (*FlagSource) Get ¶
func (s *FlagSource) Get(path Path) (Value, bool, error)
Get matches path against the post-transform Path of each flag the adapter reports as set. Never returns an error.
func (*FlagSource) Keys ¶
func (s *FlagSource) Keys() []Path
Keys returns the explicitly-set flags projected to recon paths, sorted by canonical string.
type ImmutableChangedError ¶
ImmutableChangedError reports that a reload would change a key tagged immutable. Old and New are pre-redacted when the key is also tagged secret.
func (*ImmutableChangedError) Error ¶
func (e *ImmutableChangedError) Error() string
func (*ImmutableChangedError) Is ¶
func (e *ImmutableChangedError) Is(target error) bool
type JSONSchemaValidator ¶
type JSONSchemaValidator struct {
// contains filtered or unexported fields
}
JSONSchemaValidator is the bundled SchemaValidator backed by go-rotini/jsonschema. The schema is compiled once at construction; per-snapshot validation is then cheap and lock-free.
Example ¶
ExampleJSONSchemaValidator wires a JSON Schema into the registry so every reload is validated against it. Validation failures retain the previous snapshot; the registry keeps running.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
schema := []byte(`{
"type": "object",
"properties": {
"port": {"type": "integer", "minimum": 1, "maximum": 65535}
},
"required": ["port"]
}`)
validator, err := recon.NewJSONSchemaValidator(schema)
if err != nil {
panic(err)
}
r, _ := recon.New(recon.WithValidator(validator))
defer func() { _ = r.Close() }()
_ = r.Set("port", 8080)
if err := r.Reload(); err != nil {
fmt.Println("invalid:", err)
} else {
fmt.Println("valid")
}
}
Output: valid
func NewJSONSchemaValidator ¶
func NewJSONSchemaValidator(schemaJSON []byte) (*JSONSchemaValidator, error)
NewJSONSchemaValidator compiles schemaJSON and returns a validator for WithValidator. Returns a wrapped ErrSchemaInvalid on compile failure.
func NewJSONSchemaValidatorFromSchema ¶
func NewJSONSchemaValidatorFromSchema(s *jsonschema.Schema) *JSONSchemaValidator
NewJSONSchemaValidatorFromSchema wraps an already-compiled jsonschema.Schema. Use when the caller assembled the schema with custom CompileOptions (remote $ref loaders, draft pinning).
func NewJSONSchemaValidatorJSONC ¶
func NewJSONSchemaValidatorJSONC(schemaJSONC []byte) (*JSONSchemaValidator, error)
NewJSONSchemaValidatorJSONC compiles a JSONC-encoded schema.
func NewJSONSchemaValidatorTOML ¶
func NewJSONSchemaValidatorTOML(schemaTOML []byte) (*JSONSchemaValidator, error)
NewJSONSchemaValidatorTOML compiles a TOML-encoded schema.
func NewJSONSchemaValidatorYAML ¶
func NewJSONSchemaValidatorYAML(schemaYAML []byte) (*JSONSchemaValidator, error)
NewJSONSchemaValidatorYAML compiles a YAML-encoded schema.
func (*JSONSchemaValidator) Validate ¶
func (v *JSONSchemaValidator) Validate(snapshot map[string]any) error
Validate runs snapshot through the compiled schema. On failure, each constraint violation is translated into a *ValidationError and aggregated under a *MultiError. A nil snapshot is treated as the empty object.
type KeyDescription ¶
type KeyDescription struct {
// Path is the canonical key.
Path Path
// Value is the resolved value's string form, redacted via the
// registry's secret redactor when the key is marked secret.
Value string
// Source is the name of the source that won the precedence race.
// Empty when no source supplied the key.
Source string
// Sources lists every contributor in precedence order, including
// the reserved labels "explicit" and "default".
Sources []string
// Secret reports whether the key is marked secret.
Secret bool
// Aliases lists every alias path that resolves to this key.
Aliases []Path
// Schema carries a per-key schema fragment when the registered
// validator exposes one. Empty for the bundled JSON Schema
// validator.
Schema string
}
KeyDescription is one per-key row of a Description. Value is pre-redacted when Secret is true.
type KeyTransform ¶
KeyTransform projects a recon Path onto the flat string a source's underlying store uses. The same configuration key spells differently across sources:
Path{"server","port"} ↦ "SERVER_PORT" (env var)
Path{"server","port"} ↦ "server-port" (CLI flag)
Path{"server","port"} ↦ "server.port" (file source)
The bundled transforms cover the common cases. The reverse projection (flat string → Path) is transform-specific; sources that need it ship both directions in their wiring.
func SnakeUpperPrefixTransform ¶
func SnakeUpperPrefixTransform(prefix string) KeyTransform
SnakeUpperPrefixTransform returns a KeyTransform that prepends prefix to every path's SNAKE_UPPER form. An empty prefix is equivalent to SnakeUpperTransform.
type Live ¶
type Live[T any] struct { // contains filtered or unexported fields }
Live is a typed, atomic-snapshot view of a Registry-bound struct. Each successful reload atomic-swaps the *T pointer Live hands out, so Live.Get is lock-free and always observes a complete, validated configuration.
Construct via NewLive; close via Live.Close when done. Live spawns one goroutine that consumes Registry.Events until Close or until the parent's channel closes.
func NewLive ¶
func NewLive[T any](reg *Registry, opts ...DecodeOption) (*Live[T], error)
NewLive constructs a Live for T against reg. The initial bind runs synchronously; failure returns the error and no goroutine is spawned. After that, Live subscribes to reg.Events() and re-binds on each reload. opts is forwarded verbatim to every Registry.Bind call.
Example ¶
ExampleNewLive demonstrates the typed live-config pattern. The initial bind runs synchronously inside NewLive; subsequent reloads rebind atomically. Live.Get is a single atomic load — safe for hot paths.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
src := recon.NewMapSource("config", map[string]any{
"port": 8080,
"name": "rotini",
})
r, _ := recon.New(recon.WithSource(src))
defer func() { _ = r.Close() }()
type Config struct {
Port int `recon:"port"`
Name string `recon:"name"`
}
live, err := recon.NewLive[Config](r)
if err != nil {
panic(err)
}
defer func() { _ = live.Close() }()
cfg := live.Get() // *Config — atomic load
fmt.Printf("%s on :%d\n", cfg.Name, cfg.Port)
}
Output: rotini on :8080
func (*Live[T]) Events ¶
Events returns a buffered channel of every reload Event Live observed. Use it to surface reload failures alongside the live state. Closed by [Close] or when the parent's Events channel closes.
func (*Live[T]) Get ¶
func (l *Live[T]) Get() *T
Get returns the current bound *T. The cost is one atomic.Pointer.Load — no locks, no validator re-runs.
The returned pointer is the actual instance Live owns; callers must treat it as read-only. A concurrent reload may swap a new pointer in at any time.
type MapSource ¶
type MapSource struct {
// contains filtered or unexported fields
}
MapSource is a Source backed by a nested map[string]any matching what a config-file decoder produces. Read-only: programmatic writes go through Registry.Set.
func NewMapSource ¶
NewMapSource returns a MapSource named name holding a deep copy of m so later caller mutations do not affect the source. A nil m is treated as empty.
func (*MapSource) Get ¶
Get walks the nested map along path. Returns (value, true, nil) on hit; (Value{}, false, nil) when any intermediate is missing or a non-map. Never returns a non-nil error. An empty Path returns (false, nil).
type MemoryBackend ¶
type MemoryBackend struct {
// contains filtered or unexported fields
}
MemoryBackend is the reference RemoteBackend implementation shipped for tests and local prototyping. Production backends (etcd, consul, vault) live in separate adapter modules.
Implements BackendWatcher so RemoteSource sees a push-style notification path; [Put] / [PutAll] / [Delete] fire every active subscription. Safe for concurrent use.
Example ¶
ExampleMemoryBackend demonstrates the in-memory RemoteBackend reference impl. Real adapters (etcd, consul, vault, …) live in separate sub-modules; the in-memory backend is for tests and for prototyping the remote-source plumbing.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
backend := recon.NewInMemoryBackend()
backend.Put("app/port", "8080")
backend.Put("app/host", "localhost")
src, err := recon.NewRemoteSource("remote", backend,
recon.WithRemotePrefix("app/"),
recon.WithRemoteTrimPrefix(true),
)
if err != nil {
panic(err)
}
r, _ := recon.New(recon.WithSource(src))
defer func() { _ = r.Close() }()
host, _, _ := r.GetString("host")
port, _, _ := r.GetString("port")
fmt.Printf("%s:%s\n", host, port)
}
Output: localhost:8080
func NewInMemoryBackend ¶
func NewInMemoryBackend() *MemoryBackend
NewInMemoryBackend returns an empty MemoryBackend.
func (*MemoryBackend) Close ¶
func (m *MemoryBackend) Close() error
Close releases subscriptions. Idempotent.
func (*MemoryBackend) Delete ¶
func (m *MemoryBackend) Delete(key string)
Delete removes key. Notifies subscribers only when key existed.
func (*MemoryBackend) Get ¶
Get implements RemoteBackend.
func (*MemoryBackend) List ¶
List implements RemoteBackend. Returns matching keys in sorted order.
func (*MemoryBackend) Put ¶
func (m *MemoryBackend) Put(key, value string)
Put sets key to value and notifies subscribers.
func (*MemoryBackend) PutAll ¶
func (m *MemoryBackend) PutAll(kv map[string]string)
PutAll seeds many keys with a single notification fanout.
func (*MemoryBackend) Snapshot ¶
func (m *MemoryBackend) Snapshot() map[string]string
Snapshot returns a copy of the current data, intended for tests.
func (*MemoryBackend) Watch ¶
func (m *MemoryBackend) Watch(ctx context.Context) (<-chan struct{}, error)
Watch implements BackendWatcher. Each call returns a fresh channel that closes when ctx cancels or the backend closes.
type MergeStrategy ¶
type MergeStrategy int
MergeStrategy controls how the registry combines values when multiple sources hold the same key. The default MergeShadow replaces lower-precedence values entirely; MergeAppend enables slice-and-map deep merge.
const ( // MergeShadow has the higher-precedence source replace the lower // in its entirety. The default. MergeShadow MergeStrategy = iota // MergeAppend concatenates slices and deep-merges maps; scalars // still shadow. MergeAppend // MergeReplace is an explicit alias for [MergeShadow]. MergeReplace )
type MissingRequiredError ¶
type MissingRequiredError struct {
Path Path
Sources []string // names of sources consulted, in precedence order
}
MissingRequiredError reports that a required key was not supplied by any source.
func (*MissingRequiredError) Error ¶
func (e *MissingRequiredError) Error() string
func (*MissingRequiredError) Is ¶
func (e *MissingRequiredError) Is(target error) bool
Is matches against ErrMissingRequired and against peer errors with the same path. Direct comparison against the sentinel before the peer check avoids false positives.
type MultiError ¶
type MultiError struct {
Errors []error
}
MultiError aggregates per-field / per-key errors from a single Load or Bind. Implements the Go 1.20+ errors.Unwrap() []error contract so errors.Is and errors.As traverse every contained error.
func (*MultiError) Append ¶
func (m *MultiError) Append(err error)
Append adds err to the MultiError. Nil is a no-op.
func (*MultiError) Error ¶
func (m *MultiError) Error() string
func (*MultiError) Unwrap ¶
func (m *MultiError) Unwrap() []error
type NamedEvent ¶
NamedEvent is a registry Event tagged with the source registry's registration name.
type OSEnvSource ¶
type OSEnvSource struct {
// contains filtered or unexported fields
}
OSEnvSource is a Source backed by the process environment. Path lookups go through a KeyTransform (default: SnakeUpperTransform, or SnakeUpperPrefixTransform when WithEnvPrefix is set) so "server.port" reads SERVER_PORT.
Values surface as StringKind; typed coercion happens at the Registry level. Use WithEnvTransform for a non-default forward projection and WithEnvKeyParser for the inverse used by [Keys].
Example ¶
ExampleOSEnvSource reads environment variables through the canonical OS-env source. Combine with WithEnvPrefix to scope to a single namespace.
package main
import (
"fmt"
"sort"
"github.com/go-rotini/recon"
)
func main() {
// In a real program you'd pass NewOSEnvSource() directly to
// recon.New; this example uses a Map source to keep the output
// deterministic across test runs.
src := recon.NewMapSource("env", map[string]any{
"APP_PORT": "8080",
"APP_NAME": "rotini",
})
r, _ := recon.New(recon.WithSource(src))
defer func() { _ = r.Close() }()
keys := r.AllKeys()
sort.Strings(keys)
for _, k := range keys {
v, _, _ := r.GetString(k)
fmt.Printf("%s=%s\n", k, v)
}
}
Output: APP_NAME=rotini APP_PORT=8080
func NewOSEnvSource ¶
func NewOSEnvSource(opts ...EnvOption) *OSEnvSource
NewOSEnvSource constructs an OSEnvSource with snake-upper defaults.
func (*OSEnvSource) Get ¶
func (s *OSEnvSource) Get(path Path) (Value, bool, error)
Get projects path through the KeyTransform and looks the result up in the environment. An unset env var returns (Value{}, false, nil).
func (*OSEnvSource) Keys ¶
func (s *OSEnvSource) Keys() []Path
Keys enumerates paths cached from os.Environ. The first call scans the environment; later calls return the cached set until [Refresh].
The default snake-upper inverse treats every underscore as a separator, so APP_OAUTH2_TOKEN surfaces as Path{"oauth2","token"}. Supply WithEnvKeyParser when a different convention preserves segment boundaries.
func (*OSEnvSource) Refresh ¶
func (s *OSEnvSource) Refresh() int
Refresh re-scans os.Environ to pick up additions or deletions and returns the new key count. Driven by the watch engine's WithPoll when live env coverage is wanted.
type Option ¶
type Option func(*registryOptions)
Option configures a Registry at construction time. Options are applied in the order passed to New; later options override earlier ones when they touch the same setting.
func WithCodec ¶
WithCodec registers (or replaces by name) a Codec in the registry's codec set. When called before any other codec option, the option starts from DefaultCodecs so the new codec joins (rather than replaces) the bundled defaults. Pair with WithCodecs for a clean-slate set.
func WithCodecs ¶
WithCodecs replaces the registry's entire codec set.
func WithErrorBehavior ¶
func WithErrorBehavior(b ErrorBehavior) Option
WithErrorBehavior controls per-field error aggregation during [Bind] / [Unmarshal]. See ErrorBehavior.
func WithEventBufferSize ¶
WithEventBufferSize sets the capacity of the public Events channel. Default 16.
func WithLenient ¶
func WithLenient() Option
WithLenient is the explicit opt-out from strict mode (default).
func WithLogger ¶
WithLogger installs the logger used for non-fatal diagnostics. Default slog.Default().
func WithMerge ¶
func WithMerge(strategy MergeStrategy) Option
WithMerge controls how overlapping values from multiple sources combine. See MergeStrategy.
Example ¶
ExampleWithMerge demonstrates recon.MergeAppend semantics: a lower-precedence source's slice contributes its elements first; the higher-precedence source's elements are appended. Scalar values still shadow under MergeAppend.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
high := recon.NewMapSource("flags", map[string]any{
"tags": []any{"hi-1", "hi-2"},
})
low := recon.NewMapSource("config", map[string]any{
"tags": []any{"lo-1", "lo-2"},
})
r, _ := recon.New(
recon.WithSources(high, low),
recon.WithMerge(recon.MergeAppend),
)
defer func() { _ = r.Close() }()
tags, _, _ := r.GetStringSlice("tags")
fmt.Println(tags)
}
Output: [lo-1 lo-2 hi-1 hi-2]
func WithPrecedence ¶
WithPrecedence re-orders the registered sources by name after all sources have been added. Names not in the list keep their original relative order and are appended after the named ones.
func WithReloadDebounce ¶
WithReloadDebounce sets how long the engine waits for additional change events before firing a reload. Default 50ms.
func WithSchema ¶
WithSchema compiles schemaJSON via NewJSONSchemaValidator and installs the result as the validator. A compile failure rides on the options struct and is surfaced by New's error return.
Use NewJSONSchemaValidatorYAML / NewJSONSchemaValidatorTOML / NewJSONSchemaValidatorJSONC with WithValidator for non-JSON schema sources.
Example ¶
ExampleWithSchema validates the registry's snapshot against a JSON Schema on every reload. Construction returns the compile error directly when the schema is malformed.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
schema := []byte(`{
"type": "object",
"properties": {
"port": {"type": "integer", "minimum": 1, "maximum": 65535}
},
"required": ["port"]
}`)
r, err := recon.New(recon.WithSchema(schema))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
if err := r.Set("port", 8080); err != nil {
fmt.Println("invalid:", err)
} else {
fmt.Println("valid")
}
}
Output: valid
func WithSecretRedactor ¶
WithSecretRedactor replaces the default "***" redactor.
func WithSource ¶
WithSource registers a single source. Equivalent to Registry.AddSource after construction.
func WithSources ¶
WithSources registers multiple sources in the given order. The first argument is the highest precedence among this batch.
func WithStrict ¶
func WithStrict() Option
WithStrict enables strict-mode decoding: unknown keys and ambiguous coercions become errors.
func WithValidator ¶
func WithValidator(v SchemaValidator) Option
WithValidator installs a SchemaValidator run after every load.
func WithWatcher ¶
func WithWatcher(w WatcherFactory) Option
WithWatcher installs a registry-wide WatcherFactory used by file-backed sources that don't ship their own. Default is FSWatcher.
func WithoutCodec ¶
WithoutCodec removes a codec by name. Starts from DefaultCodecs when no codec option has been applied yet, matching the "I want the defaults except X" intent.
type ParseError ¶
type ParseError struct {
Source string
Path string // file path for file sources; empty otherwise
Position Position
Cause error
}
ParseError reports that a source's underlying format parser failed.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
type Path ¶
type Path []string
Path is an ordered sequence of segments naming a value in the configuration hierarchy. Path{"server", "port"} represents "server.port" under the default delimiter.
Path is a value type. Methods that return a new Path ([Append], [After], [Clone]) do not mutate the receiver.
func MakePath ¶
MakePath constructs a Path from raw segments; no delimiter parsing is performed. Use ParsePath to parse a delimited string.
func ParsePath ¶
ParsePath parses s using DefaultDelimiter. Segments containing the delimiter are bracket-escaped on input: ParsePath("[my.key].sub") returns Path{"my.key", "sub"}. An empty string returns an empty Path.
ParsePath never errors. An unclosed bracket is treated as a literal "[" at that position; two consecutive delimiters produce an empty segment so Path.String round-trips.
func (Path) After ¶
After returns the suffix of p following prefix, or nil when prefix is not a prefix of p. After(Path{}) returns a fresh copy of p.
type PerSource ¶
type PerSource[T any] struct { // Path is the canonical key queried. Path Path // Sources lists every registered source's contribution in // precedence order. Sources []ValueSource[T] // Explicit is the [Registry.Set] override, IsSet=false when none. Explicit ValueSource[T] // Default is the [Registry.SetDefault] fallback, IsSet=false // when none. Default ValueSource[T] // Resolved is what [Get] would have returned. Resolved ValueSource[T] }
PerSource is the per-source view of one key across the registry's entire source chain. Use it when the default precedence isn't what the caller wants and they need to apply their own resolve-by-policy logic ("env wins in containers", "config-first for daemons").
Sources is ordered to match the registry's chain (first = highest precedence). Explicit and Default model the reserved layers above and below the chain. Resolved is what Get would return.
func PerSourceFor ¶
PerSourceFor returns the per-source view of key. Every source is consulted once and the result is coerced into T using the same rules Get follows.
A source without the key reports IsSet=false. A source whose value cannot be coerced into T reports IsSet=true with Err set — caller- side resolve logic can then distinguish "missing" from "wrong shape".
Returns a wrapped ErrRegistryClosed on a closed registry.
Example ¶
ExamplePerSourceFor shows how to inspect every source's contribution to one key independently — the foundation for per-key "config explain" tooling and for resolve-by-policy hooks that want to deviate from the registry's default precedence.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
flags := recon.NewMapSource("flags", map[string]any{"port": 9000})
env := recon.NewMapSource("env", map[string]any{"port": 8080})
r, _ := recon.New(recon.WithSources(flags, env))
defer func() { _ = r.Close() }()
ps, _ := recon.PerSourceFor[int](r, "port")
for _, entry := range ps.Sources {
fmt.Printf("%s: %d (set=%v)\n", entry.Source, entry.Value, entry.IsSet)
}
fmt.Printf("resolved: %d (winner=%s)\n", ps.Resolved.Value, ps.Resolved.Source)
}
Output: flags: 9000 (set=true) env: 8080 (set=true) resolved: 9000 (winner=flags)
func PerSourceForPath ¶
PerSourceForPath is the explicit-path twin of PerSourceFor.
func (PerSource[T]) BySource ¶
func (p PerSource[T]) BySource(name string) ValueSource[T]
BySource returns the entry contributed by name, or a zero entry with IsSet=false when no source by that name has a value.
type PollWatcher ¶
type PollWatcher struct {
// contains filtered or unexported fields
}
PollWatcher is the stdlib-only WatcherFactory fallback. It stats the watched path on a tick and emits a SourceChange when size, mtime, or SHA-256 digest differs from the previous sample. Use it when a native fs-notification backend is unavailable or undesirable.
func NewPollWatcher ¶
func NewPollWatcher(interval time.Duration) *PollWatcher
NewPollWatcher returns a PollWatcher that ticks at interval. An interval ≤ 0 is clamped to one second.
func (*PollWatcher) Watch ¶
func (w *PollWatcher) Watch(ctx context.Context, path string) (<-chan SourceChange, error)
Watch implements WatcherFactory. The first tick fires after interval; the channel is closed when ctx is canceled. Stat errors are surfaced as a SourceChange with non-nil Err.
type Position ¶
Position is a source-local position used by ParseError. Line and Column are 1-indexed; both zero means unknown.
type RawValue ¶
RawValue holds undecoded bytes plus a format hint. Format is a codec name registered in the registry's codec set (e.g. "json", "yaml") or any string a custom codec recognizes.
func (RawValue) Decode ¶
Decode parses rv.Data through the codec named by rv.Format and assigns the result into v. v must be a non-nil pointer:
- *map[string]any or *any receives the decoded payload directly.
- *Value receives the payload wrapped via NewValue.
- Pointer-to-struct triggers a one-shot struct walk over the decoded map.
Returns wrapped ErrUnsupportedFormat when no codec matches rv.Format.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the central configuration registry. Construct via New. Reads (Get, Bind) are lock-free; writes (Set, AddSource, etc.) take the registry's mutex, rebuild the snapshot, and atomic-store.
Close the Registry when no longer needed to release source resources and the Events channel. [Sub] returns a *Registry that shares state with its parent but resolves keys under a prefix; closing the parent invalidates every sub view.
func New ¶
New constructs a Registry from opts and runs an initial snapshot build. Sources in WithSource / WithSources are added in argument order, first being highest precedence.
The returned Registry is functional even when the error is non-nil, enabling tests that assert "this registration would have failed" while still inspecting the registry. Production callers should treat any non-nil error as a hard stop.
Example ¶
ExampleNew shows the minimum useful registry construction: a single in-memory source, register, read, close.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
src := recon.NewMapSource("config", map[string]any{
"server": map[string]any{
"host": "localhost",
"port": 8080,
},
})
r, err := recon.New(recon.WithSource(src))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
host, _, _ := r.GetString("server.host")
port, _, _ := r.GetInt("server.port")
fmt.Printf("%s:%d\n", host, port)
}
Output: localhost:8080
func (*Registry) AddSource ¶
AddSource registers s at the lowest precedence. Returns ErrSourceConflict when a source with the same Name() already exists or Name() is a reserved label.
Transactional: if the post-add rebuild fails, the source is rolled out of the chain. The source's Close is not invoked on rollback — the caller still owns it.
func (*Registry) AllKeys ¶
AllKeys returns every known key (canonical and alias) in sorted order. On a sub view, only keys under the sub's prefix are returned with the prefix stripped.
func (*Registry) Bind ¶
func (r *Registry) Bind(target any, opts ...DecodeOption) error
Bind populates target from the current snapshot. target must be a non-nil pointer to a struct. Errors aggregate into a *MultiError under FailCollect (the default) or short-circuit on the first under FailFast. Tag grammar lives on FieldTag.
Example ¶
ExampleRegistry_Bind populates a struct from the registry via the recon: tag. Defaults, required fields, and nested paths all work the same way they do in struct-driven config loaders.
package main
import (
"fmt"
"time"
"github.com/go-rotini/recon"
)
func main() {
src := recon.NewMapSource("config", map[string]any{
"server": map[string]any{
"host": "localhost",
"port": 8080,
},
"debug": true,
})
r, _ := recon.New(recon.WithSource(src))
defer func() { _ = r.Close() }()
type Config struct {
Host string `recon:"server.host,required"`
Port int `recon:"server.port,default=80"`
Debug bool `recon:"debug"`
Timeout time.Duration `recon:"timeout,default=30s"`
}
var cfg Config
if err := r.Bind(&cfg); err != nil {
panic(err)
}
fmt.Printf("host=%s port=%d debug=%v timeout=%s\n",
cfg.Host, cfg.Port, cfg.Debug, cfg.Timeout)
}
Output: host=localhost port=8080 debug=true timeout=30s
func (*Registry) BindContext ¶
BindContext is the context-aware [Bind]. ctx is threaded into any ValidatorContext hook the target implements.
func (*Registry) Close ¶
Close shuts down the registry. Idempotent. Every registered source's Close is called regardless of earlier failures; errors aggregate into a *MultiError.
func (*Registry) Describe ¶
func (r *Registry) Describe() Description
Describe returns a Description of the current snapshot. The result reflects the call instant; callers wanting a consistent view across multiple calls should pin via Registry.Snapshot and walk it themselves.
Example ¶
ExampleRegistry_Describe surfaces per-key provenance + redacted values for "myapp config show" output. The Sources slice lists every source in precedence order.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
high := recon.NewMapSource("flags", map[string]any{"port": 9000})
low := recon.NewMapSource("env", map[string]any{"port": 8080})
r, _ := recon.New(recon.WithSources(high, low))
defer func() { _ = r.Close() }()
_ = r.Set("token", "hunter2")
r.MarkSecret("token")
d := r.Describe()
for _, k := range d.Keys {
fmt.Printf("%s = %s (source=%s, secret=%v)\n",
k.Path, k.Value, k.Source, k.Secret)
}
}
Output: port = 9000 (source=flags, secret=false) token = *** (source=explicit, secret=true)
func (*Registry) DescribeKey ¶
func (r *Registry) DescribeKey(key string) (KeyDescription, bool)
DescribeKey is the per-key form of [Describe]. Alias keys resolve to their canonical row, so DescribeKey("port") and DescribeKey("server.port") return the same row when port is aliased.
func (*Registry) DrainWarnings ¶
func (r *Registry) DrainWarnings() []DeprecationWarning
DrainWarnings returns and clears the pending warning queue. The watch engine drains the same queue on every event emit; callers that run [Bind] without live reload use this to surface deprecations directly. Returns nil when empty.
Example ¶
ExampleRegistry_DrainWarnings consumes deprecation warnings the bind walker queued when a `deprecated`-tagged field actually had a value supplied by a source — the migration window's "you're still using the old key" notice.
package main
import (
"fmt"
"strings"
"github.com/go-rotini/recon"
)
func main() {
type C struct {
Old string `recon:"old_key,deprecated=use 'new_key' instead"`
}
r, _ := recon.New()
defer func() { _ = r.Close() }()
_ = r.Set("old_key", "value")
var c C
_ = r.Bind(&c)
warnings := r.DrainWarnings()
for _, w := range warnings {
// Trim a stable representation for the example output.
msg := strings.TrimPrefix(w.Message, "recon: ")
fmt.Printf("warning at %s: %s\n", w.Path, msg)
}
}
Output: warning at old_key: use 'new_key' instead
func (*Registry) Events ¶
Events returns the channel reload events are delivered on. Each reload — successful or failed — produces one Event; failures retain the previous snapshot and surface via Event.Err. The channel is buffered (capacity from WithEventBufferSize, default 16). Slow consumers cause drops surfaced on the next deliverable Event's Warnings. Returns nil on a closed-before-construction registry.
func (*Registry) GenerateTemplate ¶
func (r *Registry) GenerateTemplate(format string, opts ...SaveOption) ([]byte, error)
GenerateTemplate emits a stub configuration document with defaults included, encoded in format. Used to produce a starter config file. Secret-marked keys are redacted unless WithSaveIncludeSecrets. Returns wrapped ErrUnsupportedFormat when format is unknown or empty.
Example ¶
ExampleRegistry_GenerateTemplate emits a stub document populated with the registry's known defaults — useful as the `myapp config init` entry point.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
r, _ := recon.New()
defer func() { _ = r.Close() }()
_ = r.SetDefault("server.port", 8080)
_ = r.SetDefault("server.host", "localhost")
out, err := r.GenerateTemplate(recon.FormatJSON)
if err != nil {
panic(err)
}
// Re-decode for deterministic output (JSON key ordering is
// not guaranteed by the stdlib encoder).
decoded, _ := recon.JSON.Decode(out)
server := decoded["server"].(map[string]any)
fmt.Printf("host=%v port=%v\n", server["host"], server["port"])
}
Output: host=localhost port=8080
func (*Registry) Get ¶
Get returns the Value resolved for key. The bool reports whether any layer of the registry supplied a value; an empty string counts as set. The error is non-nil only when the registry is closed.
Example (Provenance) ¶
ExampleRegistry_Get_provenance shows how to inspect WHICH source supplied a value — the foundation for "myapp config explain" tooling and for debugging precedence surprises.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
flag := recon.NewMapSource("flags", map[string]any{"port": 9000})
env := recon.NewMapSource("env", map[string]any{"port": 8080})
r, _ := recon.New(recon.WithSources(flag, env))
defer func() { _ = r.Close() }()
v, _, _ := r.Get("port")
fmt.Println("value:", v.String())
fmt.Println("source:", v.Source())
fmt.Println("shadowed:", r.Snapshot().SourceFor(recon.MakePath("port")))
}
Output: value: 9000 source: flags shadowed: [flags env]
func (*Registry) GetAny ¶
GetAny returns the underlying Go value at key — the same shape Value.Any returns.
func (*Registry) GetDuration ¶
GetDuration returns the value at key as time.Duration. A native time.Duration value or a string parseable by time.ParseDuration is accepted.
func (*Registry) GetInt ¶
GetInt returns the value at key as int. Wraps int64 → int without overflow check; 32-bit-target callers should prefer [GetInt64].
func (*Registry) GetPath ¶
GetPath is the Path-typed variant of Registry.Get.
func (*Registry) GetString ¶
GetString returns the string value at key. Returns ErrTypeMismatch when the resolved kind is not StringKind.
func (*Registry) GetStringMap ¶
GetStringMap returns the value at key as map[string]string. The wire kind must be MapKind.
func (*Registry) GetStringSlice ¶
GetStringSlice returns the value at key as []string. The wire kind must be SliceKind; each element is projected via Value.AsString.
func (*Registry) GetTime ¶
GetTime returns the value at key as time.Time. A native time.Time or an RFC 3339 string is accepted.
func (*Registry) InsertSource ¶
InsertSource registers s at precedence index at (0 = highest). An out-of-range index is clamped to [0, len(sources)]. Same conflict and transactional semantics as [AddSource].
func (*Registry) IsImmutable ¶
IsImmutable reports whether path has an immutable baseline recorded.
func (*Registry) IsSecret ¶
IsSecret reports whether key has been marked secret, either via [MarkSecret] or by the bind walker on a `secret`-tagged field.
func (*Registry) IsSet ¶
IsSet reports whether any layer of the registry has a value for key. An empty string still counts as set.
func (*Registry) MarkSecret ¶
MarkSecret records key as containing sensitive data and rebuilds the snapshot so [Describe] / [Save] / Snapshot.String see the updated set immediately. Empty key and closed registry are silent no-ops; idempotent. A rebuild failure is logged but not returned — the typical caller is the bind walker emitting a side effect.
func (*Registry) PinSource ¶
PinSource forces resolution of key to consult only the named source. When pinned the source chain is skipped; if the pinned source has no value, the key resolves to "not set" (no default fallback).
Returns *SourceError when sourceName isn't registered. Transactional rollback on rebuild failure.
func (*Registry) Prefix ¶
Prefix returns the sub-tree path this view is rooted at, or an empty Path for a root registry.
func (*Registry) RegisterAlias ¶
RegisterAlias makes lookups of alias resolve to canonical. The alias graph is cycle-checked at registration time; a cycle returns *AliasCycleError with the alias map unchanged.
Aliases chain: alias1 → alias2 → canonical resolves in one rebuild. Multiple aliases for one canonical are allowed. Transactional rollback on rebuild failure.
Example ¶
ExampleRegistry_RegisterAlias maps an alternate key onto a canonical one. Both Get("port") and Get("server.port") return the same value after this call.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
r, _ := recon.New()
defer func() { _ = r.Close() }()
_ = r.Set("server.port", 8080)
_ = r.RegisterAlias("port", "server.port")
via := func(key string) any {
v, _, _ := r.Get(key)
i, _ := v.AsInt64()
return i
}
fmt.Println("canonical:", via("server.port"))
fmt.Println("alias: ", via("port"))
}
Output: canonical: 8080 alias: 8080
func (*Registry) ReloadContext ¶
ReloadContext is the context-aware [Reload]. The context flows to remote backends during their refresh call; in-memory sources ignore it. A canceled ctx aborts and returns ctx.Err() wrapped.
func (*Registry) RemoveSource ¶
RemoveSource removes the source named name. Idempotent: removing an unknown name is not an error.
Transactional: if the post-remove rebuild fails, the source is re-inserted at its original index. The source's Close is called only on a successful removal.
func (*Registry) Save ¶
func (r *Registry) Save(w io.Writer, opts ...SaveOption) error
Save serializes the current snapshot through a codec and writes the bytes to w. The codec is named by WithSaveFormat; without it Save returns a wrapped ErrUnsupportedFormat since an io.Writer has no extension to detect from.
Save / [SaveTo] / [SaveString] / [GenerateTemplate] differ in destination: Save writes to any io.Writer; SaveTo writes to a file path with atomic write-temp-then-rename; SaveString returns the encoded form as a string; GenerateTemplate emits a stub document with defaults included.
Default policy is safe to pipe anywhere:
- Secret-marked keys are redacted via WithSecretRedactor unless WithSaveIncludeSecrets is set.
- Default-only keys are omitted unless WithSaveIncludeDefaults is set.
- WithSaveOnly limits output to a sub-tree.
Save reads the current snapshot atomically; concurrent reloads do not interleave with the encode.
Example ¶
ExampleRegistry_Save serializes the snapshot through a bundled codec. Pass WithSaveFormat with the registry-wide [Save] (or use SaveTo with a path whose extension implies the format).
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
r, _ := recon.New()
defer func() { _ = r.Close() }()
_ = r.Set("server.host", "localhost")
_ = r.Set("server.port", 8080)
out, err := r.SaveString(recon.WithSaveFormat(recon.FormatJSON))
if err != nil {
panic(err)
}
// Re-decode for deterministic output (JSON key ordering is
// implementation-defined in encoding/json).
decoded, _ := recon.JSON.Decode([]byte(out))
server := decoded["server"].(map[string]any)
fmt.Printf("host=%v port=%v\n", server["host"], server["port"])
}
Output: host=localhost port=8080
func (*Registry) SaveString ¶
func (r *Registry) SaveString(opts ...SaveOption) (string, error)
SaveString returns the encoded form as a string.
func (*Registry) SaveTo ¶
func (r *Registry) SaveTo(path string, opts ...SaveOption) error
SaveTo is the path-aware [Save]. The format is detected from path's extension when WithSaveFormat is not supplied. The temp file lives next to the target so the rename stays atomic across exotic filesystems.
func (*Registry) Set ¶
Set installs an explicit override for key. Explicit overrides sit above every source in the precedence chain.
On a sub view, key is interpreted relative to the sub's prefix. Pass nil to clear an override (equivalent to [Unset]). The snapshot is rebuilt before Set returns; if the rebuild fails the immutable or validator check, the override is rolled back and the error is returned.
func (*Registry) SetDefault ¶
SetDefault installs a fallback value for key. Defaults sit below every source — they apply only when no source (and no explicit override) supplies the key. Same transactional semantics as [Set]; nil clears the default.
func (*Registry) Snapshot ¶
Snapshot returns the current immutable view. Useful for handing a stable resolved configuration to a goroutine or SchemaValidator without coordinating with reloads.
func (*Registry) Sources ¶
Sources returns the registered source names in precedence order (first = highest). The returned slice is a copy.
func (*Registry) Sub ¶
Sub returns a Registry view rooted at prefix. Reads, writes, and introspection on the returned registry operate on keys relative to prefix.
Sub views share state with the parent: no snapshot copy, no source duplication. A reload on the parent is visible to the sub immediately and vice versa. Closing the parent invalidates every sub view.
Sub("") returns the parent unchanged. Sub("a").Sub("b") is equivalent to Sub("a.b").
Example ¶
ExampleRegistry_Sub returns a registry view rooted at a sub-tree. Reads, writes, and AllKeys operate relative to the prefix.
package main
import (
"fmt"
"github.com/go-rotini/recon"
)
func main() {
src := recon.NewMapSource("config", map[string]any{
"server": map[string]any{
"host": "localhost",
"port": 8080,
},
"db": map[string]any{
"dsn": "postgres://x",
},
})
r, _ := recon.New(recon.WithSource(src))
defer func() { _ = r.Close() }()
server := r.Sub("server")
host, _, _ := server.GetString("host")
port, _, _ := server.GetInt("port")
fmt.Printf("server view: host=%s port=%d\n", host, port)
}
Output: server view: host=localhost port=8080
func (*Registry) TemplateKeys ¶
func (r *Registry) TemplateKeys(opts ...SaveOption) []Path
TemplateKeys returns the sorted paths [GenerateTemplate] would include with opts. Useful for `config init --keys` tooling.
func (*Registry) Unmarshal ¶
func (r *Registry) Unmarshal(target any, opts ...DecodeOption) error
Unmarshal is an alias for Registry.Bind, named to mirror the stdlib encoding/Marshal-Unmarshal convention.
func (*Registry) UnmarshalKey ¶
func (r *Registry) UnmarshalKey(key string, target any, opts ...DecodeOption) error
UnmarshalKey binds the registry's sub-tree at key into target — equivalent to r.Sub(key).Bind(target, opts...). An empty key is equivalent to [Bind].
func (*Registry) Unpin ¶
Unpin removes a previous pin for key. No-op when key was not pinned. Transactional rollback on rebuild failure.
func (*Registry) Unset ¶
Unset removes a previous explicit override for key. Does not affect sources, defaults, or aliases. Transactional: a rebuild failure rolls the value back.
func (*Registry) Validate ¶
Validate runs the configured SchemaValidator against the current snapshot. Returns nil when no validator is installed. Secret-marked keys in the returned error are redacted.
Unlike the implicit validator pass inside every rebuild, this is on-demand and does not trigger a rebuild — suitable for a `config validate` subcommand.
func (*Registry) Validator ¶
func (r *Registry) Validator() SchemaValidator
Validator returns the SchemaValidator this registry was constructed with, or nil when none was installed.
type RemoteBackend ¶
type RemoteBackend interface {
// List enumerates every key under prefix. An empty prefix lists
// every key.
List(ctx context.Context, prefix string) ([]string, error)
// Get returns the value for key. An empty string with set=true is
// "set to empty", matching [Source] semantics.
Get(ctx context.Context, key string) (string, bool, error)
// Close releases backend resources. Idempotent.
Close() error
}
RemoteBackend is the contract an out-of-process configuration store satisfies. Real adapters (etcd, consul, vault, AWS SSM, k8s) live in separate modules and depend on this interface. The core ships only the interface plus NewInMemoryBackend for tests.
Backends are string-keyed and string-valued. Adapters with structured payloads should pre-serialize values and rely on the `format=` bind tag for per-field re-decoding.
type RemoteOption ¶
type RemoteOption func(*remoteOptions)
RemoteOption configures NewRemoteSource.
func WithRemotePollInterval ¶
func WithRemotePollInterval(d time.Duration) RemoteOption
WithRemotePollInterval enables polling for backends without BackendWatcher. A zero or negative interval keeps the source non-polling. Ignored for backends that already implement BackendWatcher.
func WithRemotePrefix ¶
func WithRemotePrefix(prefix string) RemoteOption
WithRemotePrefix scopes a RemoteSource to keys under prefix. Combine with WithRemoteTrimPrefix when the registry should see keys without the prefix.
func WithRemoteTrimPrefix ¶
func WithRemoteTrimPrefix(trim bool) RemoteOption
WithRemoteTrimPrefix strips the configured prefix from cached keys before they're exposed.
type RemoteSource ¶
type RemoteSource struct {
// contains filtered or unexported fields
}
RemoteSource wraps a RemoteBackend as a Source. Construction reads every key under the configured prefix and caches the result; subsequent Get calls hit the cache.
Live reload: subscribes to BackendWatcher when available; otherwise polls at the WithRemotePollInterval cadence (default off — opt in by setting an interval).
func NewRemoteSource ¶
func NewRemoteSource(name string, backend RemoteBackend, opts ...RemoteOption) (*RemoteSource, error)
NewRemoteSource constructs a RemoteSource. The construction-time read populates the cache; a backend failure here surfaces as a wrapped *SourceError. Returns wrapped ErrInvalidPath for an empty name or nil backend.
func (*RemoteSource) Close ¶
func (s *RemoteSource) Close() error
Close releases the backend and drops the cache.
func (*RemoteSource) Get ¶
func (s *RemoteSource) Get(path Path) (Value, bool, error)
Get looks up path against the cache. Multi-segment paths join with "/" — the backend's flat keyspace convention.
func (*RemoteSource) Keys ¶
func (s *RemoteSource) Keys() []Path
Keys returns every cached key, sorted by canonical path string. The prefix is stripped when WithRemoteTrimPrefix is set.
func (*RemoteSource) Name ¶
func (s *RemoteSource) Name() string
Name returns the source identifier.
func (*RemoteSource) Refresh ¶
func (s *RemoteSource) Refresh(ctx context.Context) error
Refresh re-reads the backend, replacing the cache atomically.
func (*RemoteSource) Watch ¶
func (s *RemoteSource) Watch(ctx context.Context) (<-chan SourceChange, error)
Watch implements Watcher. Emits a SourceChange on every backend-reported activity. A source with neither a BackendWatcher backend nor a configured poll interval returns a closed channel.
type SaveOption ¶
type SaveOption func(*saveOptions)
SaveOption configures one Registry.Save / Registry.SaveTo / Registry.GenerateTemplate call. Distinct from Option because Save is per-call: each invocation can pick a different output format, secret policy, or sub-tree.
func WithSaveFormat ¶
func WithSaveFormat(format string) SaveOption
WithSaveFormat forces the output format regardless of the destination path's extension. Pass one of the canonical [Format*] constants or any registered codec name.
func WithSaveIncludeDefaults ¶
func WithSaveIncludeDefaults() SaveOption
WithSaveIncludeDefaults emits keys whose only source is SetDefault.
func WithSaveIncludeSecrets ¶
func WithSaveIncludeSecrets() SaveOption
WithSaveIncludeSecrets emits secret-tagged values verbatim.
func WithSaveOnly ¶
func WithSaveOnly(prefix string) SaveOption
WithSaveOnly scopes Save to a single key prefix.
type SchemaValidator ¶
SchemaValidator validates a fully-resolved snapshot. Implementations must be cheap to construct and safe for concurrent use; the registry calls Validate on every reload. The bundled JSONSchemaValidator is backed by go-rotini/jsonschema.
type Secret ¶
Secret aliases env.Secret so the two are the same Go type and round-trip across packages without conversion. The `secret` struct-tag option tells the bind decoder to wrap the resolved value in this type and redact every textual rendering.
type Snapshot ¶
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot is the immutable, fully-resolved view of a Registry at one point in time. It is atomic-stored on the registry; once a caller holds a *Snapshot, the underlying data never mutates. Construct via [buildSnapshot] — callers do not construct Snapshots directly.
func (*Snapshot) AsMap ¶
AsMap returns the snapshot as a nested map[string]any, splitting paths on the canonical delimiter. Mutating the returned map does not affect the snapshot.
AsMap does not redact secret-marked values; downstream validators need real data. Use Snapshot.String for a redacted human view or Registry.Save for a serialized redacted view.
func (*Snapshot) Get ¶
Get returns the resolved value at p. The bool reports whether any layer supplied a value; an empty-string value counts as set.
func (*Snapshot) IsSecret ¶
IsSecret reports whether p was marked secret when this snapshot was built.
func (*Snapshot) Keys ¶
Keys returns every known path sorted by canonical string form. The returned slice aliases the snapshot's storage and must not be mutated.
type Source ¶
type Source interface {
// Name identifies the source in [Event] and [Describe] output.
// Names must be unique within a Registry.
Name() string
// Get returns the value at path. The returned [Value] preserves
// the wire type from the underlying format; typed coercion happens
// at the registry call site.
//
// (Value, false, nil) means "not present"; (Value, true, nil)
// means "set" (an empty string is a present value);
// (Value, _, err) reports a source-internal error.
Get(path Path) (Value, bool, error)
// Keys enumerates every path this source can answer. May be
// expensive; the registry caches the result inside snapshots. The
// returned slice must not be mutated by callers — sources may
// alias internal storage.
Keys() []Path
// Close releases any resources held by the source. Idempotent;
// sources that hold no resources may return nil.
Close() error
}
Source is the contract every config-data source implements. The registry composes one or more Sources in precedence order and asks each in turn to look up a key. A Source is consulted only after the registry's own explicit / pinned / aliased layers resolve.
func NewBufferSource ¶
func NewBufferSource(name, format string, data []byte, opts ...BufferOption) (Source, error)
NewBufferSource decodes data with the codec supplied via WithBufferCodec and returns a Source named name. The format string is recorded for diagnostics but does not drive decoding.
Returns wrapped ErrUnsupportedFormat when no codec is supplied. Source construction has no access to the registry's codec set, so callers must pass the codec explicitly or use a format-named constructor (NewYAMLSource, etc.) for files.
func NewDotenvSource ¶
func NewDotenvSource(path string, opts ...FileOption) (Source, error)
NewDotenvSource is NewFileSource with the Dotenv codec pinned. The result holds a flat keyspace.
func NewEnvFamilySource ¶ added in v1.0.2
NewEnvFamilySource returns a Source that collects each family's Base<Separator>… variables from the process environment into a nested map[string]any and surfaces it as one MapKind Value at the family's Target. Unlike OSEnvSource — which maps each path to a single variable — this lets a map-typed field bind an open-ended group of variables at once.
A family with no matching variables contributes no key, so a Target bound to a required field reports the standard missing-required error.
func NewFileSource ¶
func NewFileSource(path string, opts ...FileOption) (Source, error)
NewFileSource constructs a FileSource for path. Codec resolution order: WithFileCodec > WithFileFormat > file extension. Returns wrapped ErrUnsupportedFormat when no codec resolves. Decode failures surface as *ParseError.
The source's Name is the basename of the resolved path.
func NewFileSourceFS ¶
NewFileSourceFS returns a FileSourceFS reading path from fsys. Codec resolution mirrors NewFileSource: WithFileCodec > WithFileFormat > extension lookup. Returns wrapped ErrInvalidPath for nil fsys or empty name.
func NewJSONCSource ¶
func NewJSONCSource(path string, opts ...FileOption) (Source, error)
NewJSONCSource is NewFileSource with the JSONC codec pinned. Accepts both `.jsonc` and `.json5` files.
func NewJSONSource ¶
func NewJSONSource(path string, opts ...FileOption) (Source, error)
NewJSONSource is NewFileSource with the JSON codec pinned.
func NewStdinSource ¶
func NewStdinSource(format string, opts ...StdinOption) (Source, error)
NewStdinSource reads os.Stdin to EOF, decodes the bytes through the codec resolved from format (or WithStdinCodec), and returns a Source holding the decoded map. The construction is one-shot — no streaming, no incremental decode.
Codec resolution: WithStdinCodec > codec named by format. A blank format with no WithStdinCodec returns wrapped ErrUnsupportedFormat.
TTY-safe: when stdin is a TTY with no piped data the constructor returns an empty source rather than blocking.
func NewTOMLSource ¶
func NewTOMLSource(path string, opts ...FileOption) (Source, error)
NewTOMLSource is NewFileSource with the TOML codec pinned.
func NewYAMLSource ¶
func NewYAMLSource(path string, opts ...FileOption) (Source, error)
NewYAMLSource is NewFileSource with the YAML codec pinned.
type SourceChange ¶
SourceChange is what a Watcher emits when source content may have changed. An empty Keys slice signals "re-read everything"; a non-nil Err signals an unrecoverable refresh failure.
type SourceError ¶
type SourceError struct {
Source string
Op string // "get" / "watch" / "refresh" / "close"
Cause error
}
SourceError reports that a source failed to read, watch, or refresh.
func (*SourceError) Error ¶
func (e *SourceError) Error() string
func (*SourceError) Unwrap ¶
func (e *SourceError) Unwrap() error
type StdinOption ¶
type StdinOption func(*stdinOptions)
StdinOption configures NewStdinSource.
func WithStdinCodec ¶
func WithStdinCodec(c Codec) StdinOption
WithStdinCodec pins the codec for NewStdinSource.
type UnknownKeyError ¶
UnknownKeyError reports that strict-mode decoding rejected an extra key.
func (*UnknownKeyError) Error ¶
func (e *UnknownKeyError) Error() string
func (*UnknownKeyError) Is ¶
func (e *UnknownKeyError) Is(target error) bool
type Unmarshaler ¶
Unmarshaler is the optional decode hook a bind-target field may implement to take over its own decoding. coerce tries Unmarshaler first, then UnmarshalEnv, then encoding.TextUnmarshaler.
type ValidationError ¶
ValidationError reports that a SchemaValidator rejected a key. When Secret is true, Msg is replaced by "[redacted]" so the offending value never reaches the caller's log.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) Is ¶
func (e *ValidationError) Is(target error) bool
type Validator ¶
type Validator interface {
Validate() error
}
Validator is the optional whole-struct validation hook a bind target may implement. The decoder calls Validate after every field has been populated; a non-nil return aborts the bind.
type ValidatorContext ¶
ValidatorContext is the context-aware variant of Validator. The context is the one threaded through Registry.BindContext or WithDecodeContext. Implement this when validation must honor cancellation or carry request-scoped values.
type Value ¶
type Value struct {
// contains filtered or unexported fields
}
Value is a typed, source-tagged datum returned by a Source lookup. Constructors return fresh Values and the As* methods do not mutate; once handed to a caller, a Value should be treated as read-only.
func NewRawValue ¶
NewRawValue wraps undecoded bytes plus a format hint in a Value.
func NewValue ¶
NewValue wraps a Go value, inferring the ValueKind from its dynamic type. Integer types canonicalize to int64; float32 widens to float64. Unrecognized types fall through to StringKind via fmt.Sprint.
func (Value) Any ¶
Any returns the underlying Go value. The concrete type follows the kind: string, int64, float64, bool, time.Time, time.Duration, []Value, map[string]Value, RawValue, or nil.
func (Value) AsDuration ¶
AsDuration returns the duration value if v is DurationKind. StringKind values are parsed via time.ParseDuration; IntKind values are interpreted as nanoseconds.
func (Value) AsFloat64 ¶
AsFloat64 returns the float64 value if v is FloatKind. IntKind values are widened losslessly as a convenience.
func (Value) AsMap ¶
AsMap returns the underlying map[string]Value if v is MapKind. The returned map aliases the registry's storage and must not be mutated.
func (Value) AsSlice ¶
AsSlice returns the underlying []Value if v is SliceKind. The returned slice aliases the registry's storage and must not be mutated.
func (Value) AsString ¶
AsString returns the string value if v is StringKind.
func (Value) AsTime ¶
AsTime returns the time.Time value if v is TimeKind. StringKind values are parsed as RFC 3339.
func (Value) IsZero ¶
IsZero reports whether v carries no underlying datum. A NullKind Value is zero; an empty string, slice, or map is not.
func (Value) Source ¶
Source returns the name of the Source that produced v, or "" when v was constructed directly and not yet adopted by a registry.
func (Value) String ¶
String returns the canonical string representation of v. Strings are returned verbatim; other kinds use fmt.Sprint on the raw value. Use Value.AsString for the strict accessor.
type ValueKind ¶
type ValueKind int
ValueKind identifies the wire-form type of a Value. The registry preserves the wire type from Source through coercion so callers can request typed values without losing information.
const ( // NullKind is the absence of a value. A source returning ok=true // with NullKind represents "key is set, but to null". NullKind ValueKind = iota // StringKind is a UTF-8 string. StringKind // IntKind is a signed integer, stored as int64. IntKind // FloatKind is a floating-point number, stored as float64. FloatKind // BoolKind is a boolean. BoolKind // TimeKind is a time.Time. TimeKind // DurationKind is a time.Duration. DurationKind // SliceKind is an ordered list of Values. SliceKind // MapKind is a map from string to Value. MapKind // RawKind is bytes plus a format hint, used when a source defers // parsing. RawKind )
ValueKind constants.
type ValueSource ¶
ValueSource is one source's typed contribution to one key. IsSet reports whether the source had a value (mirroring Source.Get's ok return); Err carries any coercion failure so callers can distinguish "didn't have the key" from "wrong shape".
type Watcher ¶
type Watcher interface {
Watch(ctx context.Context) (<-chan SourceChange, error)
}
Watcher is an optional Source capability. Sources implementing Watcher participate in live reload: the registry subscribes once at construction and fans every emitted SourceChange into a single reload pipeline.
Implementations must honor ctx cancellation by closing the returned channel and returning from any internal goroutine.
type WatcherFactory ¶
type WatcherFactory interface {
Watch(ctx context.Context, path string) (<-chan SourceChange, error)
}
WatcherFactory produces a SourceChange channel for a single watched path. Implementations must handle atomic-save sequences (write-temp-then-rename), debounce rapid bursts, and release resources when ctx is canceled.
Bundled implementations: FSWatcher and PollWatcher.
Source Files
¶
- alias.go
- bind_walker.go
- codec.go
- codec_dotenv.go
- codec_json.go
- codec_jsonc.go
- codec_shape.go
- codec_toml.go
- codec_yaml.go
- coerce.go
- configs.go
- doc.go
- errors.go
- events.go
- expand.go
- format.go
- format_error.go
- live.go
- options.go
- options_decode.go
- options_encode.go
- options_enums.go
- options_source.go
- path.go
- path_expansion.go
- per_source.go
- precedence.go
- recon.go
- registry.go
- registry_bind.go
- registry_describe.go
- registry_get.go
- registry_save.go
- registry_set.go
- registry_sub.go
- remote_inmemory.go
- secret.go
- snapshot.go
- source.go
- source_buffer.go
- source_env.go
- source_envfamily.go
- source_file.go
- source_filefs.go
- source_flag.go
- source_format_named.go
- source_map.go
- source_remote.go
- source_stdin.go
- tags.go
- transforms.go
- validator.go
- validator_jsonschema.go
- value.go
- watch.go
- watcher.go
- watcher_fs.go
- watcher_poll.go