structconf

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 13, 2026 License: MIT Imports: 16 Imported by: 1

README

structconf

Opinionated struct tag based configuration for go - parse CLI args, environment vars or config files into a unified config struct - based on urfave/cli.

Installation

go get github.com/tilebox/structconf

Features

  • Load configuration from CLI flags, environment variables, .toml config files or specified default values - or from all of them at once
    • Order of precedence: CLI flags, config files, environment variables, default values
  • By only defining a struct containing all the fields you want to configure
  • Structs can be nested within other structs
  • Supported data types: string, int, int8-64, uint, uint8-64, bool, float, time.Duration
  • Customize certain fields by adding tags to the struct fields
    • Using the tags flag, env, default, secret, toml, validate, global, help
  • Includes input validation using go-playground/validator
  • Help message generated out of the box
  • Composable command binding helpers for subcommand CLIs via BindCommand / NewCommand

Usage

package main

import (
    "fmt"
    "github.com/tilebox/structconf"
)

type ProgramConfig struct {
    Name  string
    Greet bool
}

func main() {
    cfg := &ProgramConfig{}
    structconf.MustLoadAndValidate(cfg, "greetings")
    if cfg.Greet {
        fmt.Printf("Hello %s!\n", cfg.Name)
    }
}

Produces the following program:

$ ./greetings --greet --name "World"
Hello World!

Alternatively, also environment variables are read out of the box:

$ GREET=true NAME=World ./greetings
Hello World!

And also a help message is generated out of the box:

$ ./greetings -h

NAME:
   simple_cli - A new cli application

USAGE:
   simple_cli [global options]

VERSION:
   1.0.0

GLOBAL OPTIONS:
   --name value    [$NAME]
   --greet    (default: false) [$GREET]
   --help, -h     show help
   --version, -v  print the version

Features

Specify default values and help messages
type ProgramConfig struct {
  Name  string `default:"World" help:"Whom to greet"`
  Greet bool   `help:"Whether or not to greet"`
}

func main() {
  cfg := &ProgramConfig{}
  structconf.MustLoadAndValidate(cfg,
    "greetings",
    structconf.WithVersion("1.0.0"),
    structconf.WithDescription("Print a greeting"),
    structconf.WithLongDescription("A CLI for printing a greeting to the console"),
  )

  if cfg.Greet {
    fmt.Printf("Hello %s!\n", cfg.Name)
  }
}

$ ./greetings -h

NAME:
   greetings - Print a greeting

USAGE:
   greetings [global options]

VERSION:
   1.0.0

DESCRIPTION:
   A CLI for printing a greeting to the console

GLOBAL OPTIONS:
   --name value   Whom to greet (default: World) [$NAME]
   --greet        Whether or not to greet (default: false) [$GREET]
   --help, -h     show help
   --version, -v  print the version
Nest structures
type DatabaseConfig struct {
    User     string
    Password string
}

type ServerConfig struct {
    Host string
    Port int
}

type AppConfig struct {
    LogLevel string `default:"INFO"`

    Server   ServerConfig
    Database DatabaseConfig
}

func main() {
    cfg := &AppConfig{}
    structconf.MustLoadAndValidate(cfg, "app")

    fmt.Printf("%v", cfg)
}
$ ./app --database-user=myuser --database-password=mypassword --server-host=localhost --server-port=8080 --log-level=DEBUG

&{DEBUG {localhost 8080} {myuser mypassword}}
Load configuration from TOML files
type DatabaseConfig struct {
    User     string
    Password string
}
type AppConfig struct {
    LogLevel string `default:"INFO"`
    Database DatabaseConfig
}

func main() {
    cfg := &AppConfig{}
    structconf.MustLoadAndValidate(cfg,
        "app",
        // adds a --load-config flag to load config from TOML files
        structconf.WithLoadConfigFlag("load-config"),
    )

    fmt.Printf("%v", cfg)
}

Define a config file

# database.toml
[database]
user = "myuser"
password = "mypassword"

Run the program

$ ./app --load-config database.toml
&{INFO {myuser mypassword}}
Build subcommands

You can bind configs directly to urfave/cli commands and compose them as subcommands.

package main

import (
    "context"
    "fmt"
    "os"
    "strings"

    "github.com/tilebox/structconf"
    "github.com/urfave/cli/v3"
)

type GreetConfig struct {
    Name string `default:"World"`
    Loud bool
}

func main() {
    greetCfg := &GreetConfig{}

    greetCmd, err := structconf.NewCommand(greetCfg, "greet", func(ctx context.Context, cmd *cli.Command) error {
        if greetCfg.Loud {
            fmt.Println(strings.ToUpper(greetCfg.Name))
            return nil
        }

        fmt.Println(greetCfg.Name)
        return nil
    })
    if err != nil {
        panic(err)
    }

    root := &cli.Command{
        Name:     "app",
        Commands: []*cli.Command{greetCmd},
    }

    if err := root.Run(context.Background(), os.Args); err != nil {
        panic(err)
    }
}

BindCommand and NewCommand currently support flags, env vars and default values. WithLoadConfigFlag is currently only supported by LoadAndValidate / MustLoadAndValidate.

Parse custom arg slices

If you need to parse a specific arg slice (for tests or embedding), use LoadAndValidateArgs:

cfg := &AppConfig{}
err := structconf.LoadAndValidateArgs(cfg, "app", []string{"app", "--log-level", "DEBUG"})
if err != nil {
    panic(err)
}
Shell completion

Enable completion in code:

structconf.MustLoadAndValidate(cfg, "app", structconf.WithShellCompletions())

Then install it in your shell:

# bash (add to ~/.bashrc)
source <(app completion bash)

# zsh (add to ~/.zshrc)
source <(app completion zsh)

# fish
app completion fish > ~/.config/fish/completions/app.fish
Override auto-generated names for fields

By default, field names are converted to flags, env vars and toml properties using the following rules:

  • For flags, (nested) field names are converted to kebab-case, e.g. MyFieldName becomes --my-field-name
  • For env vars, field names are converted to uppercase, e.g. MyFieldName becomes MY_FIELD_NAME
  • For toml properties, field names are converted to kebab-case, e.g. MyFieldName becomes my-field-name
  • Common initialisms are respected, e.g. MyServerURL becomes --my-server-url or MY_SERVER_URL

You can override these default rules at any point by using the flag, env and toml tags.

type AppConfig struct {
    // configure using either:
    // --level flag
    // $LOGGING_LEVEL env var
    // log-level toml property
    LogLevel string `flag:"level" env:"LOGGING_LEVEL" toml:"log-level" default:"INFO"`
    
    // will not be configurable at all
    Ignored string `flag:"-" env:"-" toml:"-"`
}
Configure certain fields as global regardless of how deeply nested they are
type AppConfig struct {
    Deeply DeeplyConfig
}

type DeeplyConfig struct {
    Nested NestedConfig
}

type NestedConfig struct {
    Name string `global:"true"` // will be --name (and $NAME) instead of --deeply-nested-name and $DEEPLY_NESTED_NAME
}

func main() {
    cfg := &AppConfig{}
    structconf.MustLoadAndValidate(cfg, "app")
    fmt.Println(cfg.Deeply.Nested.Name)
}
./app --name Tilebox
Tilebox

Auto-marshalling and log/slog integration

For inspection or logging purposes sometimes it is useful to marshal a whole config struct. However, when doing so, often sensitive fields need to be redacted.

structconf provides marshaling helpers for this use case.

import (
  "fmt"
  "log/slog"
  
  "github.com/tilebox/structconf"
)

type DatabaseConfig struct {
    User     string
    Password string `secret:"true"`
}

type AppConfig struct {
    LogLevel string `default:"DEBUG"`
    Database DatabaseConfig
}


func main() {
    cfg := &AppConfig{}
    structconf.MustLoadAndValidate(cfg, "app")

    asMap, err := structconf.MarshalAsMap(cfg)
    if err != nil {
        panic(err)
    }
    fmt.Println(asMap)

    // includes an integration with log/slog to convert the config struct to a recursive slog.Group structure
    config, err := structconf.MarshalAsSlogDict(cfg, "config")
    if err != nil {
        panic(err)
    }
    slog.Info("Program config loaded successfully", config)
}
$ ./app --database-user=my-user --database-password=very-secret-password --log-level=INFO
map[database:map[password:ve***rd user:my-user] log-level:DEBUG]
INFO Program config loaded successfully config.log-level=DEBUG config.database.user=my-user config.database.password=ve***rd
Validation

structconf includes validation using go-playground/validator

type AppConfig struct {
    // must be set (not empty) 
    Host string `validate:"required" help:"Hostname (required)"`
    
    // must be an integer between 1 and 65535 
    Port int `validate:"gte=1,lte=65535" default:"8080" help:"Server port"`
    
    // If set, it must be a valid path to a directory 
    Path string `validate:"omitempty,dir" help:"A valid path"`
    
    // must be one of (case insensitive): DEBUG, INFO, WARN, ERROR 
    LogLevel string `default:"INFO" validate:"oneofci=DEBUG INFO WARN ERROR" help:"Log level"`
}

func main() {
    cfg := &AppConfig{}
    structconf.MustLoadAndValidate(cfg, "app")
}
$ ./app --port=0 --path=/tmp/
Missing required configuration: AppConfig.Host
Configuration error: Port - gte

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindCommand added in v0.2.0

func BindCommand(command *cli.Command, configPointer any, opts ...Option) error

BindCommand binds the given config struct to an existing urfave/cli command.

It appends reflected flags to the command and wraps the command's Action so that config loading and validation are run before the existing Action.

The WithLoadConfigFlag option is not currently supported for BindCommand/NewCommand.

func LoadAndValidate

func LoadAndValidate(configPointer any, programName string, opts ...Option) error

LoadAndValidate loads the given config struct and validates it.

It loads the config from the following sources in the given order: 1. command line flags 2. config files (if the config struct satisfies the loadConfigFromTOMLFiles interface by embedding LoadTOMLConfig) 3. environment variables 4. default values defined in the field tags

It then validates the loaded config, using the validate tag in config fields - if it fails, it returns an error. The returned error is suitable to be printed to the user.

func LoadAndValidateArgs added in v0.2.0

func LoadAndValidateArgs(configPointer any, programName string, args []string, opts ...Option) error

LoadAndValidateArgs is like LoadAndValidate, but allows explicitly providing the CLI args.

func MarshalAsMap

func MarshalAsMap(configPointer any) (map[string]any, error)

func MarshalAsSlogDict

func MarshalAsSlogDict(configPointer any, groupName string) (slog.Attr, error)

func MustLoadAndValidate

func MustLoadAndValidate(configPointer any, programName string, opts ...Option)

MustLoadAndValidate is like LoadAndValidate, but if it fails, it prints the error to stderr and exits with a non-zero exit code.

func MustLoadAndValidateArgs added in v0.2.0

func MustLoadAndValidateArgs(configPointer any, programName string, args []string, opts ...Option)

MustLoadAndValidateArgs is like LoadAndValidateArgs, but if it fails, it prints the error to stderr and exits with a non-zero exit code.

func NewCommand added in v0.2.0

func NewCommand(configPointer any, commandName string, action cli.ActionFunc, opts ...Option) (*cli.Command, error)

NewCommand creates a urfave/cli command and binds the given config struct to it.

When the command is executed, the config is loaded from flags, env vars and default values, then validated before the optional action is executed.

The WithLoadConfigFlag option is not currently supported for BindCommand/NewCommand.

func NewMapSource

func NewMapSource(name string, m map[any]any) cli.MapSource

func NewTomlFileSource

func NewTomlFileSource(name string, file string) (cli.MapSource, error)

func NewValueSourceFromMaps

func NewValueSourceFromMaps(key string, sources ...cli.MapSource) cli.ValueSource

Types

type Option

type Option func(opts *options)

func WithDefaultLoadConfigFlag

func WithDefaultLoadConfigFlag() Option

func WithDescription

func WithDescription(description string) Option

func WithLoadConfigFlag

func WithLoadConfigFlag(flagName string) Option

func WithLongDescription

func WithLongDescription(usage string) Option

func WithShellCompletions

func WithShellCompletions() Option

func WithVersion

func WithVersion(version string) Option

type StructReflector

type StructReflector interface {
	Flags() []cli.Flag
	Apply(*cli.Command)
}

func NewStructConfigurator

func NewStructConfigurator(anyStruct any, tomlSources []cli.MapSource) (StructReflector, error)

Directories

Path Synopsis
examples
01_simple_cli command
03_nested command
04_toml command
05_override command
06_global command
07_marshal command
08_validation command
09_subcommands command

Jump to

Keyboard shortcuts

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