panfigure

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

* Panfigure

An opinionated, declarative configuration library for Go CLI applications, built
on Viper and Cobra. Panfigure declares configuration as option packages, merges
them from defaults, CLI, file, and environment in a predictable precedence, nests
keys by command path or group, tracks the source of every value, and reads the
result into a plain typed struct — no magic strings at read sites.

** Model

- An application owns one =*panfigure.App=, which owns its cobra root command, its
  own =*viper.Viper=, a registry of declared options, and source metadata. No
  package-level state; build one with =panfigure.New=.
- Configuration is declared as =CommandOptions= (the source of truth) and
  registered on the app in packages: =Root= (flat globals), =RootGroup=
  (namespaced, cross-cutting packages like =db= / =mail= that inherit to every
  subcommand), and =On= (command-local, auto-namespaced from the command path).
- After =Run= (or =Configure=), =App.Unmarshal(&cfg)= populates a plain typed
  struct: =db.host= maps to =cfg.DB.Host=, =server.start.addr= to
  =cfg.Server.Start.Addr=, tag-free.
- =App.StatusTable= shows every key, its value, and where it came from.

** Quickstart

#+begin_src go
  func main() {
      app := panfigure.New(cmd.Root())
      cmd.Configure(app)               // register option packages on the instance
      cobra.CheckErr(app.Run())        // configure + parse + finalize + validate
      var cfg config.Config
      cobra.CheckErr(app.Unmarshal(&cfg))
      cobra.CheckErr(cmd.Run(&cfg))    // thread typed config to db/server/mail
  }
#+end_src

Registering packages (typically in a consumer =cmd.Configure=):

#+begin_src go
  func Configure(app *panfigure.App) {
      app.Root(
          &panfigure.CommandOptions{LongOpt: "env-prefix", DefaultValue: "APP"},
          &panfigure.CommandOptions{LongOpt: "log-level", DefaultValue: "info"},
      )
      // a reusable, namespaced package; keys nest under "db" and inherit to all
      // subcommands (db.host, db.port, ...).
      app.RootGroup("db",
          &panfigure.CommandOptions{LongOpt: "db-host"},
          &panfigure.CommandOptions{LongOpt: "db-port", DefaultValue: 5432, OptType: panfigure.OptInt},
      )
      // command-local; keys nest under the command path (server.start.addr).
      app.On(serverStartCmd,
          &panfigure.CommandOptions{LongOpt: "addr", DefaultValue: "127.0.0.1:8080"},
      )
  }
#+end_src

See =examples/= for a runnable program.

** CommandOptions fields

| Field         | Meaning                                                                              |
|---------------|--------------------------------------------------------------------------------------|
| =LongOpt=       | long CLI flag name without =--= (e.g. ="db-host"=). Required unless =OptName= is set.    |
| =ShortOpt=      | optional one-letter flag (e.g. ="a"=).                                                |
| =OptName=       | config key leaf; if empty, derived from =LongOpt= (see Key derivation).              |
| =Description=   | shown in =--help=.                                                                   |
| =OptType=       | =OptString= (default), =OptInt=, =OptBool=, =OptCount=, =OptStringSlice=, =OptDuration=. |
| =NoCLI=         | file/env only; no CLI flag.                                                          |
| =Persistent=    | (command-local only) expose the flag on subcommands too. Root options are always persistent. |
| =Required=      | =Run= fails if the resolved value is empty, regardless of source.                    |
| =DefaultValue=  | applied when no source provides the option.                                          |

** Key derivation

Keys are computed at =Configure= time (never at registration), so registration
order and command-tree topology do not matter.

- flag = =--LongOpt= (consumer-controlled).
- leaf = =OptName= if set; otherwise =LongOpt= with a leading ="<namespace>_"=
  prefix removed when present (so ="db-host"= under namespace ="db"= → ="host"=),
  then remaining =-= → =_=.
- key = ="<namespace>.<leaf>"= (=db.host=, =server.start.addr=); flat for root.

** Precedence

From highest to lowest: CLI flag > environment > config file > default. (Viper
ignores unchanged flags, so a flag only wins when actually passed.) Each value's
source is recorded as it merges and shown by =StatusTable= / =Source=.

** Reserved keys

These are ordinary options you declare; panfigure treats their values specially:

- =env-prefix= (flat) :: the prefix for environment-variable lookup. Env is the
  default source. =db.host= with prefix =APP= → =APP_DB_HOST=.
- =config-paths= (flat, =[]string=) :: directories searched for the config file.

** Typed config and the drift check

=App.Unmarshal(&cfg)= matches config keys to struct fields case- and
separator-insensitively (snake_case keys ↔ CamelCase fields), so consumer code
needs no struct tags and no =viper.GetString=. Declarations and the struct are
two artifacts; keep them aligned with a CI test:

#+begin_src go
  func TestConfigSync(t *testing.T) {
      app := panfigure.New(cmd.Root())
      cmd.Configure(app)
      panfigure.AssertSync(t, app, &config.Config{})
  }
#+end_src

=AssertSync= reports a declared option with no matching field, a type mismatch,
or a struct field with no declaration (catches typos). A config-struct generator
that writes the struct from the declarations is planned for a later release.

** File configuration

Env is the default; files are secondary. Configure a file (read during
=Configure=, merged into the defaults/env state):

#+begin_src go
  app.AddConfigPath("/etc/myapp")
  app.AddConfigPath(".")
  app.SetConfigName("config")   // type inferred from extension, or:
  app.SetConfigType("yaml")
#+end_src

A missing config file is not an error.

** Reload

=App.Reload= discards all merged configuration (a fresh viper and metadata) and
re-runs =Configure= against the current environment and files. The registry,
root command, and file-config state persist.

** Status

#+begin_src go
  app.StatusTable(nil)            // text table of all keys, values, sources
  app.Status([]string{"db.host"}) // []*StatusInfo for specific keys
  app.Source("db.host")           // "default" | "env" | "file(name)" | "cli" | "none" | "unknown"
#+end_src

Documentation

Index

Constants

View Source
const StatusNotFound = "not found"

StatusNotFound is the error reported for a requested key that has no value.

Variables

This section is empty.

Functions

func AssertSync added in v0.1.0

func AssertSync(t *testing.T, app *App, cfg any)

AssertSync fails t if SyncErrors reports any drift. Intended for a consumer test that builds the App the same way main does (New + Root/RootGroup/On).

Types

type App added in v0.1.0

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

App is a configured application: it owns a cobra root command, its own *viper.Viper, a registry of declared options, and metadata for status reporting. Construct one with New, register option packages (Root/RootGroup/ On/OnGroup), then Run. The same App can be Reloaded to re-read sources.

func New added in v0.1.0

func New(root *cobra.Command) *App

New returns an App owning root with a fresh *viper.Viper.

func (*App) AddConfigPath added in v0.1.0

func (a *App) AddConfigPath(p string) *App

AddConfigPath adds a directory searched for the config file.

func (*App) Configure added in v0.1.0

func (a *App) Configure() error

Configure reads configuration from all sources in precedence order: defaults, CLI bindings, file, then environment. Source attribution is recorded as each source merges; CLI is attributed later, during Run's PreRun, once flags parse. Configure is safe to re-run after Reload.

func (*App) On added in v0.1.0

func (a *App) On(cmd *cobra.Command, opts ...*CommandOptions) *App

On registers options on cmd under a namespace derived from cmd's path in the tree (cmd "server start" => "server.start.addr"). Flags are local to cmd unless an option's Persistent field is set.

func (*App) OnGroup added in v0.1.0

func (a *App) OnGroup(cmd *cobra.Command, name string, opts ...*CommandOptions) *App

OnGroup registers options on cmd under an explicit namespace. Flags are local to cmd unless an option's Persistent field is set.

func (*App) Reload added in v0.1.0

func (a *App) Reload() error

Reload discards all merged configuration (fresh viper + metadata) and re-runs Configure. The registry, root, and file-config state are kept, so declared options and the command tree persist.

func (*App) Root added in v0.1.0

func (a *App) Root(opts ...*CommandOptions) *App

Root registers persistent options on the root command under the flat (empty) namespace, so their keys are un-nested (e.g. "env_prefix", "log_level").

func (*App) RootGroup added in v0.1.0

func (a *App) RootGroup(name string, opts ...*CommandOptions) *App

RootGroup registers a persistent, namespaced package of options on root, so keys nest under name (name "db" => "db.host", "db.port") and inherit to every subcommand. This is the seam for cross-cutting config such as db/mail.

func (*App) Run added in v0.1.0

func (a *App) Run() error

Run configures, installs panfigure's PreRun hooks (CLI source attribution and required validation), and executes the root command.

func (*App) SetConfigName added in v0.1.0

func (a *App) SetConfigName(name string) *App

SetConfigName sets the config file name (without directory). If name has a recognizable extension, the config type is inferred from it.

func (*App) SetConfigType added in v0.1.0

func (a *App) SetConfigType(t string) *App

SetConfigType sets the config file format (e.g. "json", "yaml"). Needed only when the config name has no recognizable extension.

func (*App) Source added in v0.1.0

func (a *App) Source(key string) string

Source reports where key was configured.

func (*App) Status added in v0.1.0

func (a *App) Status(keys []string) []*StatusInfo

Status returns StatusInfo for the requested keys; an empty slice returns all.

func (*App) StatusTable added in v0.1.0

func (a *App) StatusTable(keys []string) string

StatusTable renders a text table of keys, values, and sources suitable for a terminal "status" command, prefixed with the files parsed.

func (*App) SyncErrors added in v0.1.0

func (a *App) SyncErrors(cfg any) []error

SyncErrors reflects the App's declared options against the struct pointed to by cfg and returns one error per mismatch:

  • a declared option whose key has no compatible struct field;
  • a struct field that resolves to no declared option (catches typos like a field "Net" that should be "Network").

It uses the same key normalization as Unmarshal, so a tag-free struct that round-trips through Unmarshal should pass. Embedding and struct tags are not supported in v0.1.0. Returns nil when declarations and the struct agree. SyncErrors does not require Configure to have run.

func (*App) Unmarshal added in v0.1.0

func (a *App) Unmarshal(dst any) error

Unmarshal populates dst from the merged configuration. Config keys (snake_case, dot-nested) match struct fields case- and separator-insensitively, so "db.host" maps to field DB.Host and "base_url" maps to BaseURL without struct tags. dst must be a pointer to a struct.

func (*App) UseConfigFile added in v0.1.0

func (a *App) UseConfigFile(name string) *App

UseConfigFile is shorthand for SetConfigName. The file is read during Configure (it merges into the defaults/env state), not at this call.

type CommandOptions

type CommandOptions struct {
	// LongOpt is the long CLI flag name without "--", e.g. "db-host" or "addr".
	LongOpt string
	// ShortOpt is the optional one-letter flag, e.g. "h".
	ShortOpt string
	// OptName is the config-key leaf; if empty it is derived from LongOpt (with a
	// leading "<namespace>_" prefix stripped when present, then '-' -> '_').
	OptName string
	// Description is shown in --help.
	Description string
	// OptType selects the parser; omit for a string. Validated up front.
	OptType OptType
	// NoCLI hides the option from CLI flags (file/env only).
	NoCLI bool
	// Persistent exposes the flag on subcommands too. Only meaningful for options
	// registered via App.On/App.OnGroup; root options are always persistent.
	Persistent bool
	// Required makes App.Run fail when the resolved value is empty, regardless of
	// which source (flag, env, file) supplies it.
	Required bool
	// DefaultValue is applied when no source provides the option.
	DefaultValue any
}

CommandOptions declares a single configuration option: its CLI flag, its config key (leaf), its type, default, and required-ness. Declarations are the source of truth; a plain typed struct populated by App.Unmarshal is the read view. Keep them aligned with App.SyncErrors / AssertSync.

type OptType added in v0.1.0

type OptType string

OptType identifies the Go type used to parse a CommandOptions value from CLI flags, files, and environment variables. The zero value is equivalent to OptString.

const (
	// OptString is the default when OptType is omitted.
	OptString      OptType = "string"
	OptInt         OptType = "int"
	OptBool        OptType = "bool"
	OptCount       OptType = "count"
	OptStringSlice OptType = "[]string"
	OptDuration    OptType = "duration"
)

type Options added in v0.1.0

type Options []*CommandOptions

Options is a named slice of *CommandOptions for readable declarations.

type StatusError

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

StatusError reports that a requested key has no value.

func (*StatusError) Error

func (e *StatusError) Error() string

type StatusInfo

type StatusInfo struct {
	Key, Source string
	Value       any
	Err         error
}

StatusInfo describes one config key's value and its source.

func (*StatusInfo) String

func (s *StatusInfo) String() string

Directories

Path Synopsis
Example application showing panfigure's instance + option packages + typed config model, including precedence and source attribution.
Example application showing panfigure's instance + option packages + typed config model, including precedence and source attribution.

Jump to

Keyboard shortcuts

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