Documentation
¶
Overview ¶
Package config loads, merges and writes layered configuration, with a single component owning every read and write.
A Store is that component. It reads each source, merges them in a deterministic precedence order, records which layer supplied each value, and is the only thing that writes any of them back. Everything else is a view over the snapshot it publishes, so there is no protocol between a reader and a writer — there is only one of each, and it is the Store.
s, err := config.NewStore(ctx,
config.WithFiles(config.OS(), "/etc/app.yaml", "/home/me/.app.yaml"),
config.WithEnv("APP"),
config.WithFlags(flags))
Sources are added lowest precedence first, and a later one wins. Files, compiled-in defaults (WithReaders), environment variables and command-line flags are all ordinary layers, so precedence, provenance and shadowing work the same way for each.
Reading ¶
Store.View returns a View over the current snapshot, satisfying Reader. A snapshot is immutable, so a sequence of reads against one is coherent even if a reload lands midway; Store.With pins a single snapshot across a block of reads when several values must agree with each other.
There is a named accessor for the common types — View.GetString, View.GetInt and the rest — and Value for everything else: it reads any type at all, including a consumer's own, so Reader does not have to grow a method per type. Durations, IP addresses, URLs, timezones and anything implementing encoding.TextUnmarshaler decode from their ordinary written form.
Writing ¶
Store.Apply persists changes, and Store.Plan shows what it would do without doing it. A write is routed at the layer that already owns the key, so it lands where it will be read back, and comments, key order and block style in the target file survive it. A write that cannot change the effective value — because a higher layer still defines the key — reports that rather than appearing to succeed.
if _, err := s.Apply(ctx, config.Set("server.port", 9090)); err != nil {
return err
}
Other file formats ¶
The core reads and writes YAML. Any other file format plugs in through a Codec — Codec.Decode turns a source's bytes into values, and an EditingCodec adds in-place editing — passed to NewCodecBackend. The file machinery is shared, so a format is a codec rather than a whole backend, and each ships as its own module so no consumer inherits a parser they do not use.
Provenance ¶
View.Origin reports which source supplied a value, View.Shadowed lists every layer defining it, and View.Explain renders the whole chain for a diagnostic. These answer "why is this value what it is", which a merge-eager library cannot, because merging discards the information before anyone can ask.
Reacting to change ¶
Store.Watch notices changes made by anything other than this process, and observers registered with Store.AddObserver are told exactly once per logical change. An observer must not write configuration while reacting to a change: doing so returns ErrWriteFromObserver, because each such write is itself a change and the two would not stop. Capture what is needed, return, and write from elsewhere.
Typed sections ¶
ObserveSection decodes a section into a struct and keeps it current across reloads, publishing immutable snapshots through ObservedSection. A package consuming those settings declares a one-method interface over its own struct and never imports this one, which is what keeps configuration from spreading through a codebase.
Index ¶
- Constants
- Variables
- func MustValue[T any](cfg Reader, path string) T
- func ValidateStruct[T any](cfg Reader, opts ...SchemaOption) error
- func Value[T any](cfg Reader, path string) (T, error)
- type Backend
- func NewCodecBackend(filesystem FS, path string, codec Codec) Backend
- func NewEnvBackend(prefix string, opts ...EnvOption) Backend
- func NewFileBackend(filesystem FS, path string) Backend
- func NewFlagBackend(flags *pflag.FlagSet, opts ...FlagOption) Backend
- func NewReaderBackend(name string, content []byte) Backend
- type Binder
- type Capabilities
- type Change
- type Codec
- type Edit
- type EditingCodec
- type EnvOption
- type FS
- type FieldSchema
- type FlagOption
- type Layer
- type LinkReader
- type NamedSource
- type Observable
- type Observed
- type ObservedSection
- type ObserverFunc
- type Operation
- type Pending
- type Plan
- type PollIntervalHinter
- type Reader
- type RealPather
- type Schema
- type SchemaOption
- type Section
- type SectionBindingConfig
- type SectionBindingOption
- func WithSectionApply[T any](apply func(SectionChange[T]) error) SectionBindingOption[T]
- func WithSectionDefaultFunc[T any](defaultFunc func(Observed) T, merge func(defaults, overlay T) T) SectionBindingOption[T]
- func WithSectionDefaults[T any](defaults T, merge func(defaults, overlay T) T) SectionBindingOption[T]
- func WithSectionEqual[T any](equal func(previous, current Section[T]) bool) SectionBindingOption[T]
- func WithSectionValidator[T any](validate func(T) error) SectionBindingOption[T]
- type SectionChange
- type Snapshot
- func (s *Snapshot) Get(path string) (any, bool)
- func (s *Snapshot) Has(path string) bool
- func (s *Snapshot) Keys() []string
- func (s *Snapshot) Layers() []Layer
- func (s *Snapshot) Origin(path string) (Source, bool)
- func (s *Snapshot) Shadowed(path string) []Source
- func (s *Snapshot) Values() map[string]any
- func (s *Snapshot) Version() uint64
- type Source
- type SourceKind
- type Store
- func (s *Store) AddLayer(ctx context.Context, name string, r io.Reader) error
- func (s *Store) AddObserver(o Observable)
- func (s *Store) AddObserverFunc(f func(Observed) error)
- func (s *Store) Apply(ctx context.Context, changes ...Change) (*Snapshot, error)
- func (s *Store) Observers() []Observable
- func (s *Store) OnObserverError(f func(error))
- func (s *Store) OnReloadError(f func(error))
- func (s *Store) Plan(changes ...Change) (*Plan, error)
- func (s *Store) Reload(ctx context.Context) error
- func (s *Store) Snapshot() *Snapshot
- func (s *Store) Sources() []string
- func (s *Store) View() *View
- func (s *Store) Watch(ctx context.Context, opts ...WatchOption) (stop func(), err error)
- func (s *Store) With(fn func(*View) error) error
- type StoreOption
- func RequireFirstSource() StoreOption
- func WithBackend(b Backend) StoreOption
- func WithEnv(prefix string, opts ...EnvOption) StoreOption
- func WithFiles(filesystem FS, paths ...string) StoreOption
- func WithFlags(flags *pflag.FlagSet, opts ...FlagOption) StoreOption
- func WithReaders(sources ...NamedSource) StoreOption
- func WithSchema(schema *Schema) StoreOption
- type ValidationError
- type ValidationResult
- type View
- func (v *View) Explain(path string) string
- func (v *View) Get(path string) any
- func (v *View) GetBool(path string) bool
- func (v *View) GetDuration(path string) time.Duration
- func (v *View) GetFloat(path string) float64
- func (v *View) GetFloat64(path string) float64
- func (v *View) GetInt(path string) int
- func (v *View) GetInt32(path string) int32
- func (v *View) GetInt64(path string) int64
- func (v *View) GetIntSlice(path string) []int
- func (v *View) GetSizeInBytes(path string) uint
- func (v *View) GetString(path string) string
- func (v *View) GetStringMap(path string) map[string]any
- func (v *View) GetStringMapString(path string) map[string]string
- func (v *View) GetStringMapStringSlice(path string) map[string][]string
- func (v *View) GetStringSlice(path string) []string
- func (v *View) GetTime(path string) time.Time
- func (v *View) GetUint(path string) uint
- func (v *View) GetUint8(path string) uint8
- func (v *View) GetUint16(path string) uint16
- func (v *View) GetUint32(path string) uint32
- func (v *View) GetUint64(path string) uint64
- func (v *View) Has(path string) bool
- func (v *View) IsSet(path string) bool
- func (v *View) Keys() []string
- func (v *View) Origin(path string) (Source, bool)
- func (v *View) SectionExists(path string) bool
- func (v *View) Shadowed(path string) []Source
- func (v *View) Snapshot() *Snapshot
- func (v *View) Sub(key string) *View
- func (v *View) Unmarshal(target any) error
- func (v *View) UnmarshalKey(path string, target any) error
- func (v *View) Validate(schema *Schema) *ValidationResult
- type WatchOption
- type WatchableBackend
- type Watcher
- type WritableBackend
- type YAMLCodec
Constants ¶
const DefaultPollInterval = 2 * time.Second
DefaultPollInterval is how often the polling watcher checks for changes when the filesystem cannot notify.
const DefaultSettleInterval = 250 * time.Millisecond
DefaultSettleInterval is how long a burst of foreign change reports is allowed to settle before the Store reloads.
It exists because a logical configuration change is not always one filesystem event. A deploy replacing two overlays, a config-management run rewriting a directory, an operator saving two files — each produces separate events, and nothing in the filesystem says they were meant as one change. Without a settle window observers run once per file, and the first run sees a combination nobody intended: the first file updated, the second not yet.
250ms is inherited from the pre-Store implementation, where it was chosen to tolerate slow and networked filesystems. It is long enough to absorb the writes of a single deploy step and short enough to be imperceptible in the reload latency of a long-running service.
Variables ¶
var ( // ErrBackendUnsafe is returned when a source contains a construct that // cannot be safely round-tripped, so editing it would risk corruption. ErrBackendUnsafe = errors.New("config: source cannot be safely edited") // ErrBackendParse is returned when a source is not valid for its format. ErrBackendParse = errors.New("config: source could not be parsed") // ErrInternal is returned when an invariant of this module does not hold. // It is never the caller's fault and is always worth reporting. ErrInternal = errors.New("config: internal invariant violated") )
Backend errors. Callers should branch on these with errors.Is.
var ( // ErrNoWritableLayer is returned when a change has nowhere it can be // written: every layer that could hold it is read-only. ErrNoWritableLayer = errors.New("config: no writable layer for change") // ErrNoChanges is returned when an apply is asked to do nothing. ErrNoChanges = errors.New("config: no changes to apply") // ErrInvalidPath is returned for a malformed dotted path. ErrInvalidPath = errors.New("config: invalid path") // ErrInvalidTarget is returned when a target cannot receive what is being // asked of it: a decode target that cannot hold values, a layer with no // name, or a pinned write target naming no writable source. ErrInvalidTarget = errors.New("config: invalid target") // ErrSensitiveLeak is returned when a write would land a key a sensitive // source defines into a layer that is not sensitive — writing secret-category // material into a plain store. Because secrets backends are read-only, a // write to a key they own routes down to the next writable layer, typically a // file; refusing it is what keeps that safe. ErrSensitiveLeak = errors.New("config: refusing to write a sensitive key into a non-sensitive layer") )
Routing errors.
var ( // ErrNoSources is returned when a Store is built with nothing to read. ErrNoSources = errors.New("config: no sources configured") // ErrInvalidConfig is returned when a candidate configuration fails schema // validation. The previous configuration, if any, is retained. ErrInvalidConfig = errors.New("config: configuration is not valid") )
var ( // ErrConflict is returned when a source changed between being read and // being written, so committing would silently discard someone else's work. ErrConflict = errors.New("config: source changed since it was read") // ErrNotWritable is returned when a change is routed at a backend that // cannot persist. ErrNotWritable = errors.New("config: backend is not writable") // ErrPartialCommit is returned when a multi-source commit fails partway // and could not be fully rolled back. It always names what is in which // state: a caller must never be left guessing. ErrPartialCommit = errors.New("config: commit partially applied") )
Write errors.
var ErrAmbiguousEnvKey = errors.New("config: environment variable is ambiguous")
ErrAmbiguousEnvKey is returned when a set environment variable could designate more than one configuration key, so honouring it would mean guessing which the user meant.
var ErrEmptySchema = errors.New("config: schema has no fields defined")
ErrEmptySchema is returned when schema construction produced no fields. A schema that constrains nothing would validate everything, which is worse than having none: the caller believes their configuration is checked.
var ErrNoMergeFunc = errors.New("config: section defaults require a merge function")
ErrNoMergeFunc is returned when a section supplies defaults but no way to combine them with the configured values. Silently preferring one over the other would drop half the settings without saying so.
var ErrReadOnlyFS = errors.New("config: filesystem is read-only")
ErrReadOnlyFS is returned by the write methods of a read-only FS — WriteFile, Rename, Remove and MkdirAll. A filesystem adapter over an inherently read-only source (an io/fs.FS, an HTTP URL) returns it so a caller can errors.Is it, rather than a bare fs.ErrPermission that a genuine permission failure is indistinguishable from. See the filesystem adapters spec, D4.
ErrWatchUnavailable is returned when watching cannot work for the given sources. It is deliberately loud: a watcher that silently does nothing is worse than none, because the application believes it will hear about changes and never will.
var ErrWriteFromObserver = errors.New(
"config: cannot change configuration from inside an observer")
ErrWriteFromObserver is returned when configuration is changed from inside an observer callback.
Writing while reacting to a write is refused outright rather than made to work. Each such write is itself a change, which notifies, which runs the observer again — a cascade with no natural end, and one the Store cannot break without either dropping notifications other components need or silently reordering what changed when.
An observer that needs to update configuration must take the change *out* of the observation: record what is needed, return, and perform the write from somewhere else — a worker goroutine, a queue, the next tick. Serialising and de-duplicating those deferred writes belongs to the consumer, which is the only party that knows which of them still matter.
Functions ¶
func MustValue ¶ added in v0.3.0
MustValue reads a configuration value as T, panicking if it cannot be decoded.
For a value a program cannot run without, read at startup where a panic is a clear failure rather than a surprise later.
func ValidateStruct ¶
func ValidateStruct[T any](cfg Reader, opts ...SchemaOption) error
ValidateStruct checks a view against the schema derived from T's struct tags, returning a formatted error if any rule fails and nil if it does not.
It is the shortest way to validate the slice of configuration a command or feature cares about, without building a Schema by hand:
if err := config.ValidateStruct[MyConfig](store.View()); err != nil {
return err
}
A scoped view validates its own subtree, so a schema written for a section can be applied to that section:
if err := config.ValidateStruct[Server](store.View().Sub("server")); err != nil {
return err
}
The parameter is Reader rather than *View so that code under test can pass a mock. Validation only reads — Get, Has, Keys and Shadowed — so requiring the concrete type bought nothing and made every caller's own validation untestable without a real Store.
Schema options such as WithStrictMode may be passed through.
func Value ¶ added in v0.3.0
Value reads a configuration value as T.
This is the general form of the typed accessors: it decodes into whatever type is asked for, so it covers types this module has no method for and types it has never heard of. A struct, a slice, a map, an IP address, a URL, a timezone, or a consumer's own type implementing encoding.TextUnmarshaler all work, because the decoder is told how to read text into them.
port, err := config.Value[int](cfg, "server.port") addr, err := config.Value[netip.Addr](cfg, "server.bind") level, err := config.Value[LogLevel](cfg, "log.level") // your own type
It is a function rather than a method because Go does not allow type parameters on methods — the same reason UnmarshalSection and ValidateStruct are functions.
An absent key yields the zero value and no error, matching the accessors: a caller distinguishing "absent" from "set to zero" asks Reader.Has.
Types ¶
type Backend ¶ added in v0.3.0
type Backend interface {
// ID identifies the backend for diagnostics and provenance.
//
// For a [WritableBackend] it is also how the Store finds this backend
// again when routing a write, by matching a layer's Source.Name against
// it. Those two must therefore agree: a backend whose Load reports layers
// named something other than what ID returns cannot receive writes.
ID() string
// Load returns the layers this backend contributes, in precedence order.
// A backend may contribute more than one — a multi-document file
// contributes one layer per document.
//
// A source that does not exist returns fs.ErrNotExist, so the caller can
// decide whether that is fatal: a base file usually is, an optional
// overlay usually is not.
// Load reads this backend's sources into layers.
//
// below is everything the lower-precedence backends contributed, in order.
// Most backends ignore it; a backend whose reading of its own input depends
// on what is already defined needs it, and receiving it as an argument is
// what stops that being a separate call someone can forget to make. The
// environment backend is the case: mapping APP_SERVER_PORT back to a dotted
// key is ambiguous without knowing whether server.port already exists.
Load(ctx context.Context, below []Layer) ([]Layer, error)
// Capabilities describes what this backend can do, so callers can adapt
// rather than discover limitations by hitting them.
Capabilities() Capabilities
}
Backend is a source of configuration layers.
Read and write are deliberately separate interfaces. Reading is fetch, parse, normalise; writing carries ownership, atomicity, conflict detection, secret handling and failure modes that differ enormously between a local file and a remote parameter store. One interface pretending they are the same would either lie about those differences or degrade to the weakest member.
func NewCodecBackend ¶ added in v0.5.0
NewCodecBackend returns a backend reading a single file through a codec.
It returns a write target only when the codec can edit: the concrete type implements WritableBackend exactly when the codec implements EditingCodec, so routing and Plan agree with the type system instead of a backend advertising a capability at the type level and failing at call time. A file on a filesystem is always watchable, so both shapes implement WatchableBackend.
func NewEnvBackend ¶ added in v0.3.0
NewEnvBackend returns a backend contributing environment variables that carry the given prefix.
The prefix is required, and is a security control rather than tidiness. Without one, any environment variable matching a configuration key can silently change behaviour — on a shared CI runner, a container host or a multi-tenant box, an unrelated process setting LOG_LEVEL or AI_PROVIDER would reconfigure every tool running there. With a prefix, unprefixed variables cannot reach configuration at all.
Pass the prefix without a trailing underscore.
func NewFileBackend ¶ added in v0.3.0
NewFileBackend returns a backend reading YAML from a path on the given filesystem.
YAML is the module's built-in format: it is the default, and the comment-preserving write path — the module's headline feature — is built on yamldoc. Every other format ships as a sibling module supplying its own codec to NewCodecBackend; this is that call with the YAML codec.
func NewFlagBackend ¶ added in v0.3.0
func NewFlagBackend(flags *pflag.FlagSet, opts ...FlagOption) Backend
NewFlagBackend returns a backend contributing flags the user actually changed.
Only changed flags contribute, and that is the whole point. Binding a flag regardless of whether it was set means its default silently masks configuration: the file says port 9090, nobody passed --port, and the effective value is the flag default of 8080. The user then has no way to discover why their configuration is being ignored.
func NewReaderBackend ¶ added in v0.3.0
NewReaderBackend returns a backend contributing YAML from bytes.
The name appears in provenance, so give it something a user would recognise — "embedded:defaults.yaml" rather than "reader1".
type Binder ¶ added in v0.3.0
type Binder interface {
// View returns a read surface over the current configuration.
View() *View
// AddObserverFunc registers a function to run when configuration changes.
AddObserverFunc(func(Observed) error)
}
Binder is something that can be read and will report when it changes.
A Store is one. The interface exists so a typed section can be bound to anything that behaves like configuration — a fixed snapshot in a test, for instance — without dragging in the machinery that loads one.
type Capabilities ¶ added in v0.3.0
type Capabilities struct {
// PreservesComments reports whether an edit retains comments and
// formatting. True for document-like sources; meaningless for key-value
// stores, which have nowhere to put a comment.
PreservesComments bool
// AtomicMultiKey reports whether several keys can be written as one
// indivisible operation.
AtomicMultiKey bool
// NativeWatch reports whether the backend can notify of foreign changes,
// as opposed to needing to be polled.
NativeWatch bool
// Sensitive marks a backend as holding secret material. A value sourced
// from one must never be written into a layer that is not also sensitive;
// the write path enforces this, refusing such a write with [ErrSensitiveLeak].
Sensitive bool
}
Capabilities describes what a backend supports.
Declaring them is what lets a heterogeneous set of backends coexist without the weakest one setting the contract for all of them.
Nothing here says whether a backend can be written to or watched. Those are answered by implementing WritableBackend and WatchableBackend, so the type system checks them once instead of every caller checking a flag at runtime — and so a backend cannot claim one thing and do another.
Each field carries a consequence worth recording: a value from a Sensitive source must never be written into a layer that is not (the environment-secret leak in a new costume), the comment guarantee is document-backend-only and must be scoped rather than implied, cross-backend atomicity is impossible and must be refused or declared, and foreign-change latency differs per backend and must be stated.
Sensitive is enforced: the write path refuses a change that would land a key a Sensitive source defines into a layer that is not sensitive, returning ErrSensitiveLeak. The remaining fields are still forward-declared — stated where the reasoning is fresh so the consumer that eventually reads them inherits the intent rather than re-deriving it.
type Change ¶ added in v0.3.0
type Change struct {
// Path is the dotted key to change.
Path string
// Value is the new value. Ignored when Remove is set.
Value any
// Remove deletes the key and its subtree instead of setting it.
Remove bool
// Target optionally pins the change to a specific layer, overriding
// routing. Use when the caller genuinely knows better; routing's default
// is right far more often.
Target *Source
}
Change is a single edit to persist.
Setting and removing are one type rather than two methods because they route identically and must be applied in one batch — a caller replacing a subtree by removing some keys and setting others needs both halves to land together.
type Codec ¶ added in v0.5.0
type Codec interface {
// Decode returns one map per document in the source. A format with no
// multi-document concept returns exactly one. A document that is empty is a
// nil entry rather than an omission, so a later document keeps its index and
// its provenance.
Decode(path string, src []byte) ([]map[string]any, error)
}
Codec turns a source's bytes into layer values.
It is the format-specific half of a file-backed backend: everything else — reading the file, translating fs.ErrNotExist, fingerprinting for conflict detection, staging, atomic rename, mode preservation, symlink resolution, rollback and watching — is the same whatever the format, and lives in the backend. A codec knows only how to decode bytes to values, and (if it can) how to edit those bytes in place.
type Edit ¶ added in v0.3.0
type Edit struct {
// Document is the document index within the source.
Document int
Path string
Value any
Remove bool
}
Edit is one change addressed at a specific document of a backend.
type EditingCodec ¶ added in v0.5.0
type EditingCodec interface {
Codec
// Check reports whether this content can be round-tripped safely. It is
// called at load, so a source that cannot be edited is refused before the
// user has made any edits to lose.
Check(path string, src []byte) error
// Apply edits the source, preserving whatever the format can preserve.
Apply(path string, src []byte, edits []Edit) ([]byte, error)
// Empty returns the content of a new, empty document, for creating a file
// that does not exist yet.
Empty() []byte
}
EditingCodec is a Codec that can also edit a document in place.
A format that cannot be safely round-tripped simply does not implement this, and the backend built from it is not a write target: routing skips it and a write lands in the next writable layer down, reported as shadowed rather than failing. The type system checks the capability once, the same way WritableBackend splits from Backend, so a codec cannot claim an ability it does not have.
type EnvOption ¶ added in v0.3.0
type EnvOption func(*envBackend)
EnvOption configures the environment backend.
func WithEnviron ¶ added in v0.3.0
WithEnviron overrides where environment variables are read from. Tests use it to avoid mutating process state, which is global and makes parallel tests interfere with each other.
type FS ¶ added in v0.3.0
type FS interface {
// ReadFile returns a source's contents, or an error satisfying
// errors.Is(err, fs.ErrNotExist) when it is absent. That distinction is
// load-bearing: a missing overlay is normal and a missing base file is a
// broken installation, and only the Store can decide which.
ReadFile(name string) ([]byte, error)
// WriteFile replaces a file's contents.
WriteFile(name string, data []byte, perm fs.FileMode) error
// Stat reports a file's metadata. Used to preserve permissions across a
// write and to confirm a path is the file the watcher thinks it is.
Stat(name string) (fs.FileInfo, error)
// Rename moves a file, and is how a write is committed: content is staged
// beside the target and renamed over it, so a reader never observes a
// half-written file.
Rename(oldpath, newpath string) error
// Remove deletes a file, used to discard staged content that was never
// committed.
Remove(name string) error
// MkdirAll creates a directory and any missing parents, for writing a
// configuration file into a directory that does not exist yet.
MkdirAll(path string, perm fs.FileMode) error
}
FS is the filesystem surface this module needs, and the whole of it.
It is deliberately small. Six operations is what loading, watching and writing configuration actually requires, and stating them exactly is what lets a caller supply the operating system, a rooted directory, an in-memory filesystem or their own implementation without inheriting a dependency chosen on their behalf.
The standard library has no equivalent. io/fs.FS is read-only by design — no write, no rename, no remove — so it cannot express a module whose central feature is writing configuration back. os.Root has every operation needed and is hardened against escaping its root, but it is a concrete type with no in-memory implementation, so nothing can substitute for it in a test.
Adding a method here is a breaking change for every implementation, so capability beyond this minimum is expressed as an optional interface — RealPather and LinkReader — that an implementation may simply not satisfy. See D1 of the filesystem abstraction spec.
func Dir ¶ added in v0.3.0
Dir returns an FS rooted at a directory.
Every operation is confined to the directory: a path resolving outside it — through "..", an absolute path, or a symlink pointing away — is refused by the operating system rather than by a check this module performs. A tool reading configuration from a directory the user named gets that containment without asking for it.
The root is opened per operation rather than held open. An earlier version kept the handle for the FS's lifetime, which read as a feature — the root survived being renamed underneath it — and was a file-descriptor leak: FS has no Close, nothing in the module closes a filesystem, and a caller that builds one per unit of work accumulates descriptors until the process ends. Measured at a default 1024-descriptor limit it failed after 1021 calls, and at the 256 typical of macOS after 253 — which the documented testing pattern of config.Dir(t.TempDir()) per test would reach in a large suite.
The cost is one extra openat per operation. Configuration is read at startup, on reload and on write, so that is not a hot path; a leak that surfaces only at scale is the worse trade.
**Paths are relative to the root.** Pass "config.yaml", not "/etc/app/config.yaml" — an absolute path is treated as an attempt to escape and fails with "path escapes from parent" rather than fs.ErrNotExist. That distinction matters to the Store, which treats a missing optional source as normal and anything else as fatal, so an absolute path turns a file that is merely absent into a hard failure.
type FieldSchema ¶
type FieldSchema struct {
// Type is the expected Go type: "string", "int", "float64", "bool", "duration".
Type string
// Required indicates the field must be present and non-zero.
Required bool
// Description is used in validation error messages.
Description string
// Default is the default value for documentation and error hints only.
// The validation layer does not inject defaults — use embedded assets for that.
Default any
// Enum restricts the field to a set of allowed values.
Enum []any
}
FieldSchema describes a single configuration field.
type FlagOption ¶ added in v0.3.0
type FlagOption func(*flagBackend)
FlagOption configures the flag backend.
func BindFlag ¶ added in v0.3.0
func BindFlag(flagName, key string) FlagOption
BindFlag maps a flag name to a configuration key, for the cases where the two differ.
type Layer ¶ added in v0.3.0
Layer is one contributing set of configuration values, with the identity of where they came from.
type LinkReader ¶ added in v0.3.0
LinkReader optionally resolves a symbolic link.
A configuration file reached through a symlink should be written through it — editing the link itself would replace it with a regular file and detach every other path that pointed at the target. A filesystem with no notion of links does not implement this, and paths are used as given.
type NamedSource ¶ added in v0.3.0
NamedSource is in-memory configuration with an identity for provenance.
type Observable ¶
Observable is a component that reacts to configuration changes.
type Observed ¶ added in v0.3.0
type Observed interface {
Reader
// Sub returns a scoped view, nil when the key is absent.
Sub(key string) *View
// Snapshot exposes the exact configuration this notification describes.
Snapshot() *Snapshot
}
Observed is what an observer is handed when configuration changes.
It is pinned to the snapshot that triggered the notification rather than tracking the latest one. Without that, an observer processing one change could read values from a later change partway through its own callback and produce a result that never existed as a coherent configuration.
type ObservedSection ¶
type ObservedSection[T any] struct { // contains filtered or unexported fields }
ObservedSection stores the latest typed snapshot of an observed config section.
func ObserveSection ¶
func ObserveSection[T any]( binder Binder, key string, opts ...SectionBindingOption[T], ) (*ObservedSection[T], error)
ObserveSection decodes a typed section and keeps it current.
The returned value is the decoupling boundary this module exists for. A reusable package declares a one-method interface of its own —
type SettingsSource interface { Current() *ServerSettings }
— and *ObservedSection[T] satisfies it structurally, so the package can take live, reloadable configuration without importing this module at all.
Each snapshot is decoded in a single operation against a single configuration snapshot, so the struct handed out can never hold some fields from before a reload and some from after.
func (*ObservedSection[T]) ApplyInitial ¶ added in v0.3.0
func (s *ObservedSection[T]) ApplyInitial() error
ApplyInitial delivers the section the binding started with to the apply callback, exactly once, as an initial change.
It exists because the natural place to react to configuration is the same callback that reacts to a change — but a callback usually closes over something built after the binding, so firing it during ObserveSection would run it against a half-constructed world. Handing the caller the trigger lets them say when their dependencies exist.
A consumer that never calls this gets change-only delivery, which is what binding has always done. Calling it more than once is a no-op, so it is safe in a startup path that may run twice.
Returns nil when no apply callback was registered, and whatever the callback returns otherwise.
func (*ObservedSection[T]) Current ¶
func (s *ObservedSection[T]) Current() *T
Current returns a pointer to the latest immutable settings snapshot. Callers must treat the returned value as read-only and call Current again when they need to observe a later reload.
func (*ObservedSection[T]) Exists ¶
func (s *ObservedSection[T]) Exists() bool
Exists reports whether the latest snapshot came from an explicit section.
func (*ObservedSection[T]) Value ¶
func (s *ObservedSection[T]) Value() T
Value returns the latest complete settings snapshot.
func (*ObservedSection[T]) Version ¶
func (s *ObservedSection[T]) Version() uint64
Version returns the latest settings version. It starts at 1 after the initial snapshot and increments only when a later reload changes the observed section.
type ObserverFunc ¶ added in v0.3.0
ObserverFunc adapts a function to Observable.
func (ObserverFunc) Run ¶ added in v0.3.0
func (f ObserverFunc) Run(cfg Observed) error
Run calls the function.
type Operation ¶ added in v0.3.0
type Operation struct {
Change Change
// Target is the layer the change will be written to.
Target Source
// Creates reports whether the key is new to the target layer.
Creates bool
// ShadowedBy lists layers that will still outrank the target after the
// write, so the change will not be visible in the effective configuration.
//
// This is not an error — writing the file is what was asked — but a caller
// that cannot tell the user "written, but the environment still wins" will
// leave them confused about why nothing happened.
ShadowedBy []Source
}
Operation is one routed change: what to do, and where it will land.
type Pending ¶ added in v0.3.0
type Pending interface {
// Layers is what the backend will contribute once committed. This is what
// lets the Store build the next snapshot directly from the content it just
// wrote, rather than re-reading and hoping it gets the same answer.
Layers() []Layer
// Verify reports whether the source is still as it was when Prepare ran.
Verify(ctx context.Context) error
// Commit makes the staged content visible.
Commit(ctx context.Context) error
// Rollback restores the source to its pre-commit state. Best effort: it is
// called when a later commit in the same batch failed.
Rollback(ctx context.Context) error
// Discard abandons staged work that was never committed.
Discard(ctx context.Context) error
}
Pending is staged work that has not yet been made visible.
The three-phase shape — prepare, verify, commit — exists so that the expensive and failure-prone part happens while nothing is visible, and the window in which a partially applied set could be observed is as short as the backend can make it.
type Plan ¶ added in v0.3.0
type Plan struct {
Operations []Operation
}
Plan is a routed set of changes, inspectable before anything is written.
Producing the plan is the expensive part of an apply; executing it is not. Exposing it means a caller can show what a save would do — a CLI dry run, or a settings screen previewing its own changes — using exactly the routing the write will use, rather than an approximation of it.
func (*Plan) Effective ¶ added in v0.3.0
Effective reports whether every operation will be visible once written.
type PollIntervalHinter ¶ added in v0.9.0
PollIntervalHinter optionally reports how often a filesystem that must be polled should be checked for changes.
A filesystem with no real path is watched by polling (see RealPather), and the default cadence suits a local one. But for a remote filesystem — an object store, a network share — each poll is a round-trip, and often a billed API call, so the 2-second default is far too eager. Such a filesystem implements this to declare a sensible default (minutes, not seconds), which the watcher adopts unless the consumer sets one explicitly with WithPollInterval — an explicit interval always wins.
Implement this only when the filesystem genuinely wants a non-default cadence; a local or in-memory filesystem should not, so it keeps the responsive default.
type Reader ¶ added in v0.3.0
type Reader interface {
Get(path string) any
GetString(path string) string
GetBool(path string) bool
GetInt(path string) int
GetInt32(path string) int32
GetInt64(path string) int64
GetUint(path string) uint
GetUint8(path string) uint8
GetUint16(path string) uint16
GetUint32(path string) uint32
GetUint64(path string) uint64
GetFloat(path string) float64
GetFloat64(path string) float64
GetDuration(path string) time.Duration
GetTime(path string) time.Time
GetSizeInBytes(path string) uint
GetStringSlice(path string) []string
GetIntSlice(path string) []int
GetStringMap(path string) map[string]any
GetStringMapString(path string) map[string]string
GetStringMapStringSlice(path string) map[string][]string
Has(path string) bool
IsSet(path string) bool
SectionExists(path string) bool
Keys() []string
// Unmarshal decodes into a struct, and UnmarshalKey decodes one section.
// [Value] is the general form: it reads any type, including a consumer's
// own, so this interface does not have to grow a method per type.
Unmarshal(target any) error
UnmarshalKey(path string, target any) error
// Origin reports which layer supplied a value, and Shadowed lists every
// layer defining it. Both are what a merge-eager library cannot answer.
Origin(path string) (Source, bool)
Shadowed(path string) []Source
Explain(path string) string
}
Reader is the read surface a consumer depends on.
It is deliberately small and free of any dependency's types. The previous interface exposed the underlying configuration library directly, which made the abstraction fictional — anything reachable through that escape hatch became part of the contract, and replacing what sat behind it became impossible.
type RealPather ¶ added in v0.3.0
RealPather optionally reports the operating-system path backing a name.
Native filesystem notification works on real paths, so a filesystem that has one can be watched by fsnotify, and one that does not — an in-memory filesystem, a network abstraction — is polled instead.
This replaced a switch on concrete afero types, which could not see through a wrapper it did not recognise: a consumer decorating the real filesystem with their own type silently lost native notification and got polling with no diagnosis. Asking the filesystem about itself has no such blind spot.
A real path is a claim, not proof. The watcher still confirms that both sides see the same file before pointing fsnotify at it, because a base-path wrapper over an in-memory filesystem produces real-looking paths for files that exist only in memory.
Implement this only when the filesystem genuinely has operating-system paths. Satisfying it unconditionally — returning false from the method instead of not having the method — makes every filesystem look watchable, so native notification is selected and the absence of anything to watch is discovered one path at a time. An implementation must not satisfy an optional interface it cannot honour.
type Schema ¶
type Schema struct {
// contains filtered or unexported fields
}
Schema defines the expected structure and constraints for configuration values.
func NewSchema ¶
func NewSchema(opts ...SchemaOption) (*Schema, error)
NewSchema creates a Schema from the provided options.
func SchemaOf ¶
func SchemaOf[T any](opts ...SchemaOption) (*Schema, error)
SchemaOf returns a Schema derived from the struct tags of T. For the option-free call the result is cached per type, so repeated calls for the same T do not re-reflect. When opts are supplied (for example WithStrictMode) the schema is built fresh and not cached, since options can change the result.
T must be a struct; see WithStructSchema for the supported tags.
func (*Schema) Fields ¶
func (s *Schema) Fields() map[string]FieldSchema
Fields returns the schema field definitions.
type SchemaOption ¶
type SchemaOption func(*schemaConfig)
SchemaOption configures schema construction.
func WithStrictMode ¶
func WithStrictMode() SchemaOption
WithStrictMode treats unknown keys as errors instead of warnings.
func WithStructSchema ¶
func WithStructSchema(v any) SchemaOption
WithStructSchema derives a schema from a tagged Go struct. Supported tags: `config:"key" validate:"required" enum:"a,b,c" default:"value"`.
type Section ¶
Section is the result of unmarshalling an optional configuration section.
func MustUnmarshalSection ¶
MustUnmarshalSection decodes key into T and panics if decoding fails.
type SectionBindingConfig ¶
type SectionBindingConfig[T any] struct { // contains filtered or unexported fields }
SectionBindingConfig holds ObserveSection option state.
type SectionBindingOption ¶
type SectionBindingOption[T any] func(*SectionBindingConfig[T])
SectionBindingOption customises ObserveSection behaviour.
func WithSectionApply ¶
func WithSectionApply[T any](apply func(SectionChange[T]) error) SectionBindingOption[T]
WithSectionApply registers a callback that runs after a successful rehydrate changes the observed typed section.
func WithSectionDefaultFunc ¶
func WithSectionDefaultFunc[T any](defaultFunc func(Observed) T, merge func(defaults, overlay T) T) SectionBindingOption[T]
WithSectionDefaultFunc starts each observed section from defaults derived from the current config snapshot and merges the decoded section over it when the section exists.
func WithSectionDefaults ¶
func WithSectionDefaults[T any](defaults T, merge func(defaults, overlay T) T) SectionBindingOption[T]
WithSectionDefaults starts each observed section from defaults and merges the decoded section over it when the section exists.
func WithSectionEqual ¶
func WithSectionEqual[T any](equal func(previous, current Section[T]) bool) SectionBindingOption[T]
WithSectionEqual sets the equality function used to decide whether a successful rehydrate changed the observed section.
func WithSectionValidator ¶
func WithSectionValidator[T any](validate func(T) error) SectionBindingOption[T]
WithSectionValidator validates each settings snapshot before it is published.
type SectionChange ¶
type SectionChange[T any] struct { Previous Section[T] Current Section[T] Initial bool Changed bool Version uint64 }
SectionChange describes an observed typed section change.
Initial marks the delivery ObservedSection.ApplyInitial makes: the section the binding started with, rather than a change to it. Changed is its complement, true for every later delivery. A caller that treats both the same way can ignore them and read Current.
type Snapshot ¶ added in v0.3.0
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot is an immutable, fully-resolved view of configuration at a point in time: the layers that contributed, the values they merge to, and which layer supplied each key.
Immutability is the point. A caller holding a snapshot sees a coherent configuration for its whole lifetime, so a sequence of related reads cannot straddle a reload and observe a mixture of old and new state. Nothing mutates a Snapshot after construction; a reload produces a new one.
func (*Snapshot) Has ¶ added in v0.3.0
Has reports whether a path resolves to a value.
A key whose value is an empty map or empty sequence is present: emptiness is a value, not an absence, and code may require a parent to exist while it holds nothing.
func (*Snapshot) Keys ¶ added in v0.3.0
Keys returns every leaf path in the snapshot, sorted.
Keys whose value is an empty container are included. Excluding them would make enumeration disagree with the getters, which is the class of inconsistency this design exists to remove.
func (*Snapshot) Layers ¶ added in v0.3.0
Layers returns the contributing layers in precedence order, lowest first. The slice is a copy; the layers' values are not exposed for mutation.
func (*Snapshot) Origin ¶ added in v0.3.0
Origin returns the layer that supplied the effective value at a path.
This is the question no merge-eager configuration library can answer, and the reason provenance is recorded during the fold rather than reconstructed afterwards.
Provenance is defined for leaves — scalars, and containers that are empty. A populated subtree is assembled from however many layers contributed to it, so naming one source for it would be dishonest; Origin reports not-found and Snapshot.Shadowed answers what the caller actually wants to know.
func (*Snapshot) Shadowed ¶ added in v0.3.0
Shadowed returns every layer that defines a path, in precedence order — lowest first, so the last entry is the one in effect.
"Which file do I edit?" and "why is my edit not taking effect?" are the same question asked from two directions, and both need the full list rather than just the winner.
type Source ¶ added in v0.3.0
type Source struct {
Kind SourceKind
// Name is the file path for file sources, the variable name for env,
// the flag name for flags, and empty otherwise.
Name string
// Document is the zero-based index within a multi-document file. Always
// zero for non-file sources and single-document files.
Document int
// Writable reports whether this layer can be persisted to. Env, flags and
// compiled-in defaults are readable but not writable, and routing must
// skip them.
Writable bool
}
Source identifies one contributing layer.
A file that holds several YAML documents contributes one Source per document, because a document is a layer: that is what lets routing, provenance and precedence treat documents and files uniformly.
func (Source) Authored ¶ added in v0.3.0
Authored reports whether a source is one a person edits and a schema should therefore govern.
A file and a compiled-in default are written deliberately, so a key appearing in one that the schema does not describe is worth reporting. The environment, command-line flags and layers added at runtime are ambient: a deployment platform exporting an unrelated prefixed variable must not be able to stop an application starting.
Declared beside the kinds it derives from, so the question is answered once rather than by an allowlist in whatever code happens to need it.
type SourceKind ¶ added in v0.3.0
type SourceKind string
SourceKind identifies where a layer's values came from.
Provenance is only useful if it can name every kind of source, not just files: "came from prod.yaml line 14" and "came from the environment" are both answers a user needs when asking why a value is what it is.
const ( // SourceFile is a configuration file. SourceFile SourceKind = "file" // SourceEnv is an environment variable. SourceEnv SourceKind = "env" // SourceFlag is a bound command-line flag. SourceFlag SourceKind = "flag" // SourceDefault is a compiled-in default. SourceDefault SourceKind = "default" // SourceOverride is an ephemeral in-process override. It is never // persisted. SourceOverride SourceKind = "override" )
type Store ¶ added in v0.3.0
type Store struct {
// contains filtered or unexported fields
}
Store is the sole owner of configuration I/O.
Nothing else reads, writes or watches a configuration source. That single rule is what removes the need for coordination between components: there is no protocol between a reader and a writer because there is only one of each, and it is this.
Access is serialised internally. Concurrent loads and writes cannot interleave, so a reader never observes a partially applied change, and two writers cannot both decide to write and then overwrite one another.
func NewStore ¶ added in v0.3.0
func NewStore(ctx context.Context, opts ...StoreOption) (*Store, error)
NewStore builds a Store and performs its first load.
Construction and loading are deliberately not separable. A Store that exists but has not loaded is a state every caller would have to handle and most would forget to.
A configuration that loads and parses but violates the schema is returned *alongside* ErrInvalidConfig rather than discarded. The usual `if err != nil { return }` still fails fast, so a service refuses to start on a bad config exactly as before. But a tool whose job is to repair configuration needs a Store to repair it through, and returning nil would make one missing key unfixable by the surface designed to fix it — see D15. Such a caller checks for ErrInvalidConfig specifically and proceeds:
s, err := config.NewStore(ctx, opts...)
if err != nil && !errors.Is(err, config.ErrInvalidConfig) {
return err // genuinely unusable: no sources, unreadable, unparseable
}
Every other failure — no sources, a source that cannot be read, one that cannot be parsed — still returns nil, because there is no configuration to hand back.
func (*Store) AddLayer ¶ added in v0.3.0
AddLayer contributes configuration from an in-memory source at runtime, at the highest precedence.
This is how a tool supplies configuration it computed rather than read: a resolved path, a value negotiated with a server, a toggle that applies to this run only. It is an ordinary layer, so precedence, provenance, merging and shadowing all work on it exactly as they do on a file, and nothing special has to be remembered about it.
It is never a write target. An in-memory source has nowhere to persist to, so routing skips it — a later write lands in the file beneath and the added layer goes on winning, which the plan reports as a shadowed write rather than letting the caller believe their edit took effect.
Compiled-in defaults belong in WithReaders at construction instead, where they sit at the bottom of the order rather than the top.
The layer survives reloads: it is a source like any other, re-read each time. A layer that cannot be parsed is refused and not adopted, because leaving it in place would make every later reload fail on content the caller has no way to withdraw.
func (*Store) AddObserver ¶ added in v0.3.0
func (s *Store) AddObserver(o Observable)
AddObserver registers a component to be told when configuration changes.
func (*Store) AddObserverFunc ¶ added in v0.3.0
AddObserverFunc registers a function to be told when configuration changes.
func (*Store) Apply ¶ added in v0.3.0
Apply routes changes, writes them, and publishes the resulting snapshot.
The snapshot is constructed from the content just written rather than by re-reading afterwards. That is what makes a write self-contained: there is no round trip through the filesystem, no waiting for a watcher, and no window in which the configuration in memory disagrees with the file on disk.
Execution is prepare → verify → commit. Everything expensive or likely to fail happens while nothing is visible; commit is a sequence of renames.
func (*Store) Observers ¶ added in v0.3.0
func (s *Store) Observers() []Observable
Observers returns the registered observers, for tests and diagnostics.
func (*Store) OnObserverError ¶ added in v0.3.0
OnObserverError registers a callback for observers that failed to react.
Separate from OnReloadError because the two mean different things: a rejected reload means the configuration did not change, whereas a failing observer means it did and one component could not keep up.
func (*Store) OnReloadError ¶ added in v0.3.0
OnReloadError registers a callback for reloads that were rejected.
Rejections travel on their own channel because nothing changed: telling observers "configuration changed" when it did not would have them re-read values identical to the ones they hold.
func (*Store) Plan ¶ added in v0.3.0
Plan routes changes without writing anything.
The result is exactly what Apply would execute, so a dry run cannot drift from the real thing the way a separate preview implementation would.
func (*Store) Reload ¶ added in v0.3.0
Reload re-reads every backend and, on success, publishes a new snapshot.
It is fail-closed: if any source fails to load or parse, the error is returned and the previous snapshot is retained. A configuration that is partly the old values and partly the new is worse than either, and worse than refusing.
func (*Store) Snapshot ¶ added in v0.3.0
Snapshot returns the current configuration.
The returned snapshot is immutable and will not change underneath the caller, so a sequence of reads against one snapshot is coherent even if a reload lands midway.
func (*Store) Sources ¶ added in v0.3.0
Sources returns the identity of every backend, in precedence order.
func (*Store) View ¶ added in v0.3.0
View returns a typed read surface over the Store's current snapshot.
func (*Store) Watch ¶ added in v0.3.0
func (s *Store) Watch(ctx context.Context, opts ...WatchOption) (stop func(), err error)
Watch begins reacting to changes made outside this process.
The Store's own writes never travel this path: Apply builds the next snapshot from what it just wrote and notifies directly, so a write cannot come back round through the watcher. That is what makes a write-then-react-then-write cascade unrepresentable rather than something to be detected and broken.
Watching only covers file-backed sources. Environment variables and flags do not change under a running process, and an in-memory source is changed by the code that owns it.
A watcher that cannot function fails here rather than silently doing nothing: an application that believes it will hear about changes and never does is worse off than one that knows it must restart.
func (*Store) With ¶ added in v0.3.0
With runs fn against a view pinned to one snapshot.
Individual reads are always coherent, but a sequence of them can straddle a reload — read the host from one snapshot and the port from the next, and you connect to the new host on the old port. With closes that window for a block of related reads.
It is scoped to a closure rather than handed out as a handle because a handle can be kept indefinitely: it would pin parsed configuration in memory and serve values that quietly grew arbitrarily old.
type StoreOption ¶ added in v0.3.0
type StoreOption func(*Store)
StoreOption configures a Store.
func RequireFirstSource ¶ added in v0.3.0
func RequireFirstSource() StoreOption
RequireFirstSource makes the first backend mandatory: if it is missing, the Store fails to load rather than starting up with a hole in its configuration.
func WithBackend ¶ added in v0.3.0
func WithBackend(b Backend) StoreOption
WithBackend appends a backend. Backends are read in the order they are added, and later ones take precedence.
func WithEnv ¶ added in v0.3.0
func WithEnv(prefix string, opts ...EnvOption) StoreOption
WithEnv appends an environment backend reading variables under a prefix.
Add it after the file sources: environment variables are expected to override what is on disk, and precedence follows the order backends are added.
func WithFiles ¶ added in v0.3.0
func WithFiles(filesystem FS, paths ...string) StoreOption
WithFiles appends a file backend per path, in precedence order — the first is the base and the last wins.
func WithFlags ¶ added in v0.3.0
func WithFlags(flags *pflag.FlagSet, opts ...FlagOption) StoreOption
WithFlags appends a backend contributing the flags the user actually changed. Add it last: an explicit flag is the most deliberate input there is.
func WithReaders ¶ added in v0.3.0
func WithReaders(sources ...NamedSource) StoreOption
WithReaders appends in-memory sources, in precedence order.
Compiled-in defaults belong here: they contribute a layer like any other, so they take part in precedence and provenance rather than being a special case that has to be remembered.
func WithSchema ¶
func WithSchema(schema *Schema) StoreOption
WithSchema validates every candidate configuration before it is published.
Validation is fail-closed and applies to the resolved configuration rather than to any single layer, because a layer can be legitimately incomplete on its own — a base file may omit a key that an overlay supplies, and judging it alone would reject a perfectly valid setup.
A reload that fails validation leaves the previous configuration in place: running on the last known good values is better than running on values the application has said it cannot use.
type ValidationError ¶
type ValidationError struct {
// Key is the dot-separated config key.
Key string
// Message is a human-readable description of the failure.
Message string
// Hint is an actionable fix suggestion.
Hint string
}
ValidationError contains details about a single validation failure.
func (ValidationError) String ¶
func (e ValidationError) String() string
type ValidationResult ¶
type ValidationResult struct {
Errors []ValidationError
Warnings []ValidationError
}
ValidationResult holds the outcome of schema validation.
func (*ValidationResult) Error ¶
func (r *ValidationResult) Error() string
Error returns a formatted multi-line error string, or empty string if valid.
func (*ValidationResult) Valid ¶
func (r *ValidationResult) Valid() bool
Valid returns true if no errors were found. Warnings do not affect validity.
type View ¶ added in v0.3.0
type View struct {
// contains filtered or unexported fields
}
View is a typed read surface over one snapshot.
A View performs no I/O. It resolves values from the snapshot it was built with, so what it returns cannot change underneath a caller — a sequence of reads through one View is coherent even if the Store publishes a new snapshot midway.
Views are cheap: taking one is a pointer copy, not a load.
func (*View) Explain ¶ added in v0.3.0
Explain describes where a value came from and what else defines it.
This is the question every configuration debugging session starts with, and the one a merge-eager library cannot answer at all.
func (*View) GetDuration ¶ added in v0.3.0
GetDuration returns a path as a duration.
func (*View) GetFloat64 ¶ added in v0.3.0
GetFloat64 reads a value as a float64.
The same as View.GetFloat, under the name the incumbent used, so porting code does not have to be edited to say the same thing differently.
func (*View) GetIntSlice ¶ added in v0.3.0
GetIntSlice reads a value as a list of ints.
func (*View) GetSizeInBytes ¶ added in v0.3.0
GetSizeInBytes reads a value written as a size, such as "10MB", in bytes.
Suffixes are the binary ones — kb, mb, gb, tb — each a multiple of 1024, and a bare number is a count of bytes. A value that cannot be read as a size is zero, which is the same answer the other accessors give.
func (*View) GetStringMap ¶ added in v0.3.0
GetStringMap reads a value as a map of string to anything.
func (*View) GetStringMapString ¶ added in v0.3.0
GetStringMapString reads a value as a map of string to string.
func (*View) GetStringMapStringSlice ¶ added in v0.3.0
GetStringMapStringSlice reads a value as a map of string to list of string.
func (*View) GetStringSlice ¶ added in v0.3.0
GetStringSlice returns a path as a string slice.
func (*View) Has ¶ added in v0.3.0
Has reports whether a path is present.
A key whose value is an empty container is present: emptiness is a value.
func (*View) IsSet ¶ added in v0.3.0
IsSet is an alias for Has.
The incumbent distinguished "present in a file" from "present anywhere", because its file layer and its environment layer were different kinds of thing. Here every source is a layer, so there is one honest answer and two names for it, kept for familiarity.
func (*View) Origin ¶ added in v0.3.0
Origin reports which layer supplied the effective value at a path.
func (*View) SectionExists ¶ added in v0.3.0
SectionExists reports whether a path holds a mapping.
An empty path asks about the view's own root, which is a mapping whenever there is any configuration at all — that is what lets a caller check a scoped view without special-casing the top level.
func (*View) Shadowed ¶ added in v0.3.0
Shadowed lists every layer defining a path, lowest precedence first.
func (*View) Sub ¶ added in v0.3.0
Sub returns a view scoped to a subtree.
The result stays live against the same snapshot rather than holding a detached copy: every read is resolved through the full path, so a scoped view cannot serve values that the rest of the configuration has moved past. Returns nil when the key is absent, so `if sub != nil` guards behave.
func (*View) UnmarshalKey ¶ added in v0.3.0
UnmarshalKey decodes a subtree into a struct.
Decoding is a single operation against one snapshot, so a struct populated this way is internally consistent by construction — it cannot contain some fields from before a reload and some from after.
func (*View) Validate ¶ added in v0.3.0
func (v *View) Validate(schema *Schema) *ValidationResult
Validate checks what this view describes against a schema.
A scoped view validates its own subtree, so a schema written for a section applies to that section rather than to the whole configuration. The result carries errors and warnings separately; check ValidationResult.Valid.
type WatchOption ¶ added in v0.3.0
type WatchOption func(*watchConfig)
WatchOption configures watching.
func WithPollInterval ¶ added in v0.3.0
func WithPollInterval(d time.Duration) WatchOption
WithPollInterval sets how often a polling watcher checks for changes.
func WithSettleInterval ¶ added in v0.3.0
func WithSettleInterval(d time.Duration) WatchOption
WithSettleInterval sets how long a burst of foreign changes is allowed to settle before the Store reloads.
A logical configuration change is not always one filesystem event: a deploy replacing two overlays produces two, and nothing in the filesystem says they were meant as one. Waiting for the burst to settle turns them into a single reload, and a single notification.
Zero disables settling, reloading on each report. Tests driving an injected watcher usually want that, so the trigger and the reload stay in step without waiting on a timer.
This does not make a multi-file change atomic. Writes spaced further apart than the window are still seen as separate changes — the guarantee belongs to whoever writes the files, by writing them atomically or by keeping settings that change together in one file.
func WithWatcher ¶ added in v0.3.0
func WithWatcher(w Watcher) WatchOption
WithWatcher supplies a watcher, mainly so tests can drive change detection deterministically instead of waiting on a real filesystem.
type WatchableBackend ¶ added in v0.3.0
type WatchableBackend interface {
Backend
// Watch calls onChange when this backend's sources may have changed. The
// returned function stops watching and releases whatever it holds.
//
// It reports *possible* change rather than actual change: a backend cannot
// know whether a write altered anything that resolves, and deciding that is
// the Store's job.
Watch(ctx context.Context, interval time.Duration, onChange func()) (stop func(), err error)
}
WatchableBackend is a backend that can report when its own sources change.
Separate from Backend for the same reason writing is: being readable does not make a source watchable, and only the backend knows how to notice a change to whatever it reads — a file has a path on a filesystem, a remote store has a subscription — and the Store should not have to know which.
The Store's job is coordination: it asks each backend that can watch to say when something moved, then decides for itself whether the resolved configuration actually changed.
type Watcher ¶ added in v0.3.0
type Watcher interface {
// Watch begins watching, calling onChange when the paths may have changed.
// The returned function stops watching and releases resources.
Watch(ctx context.Context, paths []string, onChange func()) (stop func(), err error)
}
Watcher reports that watched paths may have changed.
It reports *possible* change rather than actual change: filesystem notification is noisy, delivering events for permission changes, atomic renames and editors writing in several passes. Deciding whether anything really changed belongs to the Store, which can compare configurations.
func NewWatcher ¶ added in v0.3.0
NewWatcher returns the watcher appropriate to a filesystem.
Native notification is used where it works and polling everywhere else. The distinction is not cosmetic: fsnotify operates on real paths, so it silently fails on an in-memory filesystem — which is exactly what a test, or a tool working over a virtual worktree, will be using.
type WritableBackend ¶ added in v0.3.0
type WritableBackend interface {
Backend
// Prepare stages edits without making them visible. It must not modify
// the source: everything it does has to be abandonable.
Prepare(ctx context.Context, edits []Edit) (Pending, error)
}
WritableBackend is a backend that can persist changes.
It is separate from Backend because reading and writing are genuinely different problems. A backend that can be read is not thereby able to handle atomicity, conflict detection or rollback, and pretending otherwise would make every caller check capabilities at runtime instead of the type system checking them once.
type YAMLCodec ¶ added in v0.7.0
type YAMLCodec struct{}
YAMLCodec reads and edits YAML configuration. It is what NewFileBackend and WithFiles use, and it is exported so a backend adapter can reuse it as a value decoder — a Consul or etcd key whose value is a YAML document, decoded into a subtree via that adapter's WithValueCodec option. It is a Codec (and an EditingCodec); the sibling format adapters export theirs as `Codec`, but the plain name is the interface here, so this one carries the format.
It reads the file twice, deliberately. Values are decoded by the YAML value parser, while document structure — comments, positions, and whether the file can be edited safely at all — comes from yamldoc. The two disagree about scalar types (`8080` decodes as int in one and uint64 in the other, and large integers survive in one and are destroyed in the other), so the boundary between documents and values must not be crossed. Values never come from yamldoc; documents never come from the value parser.
func (YAMLCodec) Apply ¶ added in v0.7.0
Apply edits the document tree and re-emits it.
Editing goes through yamldoc so comments, key order, quoting and block styles survive. Nothing here decodes values from that tree: the documents-versus-values boundary is what keeps the two YAML parsers from disagreeing about types.
func (YAMLCodec) Check ¶ added in v0.7.0
Check reports whether a source can be edited without risking corruption.
The judgement is this module's; the detection is yamldoc's. It reports what it cannot round-trip safely, and refusing is the policy applied to that report.
func (YAMLCodec) Decode ¶ added in v0.7.0
Decode decodes every YAML document in a source into its own map.
An empty document is a nil entry rather than an omission, so the documents that follow it keep their index — which is how routing, precedence and provenance treat documents and files uniformly, and it fixes a defect in the incumbent, which reads the first document of a multi-document file and silently discards the rest.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package backendconformance is a reusable test suite that a config backend adapter runs against its own backend to prove it behaves like a first-class layer.
|
Package backendconformance is a reusable test suite that a config backend adapter runs against its own backend to prove it behaves like a first-class layer. |
|
Package conformance is a reusable test suite that a config codec adapter runs against its own codec to prove it behaves like a first-class backend.
|
Package conformance is a reusable test suite that a config codec adapter runs against its own codec to prove it behaves like a first-class backend. |