zerocfg

package module
v0.1.9 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2025 License: MIT Imports: 13 Imported by: 9

README

Zero Effort Configuration

Mentioned in Awesome Go GoDoc Go Report Card Codecov visitors

I've always loved the elegance of Go's flag package - how clean and straightforward it is to define and use configuration options. While working on various Go projects, I found myself wanting that same simplicity but with support for YAML configs. I couldn't find anything that preserved this paradigm, so I built zerocfg.

  • 🛠️ Simple and flexible API inspired by flag package
  • 🍳 Boilerplate usage prohibited by design
  • 🚦 Early detection of mistyped config keys
  • ✨ Multiple configuration sources with priority-based value resolution
  • 🕵️‍♂️ Render running configuration with secret protection
  • 🧩 Custom option types and providers are supported

Table of Contents

Installation

go get -u github.com/chaindead/zerocfg

Quick Start

package main

import (
    "fmt"

    zfg "github.com/chaindead/zerocfg"
    "github.com/chaindead/zerocfg/env"
    "github.com/chaindead/zerocfg/yaml"
)

var (
    // Configuration variables
    path     = zfg.Str("config.path", "", "path to yaml conf file", zfg.Alias("c"))
    ip       = zfg.IP("db.ip", "127.0.0.1", "database location")
    port     = zfg.Uint("db.port", 5678, "database port")
    username = zfg.Str("db.user", "guest", "user of database")
    password = zfg.Str("db.password", "qwerty", "password for user", zfg.Secret())
)

func main() {
    // Initialize configuration with multiple sources
    err := zfg.Parse(
        env.New(),
        yaml.New(path),
    )
    if err != nil {
        panic(err)
    }

    fmt.Printf("Connect to %s:%d creds=%s:%s\n", *ip, *port, *username, *password)
    // OUTPUT: Connect to 127.0.0.1:5678 creds=guest:qwerty

    fmt.Println(zfg.Show())
    // CMD: go run ./... -c test.yaml
    // OUTPUT:
    //  config.path = test.yaml      (path to yaml conf file)
    //  db.ip       = 127.0.0.1      (database location)
    //  db.password = <secret>       (password for user)
    //  db.port     = 5678           (database port)
    //  db.user     = guest          (user of database)
}

Usage

Options naming
  • Dots (.) are used as separators for hierarchical options.
  • Option subnames preferred separation is camelCase, underscore (_), and dash (-) styles.

Example:

zfg.Str("groupOptions.thisOption", "", "camelCase usage")
zfg.Str("group_options.this_option", "", "underscore usage")
zfg.Str("group-options.this-option", "", "dash usage")
Restrictions
  • Options are registered at import time. Dynamic (runtime) option registration is not supported

    // internal/db/client.go
    package db
    
    import zfg "github.com/chaindead/zerocfg"
    
    // good: options registered at import
    var dbHost = zfg.Str("db.host", "localhost", "called on import")
    
    // bad: dynamic registration
    func AddOption() {
        zfg.Str("db.dynamic", "", "not supported")
    }
    
  • No key duplication is allowed. Each option key must be unique to ensure a single source of truth and avoid boilerplate

  • Simultaneous use of keys and sub-keys (e.g., map and map.value) are not allowed

Unknown values

If zfg.Parse encounters an unknown value (e.g. variable not registered as an option), it returns an error. This helps avoid boilerplate and ensures only declared options are used.

But you can ignore unknown values if desired.

err := zfg.Parse(
    env.New(),
    yaml.New(path),
)
if u, ok := zfg.IsUnknown(err); !ok {
    panic(err)
} else {
    // u is map <source_name> to slice of unknown keys
    fmt.Println(u)
}

env source does not trigger unknown options to avoid false positives.

Complex Types as string
  • Base values converted via fmt.Sprint("%v")
  • If a type has a String() method, it is used for string conversion (e.g., time.Duration).
  • Otherwise, JSON representation is used for complex types (e.g., slices, maps).

For converting any value to string, zfg.ToString is used internally.

var (
    _ = zfg.Dur("timeout", 5*time.Second, "duration via fmt.Stringer interface")
    _ = zfg.Floats64("floats", nil, "list via json")
)

func main() {
    _ = zfg.Parse()

    fmt.Printf(zfg.Show())
    // CMD: go run ./... --timeout 10s --floats '[1.1, 2.2, 3.3]'
    // OUTPUT:
    //   floats  = [1.1,2.2,3.3] (list via json)
    //   timeout = 10s           (duration via fmt.Stringer interface)
}

Configuration Sources

The configuration system follows a strict priority hierarchy:

  1. Command-line flags (always highest priority, enabled by default)
  2. Optional providers in order of addition (first added = higher priority)
  3. Default values (lowest priority)

For example, if you initialize configuration like this:

zfg.Parse(
    env.New(),      // Second highest priority (after cli flags)
    yaml.New(path), // Third highest priority
)

The final value resolution order will be:

  1. Command-line flags (if provided)
  2. Providers from arguments of zfg.Parse in same order as it is passed.
  3. Default values

Important notes:

  • Lower priority sources cannot override values from higher priority sources
  • All providers except flags are optional
  • Provider priority is determined by the order in Parse() function
  • Values not found in higher priority sources fall back to lower priority sources
Command-line Arguments
  • The flag source is enabled by default and always has the highest priority
  • You can define configuration options with aliases for convenient CLI usage
  • Values are passed as space-separated arguments (no = allowed)
  • Both single dash (-) and double dash (--) prefixes are supported for flags and their aliases

Example:

path := zfg.Str("config.path", "", "path to yaml conf file", zfg.Alias("c"))

You can run your application with:

go run ./... -c test.yaml
# or
go run ./... --config.path test.yaml

In both cases, the value test.yaml will be assigned to config.path.

Environment Variables

Environment variables are automatically transformed from the configuration key format:

Config Key Environment Variable Note
db.user DB_USER Basic transformation
app.api.key APP_API_KEY Multi-level path
camelCase.value CAMELCASE_VALUE CamelCase handling
api-key.secret APIKEY_SECRET Dashes removed
under_score.value UNDERSCORE_VALUE Underscores removed

The transformation rules:

  1. Remove special characters (except letters, digits, and dots)
  2. Replace dots with underscores
  3. Convert to uppercase

Example:

import (
    "fmt"
    zfg "github.com/chaindead/zerocfg"
    "github.com/chaindead/zerocfg/env"
)
var dbUser = zfg.Str("db.user", "", "database's username")

func main() {
    _ = zfg.Parse(
        env.New(),
    )
    fmt.Printf("DB user: %s", *dbUser)
}

When you run, dbUser will be set to admin.

DB_USER=admin go run main.go
# OUTPUT: DB user: admin
YAML Source
  • Options use dotted paths to map to YAML keys, supporting hierarchical configuration.
  • All naming styles are supported and mapped to YAML keys as written.

Example YAML file:

group:
  option: "foo"

numbers:
  - 1
  - 2
  - 3

limits:
  max: 10
  min: 1

Example Go config:

zfg.Str("group.option", "", "hierarchical usage")
zfg.Ints("numbers", nil, "slice of server configs")
zfg.Map("limits", nil, "map of limits")

Advanced Usage

Value Representation

[!IMPORTANT] Read this section before implementing custom options or providers.

  • All supported option values must have a string representation
  • Conversion to string is performed using zfg.ToString
  • Types must implement Set(string); the string passed is produced by ToString and parsing must be compatible
  • Providers return map[string]string where values are produced by the conv function argument in the provider interface (internally zfg.ToString is used)
Custom Options

You can define your own option types by implementing the Value interface and registering them via Any function. Methods Set and String should be compatible.

// Custom type
type MyType struct{ V string }

func newValue(val MyType, p *MyType) zfg.Value {
    *p = val
    return p
}

func (m *MyType) Set(s string) error { m.V = s; return nil }
func (m *MyType) Type() string       { return "custom" }
func (m *MyType) String() string { return m.V }

func Custom(name string, defVal MyType, desc string, opts ...zfg.OptNode) *MyType {
     return zfg.Any(name, defVal, desc, newValue, opts...)
}

// Register custom option
var myOpt = Custom("custom.opt", MyType{"default"}, "custom option")
Custom Providers

You can add your own configuration sources by implementing the Provider interface.

  • If awaited[name] == true, the name is an option
  • If awaited[name] == false, the name is an alias
type MyProvider struct{}

func (p *MyProvider) Type() string { return "my" }
func (p *MyProvider) Provide(awaited map[string]bool, conv func(any) string) (map[string]string, map[string]string, error) {
    found := map[string]string{}
    unknown := map[string]string{}
    // ... fill found/unknown based on awaited ...
    return found, unknown, nil
}

// Usage
zfg.Parse(&MyProvider{})

Documentation

For detailed documentation and advanced usage examples, visit our Godoc page.

Star History

Star History Chart

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoSuchKey is returned when a configuration key is not registered as an option.
	ErrNoSuchKey = errors.New("no such key")

	// ErrCollidingAlias is returned when an alias collides with an existing key.
	ErrCollidingAlias = errors.New("colliding alias with key")

	// ErrDuplicateKey is returned when a duplicate configuration key is registered.
	ErrDuplicateKey = errors.New("duplicate key")

	// ErrRequired is returned when required configuration fields are missing.
	ErrRequired = errors.New("missing required fields")

	// ErrRuntimeRegistration is returned when attempting to register options at runtime.
	ErrRuntimeRegistration = errors.New("misuse: runtime var registration is not allowed")

	// ErrDoubleParse is returned when Parse is called more than once.
	ErrDoubleParse = errors.New("misuse: Parse func should be called once")
)

Functions

func Any

func Any[T any](name string, defVal T, desc string, create func(T, *T) Value, opts ...OptNode) *T

Any registers a custom configuration option type and returns a pointer to its value.

Usage Example:

// Custom type
type MyType struct { V string }

func newValue(val MyType, p *MyType) Value {
    *p = val
    return (*MyType)(p)
}

func (m *MyType) Set(s string) error { m.V = s; return nil }
func (m *MyType) Type() string      { return "custom" }

// User-friendly registration function
func Custom(name string, defVal MyType, desc string, opts ...zfg.OptNode) *MyType {
    return zfg.Any(name, defVal, desc, newValue, opts...)
}

// Register custom option
myOpt := Custom("custom.opt", MyType{"default"}, "custom option")

Arguments:

  • name: unique option key (dot-separated for hierarchy)
  • defVal: default value of type T
  • desc: description for documentation and rendering
  • create: function to create a Value implementation for T (see example above)
  • opts: optional OptNode modifiers (e.g., Alias, Secret, Required)

Behavior:

  • Registers the option at import time; panics if called after Parse.
  • Returns a pointer to the registered value, which is updated by configuration sources.

func Bool

func Bool(name string, defVal bool, desc string, opts ...OptNode) *bool

Bool registers a boolean configuration option and returns a pointer to its value.

Usage:

debug := zerocfg.Bool("debug", false, "enable debug mode")

func Bools

func Bools(name string, value []bool, usage string, opts ...OptNode) *[]bool

Bools registers a slice of boolean configuration options and returns a pointer to its value.

Usage:

flags := zerocfg.Bools("feature.flags", []bool{true, false}, "feature flags")

func Dur

func Dur(name string, value time.Duration, usage string, opts ...OptNode) *time.Duration

Dur registers a time.Duration configuration option and returns a pointer to its value.

Usage:

timeout := zerocfg.Dur("timeout", 5*time.Second, "timeout for operation")

func Durs

func Durs(name string, defValue []time.Duration, desc string, opts ...OptNode) *[]time.Duration

Durs registers a slice of time.Duration configuration options and returns a pointer to its value.

Usage:

intervals := zerocfg.Durs("intervals", []time.Duration{time.Second, 2 * time.Second}, "interval durations")

func Float32

func Float32(name string, value float32, usage string, opts ...OptNode) *float32

Float32 registers a float32 configuration option and returns a pointer to its value.

Usage:

ratio := zerocfg.Float32("ratio", 0.25, "ratio value")

func Float64

func Float64(name string, value float64, usage string, opts ...OptNode) *float64

Float64 registers a float64 configuration option and returns a pointer to its value.

Usage:

threshold := zerocfg.Float64("threshold", 0.5, "threshold value")

func Floats32

func Floats32(name string, value []float32, usage string, opts ...OptNode) *[]float32

Floats32 registers a slice of float32 configuration options and returns a pointer to its value.

Usage:

factors := zerocfg.Floats32("factors", []float32{0.1, 0.2}, "factor values")

func Floats64

func Floats64(name string, value []float64, usage string, opts ...OptNode) *[]float64

Floats64 registers a slice of float64 configuration options and returns a pointer to its value.

Usage:

weights := zerocfg.Floats64("weights", []float64{1.1, 2.2}, "weight values")

func IP

func IP(name string, defValue string, desc string, opts ...OptNode) *net.IP

IP registers a net.IP configuration option and returns a pointer to its value.

Usage:

dbIP := zerocfg.IP("db.ip", "127.0.0.1", "database IP address")

func IPs added in v0.1.7

func IPs(name string, defValue []string, desc string, opts ...OptNode) *[]net.IP

func Int

func Int(name string, defVal int, desc string, opts ...OptNode) *int

Int registers an int configuration option and returns a pointer to its value.

Usage:

port := zerocfg.Int("db.port", 5432, "database port")

func Int32

func Int32(name string, defVal int32, desc string, opts ...OptNode) *int32

Int32 registers an int32 configuration option and returns a pointer to its value.

Usage:

code := zerocfg.Int32("status.code", 200, "status code")

func Int64

func Int64(name string, defVal int64, desc string, opts ...OptNode) *int64

Int64 registers an int64 configuration option and returns a pointer to its value.

Usage:

big := zerocfg.Int64("big.value", 1234567890, "big int value")

func Ints

func Ints(name string, defVal []int, desc string, opts ...OptNode) *[]int

Ints registers a slice of int configuration options and returns a pointer to its value.

Usage:

ids := zerocfg.Ints("user.ids", []int{1, 2, 3}, "user IDs")

func IsUnknown added in v0.1.1

func IsUnknown(err error) (map[string][]string, bool)

IsUnknown checks if the provided error is an UnknownFieldError. If so, it returns the underlying map and true. Otherwise, it returns nil and false.

Example usage:

err := zfg.Parse(...)
if u, ok := zfg.IsUnknown(err); ok {
    // u is map[source_name][]unknown_keys
}

func Map

func Map(name string, defVal map[string]any, desc string, opts ...OptNode) map[string]any

Map registers a map[string]any configuration option and returns the map value.

Usage:

limits := zerocfg.Map("limits", map[string]any{"max": 10, "min": 1}, "map of limits")

func Parse

func Parse(ps ...Provider) error

Parse loads configuration from the provided sources in priority order.

Usage:

err := zerocfg.Parse(env.New(), yaml.New(path))

Priority:

  1. Command-line flags (always highest)
  2. Parsers in the order provided (first = higher priority)
  3. Default values (lowest)

Behavior:

  • Applies each parser in order, setting values for registered options only.
  • Returns an error if unknown options are found (unless ignored by IsUnknown).

Error Handling:

  • UnknownFieldError: for unknown keys (see IsUnknown)
  • ErrRequired: for missing required options
  • ErrDoubleParse: if called multiple times

func Show added in v0.1.2

func Show() string

Show returns a formatted string representation of all registered configuration options and their current values.

func Str

func Str(name string, defVal string, desc string, opts ...OptNode) *string

Str registers a string configuration option and returns a pointer to its value.

Usage:

username := zerocfg.Str("db.user", "guest", "user of database")

func Strs

func Strs(name string, defVal []string, desc string, opts ...OptNode) *[]string

Strs registers a slice of string configuration options and returns a pointer to its value.

Usage:

hosts := zerocfg.Strs("hosts", []string{"a", "b"}, "list of hosts")

func ToString

func ToString(v any) string

ToString returns a string representation of any value for use in configuration serialization and display.

The conversion rules are as follows:

  • If the value implements fmt.Stringer, its String() method is used.
  • For slices or arrays whose elements implement fmt.Stringer, a JSON array of their string values is returned.
  • For other slices, arrays, or maps, the value is marshaled to JSON.
  • For all other types, fmt.Sprint is used.

ToString is used internally by zerocfg for representing option values as strings, including when passing ToString to custom parsers and for rendering configuration output.

func Uint

func Uint(name string, defVal uint, desc string, opts ...OptNode) *uint

Uint registers a uint configuration option and returns a pointer to its value.

Usage:

port := zerocfg.Uint("db.port", 5678, "database port")

func Uint32

func Uint32(name string, defVal uint32, desc string, opts ...OptNode) *uint32

Uint32 registers a uint32 configuration option and returns a pointer to its value.

Usage:

code := zerocfg.Uint32("status.code", 200, "status code")

func Uint64

func Uint64(name string, defVal uint64, desc string, opts ...OptNode) *uint64

Uint64 registers a uint64 configuration option and returns a pointer to its value.

Usage:

big := zerocfg.Uint64("big.value", 1234567890, "big uint value")

Types

type Grp

type Grp struct {
	// contains filtered or unexported fields
}

Grp represents a group of configuration options that can share a common prefix and set of option modifiers. Groups are useful for organizing related configuration options and applying shared modifiers.

func NewGroup

func NewGroup(prefix string, opts ...OptNode) *Grp

NewGroup creates a new Grp with the specified prefix and option modifiers. All options added to this group will have the prefix prepended to their names.

Example:

g := NewGroup("db")
Str("host", "localhost", "db host", Group(g)) // becomes "db.host"

func NewOptions

func NewOptions(opts ...OptNode) *Grp

NewOptions creates a new Grp with no prefix but with shared option modifiers. This is useful for applying the same modifiers (e.g., Secret, Required) to multiple options without a hierarchical prefix.

Example:

g := NewOptions(Secret())
Str("api_key", "", "API key", Group(g)) // marked as secret

type OptNode

type OptNode func(*node)

OptNode is a function that modifies a node during option registration. It is used to apply additional behaviors such as aliases, secret marking, grouping, or required flags.

Example:

Int("db.port", 5432, "database port", Alias("p"), Required())

func Alias

func Alias(alias string) OptNode

Alias returns an OptNode that adds an alias to a configuration option. Aliases allow options to be referenced by alternative names (e.g., for CLI flags).

Example:

port := Int("db.port", 5432, "database port", Alias("p"))

func Group

func Group(g *Grp) OptNode

Group returns an OptNode that applies a Grp to a configuration option. This sets the option's name prefix and applies all group modifiers.

Example:

g := NewGroup("db")
host := Str("host", "localhost", "db host", Group(g)) // becomes "db.host"

func Required added in v0.1.2

func Required() OptNode

Required returns an OptNode that marks a configuration option as required. Required options must be set by a configuration source or an error will be returned by Parse.

Example:

user := Str("db.user", "", "database user", Required())

func Secret

func Secret() OptNode

Secret returns an OptNode that marks a configuration option as secret. Secret options are masked in rendered output (e.g., Show) to avoid leaking sensitive values.

Example:

password := Str("db.password", "", "database password", Secret())

type Provider added in v0.1.6

type Provider interface {
	Type() string
	Provide(awaited map[string]bool, conv func(any) string) (found, unknown map[string]string, err error)
}

Provider defines a configuration source for zerocfg.

Custom sources must implement this interface to provide configuration values.

Methods:

  • Type() string: returns the parser's type name (e.g., "env", "yaml").
  • Parse(awaited, conv):
  • awaited: map of option names and aliases to expect (true = option, false = alias)
  • conv: function to convert values to string (usually zerocfg.ToString)

Returns:

  • found: map of recognized option names to string values
  • unknown: map of unrecognized names to string values

type UnknownFieldError added in v0.1.1

type UnknownFieldError map[string][]string

UnknownFieldError represents a mapping from configuration source names to unknown option keys encountered during parsing. It is returned by Parse when unknown values are found in configuration sources.

func (UnknownFieldError) Error added in v0.1.1

func (e UnknownFieldError) Error() string

type Value

type Value interface {
	Set(string) error
	Type() string
}

Value is the interface implemented by all configuration option types in zerocfg.

Requirements:

  • Must support setting its value from a string: Set(string) error The string is produced by zerocfg's ToString conversion.
  • Must report its type name for identification and documentation: Type() string

Directories

Path Synopsis