Skip to content

tinyauthapp/paerser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Paerser

Paerser is a Go library for loading configuration into structs from multiple sources:

  • CLI flag
  • Configuration files
  • Environment variables

It also provides a lightweight CLI command system with sub-commands, automatic help generation, and resource loaders that chain these configuration sources together.

Note

This fork is created to support the needs of the Tinyauth. Over time, we may remove some features that are not relevant to our use case, and we may also add new features that we need. If you want to contribute or have any questions, please feel free to open an issue or a pull request.

Before you ask, yes the readme is generated by an LLM because LLM readme is better than no readme ;)

Installation

go get github.com/tinyauthapp/paerser

Quick Start

Define a configuration struct and populate it from any source:

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/tinyauthapp/paerser/env"
	"github.com/tinyauthapp/paerser/file"
	"github.com/tinyauthapp/paerser/flag"
)

type Config struct {
	Host string
	Port int
	DB   DatabaseConfig
}

type DatabaseConfig struct {
	DSN     string
	MaxConn int
}

func main() {
	cfg := Config{}

	// From flags:
	err := flag.Decode([]string{"--host=localhost", "--port=8080", "--db.dsn=postgres://localhost/mydb"}, &cfg)

	// From environment variables:
	err = env.Decode(os.Environ(), "MYAPP_", &cfg)

	// From a file (YAML, TOML, or JSON):
	err = file.Decode("config.yml", &cfg)

	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", cfg)
}

Packages

flag - CLI Flag Decoding & Encoding

Decodes command-line flag arguments into a struct using dot-separated names that mirror the struct hierarchy.

Decoding

package main

import (
	"fmt"
	"log"

	"github.com/tinyauthapp/paerser/flag"
)

type Config struct {
	Server ServerConfig
	Debug  bool
}

type ServerConfig struct {
	Host string
	Port int
	Tags []string
}

func main() {
	args := []string{
		"--server.host=0.0.0.0",
		"--server.port=9090",
		"--server.tags=web,api",
		"--debug",
	}

	cfg := Config{}
	if err := flag.Decode(args, &cfg); err != nil {
		log.Fatal(err)
	}

	fmt.Println(cfg.Server.Host) // "0.0.0.0"
	fmt.Println(cfg.Server.Port) // 9090
	fmt.Println(cfg.Server.Tags) // [web api]
	fmt.Println(cfg.Debug)       // true
}

*Flag syntax rules:

Syntax Behavior
--flag value or --flag=value Standard flag with a value
-flag value or -flag=value Short-form (single dash)
--boolflag Boolean flags are set to true implicitly
--slice=a,b,c Comma-separated values populate slices
-- Stops flag parsing

Encoding

Encode a struct back into a flat list of flag definitions (useful for help text generation):

flats, err := flag.Encode(&cfg)
// Each flat has a Name (e.g. "server.host") and Default value.

Low-Level Parsing

If you only need the raw key-value map without populating a struct:

parsed, err := flag.Parse(os.Args[1:], &cfg)
// parsed is a map[string]string, e.g. {"traefik.server.host": "0.0.0.0"}

env - Environment Variable Decoding & Encoding

Decodes environment variables into a struct. Variable names are derived from the struct field path, uppercased, with underscores as separators.

Decoding

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/tinyauthapp/paerser/env"
)

type Config struct {
	Host string
	Port int
	DB   DBConfig
}

type DBConfig struct {
	DSN     string
	MaxConn int
}

func main() {
	os.Setenv("APP_HOST", "localhost")
	os.Setenv("APP_PORT", "3000")
	os.Setenv("APP_DB_DSN", "postgres://localhost/mydb")
	os.Setenv("APP_DB_MAXCONN", "10")

	cfg := Config{}
	if err := env.Decode(os.Environ(), "APP_", &cfg); err != nil {
		log.Fatal(err)
	}

	fmt.Println(cfg.Host)       // "localhost"
	fmt.Println(cfg.Port)       // 3000
	fmt.Println(cfg.DB.DSN)     // "postgres://localhost/mydb"
	fmt.Println(cfg.DB.MaxConn) // 10
}

The prefix (e.g. "APP_") must match the pattern ^[a-zA-Z0-9]+_$ - alphanumeric characters followed by a single trailing underscore.

Encoding

Encode a struct into flat environment variable representations:

flats, err := env.Encode("APP_", &cfg)
// Each flat has a Name (e.g. "APP_DB_DSN") and Default value.

Filtering

Find only the environment variables that are relevant to a given struct:

vars := env.FindPrefixedEnvVars(os.Environ(), "APP_", &cfg)
// Returns only env vars whose keys match known struct field paths.

This is more precise than a simple prefix match — it checks that the variable name corresponds to an actual field in the struct.

file - Configuration File Decoding

Decodes YAML, TOML, or JSON configuration files into a struct. The format is detected from the file extension.

From a File Path

package main

import (
	"fmt"
	"log"

	"github.com/tinyauthapp/paerser/file"
)

type Config struct {
	Server ServerConfig
	Debug  bool
}

type ServerConfig struct {
	Host string
	Port int
}

func main() {
	cfg := Config{}
	if err := file.Decode("config.yml", &cfg); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", cfg)
}

Where config.yml contains:

server:
  host: localhost
  port: 8080
debug: true

Supported extensions: .toml, .yaml, .yml, .json

From a String

If you already have the content in memory:

content := `
server:
  host: localhost
  port: 8080
`
cfg := Config{}
err := file.DecodeContent(content, ".yml", &cfg)

cli - Command System

A lightweight CLI framework that ties together flag, env, and file loaders with support for sub-commands and automatic help generation.

Basic Command

package main

import (
	"fmt"
	"log"

	"github.com/tinyauthapp/paerser/cli"
)

type Config struct {
	Host  string `description:"Server host"`
	Port  int    `description:"Server port"`
	Debug bool   `description:"Enable debug mode"`
}

func main() {
	cfg := &Config{}

	cmd := &cli.Command{
		Name:          "myapp",
		Description:   "My awesome application",
		Configuration: cfg,
		Resources: []cli.ResourceLoader{
			&cli.FlagLoader{},
		},
		Run: func(args []string) error {
			fmt.Printf("Starting server on %s:%d (debug=%v)\n", cfg.Host, cfg.Port, cfg.Debug)
			return nil
		},
	}

	if err := cli.Execute(cmd); err != nil {
		log.Fatal(err)
	}
}
$ myapp --host=0.0.0.0 --port=8080 --debug
Starting server on 0.0.0.0:8080 (debug=true)

Resource Loaders

Resource loaders populate cmd.Configuration and are executed in order. If a loader returns done == true, the chain stops.

Loader Source Description
cli.FlagLoader{} CLI flags Decodes --flag=value arguments
cli.EnvLoader{Prefix: "MYAPP_"} Environment Decodes MYAPP_* environment variables
cli.FileLoader{...} Config file Loads from a file (path given via a flag or searched from base paths)

Chaining loaders (file first, then flags to override):

cmd := &cli.Command{
	Name:          "myapp",
	Description:   "My application",
	Configuration: cfg,
	Resources: []cli.ResourceLoader{
		&cli.FileLoader{
			ConfigFileFlag: "configFile",
			BasePaths:      []string{"/etc/myapp/myapp", "$HOME/.config/myapp"},
			Extensions:     []string{"toml", "yaml", "yml"},
		},
		&cli.EnvLoader{Prefix: "MYAPP_"},
		&cli.FlagLoader{},
	},
	Run: func(args []string) error {
		// cfg is now populated from file -> env -> flags (in order)
		return nil
	},
}
$ myapp --configFile=/path/to/config.toml --port=9090

Sub-Commands

rootCmd := &cli.Command{
	Name:        "myapp",
	Description: "My application",
}

serveCmd := &cli.Command{
	Name:          "serve",
	Description:   "Start the server",
	Configuration: &ServeConfig{},
	Resources:     []cli.ResourceLoader{&cli.FlagLoader{}},
	Run: func(args []string) error {
		fmt.Println("Server started!")
		return nil
	},
}

migrateCmd := &cli.Command{
	Name:          "migrate",
	Description:   "Run database migrations",
	Configuration: &MigrateConfig{},
	Resources:     []cli.ResourceLoader{&cli.FlagLoader{}},
	Run: func(args []string) error {
		fmt.Println("Migrations complete!")
		return nil
	},
}

rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(migrateCmd)

cli.Execute(rootCmd)
$ myapp serve --port=8080
$ myapp migrate --db.dsn=postgres://localhost/mydb

Automatic Help

Help is generated automatically from the struct's flags and descriptions. Pass --help or -h to any command:

$ myapp --help
myapp    My application

Usage: myapp [command] [flags] [arguments]

Use "myapp [command] --help" for help on any command.

Commands:
    serve      Start the server
    migrate    Run database migrations

You can also provide a custom help function:

cmd := &cli.Command{
	CustomHelpFunc: func(w io.Writer, cmd *cli.Command) error {
		fmt.Fprintf(w, "Custom help for %s\n", cmd.Name)
		return nil
	},
}

generator - Struct Initialization with Defaults

Recursively initializes all pointer and struct fields in a configuration struct, calling SetDefaults() on any field that implements the initializer interface.

package main

import (
	"fmt"

	"github.com/tinyauthapp/paerser/generator"
)

type Config struct {
	Server *ServerConfig
}

type ServerConfig struct {
	Host string
	Port int
}

func (s *ServerConfig) SetDefaults() {
	s.Host = "localhost"
	s.Port = 8080
}

func main() {
	cfg := &Config{}
	generator.Generate(cfg)

	fmt.Println(cfg.Server.Host) // "localhost"
	fmt.Println(cfg.Server.Port) // 8080
}

Generate will:

  • Allocate nil pointers (so *ServerConfig is no longer nil)
  • Call SetDefaults() on any field whose pointer type implements it
  • Recurse into nested structs, maps, and slices

types - Custom Types

Duration

A duration type that works seamlessly with TOML, YAML, JSON, and plain integer values (interpreted as seconds).

package main

import (
	"fmt"

	"github.com/tinyauthapp/paerser/types"
)

func main() {
	var d types.Duration

	d.Set("30s")          // 30 seconds
	d.Set("5m30s")        // 5 minutes and 30 seconds
	d.Set("120")          // 120 seconds (suffix-less integers are treated as seconds)

	fmt.Println(d.String()) // "2m0s"
}

It implements encoding.TextMarshaler, encoding.TextUnmarshaler, json.Marshaler, and json.Unmarshaler, so it works out of the box in configuration files:

timeout: 30s
interval: 120

parser - Low-Level Parsing Engine

The parser package is the foundation that all other packages build on. It provides a tree-based intermediate representation (Node) for configuration data, along with encoding/decoding utilities.

Most users won't need to use this package directly, but it's available for advanced use cases:

// Decode a flat label map into a struct
labels := map[string]string{
	"traefik.http.routers.myrouter.rule": "Host(`example.com`)",
	"traefik.http.routers.myrouter.tls":  "true",
}
err := parser.Decode(labels, &cfg, "traefik")

// Encode a struct into a flat label map
labels, err := parser.Encode(&cfg, "traefik")

Key types and functions:

  • Node- A recursive tree node with Name, Value, Kind, Children, etc.
  • Decode(labels, element, rootName) - Flat map -> struct
  • Encode(element, rootName) - Struct -> flat map
  • Flat - A flattened key/value/default representation used by flag.Encode and env.Encode

Struct Tags

The library uses struct tags to control field naming and behavior in different contexts:

Tag Used By Purpose
description cli (help generation) Description shown in --help output
label parser, generator Controls label-based encoding/decoding. Use "-" to skip a field
file file Controls file-based decoding

License

Apache License 2.0

About

A Go library for loading configuration into structs from multiple sources.

Resources

License

Stars

Watchers

Forks

Contributors

Languages