config

package
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: May 13, 2026 License: MIT Imports: 18 Imported by: 0

README

Config

Robust configuration management wrapping spf13/viper with type safety and interface-based testability.

Features:

  • Mockable Containable interface
  • Automatic environment variable mapping
  • Built-in observer pattern for file watching
  • Support for YAML, embedded assets, and more

For detailed documentation, usage examples, and testing strategies, see the Config Component Documentation.

Documentation

Overview

Package config provides configuration loading, merging, and access via the Containable interface backed by Viper.

Configurations can be loaded from multiple sources — local files, embedded assets, environment variables, and command-line flags — and merged with deterministic precedence. Factory functions include NewFilesContainer, LoadFilesContainer, NewReaderContainer, and NewContainerFromViper.

Type-safe accessors (GetString, GetInt, GetBool, GetDuration, GetTime, etc.) are exposed through Containable. For advanced use cases, [Containable.GetViper] provides direct access to the underlying Viper instance as an intentional power-user escape hatch.

Hot-reload is supported via the Observable interface, which allows consumers to register callbacks that fire when configuration files change on disk.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNoFilesFound = errors.Newf("no configuration files found please run init, or provide a config file using the --config flag")
)

Functions

func LoadEnv

func LoadEnv(fs afero.Fs, logger logger.Logger)

LoadEnv loads environment variables from a .env file if it exists.

Types

type Containable

type Containable interface {
	Get(key string) any
	GetBool(key string) bool
	GetInt(key string) int
	GetFloat(key string) float64
	GetString(key string) string
	GetTime(key string) time.Time
	GetDuration(key string) time.Duration
	// GetViper returns the underlying Viper instance for advanced operations
	// not exposed by the Containable interface. This is an intentional escape
	// hatch for power users who need Viper's full API (e.g., MergeConfig,
	// BindPFlag, or direct access to config file watching).
	//
	// Prefer the typed accessor methods (GetString, GetInt, etc.) for standard
	// configuration reads. Use GetViper only when the Containable interface
	// does not cover your use case.
	GetViper() *viper.Viper
	Has(key string) bool
	IsSet(key string) bool
	Set(key string, value any)
	WriteConfigAs(dest string) error
	Sub(key string) Containable
	AddObserver(o Observable)
	AddObserverFunc(f func(Containable, chan error))
	ToJSON() string
	Dump(w io.Writer)
	// Validate checks the container's current values against the provided schema.
	// Returns a ValidationResult; callers should check result.Valid().
	Validate(schema *Schema) *ValidationResult
}

Containable is the primary configuration interface. It provides typed accessors, observation for hot-reload, and schema validation.

func Load

func Load(paths []string, fs afero.Fs, allowEmptyConfig bool, opts ...ContainerOption) (Containable, error)

Load reads configuration from the first available file in paths. Returns ErrNoFilesFound if no files exist and allowEmptyConfig is false.

func LoadEmbed

func LoadEmbed(paths []string, assets fs.FS, opts ...ContainerOption) (Containable, error)

LoadEmbed reads configuration from embedded filesystem assets and merges them.

func LoadFilesContainer

func LoadFilesContainer(fs afero.Fs, opts ...ContainerOption) (Containable, error)

LoadFilesContainer loads configuration from files and returns a Containable. Config files are specified via WithConfigFiles. It returns an error if the first file specified does not exist.

func LoadFilesContainerWithSchema

func LoadFilesContainerWithSchema(fs afero.Fs, schema *Schema, opts ...ContainerOption) (Containable, error)

LoadFilesContainerWithSchema loads config files and validates against the schema. Returns an error wrapping all validation errors if the config is invalid. The schema can also be provided via WithSchema; if both are present, the option takes precedence.

type Container

type Container struct {
	ID string
	// contains filtered or unexported fields
}

Container container for configuration.

Sub-containers returned by Container.Sub track two Viper instances:

  • viper (the "structural view") points at the sub-tree and is used by Write/Dump/ToJSON/Validate operations so those continue to scope correctly to the sub-path.
  • root + prefix together identify the original container; every Get/Set/Has/IsSet call is routed through root.viper with the key qualified by the accumulated prefix. This is what keeps Viper's AutomaticEnv + prefix-aware env binding alive across Sub() calls — `cfg.Sub("github").GetString("auth.value")` now honours `<TOOL_PREFIX>_GITHUB_AUTH_VALUE`, which a Viper-native Sub would silently drop.

func NewContainerFromViper

func NewContainerFromViper(l logger.Logger, v *viper.Viper) *Container

NewContainerFromViper creates a new Container from an existing Viper instance. If l is nil, a no-op logger is used.

func NewFilesContainer

func NewFilesContainer(fs afero.Fs, opts ...ContainerOption) *Container

NewFilesContainer initialises a configuration container to read files from the FS. Config files are specified via WithConfigFiles.

func NewReaderContainer

func NewReaderContainer(fs afero.Fs, opts ...ContainerOption) *Container

NewReaderContainer initialises a configuration container to read config from io.Readers. Readers are specified via WithConfigReaders; format via WithConfigFormat.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/spf13/afero"

	"gitlab.com/phpboyscout/go-tool-base/pkg/config"
	"gitlab.com/phpboyscout/go-tool-base/pkg/logger"
)

func main() {
	l := logger.NewNoop()
	yaml := `
log:
  level: debug
server:
  port: 8080
`
	cfg := config.NewReaderContainer(afero.NewMemMapFs(), config.WithLogger(l), config.WithConfigFormat("yaml"), config.WithConfigReaders(strings.NewReader(yaml)))

	fmt.Println("Level:", cfg.GetString("log.level"))
	fmt.Println("Port:", cfg.GetInt("server.port"))
}
Output:
Level: debug
Port: 8080

func (*Container) AddObserver

func (c *Container) AddObserver(o Observable)

AddObserver attach observer to trigger on config update.

func (*Container) AddObserverFunc

func (c *Container) AddObserverFunc(f func(Containable, chan error))

AddObserverFunc attach function to trigger on config update.

func (*Container) Dump

func (c *Container) Dump(w io.Writer)

func (*Container) Get

func (c *Container) Get(key string) any

Get interface value from config.

func (*Container) GetBool

func (c *Container) GetBool(key string) bool

GetBool get Bool value from config.

func (*Container) GetDuration

func (c *Container) GetDuration(key string) time.Duration

GetDuration get duration value from config.

func (*Container) GetFloat

func (c *Container) GetFloat(key string) float64

GetFloat get Float value from config.

func (*Container) GetInt

func (c *Container) GetInt(key string) int

GetInt get Bool value from config.

func (*Container) GetObservers

func (c *Container) GetObservers() []Observable

GetObservers retrieve all currently attached Observers.

func (*Container) GetString

func (c *Container) GetString(key string) string

GetString get string value from config.

func (*Container) GetTime

func (c *Container) GetTime(key string) time.Time

GetTime get time value from config.

func (*Container) GetViper

func (c *Container) GetViper() *viper.Viper

GetViper retrieves the underlying Viper configuration. On sub-containers this returns the sub-tree structural view; use this only for bulk reads that do not need env binding (the full env-aware resolution pipeline is only available via the typed Get methods).

func (*Container) Has

func (c *Container) Has(key string) bool

Has reports whether the given key exists in the underlying configuration. Routed through the root container so env vars picked up by Viper's AutomaticEnv are counted as "set".

func (*Container) IsSet

func (c *Container) IsSet(key string) bool

IsSet checks if the key has been set (file, env, or flag).

func (*Container) Set

func (c *Container) Set(key string, value any)

Set sets the value for the given key.

func (*Container) SetSchema

func (c *Container) SetSchema(schema *Schema)

SetSchema attaches a validation schema to the container. When set, hot-reload will validate config changes before notifying observers.

func (*Container) Sub

func (c *Container) Sub(key string) Containable

Sub returns a view over a subtree of the parent configuration.

Unlike Viper's native Sub — which constructs a fresh Viper instance that drops the parent's AutomaticEnv + prefix settings — this Sub returns a view that delegates every Get/Set/Has/IsSet call back to the root container's Viper with a fully-qualified key path. That means env-aware resolution (including prefix-aware env vars like `<TOOL>_GITHUB_AUTH_VALUE`) continues to apply no matter how many Sub() layers a caller walks through.

The returned view's `viper` field is the Viper-native sub-tree used only by operations that legitimately need a scoped view: WriteConfigAs, Dump, ToJSON, and Validate.

Returns nil when key is not present anywhere in the config hierarchy, matching Viper's Sub semantics.

func (*Container) ToJSON

func (c *Container) ToJSON() string

Dump return config as json string.

func (*Container) Validate

func (c *Container) Validate(schema *Schema) *ValidationResult

Validate checks the current configuration against the provided schema. Returns a ValidationResult; callers should check result.Valid().

Example
package main

import (
	"fmt"
	"strings"

	"github.com/spf13/afero"

	"gitlab.com/phpboyscout/go-tool-base/pkg/config"
	"gitlab.com/phpboyscout/go-tool-base/pkg/logger"
)

func main() {
	type AppConfig struct {
		Log struct {
			Level string `config:"log.level" enum:"debug,info,warn,error"`
		}
	}

	l := logger.NewNoop()
	cfg := config.NewReaderContainer(afero.NewMemMapFs(), config.WithLogger(l), config.WithConfigFormat("yaml"), config.WithConfigReaders(strings.NewReader("log:\n  level: verbose\n")))

	schema, _ := config.NewSchema(config.WithStructSchema(AppConfig{}))

	result := cfg.Validate(schema)
	if !result.Valid() {
		fmt.Println(result.Error())
	}
}

func (*Container) WriteConfigAs

func (c *Container) WriteConfigAs(dest string) error

WriteConfigAs writes the current configuration to the given path.

type ContainerOption

type ContainerOption func(*containerOptions)

ContainerOption configures optional behavior for config containers.

func WithConfigFiles

func WithConfigFiles(files ...string) ContainerOption

WithConfigFiles specifies one or more config file paths to load. The first file is treated as the primary config; subsequent files are merged in order.

func WithConfigFormat

func WithConfigFormat(format string) ContainerOption

WithConfigFormat sets the config format (e.g. "yaml", "json") for reader-based config loading.

func WithConfigReaders

func WithConfigReaders(readers ...io.Reader) ContainerOption

WithConfigReaders provides one or more io.Readers as config sources. The first reader is the primary config; subsequent readers are merged. Requires WithConfigFormat to be set.

func WithEnvPrefix

func WithEnvPrefix(prefix string) ContainerOption

WithEnvPrefix sets the environment variable prefix for automatic env binding. When set to "GTB", the config key "ai.provider" resolves from the environment variable "GTB_AI_PROVIDER". An empty string disables prefixing (the default, preserving backward compatibility).

func WithLogger

func WithLogger(l logger.Logger) ContainerOption

WithLogger sets the logger for the config container. When not provided, a no-op logger is used.

func WithSchema

func WithSchema(schema *Schema) ContainerOption

WithSchema attaches a validation schema to the container.

type FieldSchema

type FieldSchema struct {
	// Type is the expected Go type: "string", "int", "float64", "bool", "duration".
	Type string
	// Required indicates the field must be present and non-zero.
	Required bool
	// Description is used in validation error messages.
	Description string
	// Default is the default value for documentation and error hints only.
	// The validation layer does not inject defaults — use embedded assets for that.
	Default any
	// Enum restricts the field to a set of allowed values.
	Enum []any
	// Children defines nested fields for map/object types.
	Children map[string]FieldSchema
}

FieldSchema describes a single configuration field.

type Observable

type Observable interface {
	Run(Containable, chan error)
}

Observable is the interface for config change observers. Implementations receive the updated config and an error channel when the config file changes.

type Observer

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

Observer is a simple Observable that wraps a handler function.

func (Observer) Run

func (o Observer) Run(c Containable, errs chan error)

Run invokes the observer's handler with the updated config.

type Schema

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

Schema defines the expected structure and constraints for configuration values.

func NewSchema

func NewSchema(opts ...SchemaOption) (*Schema, error)

NewSchema creates a Schema from the provided options.

Example
package main

import (
	"fmt"

	"gitlab.com/phpboyscout/go-tool-base/pkg/config"
)

func main() {
	type AppConfig struct {
		Server struct {
			Port int    `config:"server.port" default:"8080"`
			Host string `config:"server.host"`
		}
		Log struct {
			Level string `config:"log.level" enum:"debug,info,warn,error" default:"info"`
		}
		Github struct {
			Token string `config:"github.token" validate:"required"`
		}
	}

	schema, err := config.NewSchema(config.WithStructSchema(AppConfig{}))
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	_ = schema // Use with container.Validate(schema)
}

func (*Schema) Fields

func (s *Schema) Fields() map[string]FieldSchema

Fields returns the schema field definitions.

type SchemaOption

type SchemaOption func(*schemaConfig)

SchemaOption configures schema construction.

func WithStrictMode

func WithStrictMode() SchemaOption

WithStrictMode treats unknown keys as errors instead of warnings.

func WithStructSchema

func WithStructSchema(v any) SchemaOption

WithStructSchema derives a schema from a tagged Go struct. Supported tags: `config:"key" validate:"required" enum:"a,b,c" default:"value"`.

type ValidationError

type ValidationError struct {
	// Key is the dot-separated config key.
	Key string
	// Message is a human-readable description of the failure.
	Message string
	// Hint is an actionable fix suggestion.
	Hint string
}

ValidationError contains details about a single validation failure.

func (ValidationError) String

func (e ValidationError) String() string

type ValidationResult

type ValidationResult struct {
	Errors   []ValidationError
	Warnings []ValidationError
}

ValidationResult holds the outcome of schema validation.

func (*ValidationResult) Error

func (r *ValidationResult) Error() string

Error returns a formatted multi-line error string, or empty string if valid.

func (*ValidationResult) Valid

func (r *ValidationResult) Valid() bool

Valid returns true if no errors were found. Warnings do not affect validity.

Jump to

Keyboard shortcuts

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