Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGENTS.md

## Commands
- Build: `go build ./...`
- Test all: `go test ./...`
- Test single: `go test -run TestName ./...`
- Lint: `golangci-lint run ./...`
- Format touched Go files with `gofmt`/`goimports`; the linter also enables `gci`, `gofmt`, `gofumpt`, and `goimports` formatters.

## Repo Architecture
- This is a single-package Go module: `github.com/tilebox/structconf`.
- `structconf.go`: Public loading API (`Load`, `LoadArgs`, `MustLoad`, `MustLoadArgs`), functional options, subcommand helpers (`BindCommand`, `NewCommand`), duplicate flag detection, and the two-pass TOML load flow.
- `config.go`: Reflection-based struct walker that turns exported struct fields into `urfave/cli/v3` flags and applies parsed values back into the config struct.
- `tags.go`: Struct tag parsing and default name derivation for flags, env vars, TOML/YAML/JSON keys, aliases, global fields, secrets, defaults, and help text.
- `toml.go`: TOML file loading and `cli.ValueSource` adapters used to feed TOML values into the same precedence chain as env vars/defaults.
- `validate.go`: Default validation using `go-playground/validator/v10`, with user-facing error messages.
- `marshal.go`: Config marshaling helpers (`MarshalAsMap`, `MarshalAsSlogDict`) and secret redaction.
- `examples`: Runnable examples for each major feature; keep examples small and aligned with README snippets.

## Core Behavior And Contracts
- Source precedence is CLI flags first, then TOML config files, then environment variables, then `default` tags.
- Exported fields get generated names by default: kebab-case flags/TOML/YAML keys, screaming snake env vars, and lower camel JSON names.
- Nested structs compose parent names unless a field is tagged `global:"true"`.
- Fields tagged `flag:"-"` are not configurable; analogous `env:"-"` and `toml:"-"` disable those sources.
- Supported config field types are `string`, `[]string`, signed/unsigned integer widths, floats, `bool`, and `time.Duration`.
- `LoadArgs`/`BindCommand` run validation after values are applied; custom validators replace the default validator.

## Design Patterns And Paradigms
- Public APIs use functional options (`WithVersion`, `WithDescription`, `WithValidator`, etc.).
- Keep the reflection pipeline centralized in `NewStructConfigurator`, `recurseStruct`, and `processField`; avoid one-off parsing paths that bypass the shared value-source precedence behavior.
- Prefer adding behavior at the source of truth (`config.go`, `tags.go`, or `toml.go`) rather than wrapping public APIs for special cases.
- Return errors with useful context and `%w` when wrapping underlying failures.
- Keep generated CLI help/error text stable where tests assert on it.

## Code Style
- Use `stretchr/testify` (`require` for setup/fatal checks, `assert` for value comparisons).
- Prefer table-driven tests for multi-source or multi-type behavior.
- Keep imports grouped and formatted by Go tooling.
- Respect struct tag order enforced by lint: `flag`, `env`, `default`, `secret`, `toml`, `json`, `validate`, `global`, `help`.
- Avoid new package-level globals and `init` functions unless there is a strong reason; `tags.go` currently has an intentional `init` for `strcase` initialism configuration.
- Examples may print to stdout; library code should not, except for existing `MustLoad*` error/help handling.

## Typical Development Flow
1. Make the smallest focused change in the root package and update README/examples/changelog when behavior changes.
2. Add or update focused tests in `structconf_test.go` for precedence, tag handling, validation, or CLI behavior.
3. Run `go test ./...` for behavior changes.
4. Run `go build ./...` when examples or public APIs change.
5. Run `golangci-lint run ./...` before handing off larger changes or anything likely to touch lint-sensitive code.
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.0] - 2026-05-20

### Added

- Added support for configuring `[]string` fields from comma-separated CLI flags, environment variables, and default values, plus native TOML string arrays.

## [0.3.0] - 2026-05-12

### Added

- Added custom validator registration via `WithValidateFunc`.

## [0.2.0] - 2026-03-13

### Added

- Added composable command binding helpers for subcommand CLIs via `BindCommand` and `NewCommand`.
- Added a subcommands example.Now

## [0.1.0] - 2026-01-02

### Added

- Added initial struct tag based configuration loading from CLI flags, environment variables, TOML config files, and default values.
- Added support for nested structs, field tags, generated help output, validation, and marshaling.
- Added examples covering basic CLI usage, defaults, nested structs, TOML loading, overrides, globals, marshaling, and validation.

### Fixed

- Fixed integer parsing to use the correct bit size for signed and unsigned integer fields.

[Unreleased]: https://github.com/tilebox/structconf/compare/v0.4.0...HEAD
[0.4.0]: https://github.com/tilebox/structconf/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/tilebox/structconf/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/tilebox/structconf/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/tilebox/structconf/releases/tag/v0.1.0
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ go get github.com/tilebox/structconf
- Order of precedence: CLI flags, config files, environment variables, default values
- By only defining a struct containing all the fields you want to configure
- Structs can be nested within other structs
- Supported data types: `string`, `int`, `int8-64`, `uint`, `uint8-64`, `bool`, `float`, `time.Duration`
- Supported data types: `string`, `[]string`, `int`, `int8-64`, `uint`, `uint8-64`, `bool`, `float`, `time.Duration`
- Customize certain fields by adding tags to the struct fields
- Using the tags `flag`, `env`, `default`, `secret`, `toml`, `validate`, `global`, `help`
- Includes input validation using [go-playground/validator](https://github.com/go-playground/validator)
Expand Down Expand Up @@ -202,6 +202,32 @@ $ ./app --load-config database.toml
&{INFO {myuser mypassword}}
```

### Configure string slices

`[]string` fields are configured with comma-separated values from CLI flags, environment variables and default values.
When loading from TOML, native string arrays are supported as well.

```go
type AppConfig struct {
AllowedOrigins []string `flag:"allowed-origins" env:"ALLOWED_ORIGINS" toml:"allowed-origins" help:"Allowed CORS origins"`
}

func main() {
cfg := &AppConfig{}
structconf.MustLoad(cfg, "app", structconf.WithLoadConfigFlag("load-config"))
}
```

```bash
$ ./app --allowed-origins=https://app.example.com,https://admin.example.com
$ ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com ./app
```

```toml
# app.toml
allowed-origins = ["https://app.example.com", "https://admin.example.com"]
```

### Build subcommands

You can bind configs directly to `urfave/cli` commands and compose them as subcommands.
Expand Down Expand Up @@ -406,6 +432,9 @@ type AppConfig struct {

// must be one of (case insensitive): DEBUG, INFO, WARN, ERROR
LogLevel string `default:"INFO" validate:"oneofci=DEBUG INFO WARN ERROR" help:"Log level"`

// if set, each comma-separated value must be one of (case insensitive): password, oauth, saml, magic_link
EnabledAuthProviders []string `validate:"omitempty,dive,oneofci=password oauth saml magic_link" help:"Enabled authentication providers"`
}

func main() {
Expand Down
23 changes: 23 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,29 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref
apply = func(cmd *cli.Command) {
fieldValue.SetString(cmd.String(flagName))
}
case reflect.Slice:
if field.Type.Elem().Kind() != reflect.String {
return fmt.Errorf("unsupported slice element type %s for field %s", field.Type.Elem().Kind(), field.Name)
}

flag = &cli.StringFlag{
Name: flagName,
Aliases: tags.aliases,
Usage: tags.help,
DefaultText: tags.defaultValue,
Value: tags.defaultValue,
Sources: sources,
}

apply = func(cmd *cli.Command) {
value := cmd.String(flagName)
if value == "" {
fieldValue.Set(reflect.Zero(field.Type))
return
}

fieldValue.Set(reflect.ValueOf(strings.Split(value, ",")))
}
case reflect.Int:
var value int
if tags.defaultValue != "" {
Expand Down
21 changes: 21 additions & 0 deletions examples/10_slices/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"fmt"

"github.com/tilebox/structconf"
)

type AppConfig struct {
AllowedOrigins []string `flag:"allowed-origins" env:"ALLOWED_ORIGINS" toml:"allowed-origins" validate:"omitempty,dive,url" help:"comma-separated list of allowed CORS origins"`
}

// usage: ./app --allowed-origins=https://app.example.com,https://admin.example.com
// or: ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com ./app
// or with TOML: allowed-origins = ["https://app.example.com", "https://admin.example.com"]
func main() {
cfg := &AppConfig{}
structconf.MustLoad(cfg, "app", structconf.WithVersion("1.0.0"), structconf.WithLoadConfigFlag("load-config"))

fmt.Printf("%v\n", cfg.AllowedOrigins)
}
Loading