s99config

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 26 Imported by: 0

README

S99Config

s99config is a small Go configuration loader built around CUE schemas.

It loads JSON, YAML, and TOML files, validates them against an embedded CUE definition, applies CUE defaults, resolves configured references, and decodes the final value into your generated Go config type.

Why

I usually handled configuration by copying a project-specific config package from one of my previous projects into the next one. After a while I noticed I was really using the same package everywhere, with only small changes to the schema and application types. So I separated that common part into one reusable package.

The application keeps its own CUE schema and generated Go type; s99config does the repetitive work of loading files, applying defaults, validating values, and resolving references. It is quite handy for the way I build projects. Is it the best or most efficient way? Probably not, but it works for me.

Features

  • CUE validation and defaults for application config.
  • JSON, YAML, and TOML loading through Koanf.
  • Generated Go struct support via cue exp gengotypes.
  • Optional references for environment variables, OS keyring values, config-dir paths, and data-dir paths.
  • Secret handles that redact by default in fmt, JSON, text marshaling, and log/slog.
  • Public JSON export with private values omitted.
  • Default-file writing and validated patching.

Install

Requires Go 1.25 or newer.

go get github.com/smegg99/s99config

Quick Start

Create a CUE schema:

package main

#Config: {
	server: {
		host: string | *"127.0.0.1"
		port: int & >0 & <=65535 | *8080
	}
	api_token: string @go(APIToken,type=Secret)
	log_dir:   string
	region:    string | *"local"
}

Create a config file:

server:
  host: localhost
api_token: "@{env:API_TOKEN}"
log_dir: "@{datadir:logs}"
region: "@{pubenv?:APP_REGION}"

Add local aliases for generated field types:

package main

import "github.com/smegg99/s99config"

type Secret = s99config.Secret

Load and decode:

package main

import (
	_ "embed"
	"log"

	"github.com/smegg99/s99config"
)

//go:generate go run cuelang.org/go/cmd/cue@v0.16.1 exp gengotypes .

//go:embed config.cue
var schema []byte

func main() {
	loader, err := s99config.New(
		schema,
		s99config.WithReferences(s99config.ReferenceOptions{DataDir: "./data"}),
	)
	if err != nil {
		log.Fatal(err)
	}
	if err := loader.Load("config.yaml"); err != nil {
		log.Fatal(err)
	}

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

	token := cfg.APIToken.Reveal()
	_ = token
}

Generate and run:

go generate .
API_TOKEN=secret APP_REGION=eu-west go run .

For multiple .cue files, embed an fs.FS and use NewFS:

//go:embed *.cue
var schemas embed.FS

loader, err := s99config.NewFS(schemas, ".")

References

References are resolved only when WithReferences is enabled.

Reference Reads from Public JSON
@{env:NAME} Environment variable omitted
@{keyring:NAME} OS keyring omitted
@{pubenv:NAME} Environment variable included
@{cfgdir:path} Config file directory included
@{datadir:path} ReferenceOptions.DataDir included

Add ? after the source to allow missing values:

region: "@{pubenv?:APP_REGION}"

Resolved values are treated as literal strings. If APP_REGION contains @{env:OTHER}, it stays that exact text.

Keyring references require ReferenceOptions.KeyringService. They use go-keyring, which maps to Keychain on macOS, Credential Manager on Windows, and Secret Service backends such as GNOME Keyring or KWallet on Linux. Headless Linux, containers, and CI often have no unlocked keyring backend.

loader, err := s99config.New(
	schema,
	s99config.WithReferences(s99config.ReferenceOptions{
		KeyringService: "my-application",
	}),
)

_ = loader.SetKeyringValue("api-token", "secret")
value, err := loader.GetKeyringValue("api-token")
_ = value
_ = loader.DeleteKeyringValue("api-token")

Secrets

Private references, currently env and keyring, are automatically sensitive. Sensitive values decode into s99config.Secret handles and print as [redacted].

fmt.Println(cfg.APIToken)      // [redacted]
fmt.Println(cfg.APIToken.IsSet())
token := cfg.APIToken.Reveal() // explicit plaintext access

Use @secret() for literal config values that must also be protected:

#Config: {
	api_token: string @secret() @go(APIToken,type=Secret)
}

Decode refuses to put a protected value into a plain string field. For dynamic use, Map, RawMap, and Decode(&map[string]any{}) return secret handles for protected values.

PublicJSON and ExportPublicJSON omit protected values. Values derived from a secret are not detected automatically; mark the derived field @secret() if it must stay private.

Custom redaction is available with WithSecretFactory and NewPresentedSecret. See examples/custom-redaction.

Writing Config

defaults, err := loader.DefaultsJSON()
err = loader.WriteDefaults("config.yaml")
err = loader.Patch(map[string]any{"server": map[string]any{"port": 9090}})
public, err := loader.PublicJSON()
err = loader.ExportPublicJSON("public-config.json")

WriteDefaults requires defaults for all required fields and creates new files with 0600 permissions. Patch validates before writing, preserves raw reference expressions, and rewrites the file in its original format. Comments and hand formatting are not preserved.

Examples

Run examples from their own directories, for example:

cd examples/basic
go run .

License

MIT

Documentation

Overview

defaults.go

format.go

keyring.go

load.go

loader.go

options.go

patch.go

public.go

reference_sources.go

references.go

schema.go

secret.go

sensitive.go

validate.go

value.go

write.go

Index

Constants

This section is empty.

Variables

View Source
var ErrNotLoaded = errors.New("configuration has not been loaded")

ErrNotLoaded is returned when an operation requires loaded configuration.

Functions

This section is empty.

Types

type Loader

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

Loader validates configuration with one embedded CUE schema.

func New

func New(schema []byte, opts ...Option) (*Loader, error)

New constructs a Loader from a CUE schema embedded by the application.

//go:embed config.cue
var schema []byte

loader, err := s99config.New(schema)

func NewFS

func NewFS(schemaFS fs.FS, dir string, opts ...Option) (*Loader, error)

NewFS constructs a Loader from all .cue files in dir of an embedded filesystem. The files are compiled together as one CUE package.

//go:embed schema/*.cue
var schemas embed.FS

loader, err := s99config.NewFS(schemas, "schema")

func (*Loader) ConfigPath

func (l *Loader) ConfigPath() string

ConfigPath returns the absolute path last passed to Load.

func (*Loader) Decode

func (l *Loader) Decode(dst any) error

Decode unmarshals the resolved, default-filled configuration into dst. Loaded sensitive fields must accept the configured Secret implementation.

func (*Loader) DefaultsJSON

func (l *Loader) DefaultsJSON() ([]byte, error)

DefaultsJSON returns an indented JSON configuration populated entirely from schema defaults. It returns an error if required fields do not have defaults. Sensitive defaults are materialized verbatim; treat the result as private.

func (*Loader) DeleteKeyringValue

func (l *Loader) DeleteKeyringValue(key string) error

DeleteKeyringValue removes a value from the configured keyring service.

func (*Loader) ExportPublicJSON

func (l *Loader) ExportPublicJSON(path string) error

ExportPublicJSON writes indented configuration without sensitive values.

func (*Loader) Format

func (l *Loader) Format(state fmt.State, _ rune)

Format redacts Loader values for every fmt formatting verb.

func (*Loader) GetKeyringValue

func (l *Loader) GetKeyringValue(key string) (string, error)

GetKeyringValue retrieves a value from the configured keyring service.

func (*Loader) GoString

func (l *Loader) GoString() string

GoString prevents Go-syntax formatting from exposing loaded secret state.

func (*Loader) Load

func (l *Loader) Load(path string) error

Load reads configuration based on its .json, .yaml, .yml, or .toml extension. CUE defaults are included in the loaded value.

func (*Loader) LoadJSON

func (l *Loader) LoadJSON(name string, data []byte) error

LoadJSON validates JSON from memory. name is reported in errors and provides a location for relative references.

func (*Loader) LoadWithParser

func (l *Loader) LoadWithParser(path string, parser koanf.Parser) error

LoadWithParser reads configuration using a custom Koanf parser.

func (*Loader) LogValue

func (l *Loader) LogValue() slog.Value

LogValue prevents plaintext exposure when a Loader is passed to log/slog.

func (*Loader) Map

func (l *Loader) Map() (map[string]any, error)

Map returns a copy of the resolved, default-filled configuration, replacing loaded sensitive values with configured Secret implementations.

func (*Loader) Patch

func (l *Loader) Patch(patch any) error

Patch deep-merges into the file last loaded by Load and validates before writing it. Existing raw reference expressions are preserved.

func (*Loader) PublicJSON

func (l *Loader) PublicJSON() ([]byte, error)

PublicJSON returns configuration without sensitive fields.

func (*Loader) RawMap

func (l *Loader) RawMap() (map[string]any, error)

RawMap returns a copy of the original configuration object before defaults and reference resolution, replacing loaded sensitive values with configured Secret implementations.

func (*Loader) SetKeyringValue

func (l *Loader) SetKeyringValue(key, value string) error

SetKeyringValue stores a value in the configured keyring service.

func (*Loader) String

func (l *Loader) String() string

String prevents formatting a loader from exposing its loaded secret store.

func (*Loader) WriteDefaults

func (l *Loader) WriteDefaults(path string) error

WriteDefaults writes a default-filled configuration based on the path extension. The schema must provide defaults for every required value. New configuration files are created with owner-only permissions.

func (*Loader) WriteDefaultsWithParser

func (l *Loader) WriteDefaultsWithParser(path string, parser koanf.Parser) error

WriteDefaultsWithParser writes defaults using parser.

type Option

type Option func(*options) error

Option configures a Loader.

func WithDefinition

func WithDefinition(path string) Option

WithDefinition selects the CUE definition used for validation. The default is "#Config".

func WithReferences

func WithReferences(refs ReferenceOptions) Option

WithReferences enables resolution of @{source:key} strings.

func WithSecretFactory

func WithSecretFactory(factory SecretFactory) Option

WithSecretFactory selects the Secret implementation injected for sensitive loaded values. The factory receives an opaque SecretValue handle. A nil or typed-nil Secret returned by the factory falls back to the default redacted handle.

type PresentedSecret

type PresentedSecret struct {
	SecretValue
	// contains filtered or unexported fields
}

PresentedSecret adapts a SecretValue to a custom output representation. It implements Secret for fmt, JSON, text marshaling, and log/slog consistently. Embed it in an application-defined type when distinct config field types are useful.

func NewPresentedSecret

func NewPresentedSecret(value SecretValue, presenter SecretPresenter) PresentedSecret

NewPresentedSecret wraps value with a custom output representation. A nil presenter retains the default [redacted] representation.

func (PresentedSecret) Format

func (s PresentedSecret) Format(state fmt.State, _ rune)

Format presents the configured representation for every fmt verb.

func (PresentedSecret) GoString

func (s PresentedSecret) GoString() string

GoString presents the configured representation.

func (PresentedSecret) LogValue

func (s PresentedSecret) LogValue() slog.Value

LogValue presents the configured representation through log/slog.

func (PresentedSecret) MarshalJSON

func (s PresentedSecret) MarshalJSON() ([]byte, error)

MarshalJSON marshals the configured representation.

func (PresentedSecret) MarshalText

func (s PresentedSecret) MarshalText() ([]byte, error)

MarshalText returns the configured representation.

func (PresentedSecret) String

func (s PresentedSecret) String() string

String presents the configured representation.

type ReferenceOptions

type ReferenceOptions struct {
	ConfigDir      string
	DataDir        string
	KeyringService string
}

ReferenceOptions controls opt-in @{source:key} resolution. Values returned by a source are treated as literal strings, not additional references.

type Secret

Secret is the contract for a sensitive configuration value. Implementations choose how the value is represented in output, while Reveal is the explicit operation that provides its plaintext value.

New custom implementations should normally embed PresentedSecret, which implements every formatting and logging method in this contract.

type SecretFactory

type SecretFactory func(SecretValue) Secret

SecretFactory turns an opaque sensitive value into the Secret implementation injected by Decode, Map, and RawMap.

type SecretPresenter

type SecretPresenter func(SecretValue) string

SecretPresenter returns the output representation of a sensitive value. Calling Reveal from a presenter intentionally exposes some or all of that value in formatted or marshaled output.

type SecretValue

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

SecretValue is the default Secret implementation and the value handed to a SecretFactory. It is an opaque, path-bound handle rather than plaintext and represents itself as [redacted].

func (SecretValue) Format

func (s SecretValue) Format(state fmt.State, _ rune)

Format redacts SecretValue values for every fmt formatting verb.

func (SecretValue) GoString

func (s SecretValue) GoString() string

GoString prevents plaintext exposure through Go-syntax formatting.

func (SecretValue) IsSet

func (s SecretValue) IsSet() bool

IsSet reports whether this handle references a loaded sensitive value.

func (SecretValue) LogValue

func (s SecretValue) LogValue() slog.Value

LogValue prevents plaintext exposure through log/slog.

func (SecretValue) MarshalJSON

func (s SecretValue) MarshalJSON() ([]byte, error)

MarshalJSON prevents plaintext exposure through JSON logging or exporting.

func (SecretValue) MarshalText

func (s SecretValue) MarshalText() ([]byte, error)

MarshalText prevents plaintext exposure through text-marshaling loggers.

func (SecretValue) Path

func (s SecretValue) Path() string

Path returns the encoded configuration path of this value. It can be used by a SecretFactory to choose presentation behavior for different fields.

func (SecretValue) Reveal

func (s SecretValue) Reveal() string

Reveal returns the plaintext sensitive value referenced by this handle.

func (SecretValue) String

func (s SecretValue) String() string

String prevents plaintext exposure through ordinary string formatting.

Directories

Path Synopsis
examples
basic command
formats command
references command
tests/schema.go
tests/schema.go

Jump to

Keyboard shortcuts

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