8000 GitHub - chaindead/zerocfg: Zero-effort, concise configuration management that avoids boilerplate and repetitive actions.
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Zero-effort, concise configuration management that avoids boilerplate and repetitive actions.

License

Notifications You must be signed in to change notification settings

chaindead/zerocfg

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zero Effort Configuration

GoDoc Go Report Card Codecov License: MIT 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 sources 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:

var list = zfg.Ints("list", nil, "usage example")
DB_USER=admin go run main.go

When you run, dbUser will be set to 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.

About

Zero-effort, concise configuration management that avoids boilerplate and repetitive actions.

Topics

Resources

License

Stars

Watchers

Forks

Languages

0