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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [Ubuntu]
go-version: ["1.25.x"]
go-version: ["1.26.x"]
runs-on: ${{ matrix.os }}-latest
permissions:
contents: read # for golangci-lint-action
Expand Down Expand Up @@ -41,5 +41,5 @@ jobs:
- name: Lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.7.2
version: v2.12.2
args: --timeout=5m --verbose
19 changes: 13 additions & 6 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
---
version: "2"
linters:
enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2026-01-02 for v2.7.2
enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2026-05-11 for v2.12.2
# enabled by default, but list them here to be explicit
- errcheck
- govet
Expand All @@ -16,6 +16,7 @@ linters:
- bidichk # checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- canonicalheader # checks for canonical names in HTTP headers
- clickhouselint # detects common mistakes with the ClickHouse native Go driver API
- containedctx # detects struct contained context.Context field
#- contextcheck # checks for inherited context.Context
- copyloopvar # detects places where loop variables are copied
Expand All @@ -35,7 +36,7 @@ linters:
#- exhaustruct # checks if all structure fields are initialized
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
- fatcontext # finds nested context.WithValue calls in loops
#- forbidigo # forbids identifiers
- forbidigo # forbids identifiers
#- forcetypeassert # finds forced type assertions
- funcorder # checks that functions are in the right order
#- funlen # Tool for detection of long functions
Expand All @@ -45,15 +46,16 @@ linters:
- gochecknoinits # checks that no init functions are present in Go code
- gochecksumtype # checks exhaustiveness on Go "sum types"
#- gocognit # Computes and checks the cognitive complexity of functions
- goconst # finds repeated strings that could be replaced by a constant
#- goconst # finds repeated strings that could be replaced by a constant
- gocritic # provides diagnostics that check for bugs, performance and style issues
#- gocyclo # Computes and checks the cyclomatic complexity of functions
- godoclint # Checks golang docs best practices (godoc)
#- godot # Check if comments end in a period
#- godox # Tool for detection of FIXME, TODO and other comment keywords
#- goheader # Checks is file header matches to pattern
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
#- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
- gomodguard_v2 # Allow and blocklist linter for direct Go module dependencies
- goprintffuncname # checks that printf-like functions are named with f at the end
- gosec # inspects source code for security problems
- gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase.
Expand Down Expand Up @@ -115,7 +117,7 @@ linters:
- whitespace # detects leading and trailing whitespace
#- wrapcheck # Checks that errors returned from external packages are wrapped
#- wsl # (deprecated)
# - wsl_v5 # whitespace linter - add or remove empty lines
#- wsl_v5 # whitespace linter - add or remove empty lines
#- zerologlint # checks wrong usage of zerolog
settings:
gosec:
Expand Down Expand Up @@ -166,9 +168,14 @@ linters:
- containedctx
- dogsled
path: _test.go
# Examples are allowed to write directly to stdout.
- linters:
- forbidigo
path: ^examples/
text: use of `fmt\.Print(|f|ln)` forbidden
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
- goimports
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type ProgramConfig struct {

func main() {
cfg := &ProgramConfig{}
structconf.MustLoadAndValidate(cfg, "greetings")
structconf.MustLoad(cfg, "greetings")
if cfg.Greet {
fmt.Printf("Hello %s!\n", cfg.Name)
}
Expand Down Expand Up @@ -92,7 +92,7 @@ type ProgramConfig struct {

func main() {
cfg := &ProgramConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"greetings",
structconf.WithVersion("1.0.0"),
structconf.WithDescription("Print a greeting"),
Expand Down Expand Up @@ -150,7 +150,7 @@ type AppConfig struct {

func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg, "app")
structconf.MustLoad(cfg, "app")

fmt.Printf("%v", cfg)
}
Expand All @@ -176,7 +176,7 @@ type AppConfig struct {

func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"app",
// adds a --load-config flag to load config from TOML files
structconf.WithLoadConfigFlag("load-config"),
Expand Down Expand Up @@ -251,15 +251,15 @@ func main() {
}
```

`BindCommand` and `NewCommand` currently support flags, env vars and default values. `WithLoadConfigFlag` is currently only supported by `LoadAndValidate` / `MustLoadAndValidate`.
`BindCommand` and `NewCommand` currently support flags, env vars and default values. `WithLoadConfigFlag` is currently only supported by `Load` / `MustLoad`.

### Parse custom arg slices

If you need to parse a specific arg slice (for tests or embedding), use `LoadAndValidateArgs`:
If you need to parse a specific arg slice (for tests or embedding), use `LoadArgs`:

```go
cfg := &AppConfig{}
err := structconf.LoadAndValidateArgs(cfg, "app", []string{"app", "--log-level", "DEBUG"})
err := structconf.LoadArgs(cfg, "app", []string{"app", "--log-level", "DEBUG"})
if err != nil {
panic(err)
}
Expand All @@ -270,7 +270,7 @@ if err != nil {
Enable completion in code:

```go
structconf.MustLoadAndValidate(cfg, "app", structconf.WithShellCompletions())
structconf.MustLoad(cfg, "app", structconf.WithShellCompletions())
```

Then install it in your shell:
Expand Down Expand Up @@ -327,7 +327,7 @@ type NestedConfig struct {

func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg, "app")
structconf.MustLoad(cfg, "app")
fmt.Println(cfg.Deeply.Nested.Name)
}
```
Expand Down Expand Up @@ -366,7 +366,7 @@ type AppConfig struct {

func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg, "app")
structconf.MustLoad(cfg, "app")

asMap, err := structconf.MarshalAsMap(cfg)
if err != nil {
Expand Down Expand Up @@ -410,7 +410,7 @@ type AppConfig struct {

func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg, "app")
structconf.MustLoad(cfg, "app")
}
```

Expand All @@ -419,3 +419,18 @@ $ ./app --port=0 --path=/tmp/
Missing required configuration: AppConfig.Host
Configuration error: Port - gte
```

If you want to load config without running validation, pass `WithDisableValidation`:

```go
type AppConfig struct {
Host string `validate:"required"`
}

func main() {
cfg := &AppConfig{}
structconf.MustLoad(cfg, "app", structconf.WithDisableValidation())
}
```

For configs bound to subcommands with `BindCommand` / `NewCommand`, use `WithDisableCommandValidation` instead.
4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ func (r *structReflector) recurseStruct(anyStruct any, parents []*configFieldTag
structType := reflect.TypeOf(anyStruct)
structValues := reflect.ValueOf(anyStruct)

if structType.Kind() == reflect.Ptr {
if structType.Kind() == reflect.Pointer {
structType = structType.Elem()
structValues = structValues.Elem()
}
Expand All @@ -456,7 +456,7 @@ func (r *structReflector) recurseStruct(anyStruct any, parents []*configFieldTag
continue
}

if fieldType.Type.Kind() == reflect.Ptr && fieldType.Type.Elem().Kind() == reflect.Struct {
if fieldType.Type.Kind() == reflect.Pointer && fieldType.Type.Elem().Kind() == reflect.Struct {
if fieldValue.IsNil() {
fieldValue.Set(reflect.New(fieldType.Type.Elem()))
}
Expand Down
2 changes: 1 addition & 1 deletion examples/01_simple_cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ProgramConfig struct {
// usage: ./simple_cli --greet --name "World"
func main() {
cfg := &ProgramConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"simple_cli",
structconf.WithVersion("1.0.0"),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/02_default_values/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ProgramConfig struct {
// ./simple_cli -h
func main() {
cfg := &ProgramConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"greetings",
structconf.WithVersion("1.0.0"),
structconf.WithDescription("Print a greeting"),
Expand Down
2 changes: 1 addition & 1 deletion examples/03_nested/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type AppConfig struct {
// usage: ./app --database-user=myuser --database-password=mypassword --server-host=localhost --server-port=8080 --log-level=DEBUG
func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"app",
structconf.WithVersion("1.0.0"),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/04_toml/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type AppConfig struct {
// usage: ./app --load-config database.toml --log-level=debug
func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"app",
structconf.WithVersion("1.0.0"),
// adds a --load-config flag to load config from TOML files
Expand Down
2 changes: 1 addition & 1 deletion examples/05_override/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type AppConfig struct {
// usage: ./app --load-config database.toml --log-level=debug
func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"app",
structconf.WithVersion("1.0.0"),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/06_global/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type NestedConfig struct {
// usage: ./app --load-config database.toml --log-level=debug
func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"app",
structconf.WithVersion("1.0.0"),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/07_marshal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type AppConfig struct {
// usage: ./app --database-user=my-user --database-password=very-secret-password --log-level=INFO
func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg,
structconf.MustLoad(cfg,
"app",
structconf.WithVersion("1.0.0"),
)
Expand Down
2 changes: 1 addition & 1 deletion examples/08_validation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ type AppConfig struct {
// usage: ./app --log-level=DEBUG --port=8080 --host=localhost --path=/tmp/
func main() {
cfg := &AppConfig{}
structconf.MustLoadAndValidate(cfg, "app", structconf.WithVersion("1.0.0"))
structconf.MustLoad(cfg, "app", structconf.WithVersion("1.0.0"))
}
2 changes: 1 addition & 1 deletion examples/09_subcommands/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func main() {

fmt.Println(greetCfg.Name)
return nil
}, structconf.WithDescription("Print a greeting"))
})
if err != nil {
panic(err)
}
Expand Down
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
module github.com/tilebox/structconf

go 1.24.0
go 1.25.0

require (
github.com/BurntSushi/toml v1.6.0
github.com/go-playground/validator/v10 v10.30.1
github.com/go-playground/validator/v10 v10.30.2
github.com/iancoleman/strcase v0.3.0
github.com/samber/lo v1.52.0
github.com/samber/lo v1.53.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.1
github.com/urfave/cli/v3 v3.8.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,34 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
4 changes: 2 additions & 2 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func marshalStruct(anyStruct any, into map[string]any, nameFromTags func(t *conf
structType := reflect.TypeOf(anyStruct)
structValues := reflect.ValueOf(anyStruct)

if structType.Kind() == reflect.Ptr {
if structType.Kind() == reflect.Pointer {
structType = structType.Elem()
structValues = structValues.Elem()
}
Expand Down Expand Up @@ -63,7 +63,7 @@ func marshalStruct(anyStruct any, into map[string]any, nameFromTags func(t *conf
continue
}

if fieldType.Type.Kind() == reflect.Ptr && fieldType.Type.Elem().Kind() == reflect.Struct {
if fieldType.Type.Kind() == reflect.Pointer && fieldType.Type.Elem().Kind() == reflect.Struct {
if fieldValue.IsNil() {
fieldValue.Set(reflect.New(fieldType.Type.Elem()))
}
Expand Down
Loading
Loading