configo

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: MIT Imports: 6 Imported by: 0

README

configo — Configuration with Go

CI Go Reference Go Report Card

configo supplies you with a tag driven configuration framework.

Abstract

configo fills a config struct from any number of sources — environment variables, CLI arguments, in-memory maps, or anything you implement yourself. Every field declares, per source, the key it should be read from:

type Config struct {
    Host string `configo:"env=APP_HOST,cli=--host,default=localhost"`
}

There is no central key schema and no reflection-driven naming convention: the struct tag is the mapping. Sources are consulted in the order you register them and the last one that supplies a value wins, which gives you layered configuration (defaults → file → environment → flags) by simply ordering a slice. Fields that no source can supply are collected and reported together, so a misconfigured deployment fails once with a complete list instead of one key at a time.

Status

Early stage. The pieces below work and are covered by tests; the roadmap items are not implemented yet.

  • Value sources with priorities
  • Struct tag driven key mapping, per source
  • Nested structs (parent key paths handed to the source)
  • Scalars, pointers, slices and string-keyed maps
  • Merging of sources (currently last-writer-wins, not a deep merge)
  • Validators

Quickstart

go get github.com/Marfien/configo
package main

import (
	"fmt"
	"log"

	"github.com/Marfien/configo"
)

type Config struct {
	Host    string `configo:"env=APP_HOST,default=localhost"`
	Port    int    `configo:"env=APP_PORT,default=8080"`
	Verbose bool   `configo:"env=APP_VERBOSE,default=false"`
}

func main() {
	var cfg Config
	if err := configo.Parse(&cfg); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", cfg)
}
$ go run .
{Host:localhost Port:8080 Verbose:false}

$ APP_HOST=example.com APP_PORT=9090 go run .
{Host:example.com Port:9090 Verbose:false}

The package level configo.Parse uses Default followed by Environment, so environment variables override defaults. For anything else, build your own parser (see below).

Usage

The configo tag

A tag is a comma separated list of sourceTag=key pairs:

type Config struct {
    Host string `configo:"env=APP_HOST,cli=--host,default=localhost"`
}
  • sourceTag selects the source — it must match that source's Tag().
  • key is the lookup key for that source, so each source can use its own naming convention (APP_HOST, --host, app.host).
  • A source that the field does not mention is skipped for that field.
  • Fields without a configo tag and unexported fields are ignored.
  • A pair without = is a malformed tag and Parse returns an error.
Priorities

Parse walks the sources in registration order and assigns every value it finds, so the last source that resolves a field wins. Order the slice from lowest to highest priority:

parser := configo.NewParser(
    configo.WithSources(
        configo.Default,                                  // lowest
        configo.NewMapSource("file", valuesFromYAML),
        configo.Environment,
        configo.NewCliSource("cli", os.Args[1:]),         // highest
    ),
)

var cfg Config
if err := parser.Parse(&cfg); err != nil {
    log.Fatal(err)
}

If no source supplies a value for a field, Parse continues with the remaining fields and finally returns a missing values: Host, Port error naming every unsatisfied field. A source that finds a value but fails to convert it (for example APP_PORT=abc for an int) aborts immediately with that error.

Built-in sources
Source Tag Description
configo.Environment env Reads os.LookupEnv(key).
configo.Default default Echoes the key back as the value, so default=8080 is the literal 8080.
configo.NewCliSource(tag, args) yours Parses command line arguments.
configo.NewMapSource(tag, map[string]any) yours Serves pre-typed values from a map.

NewCliSource accepts --key=value, --key value and bare --flag (which yields "true"). A repeated key is joined with commas, which makes --tag a --tag b usable as a slice. Note that the key in your tag includes the dashes: configo:"cli=--host". Anything starting with - is treated as the next key, so negative numbers must be written as --retries=-1.

MapSource returns its values as-is and does not convert between types: an int field needs an int in the map, a float64 field a float64.

Supported field types

bool, all sized and unsized int/uint variants, float32, float64, string, pointers to those, slices of those, and maps with string keys and those element types. Pointer fields are allocated on demand. Any other type (channels, functions, nested pointers to structs, non-string map keys) is rejected with an error.

String-based sources (environment, CLI, and anything built on them) split collections on commas; maps additionally split each entry on the first =:

type Config struct {
    Hosts   []string          `configo:"env=APP_HOSTS"`   // APP_HOSTS=a,b,c
    Weights map[string]int    `configo:"env=APP_WEIGHTS"` // APP_WEIGHTS=a=1,b=2
}
Nested structs

A tagged struct field contributes its key to a parent path that is handed to the source alongside the leaf key, letting hierarchical sources address nested values:

type Database struct {
    Host string `configo:"yaml=host"`
}

type Config struct {
    Database Database `configo:"yaml=database"`
}

Here the source is asked for key host with parents ["database"]. It is up to the source to decide what that means — joining it into database.host, walking a tree, and so on. If a nested struct is missing a source tag for a deeper nested struct, a blank string is used instead. Environment, Default and CliSource ignore the parent path and look up the leaf key directly.

Custom sources

Implement the Source interface (Tag() plus a getter per supported type) to plug in your own backend. If your source is fundamentally string based — files, remote key-value stores, templates — the simplest route is to let configo do the parsing for you and only supply the lookup. That is exactly how Environment and CliSource are built. For this you may utilize configo.StringSource with a provided lookup function. For an example take a look at the defintion of the Environment source.

Custom tag name
parser := configo.NewParser(
    configo.WithTag("cfg"),
    configo.WithSources(configo.Environment),
)

How to contribute

Contributions are welcome — the roadmap above is a good place to look for something to pick up. CONTRIBUTING.md covers the setup, the expectations for a change, the shape of the test suite and the file layout.

Notable changes are recorded in CHANGELOG.md.

License

MIT © Marvin Haase

Documentation

Overview

Package configo fills a configuration struct from any number of value sources — environment variables, CLI arguments, in-memory maps, or anything implementing Source.

Every field declares, per source, the key it should be read from. There is no central key schema and no naming convention: the struct tag is the mapping.

type Config struct {
    Host string `configo:"env=APP_HOST,cli=--host,default=localhost"`
}

The configo tag

A tag is a comma separated list of sourceTag=key pairs. The sourceTag selects the source and must match that source's Source.Tag; the key is the lookup key for that source alone, so each source keeps its own naming convention. A source a field does not mention is skipped for that field. Fields without a configo tag and unexported fields are ignored. A pair without "=" is a malformed tag and Parse returns an error.

Priorities

Parser.Parse walks the sources in registration order and assigns every value it finds, so the last source that resolves a field wins. Order the sources from lowest to highest priority:

parser := configo.NewParser(
    configo.WithSources(
        configo.Default,                          // lowest
        configo.NewMapSource("file", fromYAML),
        configo.Environment,
        configo.NewCliSource("cli", os.Args[1:]), // highest
    ),
)

var cfg Config
if err := parser.Parse(&cfg); err != nil {
    log.Fatal(err)
}

Fields that no source can supply are collected and reported together, so a misconfigured deployment fails once with a complete list instead of one key at a time. A source that finds a value but fails to convert it (for example APP_PORT=abc for an int field) aborts immediately with that error.

The package level Parse uses Default followed by Environment, so environment variables override defaults.

Built-in sources

  • Environment (tag "env") reads os.LookupEnv.
  • Default (tag "default") echoes the key back as the value, so default=8080 is the literal 8080.
  • NewCliSource parses command line arguments.
  • NewMapSource serves pre-typed values from a map.

Supported field types

bool, all sized and unsized int/uint variants, float32, float64, string, pointers to those, slices of those, and maps with string keys and those element types. Pointer fields are allocated on demand. Any other type (channels, functions, pointers to structs, non-string map keys) is rejected with an error.

String based sources split collections on commas; maps additionally split each entry on the first "=":

type Config struct {
    Hosts   []string       `configo:"env=APP_HOSTS"`   // APP_HOSTS=a,b,c
    Weights map[string]int `configo:"env=APP_WEIGHTS"` // APP_WEIGHTS=a=1,b=2
}

Nested structs

A tagged struct field contributes its key to a parent path that is handed to the source alongside the leaf key, letting hierarchical sources address nested values:

type Database struct {
    Host string `configo:"yaml=host"`
}

type Config struct {
    Database Database `configo:"yaml=database"`
}

Here the source is asked for key "host" with parents ["database"]. It is up to the source to decide what that means — joining it into "database.host", walking a tree, and so on. Environment, Default and the source returned by NewCliSource ignore the parent path and look up the leaf key directly.

Custom sources

Implement Source to plug in a new backend. A source reports a missing key by returning ErrNotFoundInSource, which is what makes Parser.Parse fall through to the next source. For a fundamentally string based backend — files, remote key-value stores, templates — NewStringSource does all the parsing and conversion and only needs a LookupFunc; that is how Environment and NewCliSource are built.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFoundInSource = errors.New("not found in source")

Functions

func Parse

func Parse(cfg any) error

Types

type LookupFunc

type LookupFunc func(key string, parents []string) (string, bool)

LookupFunc resolves a key to its raw string representation. The second return value reports whether the key was present at all; a found-but-empty value is ("", true). StringSource turns a false into ErrNotFoundInSource so the Parser falls through to the next source.

type MapSource

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

func NewMapSource

func NewMapSource(tag string, delegate map[string]any) MapSource

func (MapSource) GetBool

func (m MapSource) GetBool(key string, parents []string) (bool, error)

func (MapSource) GetBoolMap

func (m MapSource) GetBoolMap(key string, parents []string) (map[string]bool, error)

func (MapSource) GetBoolSlice

func (m MapSource) GetBoolSlice(key string, parents []string) ([]bool, error)

func (MapSource) GetFloat

func (m MapSource) GetFloat(key string, parents []string) (float64, error)

func (MapSource) GetFloatMap

func (m MapSource) GetFloatMap(key string, parents []string) (map[string]float64, error)

func (MapSource) GetFloatSlice

func (m MapSource) GetFloatSlice(key string, parents []string) ([]float64, error)

func (MapSource) GetInt

func (m MapSource) GetInt(key string, parents []string) (int, error)

func (MapSource) GetIntMap

func (m MapSource) GetIntMap(key string, parents []string) (map[string]int, error)

func (MapSource) GetIntSlice

func (m MapSource) GetIntSlice(key string, parents []string) ([]int, error)

func (MapSource) GetString

func (m MapSource) GetString(key string, parents []string) (string, error)

func (MapSource) GetStringMap

func (m MapSource) GetStringMap(key string, parents []string) (map[string]string, error)

func (MapSource) GetStringSlice

func (m MapSource) GetStringSlice(key string, parents []string) ([]string, error)

func (MapSource) GetUint

func (m MapSource) GetUint(key string, parents []string) (uint, error)

func (MapSource) GetUintMap

func (m MapSource) GetUintMap(key string, parents []string) (map[string]uint, error)

func (MapSource) GetUintSlice

func (m MapSource) GetUintSlice(key string, parents []string) ([]uint, error)

func (MapSource) Tag

func (m MapSource) Tag() string

type Option

type Option func(p *Parser)

func WithSources

func WithSources(sources ...Source) Option

func WithTag

func WithTag(tag string) Option

type Parser

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

func NewParser

func NewParser(opts ...Option) Parser

func (Parser) Parse

func (p Parser) Parse(cfg any) error

type Source

type Source interface {
	Tag() string
	GetBool(key string, parents []string) (bool, error)
	GetString(key string, parents []string) (string, error)
	GetInt(key string, parents []string) (int, error)
	GetUint(key string, parents []string) (uint, error)
	GetFloat(key string, parents []string) (float64, error)

	GetBoolSlice(key string, parents []string) ([]bool, error)
	GetStringSlice(key string, parents []string) ([]string, error)
	GetIntSlice(key string, parents []string) ([]int, error)
	GetUintSlice(key string, parents []string) ([]uint, error)
	GetFloatSlice(key string, parents []string) ([]float64, error)

	GetBoolMap(key string, parents []string) (map[string]bool, error)
	GetStringMap(key string, parents []string) (map[string]string, error)
	GetIntMap(key string, parents []string) (map[string]int, error)
	GetUintMap(key string, parents []string) (map[string]uint, error)
	GetFloatMap(key string, parents []string) (map[string]float64, error)
}
var Default Source = NewStringSource("default", func(key string, _ []string) (string, bool) {
	return key, true
})
var Environment Source = NewStringSource("env", func(key string, _ []string) (string, bool) {
	return os.LookupEnv(key)
})

func NewCliSource

func NewCliSource(tag string, args []string) Source

type StringSource

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

StringSource adapts a string based backend to the Source interface. It handles all conversions itself: scalars via the strconv package, slices by splitting on commas, and maps by additionally splitting each entry on the first "=".

func NewStringSource

func NewStringSource(tag string, lookup LookupFunc) StringSource

NewStringSource builds a Source that answers every lookup through lookup.

func (StringSource) GetBool

func (s StringSource) GetBool(key string, parents []string) (bool, error)

func (StringSource) GetBoolMap

func (s StringSource) GetBoolMap(key string, parents []string) (map[string]bool, error)

func (StringSource) GetBoolSlice

func (s StringSource) GetBoolSlice(key string, parents []string) ([]bool, error)

func (StringSource) GetFloat

func (s StringSource) GetFloat(key string, parents []string) (float64, error)

func (StringSource) GetFloatMap

func (s StringSource) GetFloatMap(key string, parents []string) (map[string]float64, error)

func (StringSource) GetFloatSlice

func (s StringSource) GetFloatSlice(key string, parents []string) ([]float64, error)

func (StringSource) GetInt

func (s StringSource) GetInt(key string, parents []string) (int, error)

func (StringSource) GetIntMap

func (s StringSource) GetIntMap(key string, parents []string) (map[string]int, error)

func (StringSource) GetIntSlice

func (s StringSource) GetIntSlice(key string, parents []string) ([]int, error)

func (StringSource) GetString

func (s StringSource) GetString(key string, parents []string) (string, error)

func (StringSource) GetStringMap

func (s StringSource) GetStringMap(key string, parents []string) (map[string]string, error)

func (StringSource) GetStringSlice

func (s StringSource) GetStringSlice(key string, parents []string) ([]string, error)

func (StringSource) GetUint

func (s StringSource) GetUint(key string, parents []string) (uint, error)

func (StringSource) GetUintMap

func (s StringSource) GetUintMap(key string, parents []string) (map[string]uint, error)

func (StringSource) GetUintSlice

func (s StringSource) GetUintSlice(key string, parents []string) ([]uint, error)

func (StringSource) Tag

func (s StringSource) Tag() string

Jump to

Keyboard shortcuts

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