configuration
packageAPI reference for the configuration
package.
Imports
(9)staticProvider
type staticProvider map[string]any
TestConfigurationGetAndSection
Parameters
func TestConfigurationGetAndSection(t *testing.T)
{
cfg, err := NewBuilder().
Add(staticProvider{"db:host": "localhost", "db:port": "5432", "feature": "true"}).
Build(context.Background())
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if got, ok := cfg.GetString("DB:HOST"); !ok || got != "localhost" {
t.Fatalf("GetString() = %q, %v", got, ok)
}
if got, ok := cfg.GetInt("db:port"); !ok || got != 5432 {
t.Fatalf("GetInt() = %d, %v", got, ok)
}
if got, ok := cfg.GetBool("feature"); !ok || !got {
t.Fatalf("GetBool() = %v, %v", got, ok)
}
section := cfg.GetSection("db")
if got, ok := section.GetString("host"); !ok || got != "localhost" {
t.Fatalf("section GetString() = %q, %v", got, ok)
}
}
TestConfigurationBind
Parameters
func TestConfigurationBind(t *testing.T)
{
cfg, err := NewBuilder().
Add(staticProvider{"db:host": "localhost", "port": "8080"}).
Build(context.Background())
if err != nil {
t.Fatalf("Build() error = %v", err)
}
var out struct {
Host string `conf:"db:host"`
Port int
Enabled bool `conf:"enabled" default:"true"`
}
if err := cfg.Bind(&out); err != nil {
t.Fatalf("Bind() error = %v", err)
}
if out.Host != "localhost" || out.Port != 8080 || !out.Enabled {
t.Fatalf("Bind() = %+v", out)
}
}
TestConfigurationBindRejectsNonStruct
Parameters
func TestConfigurationBindRejectsNonStruct(t *testing.T)
{
if err := New().Bind("bad"); err == nil {
t.Fatal("Bind() error = nil, want error")
}
}
BindCollection
BindCollection binds every first-level child section of prefix into a T,
keyed by section name. It is the typed counterpart of loading a directory
of files with source/dir: each file becomes one entry in the returned map.
Parameters
Returns
func BindCollection[T any](c *Configuration, prefix string) (map[string]*T, error)
{
names := c.CollectionKeys(prefix)
items := make(map[string]*T, len(names))
for _, name := range names {
section := c
if prefix != "" {
section = c.GetSection(prefix + ":" + name)
} else {
section = c.GetSection(name)
}
item := new(T)
if err := section.Bind(item); err != nil {
return nil, fmt.Errorf("configuration: entry %q: %w", name, err)
}
items[name] = item
}
return items, nil
}
Example
cfg := configuration.NewBuilder().Add(dir.New("tenants", "*.json")).Build(ctx)
tenants, err := configuration.BindCollection[TenantConfig](cfg, "")
// tenants["acme"].Quota, tenants["globex"].Quota, ...
With a non-empty prefix the collection is read one level below it:
BindCollection[T](cfg, "tenants") binds "tenants:<name>:*" into entries
keyed by <name>.
ItemValidator
ItemValidator reports problems with a single bound entry. The returned
slice holds human-readable problem descriptions; empty means valid.
type ItemValidator func(name string, item *T) []string
CrossValidator
CrossValidator reports problems that only exist across entries, such as
duplicate ports or overlapping domains. The returned slice holds
human-readable problem descriptions; empty means valid.
type CrossValidator func(items map[string]*T) []string
ValidateCollection
ValidateCollection runs the item validator on every entry (in sorted name
order) and then every cross validator on the whole collection, aggregating
all problems. Entry problems are prefixed with the entry name.
Returning every problem at once matters for the “validate my config
directory” workflow: fixing one file at a time is the slow path.
Parameters
Returns
func ValidateCollection[T any](items map[string]*T, item ItemValidator[T], cross ...CrossValidator[T]) []string
{
var problems []string
if item != nil {
names := make([]string, 0, len(items))
for name := range items {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
for _, p := range item(name, items[name]) {
problems = append(problems, fmt.Sprintf("%s: %s", name, p))
}
}
}
for _, cv := range cross {
if cv != nil {
problems = append(problems, cv(items)...)
}
}
return problems
}
siteConfig
type siteConfig struct
Fields
| Name | Type | Description |
|---|---|---|
| Domain | string | conf:"domain" |
| Port | int | conf:"port" |
collectionFixture
Parameters
Returns
func collectionFixture(t *testing.T) *Configuration
{
t.Helper()
c := New()
c.data["a.example:domain"] = "a.example"
c.data["a.example:port"] = "8080"
c.data["b.example:domain"] = "b.example"
c.data["b.example:port"] = "9090"
return c
}
TestBindCollectionBindsEachSection
Parameters
func TestBindCollectionBindsEachSection(t *testing.T)
{
sites, err := BindCollection[siteConfig](collectionFixture(t), "")
if err != nil {
t.Fatal(err)
}
if len(sites) != 2 {
t.Fatalf("got %d entries, want 2", len(sites))
}
if sites["a.example"].Domain != "a.example" || sites["a.example"].Port != 8080 {
t.Errorf("unexpected a.example: %+v", sites["a.example"])
}
if sites["b.example"].Port != 9090 {
t.Errorf("unexpected b.example: %+v", sites["b.example"])
}
}
TestBindCollectionWithPrefix
Parameters
func TestBindCollectionWithPrefix(t *testing.T)
{
c := New()
c.data["sites:a:port"] = "8080"
c.data["other:b:port"] = "1"
sites, err := BindCollection[siteConfig](c, "sites")
if err != nil {
t.Fatal(err)
}
if len(sites) != 1 || sites["a"].Port != 8080 {
t.Fatalf("unexpected result: %+v", sites)
}
}
TestCollectionKeysSortedAndTopLevelOnly
Parameters
func TestCollectionKeysSortedAndTopLevelOnly(t *testing.T)
{
c := New()
c.data["z.example:ssl:enabled"] = "true"
c.data["a.example:port"] = "1"
c.data["m.example:port"] = "2"
keys := c.CollectionKeys("")
want := []string{"a.example", "m.example", "z.example"}
if strings.Join(keys, ",") != strings.Join(want, ",") {
t.Fatalf("got %v, want %v", keys, want)
}
}
TestValidateCollectionAggregatesItemAndCrossProblems
Parameters
func TestValidateCollectionAggregatesItemAndCrossProblems(t *testing.T)
{
sites, err := BindCollection[siteConfig](collectionFixture(t), "")
if err != nil {
t.Fatal(err)
}
item := func(name string, s *siteConfig) []string {
if s.Domain == "" {
return []string{"domain is required"}
}
return nil
}
cross := func(items map[string]*siteConfig) []string {
seen := map[int]string{}
var out []string
for name, s := range items {
if prev, dup := seen[s.Port]; dup {
out = append(out, "port conflict between "+prev+" and "+name)
}
seen[s.Port] = name
}
return out
}
if problems := ValidateCollection(sites, item, cross); len(problems) != 0 {
t.Fatalf("unexpected problems: %v", problems)
}
sites["b.example"].Port = sites["a.example"].Port
sites["b.example"].Domain = ""
problems := ValidateCollection(sites, item, cross)
if len(problems) != 2 {
t.Fatalf("got %v, want 2 problems", problems)
}
if !strings.HasPrefix(problems[0], "b.example: ") {
t.Errorf("item problem not prefixed with entry name: %q", problems[0])
}
}
Provider
Provider is the interface that configuration sources must implement.
type Provider interface
Example
type MyProvider struct{}
func (p *MyProvider) Name() string { return "my" }
func (p *MyProvider) Load(ctx) (map[string]any, error) { return map[string]any{"key": "val"}, nil }
Methods
Configuration
Configuration holds merged configuration data from multiple providers.
type Configuration struct
Example
cfg := configuration.NewBuilder().
AddEnv("APP_").
AddJSONFile("config.json").
Build()
val := cfg.Get("db:host")
Methods
CollectionKeys returns the sorted names of the first-level child sections under prefix. With an empty prefix it returns the top-level key segments. Keys are stored lowercased, so returned names are lowercase regardless of the original file or key casing.
Parameters
Returns
func (*Configuration) CollectionKeys(prefix string) []string
{
p := ""
if prefix != "" {
p = strings.ToLower(prefix) + ":"
}
seen := make(map[string]struct{})
c.mu.RLock()
for k := range c.data {
if !strings.HasPrefix(k, p) {
continue
}
rest := strings.TrimPrefix(k, p)
seg, _, _ := strings.Cut(rest, ":")
if seg != "" {
seen[seg] = struct{}{}
}
}
c.mu.RUnlock()
names := make([]string, 0, len(seen))
for name := range seen {
names = append(names, name)
}
sort.Strings(names)
return names
}
Get returns the value for the given key. The key is case-insensitive. Nested keys can be accessed with colon syntax, e.g. "db:host".
Parameters
Returns
func (*Configuration) Get(key string) (any, bool)
{
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[strings.ToLower(key)]
return v, ok
}
GetString returns the string value for the given key.
Parameters
Returns
func (*Configuration) GetString(key string) (string, bool)
{
v, ok := c.Get(key)
if !ok {
return "", false
}
s, ok := v.(string)
return s, ok
}
GetInt returns the int value for the given key.
Parameters
Returns
func (*Configuration) GetInt(key string) (int, bool)
{
v, ok := c.Get(key)
if !ok {
return 0, false
}
switch n := v.(type) {
case int:
return n, true
case float64:
return int(n), true
case string:
i, err := strconv.Atoi(n)
return i, err == nil
}
return 0, false
}
GetBool returns the bool value for the given key.
Parameters
Returns
func (*Configuration) GetBool(key string) (bool, bool)
{
v, ok := c.Get(key)
if !ok {
return false, false
}
switch b := v.(type) {
case bool:
return b, true
case string:
return strings.ToLower(b) == "true" || b == "1", true
}
return false, false
}
GetSection returns a new Configuration containing only keys with the given prefix.
Parameters
Returns
func (*Configuration) GetSection(prefix string) *Configuration
{
prefix = strings.ToLower(prefix) + ":"
c.mu.RLock()
defer c.mu.RUnlock()
section := New()
for k, v := range c.data {
if strings.HasPrefix(k, prefix) {
section.data[strings.TrimPrefix(k, prefix)] = v
}
}
return section
}
Bind populates a struct from the configuration data. Fields are matched using the `conf` tag. If no tag is present, the field name is used as key.
Parameters
Returns
func (*Configuration) Bind(target any) error
{
val := reflect.ValueOf(target)
if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
return fmt.Errorf("configuration: target must be a pointer to a struct")
}
c.mu.RLock()
defer c.mu.RUnlock()
elem := val.Elem()
typ := elem.Type()
confParser := tags.NewParser("conf", tags.WithPairDelimiter(","), tags.WithIncludeUntagged())
fields := confParser.ParseStruct(target)
for _, meta := range fields {
fieldVal := elem.Field(meta.Index)
if !fieldVal.CanSet() {
continue
}
key := meta.RawTag
if key == "" {
key = strings.ToLower(meta.Name)
}
var rawVal any
found := false
if v, ok := c.data[strings.ToLower(key)]; ok {
rawVal = v
found = true
}
if !found {
if envKey := meta.Get("env"); envKey != "" {
if v, ok := c.data[strings.ToLower(envKey)]; ok {
rawVal = v
found = true
}
}
}
if !found {
if def := typ.Field(meta.Index).Tag.Get("default"); def != "" {
rawVal = def
found = true
} else if def := meta.Get("default"); def != "" {
rawVal = def
found = true
}
}
if !found {
continue
}
if err := setField(fieldVal, fmt.Sprintf("%v", rawVal)); err != nil {
return fmt.Errorf("configuration: field %s: %w", meta.Name, err)
}
}
return nil
}
type Config struct {
Host string `conf:"db:host"`
Port int `conf:"db:port" default:"5432"`
}
var cfg Config
err := configuration.Bind(&cfg)
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.RWMutex | |
| data | map[string]any |
New
New creates an empty Configuration.
Returns
func New() *Configuration
{
return &Configuration{
data: make(map[string]any),
}
}
Builder
Builder provides a fluent API for assembling a Configuration from multiple
sources with priority ordering (last added wins).
type Builder struct
Example
cfg := configuration.NewBuilder().
AddEnv("APP_").
AddJSONFile("config.json").
Build()
Methods
Add registers a configuration provider. Providers added later take priority over earlier ones for overlapping keys.
func (*Builder) Add(p Provider) *Builder
{
b.providers = append(b.providers, p)
return b
}
Build merges all registered providers into a single Configuration. Providers are queried in order; later providers override earlier ones.
Parameters
Returns
func (*Builder) Build(ctx context.Context) (*Configuration, error)
{
cfg := New()
for _, p := range b.providers {
data, err := p.Load(ctx)
if err != nil {
return nil, fmt.Errorf("configuration: source %q failed: %w", p.Name(), err)
}
cfg.mu.Lock()
for k, v := range data {
cfg.data[strings.ToLower(k)] = v
}
cfg.mu.Unlock()
}
return cfg, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| providers | []Provider |
NewBuilder
NewBuilder creates a new Builder.
Returns
func NewBuilder() *Builder
{
return &Builder{}
}
setField
Parameters
Returns
func setField(field reflect.Value, valStr string) error
{
switch field.Kind() {
case reflect.String:
field.SetString(valStr)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := strconv.ParseInt(valStr, 10, 64)
if err != nil {
return err
}
field.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := strconv.ParseUint(valStr, 10, 64)
if err != nil {
return err
}
field.SetUint(n)
case reflect.Bool:
b, err := strconv.ParseBool(valStr)
if err != nil {
return err
}
field.SetBool(b)
case reflect.Float32, reflect.Float64:
n, err := strconv.ParseFloat(valStr, 64)
if err != nil {
return err
}
field.SetFloat(n)
default:
return fmt.Errorf("unsupported type %s", field.Kind())
}
return nil
}