confx

package module
v0.0.0-...-88de330 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 23 Imported by: 1

README

ConfX

ConfX is a feature-rich configuration management library for Go that provides a comprehensive solution for handling application configuration. It combines command line arguments, environment variables, configuration files, and default values to help developers efficiently manage application configuration.

Features

  • Unified Configuration Management: Automatically binds command line flags, environment variables, and configuration files
  • Strong Type Support: Use structs to define configuration with type-safe access
  • Rich Data Types: Support for basic types, slices, maps, nested structs, and more
  • Pointer Type Support: Auto-handles nil pointers to ensure all fields have usable values after configuration loading
  • Tag-Driven: Define configuration key names, usage descriptions, and more through struct tags
  • Complete Validation Support: Integrates with go-playground/validator, supporting all its validation rules and features
  • Enhanced Conditional Validation: Extends standard validator with enhanced nested struct conditional validation
  • Struct Embedding: Flatten nested struct fields using the squash tag
  • Customizable Options: Flexible options to customize configuration loading behavior
  • Universal Configuration Reading: Support for loading configuration from various file formats

Installation

go get github.com/qor5/confx

Quick Start

Define Configuration Structs
package main

import (
	"context"
	_ "embed"
	"fmt"
	"log"
	"time"

	"github.com/qor5/confx"
)

// Define configuration structs
type ServerConfig struct {
	Host    string        `confx:"host" usage:"Server host address" validate:"required"`
	Port    int           `confx:"port" usage:"Server port" validate:"gte=1,lte=65535"`
	Timeout time.Duration `confx:"timeout" usage:"Request timeout duration" validate:"gte=0"`
}

type Config struct {
	Server   ServerConfig `confx:"server" validate:"required"`
	LogLevel string       `confx:"logLevel" usage:"Logging level" validate:"oneof=debug info warn error"`
}

// //go:embed default-config.yaml
// var defaultConfigYAML string

func main() {
	// Define default configuration if user doesn't provide one
	defaultConfig := Config{
		Server:   ServerConfig{Host: "localhost", Port: 8080, Timeout: 30 * time.Second},
		LogLevel: "info",
	}

	// NOTE:
	// We typically embed default config in the binary for three benefits:
	// 1. CLI can run independently without external config files
	// 2. The file can be delivered to users to understand available config options
	// 3. Users can copy and modify the file, simplifying custom configuration
	//
	// Example of loading embedded config:
	// defaultConfig, err := confx.Read[Config]("yaml", strings.NewReader(defaultConfigYAML))
	// if err != nil {
	// 	log.Fatalf("Failed to read default config: %v", err)
	// }

	// Initialize config loader
	loader, err := confx.Initialize(defaultConfig)
	if err != nil {
		log.Fatalf("Failed to initialize config loader: %v", err)
	}

	// Load configuration
	// The second parameter is the path to the config file, which is optional.
	// If not provided, it will use the command line argument --config.
	// If --config is not provided, means no external config file.
	config, err := loader(context.Background(), "")
	if err != nil {
		log.Fatalf("Failed to load config: %v", err)
	}

	// Use the configuration
	fmt.Printf("Server config: %s:%d\n", config.Server.Host, config.Server.Port)
	fmt.Printf("Log level: %s\n", config.LogLevel)
}

Rather than hand-writing the default struct literal, the recommended approach is to keep your defaults in a YAML file, embed it into the binary, and load it with confx.Read. A committed default config file then doubles as living documentation of your command's configuration:

  • It lists every configuration option the command supports.
  • It shows the default value of each option.
  • Comments in the file describe what each option does.
  • Users can copy it verbatim, then trim or override only what they need.
//go:embed default-config.yaml
var defaultConfigYAML string

def, err := confx.Read[*Config]("yaml", strings.NewReader(defaultConfigYAML))
if err != nil {
    log.Fatalf("Failed to read default config: %v", err)
}
loader, err := confx.Initialize(def)

Create the embedded file like this (see examples/config for a complete, working example):

# default-config.yaml

server:
  host: localhost
  port: 8080
  timeout: 30s

logLevel: info
Command Line Flags

ConfX automatically generates command line flags for each field in your configuration struct:

# View available flags
go run *.go -h

# Run with custom configuration via flags
go run *.go --server-host=127.0.0.1 --server-port=9090 --log-level=debug
Environment Variables

ConfX also binds environment variables to configuration fields:

SERVER_HOST=127.0.0.1 SERVER_PORT=9090 LOG_LEVEL=debug go run *.go
Configuration Files

You can also specify a configuration file:

# Using the default --config flag
go run *.go --config=sample.yaml

ConfX supports loading configuration from various file formats including YAML, JSON, and TOML.

Features

Custom Command Line Flags

Using the WithFlagSet option allows you to provide a custom FlagSet, which is particularly useful in the following scenarios:

  1. When you need to integrate with an existing command-line argument system
  2. When you want to customize the sorting or grouping of flags
  3. When you need to add additional command-line parameters not related to configuration
  4. When using subcommands in complex applications (such as when used with Cobra)

For example, you can create a custom FlagSet and add extra flags:

flagSet := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
flagSet.SortFlags = false

// Add custom flags
flagSet.StringVar(&configPath, "custom-config-flag", "", "Path to configuration file")

// Initialize config loader with custom FlagSet
loader, err := confx.Initialize(defaultConfig, confx.WithFlagSet(flagSet))

// IMPORTANT: When using custom flagSet with user-defined flags (like --config),
// you must parse flags before calling the loader to populate those variables.
// This is NOT needed when using Cobra, as Cobra manages flag parsing itself.
if err := flagSet.Parse(os.Args[1:]); err != nil {
    log.Fatalf("Failed to parse flags: %v", err)
}

// Load configuration
config, err := loader(context.Background(), configPath)

This allows you to have complete control over how command-line arguments are handled while still leveraging confx's automatic binding functionality.

Note: When integrating with Cobra, you don't need to manually call Parse() because Cobra handles flag parsing automatically. See examples/cobra for details.

Custom Environment Variable Prefix

You can customize the environment variable prefix using the WithEnvPrefix option:

loader, err := confx.Initialize(defaultConfig, confx.WithEnvPrefix("APP_"))

Then use environment variables with the prefix:

APP_SERVER_HOST=127.0.0.1 APP_SERVER_PORT=9090 APP_LOG_LEVEL=debug go run *.go
Validation Features

ConfX fully integrates go-playground/validator, supporting all its built-in validation rules and features. Additionally, ConfX provides enhanced functionality.

Standard Validator Features

Here are examples of common validation features provided by go-playground/validator:

type Config struct {
    // Basic validation rules
    Port      int       `validate:"required,gte=1,lte=65535"`
    Email     string    `validate:"required,email"`
    URL       string    `validate:"url"`
    CreatedAt time.Time `validate:"required"`

    // Conditional validation
    OutputPath string `validate:"required_if=OutputType file"` // Required only when OutputType is "file"

    // Slice validation
    Tags []string `validate:"required,min=1,dive,required"`
}
ConfX Enhanced Conditional Validation

ConfX extends the standard validator with the skip_nested_unless validation rule for conditionally validating entire nested structures:

type AuthConfig struct {
    Provider string    `confx:"provider" validate:"required,oneof=jwt oauth basic"`
    // Only validate JWT config when Provider is "jwt" - This is a confx enhancement
    JWT      JWTConfig `confx:"jwt" validate:"skip_nested_unless=Provider jwt"`
    // Only validate OAuth config when Provider is "oauth" - This is a confx enhancement
    OAuth    OAuthConfig `confx:"oauth" validate:"skip_nested_unless=Provider oauth"`
}
Struct Embedding

You can use the squash tag to flatten nested struct fields into the parent struct:

type CommonDBConfig struct {
    Name     string `confx:"name" validate:"required"`
    Username string `confx:"username"`
    Password string `confx:"password"`
}

type DatabaseConfig struct {
    Type string `confx:"type" validate:"required,oneof=postgres sqlite"`
    // Flatten CommonDBConfig fields into this struct
    CommonDBConfig `confx:",squash"`
    // Database-specific fields
    Host string `confx:"host"`
    Port int    `confx:"port" validate:"omitempty,gte=1,lte=65535"`
}
Ignoring Fields

Use the confx:"-" tag to have confx completely ignore certain fields in your struct. These fields won't be mapped, won't generate flags, and won't be overridden by environment variables:

type Config struct {
    // Normal field that confx will process
    Database DatabaseConfig `confx:"database"`

    // Ignored field - won't be processed by confx
    InternalState string `confx:"-"`

    // Private fields are automatically ignored (no explicit tag needed)
    internalCache map[string]any

    // Even exported fields can be ignored with the "-" tag
    HelperFunction func() `confx:"-"`
}
Custom Options

ConfX provides various options to customize configuration loading behavior:

loader, err := confx.Initialize(defaultConfig,
    confx.WithEnvPrefix("APP_"),           // Set environment variable prefix
    confx.WithFlagSet(customFlagSet),      // Use custom FlagSet
    confx.WithViper(customViper),          // Use custom Viper instance
    confx.WithValidator(customValidator),  // Use custom validator
    confx.WithTagName("custom"),           // Use custom struct tag name
    confx.WithUsageTagName("description"), // Use custom usage tag name
    confx.WithFieldHook(customFieldHook),  // Custom field processing
)

Utility Functions

Direct Configuration Loading

In addition to the Initialize method, ConfX provides simple functions to load configuration directly from files or readers:

// Load from config file
config, err := confx.Read[Config]("yaml", configFile)

// Load using custom tag name
config, err := confx.ReadWithTagName[Config]("custom", "yaml", configFile)

Integration with Viper and Cobra

ConfX seamlessly integrates with the popular Viper and Cobra libraries:

  • Viper: ConfX uses Viper as the underlying configuration management engine and allows you to use a custom Viper instance via the WithViper option
  • Cobra: Check the examples/cobra directory to learn how to integrate ConfX with the Cobra command line framework

Examples

Check the examples directory for more examples:

  • examples/basic: Basic usage example
  • examples/cobra: Integration with Cobra
  • examples/config: Shared configuration package used by examples

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests to this repository.

License

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

Documentation

Overview

Package confx unifies command-line flags, environment variables, configuration files (YAML/JSON/TOML), and struct-defined defaults into a single typed loader.

The recommended way to supply defaults is to keep them in a YAML file that you embed into the binary and load via Read, rather than hand-writing a default struct literal:

//go:embed default-config.yaml
var defaultConfigYAML string

def, err := confx.Read[*Config]("yaml", strings.NewReader(defaultConfigYAML))
if err != nil {
	return err
}
loader, err := confx.Initialize(def)

A committed, embedded default config file doubles as living documentation of the command's configuration:

  • It lists every configuration option the command supports.
  • It shows the default value of each option.
  • Comments in the file describe what each option does.
  • Users can copy it verbatim, then trim or override only what they need.

See examples/config for a complete, working example of this pattern.

Index

Constants

This section is empty.

Variables

View Source
var DecoderConfigOption = func(tagName string) func(dc *mapstructure.DecoderConfig) {
	return func(dc *mapstructure.DecoderConfig) {
		dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
			mapstructure.StringToTimeDurationHookFunc(),
			mapstructure.StringToTimeHookFunc(time.RFC3339),
			StringToSliceHookFunc(","),
			StringToMapHookFunc(",", "="),
		)
		if tagName != "" {
			dc.TagName = tagName
		} else {
			dc.TagName = DefaultTagName
		}
	}
}
View Source
var DefaultTagName = "confx"

DefaultTagName is the default tag name for struct fields

View Source
var DefaultUsageTagName = "usage"

DefaultUsageTagName is the default tag name for field usage descriptions

Functions

func Read

func Read[T any](typ string, r io.Reader) (T, error)

func ReadWithTagName

func ReadWithTagName[T any](tagName string, typ string, r io.Reader) (T, error)

func StringToMapHookFunc

func StringToMapHookFunc(separator string, pairSeparator string) mapstructure.DecodeHookFunc

func StringToSliceHookFunc

func StringToSliceHookFunc(separator string) mapstructure.DecodeHookFunc

Types

type ExpectedValidation

type ExpectedValidation struct {
	Config         any // The config instance to validate
	Name           string
	ExpectedErrors []ExpectedValidationError // Expected validation errors. if empty, no validation errors are expected.
}

ExpectedValidation represents the expected validation result for a config.

type ExpectedValidationError

type ExpectedValidationError struct {
	Path string // Path to the field that should fail validation, using dot notation for nested fields
	Tag  string // Expected validation tag that should fail
}

ExpectedValidationError represents an expected validation error for testing purposes.

type Field

type Field struct {
	ViperKey string
	FlagKey  string
	EnvKey   string
	Usage    string
}

type Loader

type Loader[T any] func(ctx context.Context, confPath string) (T, error)

func Initialize

func Initialize[T any](def T, options ...Option) (Loader[T], error)

Initialize sets up configuration binding by automatically registering command-line flags, binding environment variables, loading configuration files, and validating the final configuration.

It leverages reflection to traverse the fields of the provided default configuration struct, supports various data types including basic types, slices, maps, and nested structs defined separately, and integrates with Viper for configuration management and go-playground/validator for validation.

Note: Even if a field in the default configuration is a nil pointer, it will be assigned a zero value after loading. This is because flags typically require a default value, which is usually not nil. This behavior aligns with standard configuration requirements, ensuring that all fields are initialized with usable values rather than nil pointers.

Parameters:

  • def: The default configuration struct.

Returns:

  • Loader[T]: A generic loader function that accepts an optional configuration file path. When invoked, it parses the command-line flags, binds them along with environment variables, loads the configuration file if provided, unmarshal the configuration into the struct, and validates it.
  • error: An error object if initialization fails.

type Option

type Option func(opts *initOptions)

func WithEnvPrefix

func WithEnvPrefix(envPrefix string) Option

WithEnvPrefix sets a custom environment variable prefix for reading configuration from environment variables. If not set, environment variables are read without a prefix.

func WithFieldHook

func WithFieldHook(hook func(f *Field) (*Field, error)) Option

WithFieldHook sets a custom field hook function that maps configuration field names to Viper keys, flag names, environment variable names and usage strings.

func WithFlagSet

func WithFlagSet(flagSet *pflag.FlagSet) Option

WithFlagSet sets a custom pflag.FlagSet instance for parsing command line flags If not set, the default pflag.CommandLine is used.

func WithTagName

func WithTagName(tagName string) Option

WithTagName sets a custom struct tag name for reading configuration from struct fields. If not set, the default tag name is "confx".

func WithUsageTagName

func WithUsageTagName(usageTagName string) Option

WithUsageTagName sets a custom struct tag name for reading field usage descriptions. If not set, the default tag name is "usage".

func WithValidator

func WithValidator(v Validator) Option

WithValidator sets a custom validator instance for validating the configuration struct. If not set, the default ValidatorWithSkipNestedUnless(validator.New(validator.WithRequiredStructEnabled())) is used.

func WithViper

func WithViper(v *viper.Viper) Option

WithViper sets a custom Viper instance for reading configuration from different sources. If not set, the default viper.GetViper() is used.

type ValidationSuite

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

ValidationSuite provides utilities for testing config validation rules.

func NewValidationSuite

func NewValidationSuite(t *testing.T) *ValidationSuite

NewValidationSuite creates a new ValidationSuite for testing config validation rules.

Example:

	type MyConfig struct {
	    Name string `validate:"required"`
	    Age  int    `validate:"gte=0,lte=150"`
	}

	func TestMyConfigValidation(t *testing.T) {
	    suite := confx.NewValidationSuite(t)

	    suite.RunTests([]confx.ValidationExpectation{
	        {
				Name: 			"valid config",
	            Config:         &MyConfig{Name: "John", Age: 30},
	        },
	        {
             Name:           "invalid config",
	            Config:         &MyConfig{Age: -1},
	            ExpectedErrors: []confx.ValidationError{
	                {Path: "Name", Tag: "required"},
	                {Path: "Age", Tag: "gte"},
	            },
	        },
	    })
	}

func (*ValidationSuite) RunTests

func (s *ValidationSuite) RunTests(expectations []ExpectedValidation)

RunTests runs a set of validation tests.

It takes a slice of ExpectedValidation and runs each test case in a separate subtest. For each test case, it validates the given config using the embedded validator and checks that the validation errors match the expected errors. If the expected errors are empty, it checks that the config is valid.

It also checks for duplicate expectations and unexpected errors.

func (*ValidationSuite) WithCustomValidator

func (s *ValidationSuite) WithCustomValidator(v Validator) *ValidationSuite

WithCustomValidator returns a new ValidationSuite that uses the provided validator. This is useful when you need to test custom validation rules.

type Validator

type Validator interface {
	RegisterValidationCtx(tag string, fn validator.FuncCtx, callValidationEvenIfNull ...bool) error
	StructCtx(ctx context.Context, v any) error
}

func ValidatorWithSkipNestedUnless

func ValidatorWithSkipNestedUnless(validator Validator) Validator

ValidatorWithSkipNestedUnless wraps a validator with support for conditional nested struct validation using the "skip_nested_unless" tag. This allows you to skip validation of nested structs based on the values of other fields in the parent struct.

The wrapper performs two main functions:

  1. Registers the "skip_nested_unless" validation tag
  2. Filters out validation errors from skipped nested structs

Parameters:

  • validator: The base validator to wrap with skip_nested_unless support

Returns:

  • Validator: A wrapped validator that supports the skip_nested_unless tag

Panics if registration of the skip_nested_unless validation fails

type ValidatorFunc

type ValidatorFunc func(ctx context.Context, v any) error

Directories

Path Synopsis
examples
basic command
cobra command
pflag command

Jump to

Keyboard shortcuts

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