recon

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 29 Imported by: 0

README

go-rotini/recon

A Go data-in and configuration package: a typed registry that surveys every channel a program receives data through — environment variables, .env files, configuration files (YAML / TOML / JSONC / JSON), command-line flags, standard input, in-memory maps and buffers, programmatic defaults and overrides, and remote configuration stores — and resolves them through a documented precedence chain with first-class live reload, schema validation, and per-key provenance.

This package is used as the default configuration and data-in package for rotini.

Features

  • One-line setup: recon.New(...) wires the go-rotini family defaults — YAML / TOML / JSONC / JSON / Dotenv codecs, the go-rotini/fs-backed file watcher, the go-rotini/jsonschema-backed validator — and you're loading config
  • Generic, type-safe API: Get[T], Bind, Live[T], PerSourceFor[T], Configs — typed values where the type is statically known
  • Pluggable seams: Codec, SchemaValidator, WatcherFactory, FlagAdapter, RemoteBackend, and Source itself — every default is replaceable behind its interface with a one-line option
  • Built-in sources: NewOSEnvSource, NewFileSource / NewFileSourceFS, NewYAMLSource / NewTOMLSource / NewJSONCSource / NewJSONSource / NewDotenvSource (format-named convenience), NewBufferSource, NewMapSource, NewStdinSource, NewFlagSource, NewRemoteSource
  • Documented precedence chain (explicit → flags → env → config → remote → stdin → defaults) with per-source WithPrecedence, per-key PinSource, alias graphs with cycle detection
  • Hierarchical keys (Path) with bracket-escaping for dotted segments, configurable delimiter, case-sensitive by default
  • Aggregated multi-error reporting with per-field attribution (*MultiError) and a FormatError(r, err) pretty-printer that surfaces path, source provenance, and the precedence chain; WithErrorBehavior toggles FailCollect (default) and FailFast
  • context.Context propagation through ReloadContext and BindContext
  • Atomic, lock-free reads on reload via Live[T] and sync/atomic.Pointer — readers always observe a complete, validated snapshot
  • Per-key change detection across reloads (Event.Changed covers added / removed / modified cases); Event.Warnings carries non-fatal DeprecationWarning values out of band
  • Per-source provenance via Describe / DescribeKey / KeyDescription — every key knows which source supplied it and which other sources had a value for the same key
  • Struct-tag system: required, notEmpty, default=, secret, immutable, inline, base64, hex, layout=, separator=, kvSeparator=, plus recon-specific path=, source=, aliases=, transform=
  • immutable-tagged fields are baselined at first Bind; subsequent reload candidates that change a baselined value are rejected (the old / new pair is redacted via WithSecretRedactor when the field is also secret)
  • Schema validation via go-rotini/jsonschema; supply raw bytes via WithSchema(rawJSON) for the one-line case or WithValidator(...) for a pre-built SchemaValidator — including a custom one behind the same interface
  • Secret redaction: Secret[T] is a type alias of env.Secret[T] for free interop; the secret struct tag and MarkSecret(key) both feed Describe and Save redaction; customizable redactor via WithSecretRedactor
  • Format-agnostic encode: Save / SaveTo write the current resolved view back to any registered codec; GenerateTemplate emits a stub configuration document populated from defaults — the "myapp config init" path
  • Path expansion (POSIX shell-style: ~, $VAR, ${VAR}); first-match-wins multi-path lookup via WithSearchPaths; WithOptional for missing-file tolerance
  • Built-in support for time.Duration, time.Time, []byte (raw / base64 / hex), arrays and maps
  • Multi-named-config orchestration: Configs holds named registries (per the rotini spec's configuration_files[]) and multiplexes their events through a single <-chan NamedEvent
  • io/fs.FS-backed NewFileSourceFS for testing with testing/fstest.MapFS and for loading from embed.FS bundles
  • Remote-backend adapters (etcd / consul / vault / awsssm / k8s) live in their own modules — opt-in by go get; the core ships NewInMemoryBackend as a reference and for tests
  • Minimal third-party footprint: composes the go-rotini family

Installation

go get github.com/go-rotini/recon

Requires Go 1.26 or later.

Quick Start

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/go-rotini/recon"
)

type Config struct {
	Port    int           `recon:"server.port,default=8080"`
	DBURL   string        `recon:"database.dsn,required,secret"`
	Timeout time.Duration `recon:"http.timeout,default=30s"`
}

func main() {
	envSrc := recon.NewOSEnvSource()
	fileSrc, err := recon.NewYAMLSource("config.yaml", recon.WithOptional(true))
	if err != nil {
		log.Fatal(err)
	}

	r, err := recon.New(
		recon.WithSource(envSrc),
		recon.WithSource(fileSrc),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()

	var cfg Config
	if err := r.Bind(&cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", cfg)
}

Sources and precedence

Source chains are explicit and ordered. The first source to return ok=true for a key wins, with Set (programmatic override) always sitting above every source and SetDefault always below:

envSrc := recon.NewOSEnvSource(recon.WithEnvPrefix("APP_"))
localSrc, _ := recon.NewDotenvSource(".env.local", recon.WithOptional(true))
fileSrc, _ := recon.NewYAMLSource("config.yaml")

r, err := recon.New(
	recon.WithSource(envSrc),    // wins by default
	recon.WithSource(localSrc),  // dev overrides
	recon.WithSource(fileSrc),   // baseline
)

Per-key overrides are first-class: RegisterAlias makes one path resolve to another (cycle-checked), and PinSource(key, sourceName) forces a key to resolve only from a named source.

Live reload

Live[T] wraps the registry in an atomic.Pointer[T]-backed handle. Reads are O(1) and lock-free; readers always observe a complete, validated snapshot. Any source that implements Watcher participates; file-backed sources get watching from the registry's WatcherFactory (the bundled FSWatcher is backed by go-rotini/fs and is atomic-rename aware, debounced, multi-backend).

type Config struct {
	Port   int    `recon:"server.port,default=8080"`
	LogLvl string `recon:"log.level,default=info"`
}

live, err := recon.NewLive[Config](r)
if err != nil {
	log.Fatal(err)
}
defer live.Close()

go func() {
	for ev := range live.Events() {
		if ev.Err != nil {
			log.Printf("reload failed (source %q): %v", ev.Source, ev.Err)
			continue
		}
		log.Printf("reload: changed=%v", ev.Changed)
	}
}()

for {
	cfg := live.Get() // *Config — never nil after NewLive succeeds
	serve(cfg)
}

The reload pipeline rebuilds the snapshot, computes the changed-key delta, optionally validates, and atomic-swaps the pointer; a failed candidate retains the previous value and emits an Event with Err set.

Per-source resolution (PerSourceFor)

When a single key needs custom precedence — env-only in containers, config-first for daemons — PerSourceFor[T] returns each source's contribution separately so the caller picks a winner:

ps, _ := recon.PerSourceFor[int](r, "server.port")

if inContainer() {
	if e := ps.BySource("env"); e.IsSet {
		return e.Value
	}
}
return ps.Resolved.Value // what Get[int] would have returned

Every entry carries its own IsSet + Err, so "source supplied an unparseable value" is distinguishable from "source had nothing".

Error formatting

FormatError(r, err) renders a *MultiError (or any single typed error) into a multi-line summary with path, reason, source attribution, and — when the registry is non-nil — the full precedence chain for each failing key. Drop-in printable output for log.Fatal:

if err := r.Bind(&cfg); err != nil {
	log.Fatal(recon.FormatError(r, err))
}

Validation

Schema validation is opt-in. WithSchema(bytes) is the one-line form for raw JSON Schema; for YAML / TOML / JSONC schemas or pre-compiled *jsonschema.Schema values, build the validator explicitly and pass it via WithValidator:

r, err := recon.New(
	recon.WithSource(fileSrc),
	recon.WithSchema(schemaBytes),
)

Validation failures during reload are reported on the Registry.Events() channel and via Live.LastError(); the previous snapshot is retained so live config keeps working. For per-struct validation, implement Validator (or ValidatorContext) on the bind target — the decoder calls it after every field is populated.

Multi-named configs (Configs)

For applications with multiple independent configuration namespaces — each with its own precedence, schema, and watch policy — Configs holds named registries and multiplexes their reload events:

cs := recon.NewConfigs()
defer cs.Close()
_ = cs.Register("database", dbRegistry)
_ = cs.Register("server", srvRegistry)

go func() {
	for ev := range cs.Events() {
		log.Printf("%s reloaded: changed=%v err=%v", ev.Name, ev.Changed, ev.Err)
	}
}()

Registries added via Register after Events() has been called are folded into the stream automatically; Remove(name) tears the per-name forwarder down cleanly.

Provenance and introspection

Describe() returns the full per-key view — which source supplied each value, which other sources had a value, whether the key is secret:

for _, k := range r.Describe().Keys {
	fmt.Printf("%s = %s (from %s; aliases: %v)\n",
		k.Path, k.Value, k.Source, k.Aliases)
}

Describe redacts secret-tagged values automatically. The data feeds straight into a myapp config show / myapp config sources subcommand without further plumbing.

Encoding back out (Save / SaveTo / GenerateTemplate)

Save writes the current resolved view to an io.Writer; SaveTo writes to a file path and atomic-renames into place. Default policy is safe-to-pipe-anywhere — secret-marked keys are redacted, default-only keys are omitted; opt back in with WithSaveIncludeSecrets / WithSaveIncludeDefaults:

// Dump current config to disk.
_ = r.SaveTo("snapshot.yaml")

// Dump just one sub-tree.
_ = r.SaveTo("server.yaml",
	recon.WithSaveOnly("server"),
	recon.WithSaveFormat(recon.FormatYAML),
)

GenerateTemplate(format) emits a stub document populated from the registered defaults — the "myapp config init" entry point. Secret keys are redacted unless WithSaveIncludeSecrets is passed:

out, _ := r.GenerateTemplate(recon.FormatYAML)
_ = os.WriteFile("config.example.yaml", out, 0o644)

Documentation

Full API reference is available on pkg.go.dev.

Contributing

See CONTRIBUTING.md for guidelines on how to contribute to this project.

Code of Conduct

This project follows a code of conduct to ensure a welcoming community. See CODE_OF_CONDUCT.md.

Security

To report a vulnerability, see SECURITY.md.

License

This project is licensed under the MIT License. See LICENSE for details.

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

See the README at https://github.com/go-rotini/recon for runnable examples.

Index

Examples

Constants

View Source
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.

View Source
const DefaultDelimiter = "."

DefaultDelimiter is the path-segment delimiter ParsePath consumes and Path.String emits.

View Source
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

View Source
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

func DetectFormat(path string) (string, bool)

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

func DotTransform(p Path) string

DotTransform projects Path{"server","port"} to "server.port" — the identity projection for file sources whose storage key already matches the recon path.

func FormatError

func FormatError(reg *Registry, err error, color ...bool) string

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

func FormatErrorColor(reg *Registry, err error) string

FormatErrorColor is FormatError with ANSI colorization always on.

func Get

func Get[T any](r *Registry, key string) (T, bool, error)

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

func IdentityTransform(p Path) string

IdentityTransform is an alias for DotTransform, named to make "this source's keys are already recon-shaped" explicit at call sites.

func KebabTransform

func KebabTransform(p Path) string

KebabTransform projects Path{"server","port"} to "server-port". The default for CLI-flag sources.

func MustGet

func MustGet[T any](r *Registry, key string) T

MustGet panics on error or not-set. Useful in main() when a missing key is a programmer error.

func SnakeUpperTransform

func SnakeUpperTransform(p Path) string

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

type BackendWatcher interface {
	Watch(ctx context.Context) (<-chan struct{}, error)
}

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.

var TOML Codec = tomlCodec{}

TOML is the Codec for TOML documents. Registered in the default codec set by New.

var YAML Codec = yamlCodec{}

YAML is the Codec for YAML 1.2.2 documents (and the KYAML strict subset). 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

func NewCodecs(initial ...Codec) *Codecs

NewCodecs returns a Codecs pre-populated with initial. Later entries with the same Name shadow earlier ones.

func (*Codecs) ByExtension

func (c *Codecs) ByExtension(ext string) (Codec, bool)

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) ByName

func (c *Codecs) ByName(name string) (Codec, bool)

ByName returns the codec named name. Lookup is case-sensitive.

func (*Codecs) Clone

func (c *Codecs) Clone() *Codecs

Clone returns an independent shallow copy. The codecs themselves are shared (stateless by contract); the lookup map is fresh.

func (*Codecs) Names

func (c *Codecs) Names() []string

Names returns the registered codec names in unspecified order.

func (*Codecs) Register

func (c *Codecs) Register(codec Codec)

Register adds or replaces a codec keyed by its Name. Nil is ignored.

func (*Codecs) Unregister

func (c *Codecs) Unregister(name string)

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 NewConfigs

func NewConfigs() *Configs

NewConfigs returns an empty *Configs.

func (*Configs) Close

func (c *Configs) Close() error

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) Get

func (c *Configs) Get(name string) (*Registry, bool)

Get returns the registry registered under name.

func (*Configs) MustGet

func (c *Configs) MustGet(name string) *Registry

MustGet panics when name is unknown.

func (*Configs) Names

func (c *Configs) Names() []string

Names returns the registered names in registration order.

func (*Configs) Register

func (c *Configs) Register(name string, r *Registry) error

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.

func (*Configs) Remove

func (c *Configs) Remove(name string) error

Remove unregisters and closes the named registry. Idempotent. The registry's [Close] closes its Events channel, which lets any running forwarder exit on its own.

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

type DeprecationWarning struct {
	Path        Path
	Source      string
	Replacement Path
	Message     string
}

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

type EmptyValueError struct {
	Path   Path
	Source string
}

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

func WithEnvKeyParser(fn func(name string) Path) EnvOption

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

func WithEnvPrefix(prefix string) EnvOption

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

func WithEnvVars(vars map[string]string) EnvOption

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

func (w *FSWatcher) Watch(ctx context.Context, path string) (<-chan SourceChange, error)

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".

func ParseTag

func ParseTag(s string) FieldTag

ParseTag parses a single struct-tag value. Unknown option tokens are silently ignored to leave room for future additions. Malformed key=value pairs degrade to bare option names.

ParseTag never returns an error; tag-related problems surface at the point where the option matters.

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) Close

func (s *FlagSource) Close() error

Close is a no-op.

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.

func (*FlagSource) Name

func (s *FlagSource) Name() string

Name returns the source identifier.

type ImmutableChangedError

type ImmutableChangedError struct {
	Path Path
	Old  string
	New  string
}

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

type KeyTransform func(p Path) string

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]) Close

func (l *Live[T]) Close() error

Close stops the rebind goroutine. Idempotent.

func (*Live[T]) Events

func (l *Live[T]) Events() <-chan Event

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.

func (*Live[T]) LastError

func (l *Live[T]) LastError() error

LastError returns the most recent rebind error, or nil if the most recent reload succeeded. Useful for health-check endpoints that want a single "is my config currently broken?" boolean.

func (*Live[T]) Reload

func (l *Live[T]) Reload(ctx context.Context) error

Reload forces a rebind off-cycle, bypassing the registry's reload engine. Useful in tests that need deterministic timing. The Events channel does not receive an entry for an off-cycle Reload.

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

func NewMapSource(name string, m map[string]any) *MapSource

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) Close

func (s *MapSource) Close() error

Close is a no-op.

func (*MapSource) Get

func (s *MapSource) Get(path Path) (Value, bool, error)

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).

func (*MapSource) Keys

func (s *MapSource) Keys() []Path

Keys enumerates every leaf path in the underlying map. Intermediate map nodes are not reported. The returned slice is sorted by canonical path string.

func (*MapSource) Name

func (s *MapSource) Name() string

Name returns the source identifier.

func (*MapSource) Replace

func (s *MapSource) Replace(m map[string]any)

Replace atomically swaps the underlying map. Useful in tests. The caller must not mutate m after the call; Replace takes ownership via deep copy.

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

func (m *MemoryBackend) Get(_ context.Context, key string) (string, bool, error)

Get implements RemoteBackend.

func (*MemoryBackend) List

func (m *MemoryBackend) List(_ context.Context, prefix string) ([]string, error)

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

type NamedEvent struct {
	Event

	Name string
}

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) Close

func (s *OSEnvSource) Close() error

Close is a no-op.

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) Name

func (s *OSEnvSource) Name() string

Name is fixed to "osenv".

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

func WithCodec(c Codec) Option

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

func WithCodecs(cs *Codecs) Option

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

func WithEventBufferSize(n int) Option

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

func WithLogger(l *slog.Logger) Option

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 WithPoll

func WithPoll(interval time.Duration) Option

WithPoll polls non-Watcher sources at interval. Off by default.

func WithPrecedence

func WithPrecedence(order ...string) Option

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

func WithReloadDebounce(d time.Duration) Option

WithReloadDebounce sets how long the engine waits for additional change events before firing a reload. Default 50ms.

func WithSchema

func WithSchema(schemaJSON []byte) Option

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

func WithSecretRedactor(fn func(string) string) Option

WithSecretRedactor replaces the default "***" redactor.

func WithSource

func WithSource(s Source) Option

WithSource registers a single source. Equivalent to Registry.AddSource after construction.

func WithSources

func WithSources(s ...Source) Option

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

func WithoutCodec(name string) Option

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

func MakePath(segments ...string) Path

MakePath constructs a Path from raw segments; no delimiter parsing is performed. Use ParsePath to parse a delimited string.

func ParsePath

func ParsePath(s string) Path

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

func (p Path) After(prefix Path) Path

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.

func (Path) Append

func (p Path) Append(seg ...string) Path

Append returns a new Path with seg appended.

func (Path) Clone

func (p Path) Clone() Path

Clone returns an independent copy of p.

func (Path) Equal

func (p Path) Equal(other Path) bool

Equal reports whether p and other have identical segments.

func (Path) HasPrefix

func (p Path) HasPrefix(prefix Path) bool

HasPrefix reports whether prefix is a segment-wise prefix of p. An empty prefix matches every Path.

func (Path) String

func (p Path) String() string

String returns the canonical delimited form of p. Segments containing the delimiter are bracket-escaped.

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

func PerSourceFor[T any](r *Registry, key string) (PerSource[T], error)

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

func PerSourceForPath[T any](r *Registry, p Path) (PerSource[T], error)

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

type Position struct {
	Line   int
	Column int
}

Position is a source-local position used by ParseError. Line and Column are 1-indexed; both zero means unknown.

func (Position) String

func (p Position) String() string

String formats the position as "line:col", or returns "" for the zero value.

type RawValue

type RawValue struct {
	Format string
	Data   []byte
}

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

func (rv RawValue) Decode(v any) error

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

func New(opts ...Option) (*Registry, error)

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

func (r *Registry) AddSource(s Source) error

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

func (r *Registry) AllKeys() []string

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

func (r *Registry) BindContext(ctx context.Context, target any, opts ...DecodeOption) error

BindContext is the context-aware [Bind]. ctx is threaded into any ValidatorContext hook the target implements.

func (*Registry) Close

func (r *Registry) Close() error

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

func (r *Registry) Events() <-chan Event

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

func (r *Registry) Get(key string) (Value, bool, error)

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

func (r *Registry) GetAny(key string) (any, bool, error)

GetAny returns the underlying Go value at key — the same shape Value.Any returns.

func (*Registry) GetBool

func (r *Registry) GetBool(key string) (bool, bool, error)

GetBool returns the value at key as bool.

func (*Registry) GetDuration

func (r *Registry) GetDuration(key string) (time.Duration, bool, error)

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) GetFloat

func (r *Registry) GetFloat(key string) (float64, bool, error)

GetFloat returns the value at key as float64.

func (*Registry) GetInt

func (r *Registry) GetInt(key string) (int, bool, error)

GetInt returns the value at key as int. Wraps int64 → int without overflow check; 32-bit-target callers should prefer [GetInt64].

func (*Registry) GetInt64

func (r *Registry) GetInt64(key string) (int64, bool, error)

GetInt64 returns the value at key as int64.

func (*Registry) GetPath

func (r *Registry) GetPath(p Path) (Value, bool, error)

GetPath is the Path-typed variant of Registry.Get.

func (*Registry) GetString

func (r *Registry) GetString(key string) (string, bool, error)

GetString returns the string value at key. Returns ErrTypeMismatch when the resolved kind is not StringKind.

func (*Registry) GetStringMap

func (r *Registry) GetStringMap(key string) (map[string]string, bool, error)

GetStringMap returns the value at key as map[string]string. The wire kind must be MapKind.

func (*Registry) GetStringSlice

func (r *Registry) GetStringSlice(key string) ([]string, bool, error)

GetStringSlice returns the value at key as []string. The wire kind must be SliceKind; each element is projected via Value.AsString.

func (*Registry) GetTime

func (r *Registry) GetTime(key string) (time.Time, bool, error)

GetTime returns the value at key as time.Time. A native time.Time or an RFC 3339 string is accepted.

func (*Registry) InsertSource

func (r *Registry) InsertSource(at int, s Source) error

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

func (r *Registry) IsImmutable(key string) bool

IsImmutable reports whether path has an immutable baseline recorded.

func (*Registry) IsSecret

func (r *Registry) IsSecret(key string) bool

IsSecret reports whether key has been marked secret, either via [MarkSecret] or by the bind walker on a `secret`-tagged field.

func (*Registry) IsSet

func (r *Registry) IsSet(key string) bool

IsSet reports whether any layer of the registry has a value for key. An empty string still counts as set.

func (*Registry) MarkSecret

func (r *Registry) MarkSecret(key string)

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

func (r *Registry) PinSource(key, sourceName string) error

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

func (r *Registry) Prefix() Path

Prefix returns the sub-tree path this view is rooted at, or an empty Path for a root registry.

func (*Registry) RegisterAlias

func (r *Registry) RegisterAlias(alias, canonical string) error

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) Reload

func (r *Registry) Reload() error

Reload re-reads every watched source and rebuilds the snapshot.

func (*Registry) ReloadContext

func (r *Registry) ReloadContext(ctx context.Context) error

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

func (r *Registry) RemoveSource(name string) error

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:

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

func (r *Registry) Set(key string, value any) error

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

func (r *Registry) SetDefault(key string, value any) error

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

func (r *Registry) Snapshot() *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

func (r *Registry) Sources() []string

Sources returns the registered source names in precedence order (first = highest). The returned slice is a copy.

func (*Registry) Sub

func (r *Registry) Sub(prefix string) *Registry

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

func (r *Registry) Unpin(key string) error

Unpin removes a previous pin for key. No-op when key was not pinned. Transactional rollback on rebuild failure.

func (*Registry) Unset

func (r *Registry) Unset(key string) error

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

func (r *Registry) Validate() error

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

type SchemaValidator interface {
	Validate(snapshot map[string]any) error
}

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

type Secret[T any] = env.Secret[T]

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.

func NewSecret

func NewSecret[T any](v T) Secret[T]

NewSecret aliases env.NewSecret — wraps v in a Secret so it redacts on every textual output.

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

func (s *Snapshot) AsMap() map[string]any

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

func (s *Snapshot) Get(p Path) (Value, bool)

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

func (s *Snapshot) IsSecret(p Path) bool

IsSecret reports whether p was marked secret when this snapshot was built.

func (*Snapshot) Keys

func (s *Snapshot) Keys() []Path

Keys returns every known path sorted by canonical string form. The returned slice aliases the snapshot's storage and must not be mutated.

func (*Snapshot) SourceFor

func (s *Snapshot) SourceFor(p Path) []string

SourceFor returns the precedence-ordered list of source names that supplied a value for p. The first element is the winner; subsequent elements are shadowed entries.

func (*Snapshot) String

func (s *Snapshot) String() string

String returns a compact human-readable form of the snapshot. Keys marked secret are rendered through the snapshot's redactor.

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

func NewEnvFamilySource(name string, families ...EnvFamily) Source

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

func NewFileSourceFS(name string, fsys fs.FS, path string, opts ...FileOption) (Source, error)

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

type SourceChange struct {
	Keys []Path
	Err  error
}

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

type UnknownKeyError struct {
	Path   Path
	Source string
}

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

type Unmarshaler interface {
	UnmarshalRecon(v Value) error
}

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

type ValidationError struct {
	Path   Path
	Rule   string
	Msg    string
	Secret bool
}

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

type ValidatorContext interface {
	Validate(ctx context.Context) error
}

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

func NewRawValue(format string, data []byte) Value

NewRawValue wraps undecoded bytes plus a format hint in a Value.

func NewValue

func NewValue(v any) Value

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

func (v Value) Any() 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) AsBool

func (v Value) AsBool() (bool, error)

AsBool returns the bool value if v is BoolKind.

func (Value) AsDuration

func (v Value) AsDuration() (time.Duration, error)

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

func (v Value) AsFloat64() (float64, error)

AsFloat64 returns the float64 value if v is FloatKind. IntKind values are widened losslessly as a convenience.

func (Value) AsInt64

func (v Value) AsInt64() (int64, error)

AsInt64 returns the int64 value if v is IntKind.

func (Value) AsMap

func (v Value) AsMap() (map[string]Value, error)

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) AsRaw

func (v Value) AsRaw() (RawValue, error)

AsRaw returns the RawValue if v is RawKind.

func (Value) AsSlice

func (v Value) AsSlice() ([]Value, error)

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

func (v Value) AsString() (string, error)

AsString returns the string value if v is StringKind.

func (Value) AsTime

func (v Value) AsTime() (time.Time, error)

AsTime returns the time.Time value if v is TimeKind. StringKind values are parsed as RFC 3339.

func (Value) IsZero

func (v Value) IsZero() bool

IsZero reports whether v carries no underlying datum. A NullKind Value is zero; an empty string, slice, or map is not.

func (Value) Kind

func (v Value) Kind() ValueKind

Kind reports the wire-form type of v.

func (Value) Source

func (v Value) Source() string

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

func (v Value) String() 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.

func (ValueKind) String

func (k ValueKind) String() string

String returns the lowercase name of the kind.

type ValueSource

type ValueSource[T any] struct {
	Source string
	Value  T
	IsSet  bool
	Err    error
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL