Variants & registry¶
Polymorphic configuration: a config entry whose concrete Go type is chosen at
load time by a discriminator key (default type), bound against that
variant's strict sub-schema, and indexed in a registry for selector-based
lookup.
Normative spec: variants.md.
Declaring a family¶
type Notifier interface {
Notify(ctx context.Context, msg string) error
}
type EmailNotifier struct {
From string `flexconf:"from"`
Team string `flexconf:"team,selector"` // exposed as selector team=<value>
}
func (e *EmailNotifier) Notify(...) error { ... }
// Process-wide (the Loader uses this by default):
flexconf.RegisterVariant[Notifier]("email", func() Notifier {
return &EmailNotifier{ /* per-variant defaults */ }
})
// Or an explicit registry (isolation, tests, custom discriminator):
reg := flexconf.NewRegistry[Notifier](flexconf.WithDiscriminator("kind"))
reg.RegisterVariant("email", ...)
ld := flexconf.New(dir).WithRegistry(reg)
Factories return pre-populated instances — that is where per-variant defaults live. Registering a name twice panics.
Variant locations¶
Any schema position whose type is a family interface is a variant location — single field, map value, or list element:
type AppConfig struct {
Default Notifier `flexconf:"default_notifier"` // name=default_notifier
Named map[string]Notifier `flexconf:"notifiers"` // name=<map key>
Fanout []Notifier `flexconf:"fanout"` // list items: no name
}
default_notifier:
type: slack # discriminator — a literal, never a token
webhook: https://hooks.example/deploy
team: platform # selector-tagged field
Rules enforced at load:
- Missing discriminator → error listing the known variants.
- Unknown discriminator value → error naming it and listing known variants.
- The discriminator must be a plain string literal (a token is rejected); other sub-schema values may be tokens.
- Sub-schema binding is strict: unknown keys error;
required, coercion and theValidate()hook apply as usual. - Two instances with identical full selector sets fail the load
(
ErrDuplicateVariant, naming both locations). - Registration is all-or-nothing with the Load: a failed load leaves the registry as it was.
Selectors & resolution¶
An instance's selectors are: the discriminator (type=email), the nearest
naming key (name=<field or map key>; list items have none), and every
selector-tagged field.
n, err := flexconf.Get[Notifier](flexconf.Select("name", "oncall")) // process-wide
n, err := flexconf.Resolve[Notifier](reg, flexconf.Select("team", "noc")) // explicit registry
Matching is subset-based and exactly-one-or-fail:
| Matches | Result |
|---|---|
| exactly one | the instance |
| none | ErrVariantNotFound |
| more than one | ErrVariantAmbiguous (listing the matches) |
There is no lazy creation — resolution only ever returns instances the loaded config actually configured. The struct field remains the primary binding target; the registry is a convenience index.