configuration API

configuration

package

API reference for the configuration package.

T
type

staticProvider

core/configuration/config_test.go:8-8
type staticProvider map[string]any
F
function

TestConfigurationGetAndSection

Parameters

core/configuration/config_test.go:16-38
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)
	}
}
F
function

TestConfigurationBind

Parameters

core/configuration/config_test.go:40-59
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)
	}
}
F
function

TestConfigurationBindRejectsNonStruct

Parameters

core/configuration/config_test.go:61-65
func TestConfigurationBindRejectsNonStruct(t *testing.T)

{
	if err := New().Bind("bad"); err == nil {
		t.Fatal("Bind() error = nil, want error")
	}
}
F
function

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

prefix
string

Returns

map[string]*T
error
core/configuration/collection.go:22-39
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>.
T
type

ItemValidator

ItemValidator reports problems with a single bound entry. The returned
slice holds human-readable problem descriptions; empty means valid.

core/configuration/collection.go:76-76
type ItemValidator func(name string, item *T) []string
T
type

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.

core/configuration/collection.go:81-81
type CrossValidator func(items map[string]*T) []string
F
function

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

items
map[string]*T
item
ItemValidator[T]
cross
...CrossValidator[T]

Returns

[]string
core/configuration/collection.go:89-111
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
}
S
struct

siteConfig

core/configuration/collection_test.go:8-11
type siteConfig struct

Fields

Name Type Description
Domain string conf:"domain"
Port int conf:"port"
F
function

collectionFixture

Parameters

Returns

core/configuration/collection_test.go:13-21
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
}
F
function

TestBindCollectionBindsEachSection

Parameters

core/configuration/collection_test.go:23-37
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"])
	}
}
F
function

TestBindCollectionWithPrefix

Parameters

core/configuration/collection_test.go:39-51
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)
	}
}
F
function

TestCollectionKeysSortedAndTopLevelOnly

Parameters

core/configuration/collection_test.go:53-64
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)
	}
}
F
function

TestValidateCollectionAggregatesItemAndCrossProblems

Parameters

core/configuration/collection_test.go:66-103
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])
	}
}
I
interface

Provider

Provider is the interface that configuration sources must implement.

core/configuration/config.go:25-31
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

Name
Method

Returns

string
func Name(...)
Load
Method

Parameters

Returns

map[string]any
error
func Load(...)
S
struct

Configuration

Configuration holds merged configuration data from multiple providers.

core/configuration/config.go:42-45
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

prefix string

Returns

[]string
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
Method

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

key string

Returns

any
bool
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
Method

GetString returns the string value for the given key.

Parameters

key string

Returns

string
bool
func (*Configuration) GetString(key string) (string, bool)
{
	v, ok := c.Get(key)
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	return s, ok
}
GetInt
Method

GetInt returns the int value for the given key.

Parameters

key string

Returns

int
bool
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
Method

GetBool returns the bool value for the given key.

Parameters

key string

Returns

bool
bool
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
Method

GetSection returns a new Configuration containing only keys with the given prefix.

Parameters

prefix string

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
Method

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

target any

Returns

error
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
}
Example
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
F
function

New

New creates an empty Configuration.

Returns

core/configuration/config.go:206-210
func New() *Configuration

{
	return &Configuration{
		data: make(map[string]any),
	}
}
S
struct

Builder

Builder provides a fluent API for assembling a Configuration from multiple
sources with priority ordering (last added wins).

core/configuration/config.go:221-223
type Builder struct

Example

cfg := configuration.NewBuilder().
    AddEnv("APP_").
    AddJSONFile("config.json").
    Build()

Methods

Add
Method

Add registers a configuration provider. Providers added later take priority over earlier ones for overlapping keys.

Parameters

Returns

func (*Builder) Add(p Provider) *Builder
{
	b.providers = append(b.providers, p)
	return b
}
Build
Method

Build merges all registered providers into a single Configuration. Providers are queried in order; later providers override earlier ones.

Parameters

Returns

error
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
F
function

NewBuilder

NewBuilder creates a new Builder.

Returns

core/configuration/config.go:226-228
func NewBuilder() *Builder

{
	return &Builder{}
}
F
function

setField

Parameters

valStr
string

Returns

error
core/configuration/config.go:255-287
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
}