confkit

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2025 License: MIT Imports: 7 Imported by: 0

README

Confkit

Go Reference

A minimalist Go configuration library with transparent layering.

What Confkit Is (and Isn't)

Confkit embraces simplicity over features. While other configuration libraries offer dozens of integrations, complex validation rules, and automatic reloading, confkit provides just the essentials:

  • Everything is a string until you need it to be something else
  • Layers are just maps that you can inspect, modify, and debug
  • Keys are normalized so server.port, SERVER_PORT, and server-port are the same
  • Source tracking tells you exactly where each value came from
  • No magic - no reflection on your types, no struct tags for defaults, no hidden behavior
// See exactly what's happening
fmt.Printf("%+v\n", cfg.Layers)  // It's just data

// Know where values come from
value, originalKey, sourceName, _ := cfg.Get("server.port")
// value="8080", originalKey="SERVER_PORT", sourceName="env"

Why Another Config Library?

Most configuration libraries are either too simple (just os.Getenv) or too complex (hundreds of features you'll never use). Confkit sits in the sweet spot:

  • Transparent: Inspect your entire config state. Examine every layer. Track value origins for audit trails, config documentation, or troubleshooting.
  • Composable: Layers are just map[string]string. Create them however you want.
  • Predictable: Later layers win. Keys are normalized. That's it.
  • Runtime-friendly: Modify layers after adding them - perfect for feature flags or admin overrides.
  • Type-safe: Unmarshal to structs when you need to, with full mapstructure support.

Core Features

  • Layered configuration with clear precedence (last layer wins)
  • String-based storage for universal compatibility
  • Source tracking - know exactly where each value came from
  • Key normalization - server.port = SERVER_PORT = server-port
  • Runtime modification - layers are mutable, perfect for feature flags
  • Type-safe unmarshaling via mapstructure, when you need it
  • Zero magic - no struct tags for defaults, no hidden behavior

Works With Everything

Confkit doesn't parse files or connect to databases - it just manages string key-value pairs. This means it works seamlessly with whatever you're already using:

// From JSON/YAML/TOML files
var data map[string]any
json.Unmarshal(configBytes, &data)
cfg.Add(confkit.FromMap("config.json", data))

// From SQL databases
rows := db.Query("SELECT key, value FROM settings WHERE app = ?", appID)
cfg.Add(confkit.FromMap("database", sqlRowsToMap(rows)))

// From Redis, etcd, Consul
values := consul.GetAll("myapp/config")
cfg.Add(&confkit.Layer{Name: "consul", Values: values})

// From your custom API
settings := apiClient.GetSettings()
cfg.Add(confkit.FromMap("api", settings))

The flat map[string]string design means confkit works with any storage system that can produce key-value pairs.

Installation

go get github.com/adnsv/confkit

Quick Example

// Define your configuration struct
type Config struct {
    Server struct {
        Port int `mapstructure:"port"`
        Host string `mapstructure:"host"`
    } `mapstructure:"server"`
}

// Create defaults using a function (testable, reusable)
func DefaultConfig() *Config {
    return &Config{
        Server: struct{
            Port int `mapstructure:"port"`
            Host string `mapstructure:"host"`
        }{
            Port: 8080,
            Host: "localhost",
        },
    }
}

// Build configuration with clear precedence
cfg := confkit.NewConfig()

// Layer 1: Defaults
defaults := cfg.Add(confkit.FromDefaults("defaults", DefaultConfig()))

// Layer 2: Config file (you load it however you want)
configData := loadMyConfigFile() // Returns map[string]any
cfg.Add(confkit.FromMap("config.json", configData))

// Layer 3: Environment (highest precedence)
cfg.Add(confkit.FromEnv("env", "MYAPP_"))

// Use it - multiple ways
var config Config
cfg.Unmarshal(&config)

// Or get individual values with full visibility
port, originalKey, source, _ := cfg.Get("server.port")
fmt.Printf("Port %s came from %s (via %s)\n", port, source, originalKey)

// Runtime updates? Just modify the layer
defaults.Values["feature.newThing"] = "enabled"

The Confkit Philosophy

Simple is harder than complex. Every feature in confkit must justify its existence without compromising the core simplicity.

Transparency over magic. You should be able to access your configuration state and understand it completely. No hidden state, no surprising behavior.

Composition over configuration. Confkit provides building blocks that combine into complete solutions. Start simple, add only what you need.

Strings are universal. Every system understands strings. Type conversion happens at the edges, not in the core.

Documentation

Dependencies

License

MIT

Documentation

Overview

Package confkit provides a minimalist configuration management library with transparent layered configuration sources.

Key Features:

  • Layered configuration with clear precedence (later layers win)
  • Flexible key matching through normalization
  • Full transparency - see which layer provided each value
  • Simple string-based storage for easy debugging

Key Normalization:

Confkit automatically normalizes keys when matching, making these equivalent:

  • "database.host" (dots - common in JSON/YAML, when flattened)
  • "database_host" (underscores - common in env vars)
  • "database-host" (hyphens - common in CLI flags)
  • "DATABASE_HOST" (uppercase - typical for env vars)

This allows configuration from different sources (files, environment, flags) to work together seamlessly without manual key transformation.

Struct Unmarshaling:

When using Unmarshal with structs, fields are mapped using mapstructure tags:

type Config struct {
    Database struct {
        Host string `mapstructure:"host"`
        Port int    `mapstructure:"port"`
    } `mapstructure:"database"`
}

This creates the configuration paths "database.host" and "database.port", which can be set by any normalized variant (DATABASE_HOST, database-host, etc).

Handling Defaults (RECOMMENDED PATTERN):

Instead of using struct tags for defaults, create a DefaultConfig function:

func DefaultConfig() *Config {
    return &Config{
        Server: ServerConfig{
            Host: "localhost",
            Port: 8080,
        },
        Database: DatabaseConfig{
            Port: 5432,
        },
    }
}

cfg := confkit.NewConfig()
cfg.Add(confkit.FromDefaults("defaults", DefaultConfig()))
cfg.Add(confkit.FromEnv("env", "APP_"))

This approach keeps defaults in code (not in tags), makes them testable, and allows the same defaults to be used for documentation generation.

Configuration Validation:

Confkit provides helpers for validation without enforcing any validation rules:

// Detect unknown configuration keys
unmatched := confkit.UnmatchedKeys(cfg, &config)
if len(unmatched) > 0 {
    log.Printf("Warning: unknown keys: %v", unmatched)
}

// Enforce custom validation rules
rules := []confkit.ValidationRule{
    {Path: "database.password", Required: true, Sources: []string{"env"}},
    {Path: "server.tls.cert", RequiredWith: []string{"server.tls.key"}},
}
if errs := confkit.ValidateRules(cfg, rules); len(errs) > 0 {
    // Handle validation errors
}

This design keeps confkit minimal while enabling sophisticated validation patterns.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Flatten

func Flatten(m map[string]any) (map[string]string, error)

Flatten converts a hierarchical map[string]any to a flat map[string]string. Nested maps are flattened using dot notation. Arrays/slices are skipped as they don't make sense for configuration. Nil values are skipped (key not present in result). Empty strings are preserved as "" (different from missing).

func LayerUnmatchedKeys

func LayerUnmatchedKeys(layer *Layer, target any) []string

LayerUnmatchedKeys returns unmatched keys for a specific layer. This is useful for validating configuration from specific sources.

func RemapKeys

func RemapKeys(values, aliases map[string]string) map[string]string

RemapKeys transforms keys in a map according to the provided aliases. The aliases map contains oldKey → newKey mappings. If multiple old keys map to the same new key, the last one wins. Keys not found in aliases are passed through unchanged.

func Transform

func Transform(key string, transformer TransformFunc) string

Transform applies a transformation function to a simple key.

func TransformFlat

func TransformFlat(key string, transformer TransformFunc, prefix string) string

TransformFlat flattens dots to underscores and applies transformation. Useful for environment variables: "server.port" → "SERVER_PORT".

func TransformSegments

func TransformSegments(key string, transformer TransformFunc, separator string) string

TransformSegments applies transformation to each dot-separated segment. Useful for CLI flags: "server.maxConns" → "server-max-conns" The separator parameter is used as a prefix and between segments.

func UnmatchedKeys

func UnmatchedKeys(cfg *Config, target any) []string

UnmatchedKeys returns all configuration keys that don't match any field in the target struct. This is useful for detecting typos, deprecated keys, or keys from wrong configuration files.

Example:

unmatched := confkit.UnmatchedKeys(cfg, &config)
if len(unmatched) > 0 {
    log.Printf("Warning: unknown configuration keys: %v", unmatched)
}

func ValidateRules

func ValidateRules(cfg *Config, rules []ValidationRule) []error

ValidateRules checks configuration against custom validation rules. Returns a slice of validation errors. Empty slice means validation passed.

This is a helper for implementing custom validation logic. Confkit itself doesn't enforce validation - that's up to the application.

Example:

rules := []confkit.ValidationRule{
    {Path: "database.password", Required: true, Sources: []string{"env"}},
    {Path: "server.tls.cert", RequiredWith: []string{"server.tls.key"}},
}
if errs := confkit.ValidateRules(cfg, rules); len(errs) > 0 {
    for _, err := range errs {
        log.Printf("Validation error: %s", err)
    }
    os.Exit(1)
}

Types

type Config

type Config struct {
	Layers []*Layer // Public for direct inspection
}

Config holds multiple configuration layers and provides resolution methods. Layers are evaluated in order, with later layers taking precedence.

func NewConfig

func NewConfig() *Config

NewConfig creates a new configuration with no layers.

func (*Config) Add

func (c *Config) Add(layer *Layer) *Layer

Add appends a Layer to the configuration and returns the added layer. Later layers take precedence over earlier ones during resolution. The returned *Layer can be used to update values at runtime:

envLayer := cfg.Add(confkit.FromEnv("env", "APP_"))
// Later: envLayer.Values["NEW_KEY"] = "new_value"

func (*Config) Get

func (c *Config) Get(key string) (value, sourceKey, sourceName string, found bool)

Get resolves a configuration key across all layers.

Key matching uses normalization, making these formats equivalent:

  • "database.host" (dots - common in JSON/YAML)
  • "database_host" (underscores - common in env vars)
  • "database-host" (hyphens - common in CLI flags)
  • "DATABASE_HOST" (uppercase - typical for env vars)
  • Any combination: "DATABASE.HOST", "Database-Host", etc.

Returns:

  • value: The configuration value
  • sourceKey: The original key as it appears in the source (before normalization)
  • sourceName: The name of the layer containing this value
  • found: Whether the key was found

Later layers take precedence over earlier ones.

func (*Config) GetBool

func (c *Config) GetBool(key string) (bool, error)

GetBool retrieves a configuration value as a boolean. Returns an error if the key is not found or the value cannot be parsed as a boolean. Accepts: 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False.

func (*Config) GetDuration

func (c *Config) GetDuration(key string) (time.Duration, error)

GetDuration retrieves a configuration value as a time.Duration. Returns an error if the key is not found or the value cannot be parsed as a duration. Accepts any valid duration string: "300ms", "1.5h", "2h45m".

func (*Config) GetFloat

func (c *Config) GetFloat(key string) (float64, error)

GetFloat retrieves a configuration value as a float64. Returns an error if the key is not found or the value cannot be parsed as a float.

func (*Config) GetInt

func (c *Config) GetInt(key string) (int, error)

GetInt retrieves a configuration value as an integer. Returns an error if the key is not found or the value cannot be parsed as an integer.

func (*Config) GetValue

func (c *Config) GetValue(key string) (string, bool)

GetValue is a simplified version of Get that returns just the value and whether it was found. This is convenient for cases where you don't need source information.

func (*Config) Unmarshal

func (c *Config) Unmarshal(target any) error

Unmarshal populates the target struct with configuration values.

It uses the mapstructure library to decode configuration into the target struct. The mapping between configuration keys and struct fields works as follows:

1. Struct fields are identified by their mapstructure tags 2. Nested structs create dotted paths (e.g., "server.port") 3. Keys are matched using normalization - these are all equivalent:

  • "server.port" (dots)
  • "server_port" (underscores)
  • "server-port" (hyphens)
  • "SERVER_PORT" (uppercase)
  • "SERVER.PORT" (uppercase with dots)

Example struct and matching configuration keys:

type Config struct {
    Server struct {
        Host string `mapstructure:"host"`
        Port int    `mapstructure:"port"`
    } `mapstructure:"server"`
}

// These configuration keys all map to Config.Server.Port:
cfg.Add(&Layer{
    Name: "config",
    Values: map[string]string{
        "SERVER_PORT": "8080",      // From environment
        "server.port": "8080",      // From JSON/YAML
        "server-port": "8080",      // From CLI flags
    },
})

The target must be a pointer to a struct. Unmarshal returns an error if the target is not a pointer to a struct or if decoding fails.

type Field

type Field struct {
	Path string            // Dot-separated path (e.g., "server.port")
	Type string            // Go type name (e.g., "int", "string")
	Tags map[string]string // Tag values keyed by tag name
}

Field represents a configuration field extracted from a struct.

func ExtractFields

func ExtractFields(structValue any, tagNames ...string) []Field

ExtractFields returns a flattened list of fields from a struct with specified tags. The struct parameter should be a struct value or pointer to struct. Tag names specify which struct tags to extract (e.g., "mapstructure", "json", "description").

type Layer

type Layer struct {
	Name   string            // Source identifier (e.g., "env", "yaml:config.yaml", "sql:runtime")
	Prefix string            // Optional prefix to strip during matching (e.g., "MYAPP_")
	Values map[string]string // Flat map with dot-separated keys
}

Layer represents a named configuration source with string key-value pairs. Keys are stored in their original format to support round-tripping.

func FromDefaults

func FromDefaults(name string, defaults any) (*Layer, error)

FromDefaults creates a configuration layer from a struct with default values. It uses reflection to extract non-zero values from the struct and creates a flat map of configuration values. This is the recommended way to handle defaults in confkit.

Example:

func DefaultConfig() *Config {
    return &Config{
        Server: ServerConfig{
            Host: "localhost",
            Port: 8080,
        },
        Database: DatabaseConfig{
            Port: 5432,
            MaxConnections: 100,
        },
    }
}

cfg := confkit.NewConfig()
cfg.Add(confkit.FromDefaults("defaults", DefaultConfig()))

func FromEnv

func FromEnv(name, prefix string) *Layer

FromEnv creates a configuration layer from environment variables. Only variables starting with the given prefix are included. The prefix is preserved in the keys and set on the layer for matching. Pass an empty prefix to include all environment variables.

func FromMap

func FromMap(name string, m map[string]any) (*Layer, error)

FromMap creates a configuration layer from a hierarchical map. The map is flattened using dot notation for nested keys. The layer name should describe the source (e.g., "yaml:config.yaml").

type Node

type Node struct {
	Name     string            // Field or struct name
	Type     string            // Go type (empty for non-leaf nodes)
	Tags     map[string]string // Tag values
	Children []*Node           // Child nodes (for structs)
}

Node represents a hierarchical structure element.

func ExtractStructure

func ExtractStructure(structValue any, tagNames ...string) *Node

ExtractStructure returns a hierarchical representation of a struct with specified tags. The struct parameter should be a struct value or pointer to struct. Tag names specify which struct tags to extract.

type TransformFunc

type TransformFunc func(string) string

TransformFunc is a function that transforms a single string segment.

var (
	// LowerCase converts to lowercase: "ServerPort" → "serverport".
	LowerCase TransformFunc = toLowerCaseASCII

	// UpperCase converts to uppercase: "serverPort" → "SERVERPORT".
	UpperCase TransformFunc = toUpperCaseASCII

	// SnakeCase converts to snake_case: "ServerPort" → "server_port".
	SnakeCase TransformFunc = toSnakeCase

	// UpperSnakeCase converts to UPPER_SNAKE_CASE: "serverPort" → "SERVER_PORT".
	UpperSnakeCase TransformFunc = toUpperSnakeCase

	// KebabCase converts to kebab-case: "serverPort" → "server-port".
	KebabCase TransformFunc = toKebabCase
)

Standard transformers.

type ValidationRule

type ValidationRule struct {
	Path              string   // Field path (e.g., "database.password")
	Required          bool     // Whether this field must be set
	Sources           []string // Allowed source layer names (empty = any source)
	DisallowedSources []string // Disallowed source layer names
	RequiredWith      []string // Other fields that must be set if this is set
	ConflictsWith     []string // Fields that cannot be set if this is set
}

ValidationRule represents a custom validation rule for configuration fields.

Directories

Path Synopsis
examples
01-basic command
02-environment command
03-structs command
04-typed-access command
05-layering command
06-struct-tags command
08-aliases command
09-key-mapping command
10-validation command

Jump to

Keyboard shortcuts

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