diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cee89c2..321b43e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,16 +13,16 @@ jobs: strategy: matrix: os: [Ubuntu] - go-version: ["1.24.x"] + go-version: ["1.25.x"] runs-on: ${{ matrix.os }}-latest permissions: contents: read # for golangci-lint-action steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Install dependencies @@ -39,7 +39,7 @@ jobs: paths: "test-report.xml" if: always() - name: Lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: - version: v2.1.6 + version: v2.7.2 args: --timeout=5m --verbose diff --git a/.golangci.yaml b/.golangci.yaml index 9fdf301..11e3545 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,7 +2,7 @@ --- version: "2" linters: - enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2025-02-14 for v1.64.5 + enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2026-01-02 for v2.7.2 # enabled by default, but list them here to be explicit - errcheck - govet @@ -10,6 +10,7 @@ linters: - staticcheck - unused # other linters (that would be disabled by default) + - arangolint # linter for arangoDB - asasalint # checks for pass []any as any in variadic func(...any) - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences @@ -25,6 +26,7 @@ linters: #- dupl # tool for code clone detection - dupword # checks for duplicate words in the source code - durationcheck # checks for two durations multiplied together + - embeddedstructfieldcheck # embedded types should be at the top of a struct #- err113 # checks the errors handling expressions - errchkjson # checks types passed to the json encoding functions. - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error @@ -35,6 +37,7 @@ linters: - fatcontext # finds nested context.WithValue calls in loops #- forbidigo # forbids identifiers #- forcetypeassert # finds forced type assertions + - funcorder # checks that functions are in the right order #- funlen # Tool for detection of long functions #- ginkgolinter # Enforces the Ginkgo testing package guidelines. - gocheckcompilerdirectives # validates go compiler directive comments (//go:) @@ -45,6 +48,7 @@ linters: - 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 @@ -59,6 +63,7 @@ linters: #- inamedparam # Reports interfaces with unnamed method parameters. - interfacebloat # Checks the number of methods in an interface - intrange # finds places where for loops could make use of an integer range + - iotamixing # checks that iota is not mixed with other constants #- ireturn # Accept Interfaces, Return Concrete Types #- lll # Reports long lines - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) @@ -67,6 +72,7 @@ linters: - mirror # reports wrong mirror patterns of bytes/strings usage - misspell # finds commonly misspelled English words in comments #- mnd # Detects magic numbers + - modernize # reports wrong mirror patterns of bytes/strings usage - musttag # enforces field tags in (un)marshaled structs - nakedret # finds naked returns in functions greater than a specified function length #- nestif # Reports deeply nested if statements @@ -74,13 +80,13 @@ linters: - nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error. - nilnil # checks that there is no simultaneous return of nil error and an invalid value #- nlreturn # Checks for a new line before return and branch statements to increase code clarity + #- noinlineerr # Disallows inline error handling - noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives - nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative #- paralleltest # detects missing usage of t.Parallel() method in your Go test - - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative - prealloc # finds slice declarations that could potentially be preallocated - predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint @@ -101,15 +107,16 @@ linters: - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions - unparam # reports unused function parameters + - unqueryvet # sql linter disallowing select * - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - usetesting # reports uses of functions with replacement inside the testing package #- varnamelen # checks that the length of a variable's name matches its scope - wastedassign # finds wasted assignment statements - whitespace # detects leading and trailing whitespace #- wrapcheck # Checks that errors returned from external packages are wrapped - #- wsl # whitespace linter - add or remove empty lines + #- wsl # (deprecated) + # - wsl_v5 # whitespace linter - add or remove empty lines #- zerologlint # checks wrong usage of zerolog - #- tenv # deprecated in favor of usetesting settings: gosec: excludes: @@ -147,7 +154,7 @@ linters: strict: false exclusions: generated: strict - warn-unused: true + warn-unused: false presets: - comments - common-false-positives @@ -164,4 +171,4 @@ formatters: - gci - gofmt - gofumpt - - goimports + - goimports \ No newline at end of file diff --git a/config.go b/config.go index 340aa90..73b0afb 100644 --- a/config.go +++ b/config.go @@ -48,6 +48,7 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref tomlKeys = append(tomlKeys, tags.toml) tomlKey = strings.Join(tomlKeys, ".") } + valueSources = append(valueSources, NewValueSourceFromMaps(tomlKey, r.tomlSources...)) } @@ -58,6 +59,7 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref emvKeys = append(emvKeys, tags.env) envKey = strings.Join(emvKeys, "_") } + valueSources = append(valueSources, cli.EnvVar(envKey)) } @@ -70,10 +72,12 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref sources := cli.NewValueSourceChain(valueSources...) - var flag cli.Flag - var apply func(*cli.Command) + var ( + flag cli.Flag + apply func(*cli.Command) + ) - switch field.Type.Kind() { //nolint:exhaustive + switch field.Type.Kind() { //nolint:exhaustive // we have a default: clause that results in an error case reflect.String: flag = &cli.StringFlag{ Name: flagName, @@ -88,80 +92,90 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref fieldValue.SetString(cmd.String(flagName)) } case reflect.Int: - var value int64 - var err error + var value int if tags.defaultValue != "" { - value, err = strconv.ParseInt(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseInt(tags.defaultValue, 10, strconv.IntSize) if err != nil { return fmt.Errorf("failed to parse int value %s for field %s: %w", tags.defaultValue, field.Name, err) } + value = int(valueParsed) } + flag = &cli.IntFlag{ Name: flagName, Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: int(value), + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetInt(int64(cmd.Int(flagName))) } case reflect.Int8: - var value int64 - var err error + var value int8 + if tags.defaultValue != "" { - value, err = strconv.ParseInt(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseInt(tags.defaultValue, 10, 8) if err != nil { return fmt.Errorf("failed to parse int value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = int8(valueParsed) } + flag = &cli.Int8Flag{ Name: flagName, Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: int8(value), //nolint: gosec + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetInt(int64(cmd.Int8(flagName))) } case reflect.Int16: - var value int64 - var err error + var value int16 + if tags.defaultValue != "" { - value, err = strconv.ParseInt(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseInt(tags.defaultValue, 10, 16) if err != nil { return fmt.Errorf("failed to parse int value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = int16(valueParsed) } + flag = &cli.Int16Flag{ Name: flagName, Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: int16(value), //nolint: gosec + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetInt(int64(cmd.Int16(flagName))) } case reflect.Int32: - var value int64 - var err error + var value int32 + if tags.defaultValue != "" { - value, err = strconv.ParseInt(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseInt(tags.defaultValue, 10, 32) if err != nil { return fmt.Errorf("failed to parse int value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = int32(valueParsed) } + flag = &cli.Int32Flag{ Name: flagName, Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: int32(value), //nolint: gosec + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { @@ -169,8 +183,11 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref } case reflect.Int64: if _, ok := fieldValue.Interface().(time.Duration); ok { // special handling for time.Duration, which is a int64 - var value time.Duration - var err error + var ( + value time.Duration + err error + ) + if tags.defaultValue != "" { value, err = time.ParseDuration(tags.defaultValue) if err != nil { @@ -190,8 +207,11 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref fieldValue.SetInt(int64(cmd.Duration(flagName))) } } else { - var value int64 - var err error + var ( + value int64 + err error + ) + if tags.defaultValue != "" { value, err = strconv.ParseInt(tags.defaultValue, 10, 64) if err != nil { @@ -212,10 +232,13 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref } } case reflect.Uint: - var value uint64 - var err error + var ( + value uint64 + err error + ) + if tags.defaultValue != "" { - value, err = strconv.ParseUint(tags.defaultValue, 10, 64) + value, err = strconv.ParseUint(tags.defaultValue, 10, strconv.IntSize) if err != nil { return fmt.Errorf("failed to parse uint value %s for field %s: %w", tags.defaultValue, field.Name, err) } @@ -233,13 +256,15 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref fieldValue.SetUint(uint64(cmd.Uint(flagName))) } case reflect.Uint8: - var value uint64 - var err error + var value uint8 + if tags.defaultValue != "" { - value, err = strconv.ParseUint(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseUint(tags.defaultValue, 10, 8) if err != nil { return fmt.Errorf("failed to parse uint value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = uint8(valueParsed) } flag = &cli.Uint8Flag{ @@ -247,20 +272,22 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: uint8(value), //nolint: gosec + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetUint(uint64(cmd.Uint8(flagName))) } case reflect.Uint16: - var value uint64 - var err error + var value uint16 + if tags.defaultValue != "" { - value, err = strconv.ParseUint(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseUint(tags.defaultValue, 10, 16) if err != nil { return fmt.Errorf("failed to parse uint value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = uint16(valueParsed) } flag = &cli.Uint16Flag{ @@ -268,20 +295,22 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: uint16(value), //nolint: gosec + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetUint(uint64(cmd.Uint16(flagName))) } case reflect.Uint32: - var value uint64 - var err error + var value uint32 + if tags.defaultValue != "" { - value, err = strconv.ParseUint(tags.defaultValue, 10, 64) + valueParsed, err := strconv.ParseUint(tags.defaultValue, 10, 32) if err != nil { return fmt.Errorf("failed to parse uint value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = uint32(valueParsed) } flag = &cli.Uint32Flag{ @@ -289,15 +318,18 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: uint32(value), //nolint: gosec + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetUint(uint64(cmd.Uint32(flagName))) } case reflect.Uint64: - var value uint64 - var err error + var ( + value uint64 + err error + ) + if tags.defaultValue != "" { value, err = strconv.ParseUint(tags.defaultValue, 10, 64) if err != nil { @@ -317,13 +349,15 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref fieldValue.SetUint(cmd.Uint64(flagName)) } case reflect.Float32: - var value float64 - var err error + var value float32 + if tags.defaultValue != "" { - value, err = strconv.ParseFloat(tags.defaultValue, 64) + valueParsed, err := strconv.ParseFloat(tags.defaultValue, 32) if err != nil { return fmt.Errorf("failed to parse float value %s for field %s: %w", tags.defaultValue, field.Name, err) } + + value = float32(valueParsed) } flag = &cli.Float32Flag{ @@ -331,15 +365,18 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref Aliases: tags.aliases, Usage: tags.help, DefaultText: tags.defaultValue, - Value: float32(value), + Value: value, Sources: sources, } apply = func(cmd *cli.Command) { fieldValue.SetFloat(float64(cmd.Float32(flagName))) } case reflect.Float64: - var value float64 - var err error + var ( + value float64 + err error + ) + if tags.defaultValue != "" { value, err = strconv.ParseFloat(tags.defaultValue, 64) if err != nil { @@ -359,8 +396,11 @@ func (r *structReflector) processField(field reflect.StructField, fieldValue ref fieldValue.SetFloat(cmd.Float64(flagName)) } case reflect.Bool: - var value bool - var err error + var ( + value bool + err error + ) + if tags.defaultValue != "" { value, err = strconv.ParseBool(tags.defaultValue) if err != nil { @@ -412,6 +452,7 @@ func (r *structReflector) recurseStruct(anyStruct any, parents []*configFieldTag if err != nil { return err } + continue } @@ -419,10 +460,12 @@ func (r *structReflector) recurseStruct(anyStruct any, parents []*configFieldTag if fieldValue.IsNil() { fieldValue.Set(reflect.New(fieldType.Type.Elem())) } + err := r.recurseStruct(fieldValue.Interface(), nested) if err != nil { return err } + continue } @@ -446,5 +489,6 @@ func NewStructConfigurator(anyStruct any, tomlSources []cli.MapSource) (StructRe if err != nil { return nil, err } + return reflector, nil } diff --git a/examples/07_marshal/main.go b/examples/07_marshal/main.go index 5848fb8..741d5f4 100644 --- a/examples/07_marshal/main.go +++ b/examples/07_marshal/main.go @@ -29,6 +29,7 @@ func main() { if err != nil { panic(err) } + fmt.Println(asMap) // includes an integration with log/slog to convert the config struct to a recursive slog.Group structure @@ -36,5 +37,6 @@ func main() { if err != nil { panic(err) } + slog.Info("Program config loaded successfully", config) } diff --git a/go.mod b/go.mod index 079eead..9a8b631 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,25 @@ module github.com/tilebox/structconf -go 1.23.4 +go 1.24.0 require ( - github.com/BurntSushi/toml v1.5.0 - github.com/go-playground/validator/v10 v10.26.0 + github.com/BurntSushi/toml v1.6.0 + github.com/go-playground/validator/v10 v10.30.1 github.com/iancoleman/strcase v0.3.0 - github.com/samber/lo v1.50.0 - github.com/stretchr/testify v1.10.0 - github.com/urfave/cli/v3 v3.3.3 + github.com/samber/lo v1.52.0 + github.com/stretchr/testify v1.11.1 + github.com/urfave/cli/v3 v3.6.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // 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.38.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.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 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3edbf61..a34baba 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,36 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +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.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +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/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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +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/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= -github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= -github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +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/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= +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/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/marshal.go b/marshal.go index ae65528..95e9e0b 100644 --- a/marshal.go +++ b/marshal.go @@ -92,7 +92,7 @@ func marshalStruct(anyStruct any, into map[string]any, nameFromTags func(t *conf } func getFieldValue(field reflect.StructField, fieldValue reflect.Value, tags *configFieldTags) (any, error) { - switch field.Type.Kind() { //nolint:exhaustive + switch field.Type.Kind() { //nolint:exhaustive // we have a default: clause that results in an error case reflect.String: if tags.isSecret { return redactSecret(fieldValue.String()), nil @@ -126,6 +126,7 @@ func mapToSlogAttrs(m map[string]any) []any { attrs = append(attrs, slog.String(key, fmt.Sprintf("%v", value))) } } + return attrs } diff --git a/structconf.go b/structconf.go index 2525afb..bd0fc58 100644 --- a/structconf.go +++ b/structconf.go @@ -108,6 +108,7 @@ func loadConfig(configPointer any, programName string, opts ...Option) error { } tomlSources := make([]cli.MapSource, 0) + var loadConfigFlag cli.Flag if cfg.loadConfigFlagName != "" { loadConfigFlag = &cli.StringSliceFlag{ @@ -119,6 +120,7 @@ func loadConfig(configPointer any, programName string, opts ...Option) error { if err != nil { return err } + flags := config.Flags() flags = append(flags, loadConfigFlag) if duplicate := firstDuplicateFlagName(flags); duplicate != "" { @@ -149,6 +151,7 @@ func loadConfig(configPointer any, programName string, opts ...Option) error { return nil }, } + err = cmd.Run(context.Background(), os.Args) if err != nil { if stdout.Len() > 0 { @@ -156,6 +159,7 @@ func loadConfig(configPointer any, programName string, opts ...Option) error { } return err } + if stdout.Len() > 0 { // help was requested -> return an error so that we can exit return &helpRequestedError{ helpText: stdout.String(), @@ -167,10 +171,12 @@ func loadConfig(configPointer any, programName string, opts ...Option) error { if err != nil { return err } + flags := config.Flags() if loadConfigFlag != nil { flags = append(flags, loadConfigFlag) } + if duplicate := firstDuplicateFlagName(flags); duplicate != "" { return fmt.Errorf("duplicate flag: --%s", duplicate) } @@ -201,16 +207,19 @@ func loadConfig(configPointer any, programName string, opts ...Option) error { } return err } + if stdout.Len() > 0 { // help was requested -> return an error so that we can exit return &helpRequestedError{ helpText: strings.TrimSpace(stdout.String()), } } + return nil } func firstDuplicateFlagName(flags []cli.Flag) string { seen := make(map[string]bool) + for _, flag := range flags { for _, name := range flag.Names() { isDuplicate, ok := seen[name] @@ -220,5 +229,6 @@ func firstDuplicateFlagName(flags []cli.Flag) string { seen[name] = true } } + return "" } diff --git a/structconf_test.go b/structconf_test.go index 9f49ce6..100d157 100644 --- a/structconf_test.go +++ b/structconf_test.go @@ -49,11 +49,14 @@ func Test_loadConfigFullyTagged(t *testing.T) { wantDuration: 2 * time.Second, }, } + _ = tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &config{} + SetArgsForTest(t, tt.args.cliArgs) // set cli args, and clean up after the test + err := loadConfig(config, "my-program", WithDefaultLoadConfigFlag()) require.NoError(t, err) @@ -102,11 +105,14 @@ func Test_loadConfigDefaultTags(t *testing.T) { wantDuration: 2 * time.Second, }, } + _ = tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &config{} + SetArgsForTest(t, tt.args.cliArgs) // set cli args, and clean up after the test + err := loadConfig(config, "my-program", WithDefaultLoadConfigFlag()) require.NoError(t, err) @@ -196,6 +202,7 @@ duration = "1m5s" wantDuration: 10 * time.Second, }, } + _ = tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -291,6 +298,7 @@ func Test_loadConfigExtraFlags(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { SetArgsForTest(t, []string{"my-program", "--some-string", "hello", "--some-int", "42", "--unknown-flag", "value"}) + err := loadConfig(tt.cfg, "my-program", tt.loadOpts...) require.Error(t, err) assert.Contains(t, err.Error(), "flag provided but not defined: -unknown-flag") @@ -307,6 +315,7 @@ func Test_PrintCorrectUsage(t *testing.T) { } SetArgsForTest(t, []string{"my-program", "--unknown-value", "to_trigger_usage"}) + err := loadConfig(&config{}, "my-program") require.Error(t, err) @@ -353,6 +362,7 @@ func Test_loadConfigDuplicates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { SetArgsForTest(t, []string{"my-program"}) // no args set + err := loadConfig(tt.cfg, "my-program") if tt.wantError != "" { require.Error(t, err) @@ -366,9 +376,11 @@ func Test_loadConfigDuplicates(t *testing.T) { func SetArgsForTest(t *testing.T, args []string) { oldArgs := os.Args + t.Cleanup(func() { os.Args = oldArgs }) + os.Args = args } @@ -383,6 +395,7 @@ func Test_validate(t *testing.T) { type args struct { config any } + tests := []struct { name string args args diff --git a/tags.go b/tags.go index b80cafa..9921637 100644 --- a/tags.go +++ b/tags.go @@ -90,12 +90,13 @@ func parseTags(tag *reflect.StructTag) *configFieldTags { defaultValue: tag.Get("default"), help: tag.Get("help"), } + alias := tag.Get("alias") if alias != "" { - parts := strings.Split(alias, ",") - for _, part := range parts { - if strings.HasPrefix(part, "-") { - parsed.aliases = append(parsed.aliases, strings.TrimPrefix(part, "-")) + parts := strings.SplitSeq(alias, ",") + for part := range parts { + if after, ok := strings.CutPrefix(part, "-"); ok { + parsed.aliases = append(parsed.aliases, after) } } } @@ -112,17 +113,22 @@ func parseTagsWithFieldNameDefault(tag *reflect.StructTag, fieldName string) *co if isExported && tags.flag == "" { tags.flag = kebab } + if isExported && tags.json == "" { tags.json = strcase.ToLowerCamel(fieldName) } + if isExported && tags.toml == "" { tags.toml = kebab } + if isExported && tags.yaml == "" { tags.yaml = kebab } + if isExported && tags.env == "" { tags.env = strcase.ToScreamingSnake(fieldName) } + return tags } diff --git a/toml.go b/toml.go index fda4f3e..c4a6cf7 100644 --- a/toml.go +++ b/toml.go @@ -63,7 +63,7 @@ func (ms *mapSource) Lookup(name string) (any, bool) { case map[string]any: node = make(map[any]any, len(child)) for k, v := range child { - node[k] = v + node[k] = v //nolint: modernize } case map[any]any: node = child @@ -100,6 +100,7 @@ func (mvs *mapsValueSource) Lookup() (string, bool) { return fmt.Sprintf("%+v", v), true } } + return "", false } diff --git a/validate.go b/validate.go index c2d5b22..785d1e9 100644 --- a/validate.go +++ b/validate.go @@ -10,11 +10,13 @@ import ( func validate(configPointer any) error { configValidator := validator.New(validator.WithRequiredStructEnabled()) + err := configValidator.Struct(configPointer) if err != nil { var validationErrors validator.ValidationErrors if errors.As(err, &validationErrors) { errorMessage := &bytes.Buffer{} + for _, fieldError := range validationErrors { validationTag := fieldError.Tag() if validationTag == "required" { @@ -23,8 +25,10 @@ func validate(configPointer any) error { fmt.Fprintf(errorMessage, "Configuration error: %s - %s\n", fieldError.StructField(), fieldError.ActualTag()) } } + return errors.New(errorMessage.String()) } } + return nil }