An open API service indexing awesome lists of open source software.

https://github.com/tinyauthapp/paerser

A Go library for loading configuration into structs from multiple sources.
https://github.com/tinyauthapp/paerser

Last synced: 9 days ago
JSON representation

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

Awesome Lists containing this project

README

          

# 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](https://github.com/steveiliop56/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:

```go
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

```go
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):

```go
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:

```go
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

```go
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:

```go
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:

```go
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

```go
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:

```yaml
server:
host: localhost
port: 8080
debug: true
```

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

#### From a String

If you already have the content in memory:

```go
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

```go
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):

```go
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

```go
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:

```go
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.

```go
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).

```go
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:

```yaml
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:

```go
// 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](LICENSE)