config

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jan 28, 2026 License: MIT Imports: 10 Imported by: 0

README

jp-go-config

Configuration management for Go applications with auto-discovery, typed configuration structs, and comprehensive validation.

Installation

go get github.com/JohnPlummer/jp-go-config

Quick Start

package main

import (
    "log"
    "github.com/JohnPlummer/jp-go-config"
)

func main() {
    // Load configuration with auto-discovery
    cfg, err := config.Load()
    if err != nil {
        log.Fatal(err)
    }

    // Get validated database configuration
    db, err := cfg.Database()
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Connecting to %s", db.ConnectionString())
}

Configuration Precedence

Configuration values are resolved in this order (highest to lowest priority):

  1. Environment variables
  2. .env file values
  3. config.yaml file values
  4. Default values

Environment variables always win. The .env file does not override existing environment variables.

Auto-Discovery

Load() automatically discovers and loads configuration files from:

  1. Current working directory
  2. Go module root (directory containing go.mod)
  3. Git repository root (directory containing .git)

Files discovered:

  • .env - Environment variables (loaded first, does not override existing env vars)
  • config.yaml or config.yml - Configuration values

No configuration files are required. Missing files are silently skipped.

Monorepo and Git Worktree Support

The discovery order supports monorepos and git worktrees where configuration files are at the repository root but the Go module is in a subdirectory.

Example monorepo structure:

/repo-root/
  ├── .git/
  ├── .env              # Found at step 3 (git root)
  ├── config.yaml
  └── services/
      └── api/          # Your Go module
          ├── go.mod
          └── main.go   # cwd when running

When running from services/api/, the discovery searches:

  1. services/api/ (cwd) - not found
  2. services/api/ (go.mod root, same as cwd) - deduplicated
  3. /repo-root/ (git root) - found!

This also works with git worktrees where .git is a file pointing to the bare repository.

Environment Variables

Database Configuration
Variable Default Description
DATABASE_URL (none) PostgreSQL connection URL (takes precedence over individual vars)
DB_HOST localhost Database host
DB_PORT 5432 Database port
DB_NAME postgres Database name
DB_USER postgres Database user
DB_PASSWORD (none) Database password (required)
DB_SSLMODE disable SSL mode (disable, require, verify-ca, verify-full)
DB_MAX_CONNS 25 Maximum connections in pool
DB_MIN_CONNS 5 Minimum connections in pool
DB_CONN_MAX_LIFETIME 1h Maximum connection lifetime
DB_CONN_MAX_IDLE_TIME 10m Maximum connection idle time
DB_RETRY_ATTEMPTS 3 Number of retry attempts
DB_RETRY_DELAY 2s Delay between retries
DB_HEALTH_CHECK_PERIOD 30s Health check interval
Server Configuration
Variable Default Description
SERVER_HOST localhost Server host
SERVER_PORT 8080 Server port
SERVER_READ_TIMEOUT 15s Read timeout
SERVER_WRITE_TIMEOUT 15s Write timeout
SERVER_IDLE_TIMEOUT 60s Idle timeout
OpenAI Configuration
Variable Default Description
OPENAI_API_KEY (none) OpenAI API key (required)
OPENAI_MODEL gpt-3.5-turbo Model to use
OPENAI_TEMPERATURE 0.7 Temperature (0.0 - 2.0)
OPENAI_MAX_TOKENS 2000 Maximum tokens in response
OPENAI_TIMEOUT 30s Request timeout
Resilience Configuration
Variable Default Description
RESILIENCE_MAX_RETRIES 3 Maximum retry attempts
RESILIENCE_INITIAL_DELAY 1s Initial retry delay
RESILIENCE_MAX_DELAY 30s Maximum retry delay
RESILIENCE_MULTIPLIER 2.0 Backoff multiplier
RESILIENCE_MAX_REQUESTS 10 Circuit breaker max requests
RESILIENCE_INTERVAL 10s Circuit breaker interval
RESILIENCE_TIMEOUT 60s Circuit breaker timeout
RESILIENCE_FAILURE_THRESHOLD 0.6 Failure threshold (0.0 - 1.0)

DATABASE_URL Support

Use a PostgreSQL connection URL instead of individual environment variables:

DATABASE_URL=postgres://user:password@localhost:5432/mydb?sslmode=require

When DATABASE_URL is set, it takes precedence over DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, and DB_SSLMODE.

Pool settings (DB_MAX_CONNS, etc.) are always read from individual environment variables and applied regardless of whether DATABASE_URL is used.

Supported URL schemes: postgres:// and postgresql://

Explicit Paths

Override auto-discovery with explicit file paths:

cfg, err := config.Load(
    config.WithEnvPath("/path/to/.env"),
    config.WithConfigPath("/path/to/config.yaml"),
)

When explicit paths are provided:

  • The file must exist (returns error if not found)
  • Auto-discovery is disabled for that file type

Load Options

Option Description
WithEnvPath(path) Explicit .env file path
WithConfigPath(path) Explicit config file path
WithPrefix(prefix) Environment variable prefix (default: APP)
WithLogger(logger) Custom slog.Logger for discovery events

Accessing Configuration

The Config struct provides typed accessor methods that validate and cache on first access:

cfg, err := config.Load()
if err != nil {
    log.Fatal(err)
}

// Each accessor validates and caches the config
db, err := cfg.Database()
server, err := cfg.Server()
openai, err := cfg.OpenAI()
resilience, err := cfg.Resilience()

Fail-Fast Validation

Use ValidateAll() to validate all configuration types at startup:

cfg, err := config.Load()
if err != nil {
    log.Fatal(err)
}

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

// All configs are now validated and cached
db, _ := cfg.Database()
server, _ := cfg.Server()

Validation order: database, server, openai, resilience.

Advanced Usage

For lower-level access or custom configurations, use NewStandard() directly:

// Create standard Viper wrapper
std, err := config.NewStandard(
    config.WithEnvPrefix("MYAPP"),
    config.WithConfigFile("config.yaml"),
)
if err != nil {
    log.Fatal(err)
}

// Load typed configuration manually
dbConfig := config.DatabaseConfigFromViper(std)
if err := dbConfig.Validate(); err != nil {
    log.Fatal(err)
}
Custom Configuration Structs
type MyConfig struct {
    APIKey  string `mapstructure:"api_key"`
    Timeout int    `mapstructure:"timeout"`
}

std, _ := config.NewStandard()
std.BindEnv("myservice.api_key", "MYSERVICE_API_KEY")
std.BindEnv("myservice.timeout", "MYSERVICE_TIMEOUT")

var myConfig MyConfig
if err := std.Unmarshal(&myConfig); err != nil {
    log.Fatal(err)
}
Access Underlying Standard
cfg, _ := config.Load()
std := cfg.Standard()

// Use Standard methods
value := std.GetString("custom.key")

Validation Helpers

Use these functions for custom configuration validation:

// Required string field
if err := config.ValidateRequired("field.name", value); err != nil {
    return err
}

// Port number (1-65535)
if err := config.ValidatePort("server.port", port); err != nil {
    return err
}

// Positive duration
if err := config.ValidateDuration("timeout", duration); err != nil {
    return err
}

// Positive integer
if err := config.ValidatePositive("count", count); err != nil {
    return err
}

// Value in range
if err := config.ValidateRange("temperature", temp, 0.0, 2.0); err != nil {
    return err
}

Development

Requirements
  • Go 1.21 or higher
Testing
go test -v ./...
go test -v -race -cover ./...
Linting
golangci-lint run

License

MIT License - see LICENSE for details.

Documentation

Overview

Package config provides enterprise-standard configuration management wrapping Viper.

This package offers typed configuration with automatic .env file loading, environment variable precedence, and comprehensive validation.

Package config provides enterprise-standard configuration management wrapping Viper.

Package config provides enterprise-standard configuration management wrapping Viper.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExpandEnvStrict added in v0.3.0

func ExpandEnvStrict(value string) (string, error)

ExpandEnvStrict expands ${VAR} patterns in value, returning error if VAR is not set. Unlike os.ExpandEnv, this function:

  • Fails if any referenced environment variable is not set
  • Does NOT support ${VAR:-default} syntax (ignores default values)
  • Forces explicit configuration to prevent production surprises

IMPORTANT: Call AFTER LoadEnvFile() so .env values are available.

Example:

// Given DATABASE_URL is set in environment
value := "postgres://${DB_HOST}:${DB_PORT}/mydb"
expanded, err := ExpandEnvStrict(value)
if err != nil {
    // Error: environment variable DB_HOST not set
}

func FindProjectRoot added in v0.3.0

func FindProjectRoot(markerFiles ...string) string

FindProjectRoot searches parent directories for project root markers. Looks for directories containing ALL specified markerFiles. Returns empty string if no root found or if no markers specified.

Example:

// Find directory containing both CLAUDE.md and config.yaml
root := FindProjectRoot("CLAUDE.md", "config.yaml")
if root != "" {
    configPath := root + "/config.yaml"
}

func LoadEnvFile

func LoadEnvFile(paths ...string) error

LoadEnvFile loads environment variables from a .env file. Does not override existing environment variables. Silently succeeds if the file doesn't exist.

func LoadEnvFiles added in v0.3.0

func LoadEnvFiles(env string, searchPaths ...string) error

LoadEnvFiles loads .env and optional .env.{env} in correct order. Environment-specific file overrides base .env values. Returns nil if files are missing (this is expected in production).

Search paths are checked in order; first existing file in each category is used. If env is empty, only loads base .env file.

Example:

// Load .env then .env.test for test environment
if err := LoadEnvFiles("test", ".", ".."); err != nil {
    log.Printf("Warning: failed to load env files: %v", err)
}

// Load only base .env
_ = LoadEnvFiles("", ".", "..")

func ValidateDuration

func ValidateDuration(field string, duration time.Duration) error

ValidateDuration validates that a duration is positive

func ValidatePort

func ValidatePort(field string, port int) error

ValidatePort validates that a port number is in the valid range (1-65535)

func ValidatePositive

func ValidatePositive(field string, value int) error

ValidatePositive validates that an integer is positive (> 0)

func ValidateRange

func ValidateRange[T int | float64](field string, value, min, max T) error

ValidateRange validates that a value is within a range (inclusive)

func ValidateRequired

func ValidateRequired(field, value string) error

ValidateRequired validates that a string field is not empty

Types

type Config added in v0.4.0

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

Config holds all application configuration loaded via Load(). Access typed configs via accessor methods like Database(), Server(), etc.

func Load added in v0.4.0

func Load(opts ...LoadOption) (*Config, error)

Load creates a fully configured Config by auto-discovering and loading .env and config.yaml files from the current directory or project root.

Discovery order (first found wins):

  1. Current working directory
  2. Go module root (directory containing go.mod)
  3. Git repository root (directory containing .git)

This supports monorepos and git worktrees where config files are at the repository root but the Go module is in a subdirectory.

Files loaded:

  • .env: Environment variables (does not override existing)
  • config.yaml: Configuration values

Use WithEnvPath/WithConfigPath to override auto-discovery.

Example:

cfg, err := config.Load()
if err != nil {
    log.Fatal(err)
}
db := cfg.Database()

func (*Config) Database added in v0.4.0

func (c *Config) Database() (*DatabaseConfig, error)

Database returns the database configuration. Validates on first access and caches the result.

func (*Config) OpenAI added in v0.4.0

func (c *Config) OpenAI() (*OpenAIConfig, error)

OpenAI returns the OpenAI configuration. Validates on first access and caches the result.

func (*Config) Resilience added in v0.4.0

func (c *Config) Resilience() (*ResilienceConfig, error)

Resilience returns the resilience configuration. Validates on first access and caches the result.

func (*Config) Server added in v0.4.0

func (c *Config) Server() (*ServerConfig, error)

Server returns the server configuration. Validates on first access and caches the result.

func (*Config) Standard added in v0.4.0

func (c *Config) Standard() *Standard

Standard returns the underlying Standard config for advanced usage.

func (*Config) ValidateAll added in v0.4.0

func (c *Config) ValidateAll() error

ValidateAll validates all configuration types and returns the first error. Validation order: database, server, openai, resilience. Use this method for fail-fast behavior when all configs are required.

Example:

cfg, err := config.Load()
if err != nil {
    log.Fatal(err)
}
if err := cfg.ValidateAll(); err != nil {
    log.Fatal(err)
}

type DatabaseConfig

type DatabaseConfig struct {
	Host     string `mapstructure:"host"`
	Port     int    `mapstructure:"port"`
	Database string `mapstructure:"database"`
	User     string `mapstructure:"user"`
	Password string `mapstructure:"password"`
	SSLMode  string `mapstructure:"ssl_mode"`

	// Connection pool settings
	MaxConns        int           `mapstructure:"max_conns"`
	MinConns        int           `mapstructure:"min_conns"`
	ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
	ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`

	// Retry settings
	RetryAttempts int           `mapstructure:"retry_attempts"`
	RetryDelay    time.Duration `mapstructure:"retry_delay"`

	// Health check
	HealthCheckPeriod time.Duration `mapstructure:"health_check_period"`
}

DatabaseConfig holds PostgreSQL database configuration with connection pooling settings.

func DatabaseConfigFromViper

func DatabaseConfigFromViper(s *Standard) DatabaseConfig

DatabaseConfigFromViper creates a DatabaseConfig from a Standard config loader.

Environment variable mappings:

  • DATABASE_URL -> full connection URL (takes precedence over individual vars)
  • DB_HOST -> host (default: localhost)
  • DB_PORT -> port (default: 5432)
  • DB_NAME -> database (default: postgres)
  • DB_USER -> user (default: postgres)
  • DB_PASSWORD -> password
  • DB_SSLMODE -> ssl_mode (default: disable)
  • DB_MAX_CONNS -> max_conns (default: 25)
  • DB_MIN_CONNS -> min_conns (default: 5)
  • DB_CONN_MAX_LIFETIME -> conn_max_lifetime (default: 1h)
  • DB_CONN_MAX_IDLE_TIME -> conn_max_idle_time (default: 10m)
  • DB_RETRY_ATTEMPTS -> retry_attempts (default: 3)
  • DB_RETRY_DELAY -> retry_delay (default: 2s)
  • DB_HEALTH_CHECK_PERIOD -> health_check_period (default: 30s)

When DATABASE_URL is set, it takes precedence over individual connection variables (DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSLMODE). Pool settings (DB_MAX_CONNS, etc.) are always read from individual env vars and applied regardless of whether DATABASE_URL is used.

func ParseDatabaseURL added in v0.4.0

func ParseDatabaseURL(rawURL string) (*DatabaseConfig, error)

ParseDatabaseURL parses a PostgreSQL connection URL into a DatabaseConfig.

Supported URL schemes: postgres://, postgresql://

URL format: postgres://user:password@host:port/database?sslmode=value

The function extracts:

  • host: from URL host
  • port: from URL port (default: 5432)
  • database: from URL path (without leading slash)
  • user: from URL userinfo
  • password: from URL userinfo
  • sslmode: from query parameter (default: disable)

Returns an error for invalid URLs or unsupported schemes.

Example:

cfg, err := config.ParseDatabaseURL("postgres://user:pass@localhost:5432/mydb?sslmode=require")
if err != nil {
    log.Fatal(err)
}

func (*DatabaseConfig) ConnectionString

func (c *DatabaseConfig) ConnectionString() string

ConnectionString returns a PostgreSQL connection string

func (*DatabaseConfig) Validate

func (c *DatabaseConfig) Validate() error

Validate validates the database configuration

type LoadOption added in v0.4.0

type LoadOption func(*loadOptions)

LoadOption configures the Load function using the functional options pattern.

func WithConfigPath added in v0.4.0

func WithConfigPath(path string) LoadOption

WithConfigPath specifies an explicit config file path, disabling auto-discovery.

func WithEnvPath added in v0.4.0

func WithEnvPath(path string) LoadOption

WithEnvPath specifies an explicit .env file path, disabling auto-discovery.

func WithLogger added in v0.4.0

func WithLogger(logger *slog.Logger) LoadOption

WithLogger sets a custom slog logger for discovery events.

func WithPrefix added in v0.4.0

func WithPrefix(prefix string) LoadOption

WithPrefix sets the environment variable prefix (default: APP).

type OpenAIConfig

type OpenAIConfig struct {
	APIKey      string        `mapstructure:"api_key"`
	Model       string        `mapstructure:"model"`
	Temperature float64       `mapstructure:"temperature"`
	MaxTokens   int           `mapstructure:"max_tokens"`
	Timeout     time.Duration `mapstructure:"timeout"`
}

OpenAIConfig holds OpenAI API configuration

func OpenAIConfigFromViper

func OpenAIConfigFromViper(s *Standard) OpenAIConfig

OpenAIConfigFromViper creates an OpenAIConfig from a Standard config loader.

Environment variable mappings:

  • OPENAI_API_KEY -> api_key (required)
  • OPENAI_MODEL -> model (default: gpt-3.5-turbo)
  • OPENAI_TEMPERATURE -> temperature (default: 0.7)
  • OPENAI_MAX_TOKENS -> max_tokens (default: 2000)
  • OPENAI_TIMEOUT -> timeout (default: 30s)

func (*OpenAIConfig) Validate

func (c *OpenAIConfig) Validate() error

Validate validates the OpenAI configuration

type Option

type Option func(*Standard) error

Option configures the Standard config loader using the functional options pattern.

func WithConfigFile

func WithConfigFile(path string) Option

WithConfigFile specifies a config file to load (YAML, JSON, TOML, etc.)

func WithConfigName

func WithConfigName(name string) Option

WithConfigName sets the name of the config file to search for (without extension)

func WithConfigPaths

func WithConfigPaths(paths ...string) Option

WithConfigPaths adds paths to search for the config file

func WithConfigType

func WithConfigType(configType string) Option

WithConfigType sets the type of the config file (yaml, json, toml, etc.)

func WithEnvFile

func WithEnvFile(path string) Option

WithEnvFile loads environment variables from a specific .env file

func WithEnvPrefix

func WithEnvPrefix(prefix string) Option

WithEnvPrefix sets the environment variable prefix (default: APP_)

func WithoutEnvFile

func WithoutEnvFile() Option

WithoutEnvFile disables automatic .env file loading

type ResilienceConfig added in v0.1.2

type ResilienceConfig struct {
	// Retry settings
	MaxRetries   int           `mapstructure:"max_retries"`
	InitialDelay time.Duration `mapstructure:"initial_delay"`
	MaxDelay     time.Duration `mapstructure:"max_delay"`
	Multiplier   float64       `mapstructure:"multiplier"`

	// Circuit breaker settings
	MaxRequests      uint32        `mapstructure:"max_requests"`
	Interval         time.Duration `mapstructure:"interval"`
	Timeout          time.Duration `mapstructure:"timeout"`
	FailureThreshold float64       `mapstructure:"failure_threshold"`
}

ResilienceConfig holds retry and circuit breaker configuration. This provides standardized resilience settings that can be used across all packages that implement retry and circuit breaker patterns.

func ResilienceConfigFromViper added in v0.1.2

func ResilienceConfigFromViper(s *Standard) ResilienceConfig

ResilienceConfigFromViper creates a ResilienceConfig from a Standard config loader.

Environment variable mappings:

  • RESILIENCE_MAX_RETRIES -> max_retries (default: 3)
  • RESILIENCE_INITIAL_DELAY -> initial_delay (default: 1s)
  • RESILIENCE_MAX_DELAY -> max_delay (default: 30s)
  • RESILIENCE_MULTIPLIER -> multiplier (default: 2.0)
  • RESILIENCE_MAX_REQUESTS -> max_requests (default: 10)
  • RESILIENCE_INTERVAL -> interval (default: 10s)
  • RESILIENCE_TIMEOUT -> timeout (default: 60s)
  • RESILIENCE_FAILURE_THRESHOLD -> failure_threshold (default: 0.6)

func (*ResilienceConfig) Validate added in v0.1.2

func (c *ResilienceConfig) Validate() error

Validate validates the resilience configuration

type ServerConfig

type ServerConfig struct {
	Host         string        `mapstructure:"host"`
	Port         int           `mapstructure:"port"`
	ReadTimeout  time.Duration `mapstructure:"read_timeout"`
	WriteTimeout time.Duration `mapstructure:"write_timeout"`
	IdleTimeout  time.Duration `mapstructure:"idle_timeout"`
}

ServerConfig holds HTTP server configuration

func ServerConfigFromViper

func ServerConfigFromViper(s *Standard) ServerConfig

ServerConfigFromViper creates a ServerConfig from a Standard config loader.

Environment variable mappings:

  • SERVER_HOST -> host (default: localhost)
  • SERVER_PORT -> port (default: 8080)
  • SERVER_READ_TIMEOUT -> read_timeout (default: 15s)
  • SERVER_WRITE_TIMEOUT -> write_timeout (default: 15s)
  • SERVER_IDLE_TIMEOUT -> idle_timeout (default: 60s)

func (*ServerConfig) Address

func (c *ServerConfig) Address() string

Address returns the server address in host:port format

func (*ServerConfig) Validate

func (c *ServerConfig) Validate() error

Validate validates the server configuration

type Standard

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

Standard wraps Viper to provide enterprise-standard configuration loading with automatic .env file support, environment variable precedence, and validation.

func NewStandard

func NewStandard(options ...Option) (*Standard, error)

NewStandard creates a new Standard config loader with the given options.

By default: - Loads .env files from current directory (silently ignored if missing) - Reads environment variables with APP_ prefix - Replaces dots and hyphens with underscores in env var names

Options can override any of these defaults.

func (*Standard) AllKeys

func (s *Standard) AllKeys() []string

AllKeys returns all keys in the config

func (*Standard) BindEnv

func (s *Standard) BindEnv(key string, envVars ...string) error

BindEnv binds a config key to environment variables. With no envVars argument, it uses the key as the env var name. With one or more envVars, it checks each in order until finding a set value.

func (*Standard) Get

func (s *Standard) Get(key string) interface{}

Get retrieves a value by key

func (*Standard) GetBool

func (s *Standard) GetBool(key string) bool

GetBool retrieves a boolean value

func (*Standard) GetDuration

func (s *Standard) GetDuration(key string) interface{}

GetDuration retrieves a duration value

func (*Standard) GetInt

func (s *Standard) GetInt(key string) int

GetInt retrieves an integer value

func (*Standard) GetString

func (s *Standard) GetString(key string) string

GetString retrieves a string value

func (*Standard) IsSet

func (s *Standard) IsSet(key string) bool

IsSet checks if a key is set in the config

func (*Standard) Set

func (s *Standard) Set(key string, value interface{})

Set sets a value for a key

func (*Standard) Unmarshal

func (s *Standard) Unmarshal(rawVal interface{}) error

Unmarshal unmarshals the config into a struct

func (*Standard) Viper

func (s *Standard) Viper() *viper.Viper

Viper returns the underlying Viper instance for advanced usage

Jump to

Keyboard shortcuts

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