xconf

package module
v1.1.2 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 2 Imported by: 0

README

xconf

CI

Typed, declarative configuration for Go. Fluent schema DSL, automatic env naming, runtime loading from env/JSON/YAML/TOML, and go generate-based codegen of a typed Config struct + a zero-reflection LoadFromEnv entry point.

Why

  • Type-safe at definition time. xconf.Int("Port").Validate(validate.Range(1, 65535)) rejects type mismatches at compile time via generics.
  • Composable. External libraries export their own *Schema; consumers Embed("Redis", redislib.ConfigSchema) to scope it under any name. No duplication of struct shapes.
  • Multi-source. Defaults < JSON/YAML/TOML files < env vars, with the last source winning. Sources are a small interface — bring your own.
  • Two loading paths.
    • Load(sources ...load.Source) — reflection-based, plugs in any source.
    • LoadFromEnv() — generated, zero-reflection on the hot path.
  • Two authoring paths.
    • Fluent DSL (xconf.Define(...)) — primary, most expressive.
    • Struct tags (pkg/structtag) — derive a schema from xconf:"..." tags on an existing struct.

Install

go get github.com/gopherex/xconf@latest
go install github.com/gopherex/xconf/cmd/xconfgen@latest

go get pulls the library. go install puts the xconfgen binary on your $PATH so //go:generate xconfgen ... works. Make sure $(go env GOBIN) (or $(go env GOPATH)/bin if GOBIN is empty) is in your PATH.

Optional sub-packages are pulled transitively when imported:

import (
    "github.com/gopherex/xconf"
    "github.com/gopherex/xconf/pkg/validate"
    "github.com/gopherex/xconf/pkg/load"
    "github.com/gopherex/xconf/pkg/structtag" // only if you use struct tags
)

Quick start

Full copy-paste flow from zero to a working typed config:

mkdir myapp && cd myapp
go mod init example.com/myapp
go get github.com/gopherex/xconf@latest
go install github.com/gopherex/xconf/cmd/xconfgen@latest

Create config/schema.go:

package config

import (
    "github.com/gopherex/xconf"
    "github.com/gopherex/xconf/pkg/validate"
)

//go:generate xconfgen -type AppConfig

var Schema = xconf.Define("AppConfig",
    xconf.Int("Port").Default(8080).Validate(validate.Range(1, 65535)),
    xconf.String("DSN").Env("DB_DSN").Required(),
)

Create main.go:

package main

import (
    "fmt"

    "example.com/myapp/config"
    "github.com/gopherex/xconf/pkg/load"
)

func main() {
    cfg, err := config.LoadFromEnv()
    if err != nil { panic(err) }
    fmt.Printf("%+v\n", cfg)
    _ = load.FromEnv // referenced for the Load() variant
}

Generate and run:

go generate ./...
DB_DSN=postgres://localhost/x go run .
Schema with composition and validation
package app

import (
    "github.com/gopherex/xconf"
    "github.com/gopherex/xconf/example/redislib"
    "github.com/gopherex/xconf/pkg/validate"
)

//go:generate xconfgen -type AppConfig

var Schema = xconf.Define("AppConfig",
    xconf.Int("Port").
        Default(8080).
        Validate(validate.Range(1, 65535)),

    xconf.String("DSN").
        Env("DB_DSN").
        Required().
        Validate(validate.NonEmpty()),

    xconf.Slice[string]("AllowedHosts").
        EnvSplit(",").
        Validate(validate.Each(validate.NonEmpty())),

    xconf.Map[string, int]("RateLimits").
        EnvSplit(",").KVSplit("="),

    xconf.Embed("Redis", redislib.ConfigSchema),
)

Run go generate ./... to produce appconfig_gen.go containing type AppConfig struct { ... }, Load(sources ...) (*AppConfig, error), and LoadFromEnv() (*AppConfig, error).

Use it:

import (
    "github.com/gopherex/gopherex/xconf/example/app"
    "github.com/gopherex/xconf/pkg/load"
)

func main() {
    cfg, err := app.Load(
        must(load.FromYAMLFileOptional("config.yaml")),
        load.FromEnv(nil), // env wins
    )
    if err != nil { panic(err) }
    _ = cfg.Port
}

Or, for the no-reflection hot path:

cfg, err := app.LoadFromEnv() // env-only, typed parsing

Authoring schemas

Fluent DSL
Constructor Field type
Int, Int8/16/32/64 signed integers
Uint, Uint8/16/32/64 unsigned integers
Float32, Float64 floats
String, Bytes string, []byte
Bool bool
Duration, Time time.Duration, time.Time
Slice[T] []T
Map[K, V] map[K]V
Group(name, ...) inline nested struct
GroupAs[T](name, ...) nested group bound to existing Go type T
WithLoader(s, fn) attach external loader to a group
Embed(name, sub) re-root an external schema under a new name

Chain methods on *Field[T]:

.Default(v) .Env(name) .Required() .Description(s) .Validate(v)

*SliceField[T] adds .EnvSplit(sep). *MapField[K,V] adds .EnvSplit, .KVSplit. *Schema adds .EnvPrefix(p).

Automatic env names

Auto-derived as <GROUP_PREFIX>_<FIELD_NAME> in SCREAMING_SNAKE_CASE.

  • Root Define contributes no prefix by default.
  • Nested Group("DB") adds DB_ to descendants.
  • Embed("Redis", sub) rescopes sub under REDIS_.
  • Explicit .Env("X") wins.

HTTPServerHTTP_SERVER. AllowedHostsALLOWED_HOSTS.

External library composition

Libraries export a schema and a Go type. Consumers don't redeclare either:

// redislib/redislib.go
type Config struct {
    Addr    string
    Timeout time.Duration
}
var ConfigSchema = xconf.GroupAs[Config]("Config",
    xconf.String("Addr").Default("localhost:6379"),
    xconf.Duration("Timeout").Default(5*time.Second),
)
// app/schema.go
var Schema = xconf.Define("AppConfig",
    xconf.Embed("Redis", redislib.ConfigSchema),
)
// generated AppConfig has: Redis redislib.Config

Add WithLoader to delegate loading of that subtree to the library's own loader:

var ConfigSchema = xconf.WithLoader(
    xconf.GroupAs[Config]("Config", ...),
    LoadConfig, // func() (*Config, error)
)

The loader's fully-qualified name is captured via runtime.FuncForPC; both the codegen path (Load, LoadFromEnv) and the reflective load.Load delegate to it.

Struct tags (alternative)

For code-first projects, derive a schema from struct tags:

type AppCfg struct {
    Port    int           `xconf:"default=8080"`
    DSN     string        `xconf:"env=DB_DSN,required"`
    Tags    []string      `xconf:"split=|"`
    Limits  map[string]int `xconf:"split=;,kv=:"`
    Timeout time.Duration `xconf:"default=2s"`
    DB      DBCfg          // nested struct → Group
    Skipped string        `xconf:"skip"`
}

schema, _ := structtag.SchemaFromStruct[AppCfg]("App")

Supported keys: env, default, required, desc, split, kv, skip. Validators are not expressible in tags (they're typed closures) — compose with the fluent API if needed.

Sources

load.FromEnv(nil)                    // os.Getenv
load.FromEnv(map[string]string{...}) // injected env (tests)
load.FromMap(map[string]any{...})    // nested map
load.FromJSONFile("c.json")
load.FromYAMLFile("c.yaml")
load.FromTOMLFile("c.toml")
load.FromJSONFileOptional(...)       // no error if missing

Sources are passed in priority order; later wins. Defaults apply if no source provides a value. .Required() fields error when no value is found.

Implement your own:

type Source interface {
    Lookup(d xconf.FieldDesc, path []string) (raw any, ok bool, err error)
}

Struct configuration (pkg/structconf)

structconf.Load[T] loads structs using mapstructure, default, and validate tags. Validation runs after binding, with precedence default < files < .env < environment.

type Config struct {
    URL   string   `mapstructure:"url" validate:"omitempty,url"`
    Port  int      `mapstructure:"port" validate:"omitempty,min=1,max=65535"`
    Links []string `mapstructure:"links" validate:"omitempty,dive,omitempty,url"`
}

cfg, err := structconf.Load[Config](structconf.WithYAMLFile("config.yaml"))

omitempty skips the remaining rules for the current value when it is empty. Its presence semantics follow go-playground/validator:

Value Skipped by omitempty?
Empty string, zero number/duration, false Yes
Zero array, zero struct, zero time.Time Yes
Nil slice, map, pointer, interface Yes
Allocated empty slice or map ([], {}) No
Non-nil pointer/interface containing a zero scalar or struct No
Interface/pointer resolving to a nil value Yes
String containing only spaces No

An empty value explicitly supplied by a source overrides a default and is then checked using these rules. omitempty does not suppress binding errors. A zero struct with omitempty skips validation of its fields; a present pointer section still validates its fields. Absent pointer fields/sections retain the loader's optional behavior and are not validated.

Rules run left to right: omitempty,url accepts "", while url,omitempty fails before reaching omitempty. Likewise, place conditional requirements first, for example required_if=Mode tls,omitempty,url.

dive applies subsequent rules to each slice/array element or map value, at any nesting depth. omitempty,dive,url makes the collection optional; dive,omitempty,url makes each element optional. An omitted element does not skip its siblings. omitempty,hostname|ip is valid; omitempty itself must be a separate comma-delimited rule, not an alternative inside |.

Validators (pkg/validate)

Typed via generics. Mismatched T fails at compile time.

  • Numeric: Range, Min, Max, Positive, NonNegative, NonZero
  • Equality: OneOf, Equal
  • String: NonEmpty, MinLen, MaxLen, LenBetween, Regex, HasPrefix, HasSuffix, Contains, URL, Email
  • Slice: MinItems, MaxItems, Unique, Each
  • Map: MapMinSize, MapMaxSize, MapHasKey, MapKeys, MapValues
  • Combinators: All, Any, Not

Codegen

xconfgen is installed once (go install ./cmd/xconfgen) and invoked via go:generate:

//go:generate xconfgen -type AppConfig

Flags:

  • -pkg — schema package path (default .)
  • -var — schema variable name (default Schema)
  • -type — root struct name (required)
  • -out — output file (default <lower(type)>_gen.go)
  • -loadfn — generated load function name (default Load)

What gets emitted:

  • One struct per inline Group (root + nested non-bound)
  • BindType groups reuse the external type (no duplicate struct)
  • BindLoader groups: cfg.X = *LoaderFn() after env/source pass
  • Load(sources ...load.Source) (*T, error) — runtime path
  • LoadFromEnv() (*T, error) — typed env parsing inline, then validators via load.Validate

Layout

xconf/
  xconf.go                       public facade (type aliases, constructors)
  cmd/xconfgen/                  CLI for go:generate
  internal/core/                 implementation
  pkg/
    validate/                    typed validators
    load/                        runtime sources + loader
    codegen/                     Render(*Schema) → Go source
    structtag/                   schema-from-struct-tags
    structconf/                  load and validate plain tagged structs
  example/
    redislib/                    external-library schema example
    app/                         consumer schema + generated file

Documentation

Overview

Package xconf provides a typed, declarative configuration DSL.

Schemas are built with fluent constructors (Int, String, Duration, Slice, Group, GroupAs, ...) and compose into a tree of Nodes. The same schema can be:

  • serialized via Describe() and consumed by the xconfgen code generator to produce a typed *Config struct plus a Load() function with zero runtime reflection.
  • bound at runtime via reflection (planned).

Validators live in github.com/gopherex/xconf/pkg/validate. Implementation lives in internal/core; this file is the public facade.

Index

Constants

View Source
const (
	KindInvalid  = core.KindInvalid
	KindInt      = core.KindInt
	KindInt8     = core.KindInt8
	KindInt16    = core.KindInt16
	KindInt32    = core.KindInt32
	KindInt64    = core.KindInt64
	KindUint     = core.KindUint
	KindUint8    = core.KindUint8
	KindUint16   = core.KindUint16
	KindUint32   = core.KindUint32
	KindUint64   = core.KindUint64
	KindFloat32  = core.KindFloat32
	KindFloat64  = core.KindFloat64
	KindString   = core.KindString
	KindBytes    = core.KindBytes
	KindBool     = core.KindBool
	KindDuration = core.KindDuration
	KindTime     = core.KindTime
	KindSlice    = core.KindSlice
	KindMap      = core.KindMap
	KindGroup    = core.KindGroup
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Field

type Field[T any] = core.Field[T]

func Bool

func Bool(name string) *Field[bool]

func Bytes

func Bytes(name string) *Field[[]byte]

func Duration

func Duration(name string) *Field[time.Duration]

func Float32

func Float32(name string) *Field[float32]

func Float64

func Float64(name string) *Field[float64]

func Int

func Int(name string) *Field[int]

func Int8

func Int8(name string) *Field[int8]

func Int16

func Int16(name string) *Field[int16]

func Int32

func Int32(name string) *Field[int32]

func Int64

func Int64(name string) *Field[int64]

func String

func String(name string) *Field[string]

func Time

func Time(name string) *Field[time.Time]

func Uint

func Uint(name string) *Field[uint]

func Uint8

func Uint8(name string) *Field[uint8]

func Uint16

func Uint16(name string) *Field[uint16]

func Uint32

func Uint32(name string) *Field[uint32]

func Uint64

func Uint64(name string) *Field[uint64]

type FieldDesc

type FieldDesc = core.FieldDesc

type Kind

type Kind = core.Kind

type MapField

type MapField[K comparable, V any] = core.MapField[K, V]

func Map

func Map[K comparable, V any](name string) *MapField[K, V]

type Node

type Node = core.Node

type Schema

type Schema = core.Schema

func Define

func Define(name string, fields ...Node) *Schema

func Embed

func Embed(name string, sub *Schema) *Schema

func Group

func Group(name string, fields ...Node) *Schema

func GroupAs

func GroupAs[T any](name string, fields ...Node) *Schema

func WithLoader

func WithLoader[T any](s *Schema, loader func() (*T, error)) *Schema

type SliceField

type SliceField[T any] = core.SliceField[T]

func Slice

func Slice[T any](name string) *SliceField[T]

type Validator

type Validator[T any] = core.Validator[T]

Directories

Path Synopsis
cmd
xconfgen command
xconfgen renders a typed Go config struct + Load wrapper from an xconf schema variable.
xconfgen renders a typed Go config struct + Load wrapper from an xconf schema variable.
example
app
Package app demonstrates an application schema that composes an external library schema (redislib.ConfigSchema).
Package app demonstrates an application schema that composes an external library schema (redislib.ConfigSchema).
redislib
Package redislib is a hypothetical external library that exposes both a Config struct (consumed by users) and a ConfigSchema (consumed by xconfgen when an application embeds this library's configuration).
Package redislib is a hypothetical external library that exposes both a Config struct (consumed by users) and a ConfigSchema (consumed by xconfgen when an application embeds this library's configuration).
internal
core
Package core holds the implementation of xconf.
Package core holds the implementation of xconf.
pkg
codegen
Package codegen renders typed Go source for an xconf schema.
Package codegen renders typed Go source for an xconf schema.
load
Package load is the runtime, reflection-based loader for xconf schemas.
Package load is the runtime, reflection-based loader for xconf schemas.
structconf
Package structconf loads configuration into plain Go structs annotated with the classic `mapstructure` / `default` / `validate` tag triple, with no dependency on viper.
Package structconf loads configuration into plain Go structs annotated with the classic `mapstructure` / `default` / `validate` tag triple, with no dependency on viper.
structtag
Package structtag provides a struct-first entry point to xconf.
Package structtag provides a struct-first entry point to xconf.
validate
Package validate provides typed validators for xconf fields.
Package validate provides typed validators for xconf fields.

Jump to

Keyboard shortcuts

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