Skip to content

xraph/confy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

confy

A configuration library for Go that doesn't get in your way.

cfg := confy.New(confy.Config{})
cfg.LoadFrom(sources.NewFileSource("config.yaml", sources.FileSourceOptions{}))

port := cfg.GetInt("server.port", 8080)
debug := cfg.GetBool("debug", false)
timeout := cfg.GetDuration("timeout", 30*time.Second)

Why confy?

Most config libraries either do too little or too much. confy sits in the middle:

  • Multiple sources - files, environment variables, Consul, Kubernetes ConfigMaps
  • Type-safe getters with sensible defaults
  • Struct binding when you want it
  • Hot reload with file watching
  • Auto-discovery that finds your config files

No magic globals. No init() surprises. Just a struct you control.

Install

go get github.com/xraph/confy

Quick Start

Load from a file

package main

import (
    "github.com/xraph/confy"
    "github.com/xraph/confy/sources"
)

func main() {
    cfg := confy.New(confy.Config{})
    
    source, _ := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
        WatchEnabled: true,
    })
    cfg.LoadFrom(source)
    
    // Get values with defaults
    host := cfg.GetString("database.host", "localhost")
    port := cfg.GetInt("database.port", 5432)
    maxConns := cfg.GetInt("database.max_connections", 10)
}

Auto-discover config files

confy can find your config files automatically. It searches the current directory and parent directories for config.yaml and config.local.yaml.

cfg, err := confy.AutoLoadConfy("myapp", nil)
if err != nil {
    log.Fatal(err)
}

This is useful for monorepos where your app might be nested several directories deep.

Bind to a struct

type DatabaseConfig struct {
    Host        string        `yaml:"host"`
    Port        int           `yaml:"port"`
    MaxConns    int           `yaml:"max_connections" default:"10"`
    IdleTimeout time.Duration `yaml:"idle_timeout" default:"5m"`
}

var dbCfg DatabaseConfig
cfg.Bind("database", &dbCfg)

The default tag works when the key is missing from your config.

Environment variables

envSource := sources.NewEnvSource(sources.EnvSourceOptions{
    Prefix:    "MYAPP_",
    Separator: "_",
})
cfg.LoadFrom(envSource)

// MYAPP_DATABASE_HOST becomes database.host
host := cfg.GetString("database.host")

Watch for changes

cfg.WatchChanges(func(change confy.ConfigChange) {
    log.Printf("config changed: %s = %v", change.Key, change.NewValue)
})

cfg.Watch(context.Background())

Sources

confy supports multiple configuration sources out of the box:

Source Description
sources.FileSource YAML, JSON, TOML files
sources.EnvSource Environment variables
sources.ConsulSource HashiCorp Consul KV
sources.K8sConfigMapSource Kubernetes ConfigMaps

Sources have priorities. Higher priority sources override lower ones. By default:

  • Base config file: 100
  • Local config file: 200
  • Environment variables: 300

File source

source, err := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
    Priority:      100,
    WatchEnabled:  true,
    ExpandEnvVars: true,  // expands ${VAR} in values
})

Variable Resolution

confy supports environment variable expansion in YAML files with bash-style default value syntax.

Basic expansion

# config.yaml
database:
  host: ${DB_HOST}
  port: ${DB_PORT}
export DB_HOST=localhost
export DB_PORT=5432

Default values

Use bash-style syntax for default values when environment variables aren't set:

database:
  # Use default if DB_HOST is unset or empty
  host: ${DB_HOST:-localhost}
  
  # Use default only if DB_PORT is unset (not if empty)
  port: ${DB_PORT-5432}
  
  # Full connection string with multiple defaults
  dsn: postgres://${DB_USER:-postgres}:${DB_PASS:-postgres}@${DB_HOST:-localhost}:${DB_PORT:-5432}/${DB_NAME:-mydb}

Syntax variants:

Syntax Behavior
${VAR} or $VAR Standard expansion, empty string if not set
${VAR:-default} Use default if VAR is unset or empty
${VAR-default} Use default only if VAR is unset (not if empty)
${VAR:=default} Assign and use default if VAR is unset or empty
${VAR=default} Assign and use default only if VAR is unset

Example with some variables set:

server:
  address: ${SERVER_HOST:-0.0.0.0}:${SERVER_PORT:-8080}
  timeout: ${TIMEOUT:-30s}
export SERVER_PORT=3000
# SERVER_HOST and TIMEOUT will use defaults
# Result: address = "0.0.0.0:3000", timeout = "30s"

Complex examples

Database connection string:

database:
  dsn: ${DATABASE_DSN:-postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable}

This allows you to either:

  • Set DATABASE_DSN to override the entire connection string, or
  • Use individual environment variables with defaults for each component

Service URLs:

services:
  auth: ${AUTH_URL:-http://localhost:8080}
  api: ${API_URL:-http://localhost:3000}
  cache: ${REDIS_URL:-redis://localhost:6379/0}

Secret references

confy can resolve secret references from external secret managers (AWS Secrets Manager, HashiCorp Vault, etc.):

database:
  password: ${secret:db/production/password}
  api_key: ${secret:services/stripe/api_key}

Enable secret expansion in your file source:

source, err := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
    ExpandSecrets: true,
})

// Provide a secrets manager implementation
cfg.SetSecretsManager(mySecretsManager)

Secret references use the format ${secret:key} where key is passed to your SecretsManager implementation.

Consul source

source, err := sources.NewConsulSource(sources.ConsulSourceOptions{
    Address: "localhost:8500",
    Path:    "myapp/config",
    Token:   os.Getenv("CONSUL_TOKEN"),
})

Kubernetes ConfigMap

source, err := sources.NewK8sConfigMapSource(sources.K8sConfigMapSourceOptions{
    Namespace:     "default",
    ConfigMapName: "myapp-config",
    Key:           "config.yaml",
})

Type-safe getters

Every getter has an optional default value:

cfg.GetString("key")                    // returns "" if missing
cfg.GetString("key", "default")         // returns "default" if missing

cfg.GetInt("port", 8080)
cfg.GetBool("debug", false)
cfg.GetDuration("timeout", 10*time.Second)
cfg.GetFloat64("rate", 1.5)
cfg.GetStringSlice("hosts", []string{"localhost"})
cfg.GetSizeInBytes("max_size", 1024*1024)  // supports "10MB", "1GB" strings

App-scoped config

For monorepos, you can scope config per application:

# config.yaml
database:
  host: shared-db.internal

apps:
  api:
    port: 8080
    database:
      host: api-db.internal
  
  worker:
    concurrency: 10
cfg, _ := confy.LoadConfigWithAppScope("api", logger, nil)

// Gets "api-db.internal" - app config overrides global
host := cfg.GetString("database.host")

Validation

validator := confy.NewValidator(confy.ValidatorConfig{
    Mode: confy.ValidationModeStrict,
})

validator.AddRule(confy.ValidationRule{
    Key:      "server.port",
    Required: true,
    Min:      1,
    Max:      65535,
})

if err := cfg.Validate(); err != nil {
    log.Fatal(err)
}

Testing

confy includes a test implementation for unit tests:

func TestMyHandler(t *testing.T) {
    cfg := confy.NewTestConfyImpl()
    cfg.Set("feature.enabled", true)
    cfg.Set("timeout", "5s")
    
    handler := NewHandler(cfg)
    // ...
}

Or use the builder:

cfg := confy.NewTestConfigBuilder().
    WithString("api.url", "http://test.local").
    WithInt("retry.count", 3).
    WithBool("debug", true).
    Build()

License

MIT