├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── complex-etcd │ ├── data.json │ ├── koanfetcd.sh │ ├── main.go │ ├── req1.json │ ├── req2.json │ └── req3.json ├── default-values │ └── main.go ├── go.mod ├── go.sum ├── read-appconfig │ └── main.go ├── read-azkeyvault │ └── main.go ├── read-commandline │ └── main.go ├── read-consul │ ├── data.json │ ├── koanfconsul.sh │ ├── main.go │ ├── req1.json │ └── req2.json ├── read-environment │ └── main.go ├── read-file │ └── main.go ├── read-parameterstore │ ├── README.md │ └── main.go ├── read-raw-bytes │ └── main.go ├── read-s3 │ └── main.go ├── read-secretsmanager │ └── README.md ├── read-struct │ └── main.go ├── read-vault │ └── main.go ├── unmarshal-flat │ └── main.go └── unmarshal │ └── main.go ├── getters.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── interfaces.go ├── koanf.go ├── maps ├── go.mod ├── go.sum └── maps.go ├── mock ├── mock.env ├── mock.hcl ├── mock.hjson ├── mock.json ├── mock.prop ├── mock.toml └── mock.yml ├── options.go ├── parsers ├── dotenv │ ├── dotenv.go │ ├── dotenv_test.go │ ├── go.mod │ └── go.sum ├── hcl │ ├── go.mod │ ├── go.sum │ ├── hcl.go │ └── hcl_test.go ├── hjson │ ├── go.mod │ ├── go.sum │ ├── hjson.go │ └── hjson_test.go ├── json │ ├── go.mod │ ├── go.sum │ ├── json.go │ └── json_test.go ├── kdl │ ├── go.mod │ ├── go.sum │ ├── kdl.go │ └── kdl_test.go ├── nestedtext │ ├── go.mod │ ├── go.sum │ ├── nestedtext.go │ └── nt_test.go ├── toml │ ├── go.mod │ ├── go.sum │ ├── toml.go │ └── toml_test.go └── yaml │ ├── go.mod │ ├── go.sum │ ├── yaml.go │ └── yaml_test.go ├── providers ├── appconfig │ ├── appconfig.go │ ├── go.mod │ └── go.sum ├── azkeyvault │ ├── azkeyvault.go │ ├── go.mod │ └── go.sum ├── basicflag │ ├── basicflag.go │ ├── go.mod │ └── go.sum ├── cliflagv2 │ ├── cliflagv2.go │ ├── cliflagv2_test.go │ ├── go.mod │ └── go.sum ├── cliflagv3 │ ├── cliflagv3.go │ ├── cliflagv3_test.go │ ├── go.mod │ └── go.sum ├── confmap │ ├── confmap.go │ ├── go.mod │ └── go.sum ├── consul │ ├── consul.go │ ├── go.mod │ └── go.sum ├── env │ ├── env.go │ ├── env_test.go │ ├── go.mod │ └── go.sum ├── etcd │ ├── etcd.go │ ├── go.mod │ └── go.sum ├── file │ ├── file.go │ ├── go.mod │ └── go.sum ├── fs │ ├── fs.go │ └── go.mod ├── nats │ ├── go.mod │ ├── go.sum │ ├── nats.go │ ├── nats_test.go │ └── testrunner_test.go ├── parameterstore │ ├── go.mod │ ├── go.sum │ ├── parameterstore.go │ └── parameterstore_test.go ├── posflag │ ├── go.mod │ ├── go.sum │ └── posflag.go ├── rawbytes │ ├── go.mod │ └── rawbytes.go ├── s3 │ ├── go.mod │ ├── go.sum │ └── s3.go ├── structs │ ├── go.mod │ ├── go.sum │ ├── structs.go │ └── structs_test.go └── vault │ ├── go.mod │ ├── go.sum │ └── vault.go └── tests ├── fs_test.go ├── go.mod ├── go.sum ├── koanf_test.go ├── maps_test.go ├── posflag_test.go └── textmarshal_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps / breaking unit test / example to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Please provide the following information):** 20 | - OS: [e.g. linux/osx/windows] 21 | - Koanf Version [e.g. v1.0.0] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | # Triggers the workflow on push or pull request events 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go: [ '1.23', '1.24' ] 11 | 12 | runs-on: ubuntu-latest 13 | 14 | name: Go ${{ matrix.go }} Tests 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go }} 22 | check-latest: true 23 | 24 | - name: Run all tests 25 | run: go test -v github.com/knadh/koanf... 26 | 27 | - name: Run Coverage 28 | run: go test -v -cover ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # IDE 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019, Kailash Nadh. https://github.com/knadh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/complex-etcd/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent1": "father", 3 | "parent2": "mother", 4 | "child1": "Alex", 5 | "child2": "Julia", 6 | "child3": "Michael", 7 | "child4": "Natalie", 8 | "child5": "Rosalind", 9 | "child6": "Tim" 10 | } 11 | -------------------------------------------------------------------------------- /examples/complex-etcd/koanfetcd.sh: -------------------------------------------------------------------------------- 1 | # Testing script. Requirements: installed etcd 2 | 3 | # setting the directory for the etcd 4 | CURRENT_DIR=`pwd` 5 | ETCD_TESTDIR="$CURRENT_DIR/etcdtest" 6 | 7 | etcd --data-dir "$ETCD_TESTDIR" & 8 | 9 | etcdctl endpoint health --dial-timeout=4s 10 | 11 | # main test 12 | echo -e "\nChecking...\n" 13 | go run main.go 14 | 15 | ETCD_PID=`pidof etcd` 16 | kill -9 $ETCD_PID 17 | 18 | rm -rf "$ETCD_TESTDIR" 19 | #rm k_etcd_check 20 | -------------------------------------------------------------------------------- /examples/complex-etcd/req1.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent1": "father", 3 | "parent2": "mother" 4 | } 5 | -------------------------------------------------------------------------------- /examples/complex-etcd/req2.json: -------------------------------------------------------------------------------- 1 | { 2 | "child1": "Alex", 3 | "child2": "Julia", 4 | "child3": "Michael", 5 | "child4": "Natalie", 6 | "child5": "Rosalind", 7 | "child6": "Tim" 8 | } 9 | -------------------------------------------------------------------------------- /examples/complex-etcd/req3.json: -------------------------------------------------------------------------------- 1 | { 2 | "child1": "Alex", 3 | "child2": "Julia", 4 | "child3": "Michael", 5 | "child4": "Natalie" 6 | } 7 | -------------------------------------------------------------------------------- /examples/default-values/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/knadh/koanf/v2" 8 | "github.com/knadh/koanf/parsers/json" 9 | "github.com/knadh/koanf/parsers/yaml" 10 | "github.com/knadh/koanf/providers/confmap" 11 | "github.com/knadh/koanf/providers/file" 12 | ) 13 | 14 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 15 | var k = koanf.New(".") 16 | 17 | func main() { 18 | // Load default values using the confmap provider. 19 | // We provide a flat map with the "." delimiter. 20 | // A nested map can be loaded by setting the delimiter to an empty string "". 21 | k.Load(confmap.Provider(map[string]interface{}{ 22 | "parent1.name": "Default Name", 23 | "parent3.name": "New name here", 24 | }, "."), nil) 25 | 26 | // Load JSON config on top of the default values. 27 | if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil { 28 | log.Fatalf("error loading config: %v", err) 29 | } 30 | 31 | // Load YAML config and merge into the previously loaded config (because we can). 32 | k.Load(file.Provider("mock/mock.yml"), yaml.Parser()) 33 | 34 | fmt.Println("parent's name is = ", k.String("parent1.name")) 35 | fmt.Println("parent's ID is = ", k.Int("parent1.id")) 36 | } 37 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/examples 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 7 | github.com/aws/aws-sdk-go-v2 v1.21.0 8 | github.com/aws/aws-sdk-go-v2/config v1.18.37 9 | github.com/aws/aws-sdk-go-v2/service/ssm v1.37.5 10 | github.com/hashicorp/consul/api v1.19.1 11 | github.com/knadh/koanf/parsers/json v0.1.0 12 | github.com/knadh/koanf/parsers/toml v0.1.0 13 | github.com/knadh/koanf/parsers/yaml v0.1.0 14 | github.com/knadh/koanf/providers/appconfig/v2 v2.0.0 15 | github.com/knadh/koanf/providers/azkeyvault v0.0.0-20250408181429-ede86a07342e 16 | github.com/knadh/koanf/providers/confmap v0.1.0 17 | github.com/knadh/koanf/providers/consul/v2 v2.0.0 18 | github.com/knadh/koanf/providers/env v0.1.0 19 | github.com/knadh/koanf/providers/etcd/v2 v2.0.0 20 | github.com/knadh/koanf/providers/file v0.1.0 21 | github.com/knadh/koanf/providers/parameterstore/v2 v2.0.0 22 | github.com/knadh/koanf/providers/posflag v0.1.0 23 | github.com/knadh/koanf/providers/rawbytes v0.1.0 24 | github.com/knadh/koanf/providers/s3 v0.1.0 25 | github.com/knadh/koanf/providers/structs v0.1.0 26 | github.com/knadh/koanf/providers/vault/v2 v2.0.1 27 | github.com/knadh/koanf/v2 v2.1.0 28 | github.com/spf13/pflag v1.0.5 29 | go.etcd.io/etcd/client/v3 v3.5.7 30 | ) 31 | 32 | require ( 33 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect 34 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect 35 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect 36 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect 37 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect 38 | github.com/armon/go-metrics v0.4.1 // indirect 39 | github.com/aws/aws-sdk-go-v2/credentials v1.13.35 // indirect 40 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect 41 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect 42 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect 43 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect 44 | github.com/aws/aws-sdk-go-v2/service/appconfig v1.16.1 // indirect 45 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect 46 | github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 // indirect 47 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 // indirect 48 | github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect 49 | github.com/aws/smithy-go v1.14.2 // indirect 50 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 51 | github.com/coreos/go-semver v0.3.1 // indirect 52 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 53 | github.com/fatih/color v1.16.0 // indirect 54 | github.com/fatih/structs v1.1.0 // indirect 55 | github.com/fsnotify/fsnotify v1.6.0 // indirect 56 | github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect 57 | github.com/gogo/protobuf v1.3.2 // indirect 58 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 59 | github.com/golang/protobuf v1.5.3 // indirect 60 | github.com/google/uuid v1.6.0 // indirect 61 | github.com/hashicorp/errwrap v1.1.0 // indirect 62 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 63 | github.com/hashicorp/go-hclog v1.6.3 // indirect 64 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 65 | github.com/hashicorp/go-multierror v1.1.1 // indirect 66 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 67 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 68 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect 69 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 70 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 71 | github.com/hashicorp/golang-lru v0.5.4 // indirect 72 | github.com/hashicorp/hcl v1.0.0 // indirect 73 | github.com/hashicorp/serf v0.10.1 // indirect 74 | github.com/hashicorp/vault/api v1.9.0 // indirect 75 | github.com/jmespath/go-jmespath v0.4.0 // indirect 76 | github.com/knadh/koanf/maps v0.1.1 // indirect 77 | github.com/kylelemons/godebug v1.1.0 // indirect 78 | github.com/mattn/go-colorable v0.1.13 // indirect 79 | github.com/mattn/go-isatty v0.0.20 // indirect 80 | github.com/mitchellh/copystructure v1.2.0 // indirect 81 | github.com/mitchellh/go-homedir v1.1.0 // indirect 82 | github.com/mitchellh/mapstructure v1.5.0 // indirect 83 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 84 | github.com/pelletier/go-toml v1.9.5 // indirect 85 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 86 | github.com/rhnvrm/simples3 v0.8.3 // indirect 87 | github.com/ryanuber/go-glob v1.0.0 // indirect 88 | github.com/stretchr/objx v0.5.0 // indirect 89 | go.etcd.io/etcd/api/v3 v3.5.7 // indirect 90 | go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect 91 | go.uber.org/atomic v1.10.0 // indirect 92 | go.uber.org/multierr v1.9.0 // indirect 93 | go.uber.org/zap v1.24.0 // indirect 94 | golang.org/x/crypto v0.36.0 // indirect 95 | golang.org/x/net v0.38.0 // indirect 96 | golang.org/x/sys v0.31.0 // indirect 97 | golang.org/x/text v0.23.0 // indirect 98 | golang.org/x/time v0.3.0 // indirect 99 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 100 | google.golang.org/grpc v1.56.3 // indirect 101 | google.golang.org/protobuf v1.33.0 // indirect 102 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 103 | gopkg.in/yaml.v3 v3.0.1 // indirect 104 | ) 105 | -------------------------------------------------------------------------------- /examples/read-appconfig/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/knadh/koanf/parsers/json" 8 | "github.com/knadh/koanf/providers/appconfig/v2" 9 | "github.com/knadh/koanf/v2" 10 | ) 11 | 12 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 13 | var k = koanf.New(".") 14 | 15 | func main() { 16 | provider, err := appconfig.Provider(appconfig.Config{ 17 | Application: os.Getenv("AWS_APPCONFIG_APPLICATION"), 18 | ClientID: os.Getenv("AWS_APPCONFIG_CLIENT_ID"), 19 | Configuration: os.Getenv("AWS_APPCONFIG_CONFIG_NAME"), 20 | Environment: os.Getenv("AWS_APPCONFIG_ENVIRONMENT"), 21 | }) 22 | if err != nil { 23 | log.Fatalf("Failed to instantiate appconfig provider: %v", err) 24 | } 25 | 26 | // Load the provider and parse configuration as JSON. 27 | if err := k.Load(provider, json.Parser()); err != nil { 28 | log.Fatalf("error loading config: %v", err) 29 | } 30 | 31 | k.Print() 32 | 33 | // Watch for all configuration updates. 34 | provider.Watch(func(event interface{}, err error) { 35 | if err != nil { 36 | log.Printf("watch error: %v", err) 37 | return 38 | } 39 | 40 | log.Println("config changed. Reloading ...") 41 | k = koanf.New(".") 42 | k.Load(provider, json.Parser()) 43 | k.Print() 44 | }) 45 | 46 | log.Println("waiting forever.") 47 | <-make(chan bool) 48 | } 49 | -------------------------------------------------------------------------------- /examples/read-azkeyvault/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 6 | "github.com/knadh/koanf/providers/azkeyvault" 7 | "github.com/knadh/koanf/v2" 8 | "log" 9 | "os" 10 | ) 11 | 12 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 13 | var k = koanf.New(".") 14 | 15 | func main() { 16 | clientId := os.Getenv("ARM_CLIENT_ID") 17 | clientSecret := os.Getenv("ARM_CLIENT_SECRET") 18 | tenantId := os.Getenv("ARM_TENANT_ID") 19 | 20 | tokenCred, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret, nil) 21 | if err != nil { 22 | log.Fatalf("error creating token credential: %v", err) 23 | } 24 | 25 | config := azkeyvault.Config{ 26 | KeyVaultUrl: "https://mykeyvault.vault.azure.net", 27 | TokenCredential: tokenCred, 28 | } 29 | 30 | provider, err := azkeyvault.Provider(config) 31 | if err != nil { 32 | log.Fatalf("Failed to instantiate azure key vault provider: %v", err) 33 | } 34 | 35 | if err := k.Load(provider, nil); err != nil { 36 | log.Fatalf("error loading config: %v", err) 37 | } 38 | 39 | fmt.Println("database's host is = ", k.String("database.host")) 40 | fmt.Println("database's port is = ", k.Int("database.port")) 41 | } 42 | -------------------------------------------------------------------------------- /examples/read-commandline/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/knadh/koanf/parsers/toml" 9 | "github.com/knadh/koanf/providers/file" 10 | "github.com/knadh/koanf/providers/posflag" 11 | "github.com/knadh/koanf/v2" 12 | flag "github.com/spf13/pflag" 13 | ) 14 | 15 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 16 | var k = koanf.New(".") 17 | 18 | func main() { 19 | // Use the POSIX compliant pflag lib instead of Go's flag lib. 20 | f := flag.NewFlagSet("config", flag.ContinueOnError) 21 | f.Usage = func() { 22 | fmt.Println(f.FlagUsages()) 23 | os.Exit(0) 24 | } 25 | // Path to one or more config files to load into koanf along with some config params. 26 | f.StringSlice("conf", []string{"mock/mock.toml"}, "path to one or more .toml config files") 27 | f.String("time", "2020-01-01", "a time string") 28 | f.String("type", "xxx", "type of the app") 29 | f.Parse(os.Args[1:]) 30 | 31 | // Load the config files provided in the commandline. 32 | cFiles, _ := f.GetStringSlice("conf") 33 | for _, c := range cFiles { 34 | if err := k.Load(file.Provider(c), toml.Parser()); err != nil { 35 | log.Fatalf("error loading file: %v", err) 36 | } 37 | } 38 | 39 | // "time" and "type" may have been loaded from the config file, but 40 | // they can still be overridden with the values from the command line. 41 | // The bundled posflag.Provider takes a flagset from the spf13/pflag lib. 42 | // Passing the Koanf instance to posflag helps it deal with default command 43 | // line flag values that are not present in conf maps from previously loaded 44 | // providers. 45 | if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil { 46 | log.Fatalf("error loading config: %v", err) 47 | } 48 | 49 | fmt.Println("time is = ", k.String("time")) 50 | } 51 | -------------------------------------------------------------------------------- /examples/read-consul/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent1": "father", 3 | "parent2": "mother", 4 | "child1": "Alex", 5 | "child2": "Julia", 6 | "child3": "Michael", 7 | "child4": "Natalie", 8 | "child5": "Rosalind", 9 | "child6": "Tim" 10 | } 11 | -------------------------------------------------------------------------------- /examples/read-consul/koanfconsul.sh: -------------------------------------------------------------------------------- 1 | consul agent -dev & 2 | 3 | exitcode=1 4 | iterations=0 5 | 6 | while [ $exitcode -ne 0 ] 7 | do 8 | consul members 9 | exitcode=$? 10 | 11 | sleep 1 12 | ((iterations++)) 13 | 14 | if [ $iterations -gt 5 ]; then 15 | break 16 | fi 17 | done 18 | 19 | if [ $exitcode -eq 0 ]; then 20 | echo -e "\nTest program running..." 21 | go run main.go 22 | echo -e "\nShutdown..." 23 | consul leave 24 | else 25 | echo "Consul server is unavailable." 26 | consul leave 27 | fi 28 | -------------------------------------------------------------------------------- /examples/read-consul/req1.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent1": "father", 3 | "parent2": "mother" 4 | } 5 | -------------------------------------------------------------------------------- /examples/read-consul/req2.json: -------------------------------------------------------------------------------- 1 | { 2 | "child1": "Alex", 3 | "child2": "Julia", 4 | "child3": "Michael", 5 | "child4": "Natalie", 6 | "child5": "Rosalind", 7 | "child6": "Tim" 8 | } 9 | -------------------------------------------------------------------------------- /examples/read-environment/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/knadh/koanf/parsers/json" 9 | "github.com/knadh/koanf/providers/env" 10 | "github.com/knadh/koanf/providers/file" 11 | "github.com/knadh/koanf/v2" 12 | ) 13 | 14 | var k = koanf.New(".") 15 | 16 | func main() { 17 | // Load JSON config. 18 | if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil { 19 | log.Fatalf("error loading config: %v", err) 20 | } 21 | 22 | // Load environment variables and merge into the loaded config. 23 | // "MYVAR" is the prefix to filter the env vars by. 24 | // "." is the delimiter used to represent the key hierarchy in env vars. 25 | // The (optional, or can be nil) function can be used to transform 26 | // the env var names, for instance, to lowercase them. 27 | // 28 | // For example, env vars: MYVAR_TYPE and MYVAR_PARENT1_CHILD1_NAME 29 | // will be merged into the "type" and the nested "parent1.child1.name" 30 | // keys in the config file here as we lowercase the key, 31 | // replace `_` with `.` and strip the MYVAR_ prefix so that 32 | // only "parent1.child1.name" remains. 33 | k.Load(env.Provider("MYVAR_", ".", func(s string) string { 34 | return strings.Replace(strings.ToLower( 35 | strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1) 36 | }), nil) 37 | 38 | // Use ProviderWithValue() to process both keys and values into types other than strings, 39 | // for example, turn space separated env vars into slices. 40 | // k.Load(env.ProviderWithValue("MYVAR_", ".", func(s string, v string) (string, interface{}) { 41 | // // Strip out the MYVAR_ prefix and lowercase and get the key while also replacing 42 | // // the _ character with . in the key (koanf delimiter). 43 | // key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1) 44 | 45 | // // If there is a space in the value, split the value into a slice by the space. 46 | // if strings.Contains(v, " ") { 47 | // return key, strings.Split(v, " ") 48 | // } 49 | 50 | // // Otherwise, return the plain string. 51 | // return key, v 52 | // }), nil) 53 | 54 | fmt.Println("name is = ", k.String("parent1.child1.name")) 55 | } 56 | -------------------------------------------------------------------------------- /examples/read-file/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/knadh/koanf/parsers/json" 8 | "github.com/knadh/koanf/parsers/yaml" 9 | "github.com/knadh/koanf/providers/file" 10 | "github.com/knadh/koanf/v2" 11 | ) 12 | 13 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 14 | var k = koanf.New(".") 15 | 16 | func main() { 17 | // Load JSON config. 18 | f := file.Provider("mock/mock.json") 19 | if err := k.Load(f, json.Parser()); err != nil { 20 | log.Fatalf("error loading config: %v", err) 21 | } 22 | 23 | // Load YAML config and merge into the previously loaded config (because we can). 24 | k.Load(file.Provider("mock/mock.yml"), yaml.Parser()) 25 | 26 | fmt.Println("parent's name is = ", k.String("parent1.name")) 27 | fmt.Println("parent's ID is = ", k.Int("parent1.id")) 28 | 29 | // Watch the file and get a callback on change. The callback can do whatever, 30 | // like re-load the configuration. 31 | // File provider always returns a nil `event`. 32 | f.Watch(func(event interface{}, err error) { 33 | if err != nil { 34 | log.Printf("watch error: %v", err) 35 | return 36 | } 37 | 38 | log.Println("config changed. Reloading ...") 39 | k.Load(f, json.Parser()) 40 | k.Print() 41 | }) 42 | 43 | // Block forever (and manually make a change to mock/mock.json) to 44 | // reload the config. 45 | log.Println("waiting forever. Try making a change to mock/mock.json to live reload") 46 | <-make(chan bool) 47 | } 48 | -------------------------------------------------------------------------------- /examples/read-parameterstore/README.md: -------------------------------------------------------------------------------- 1 | # AWS Parameter Store Example 2 | 3 | ## Link 4 | [another example](https://github.com/defensestation/koanf/blob/main/examples/read-parameterstore/main.go) 5 | -------------------------------------------------------------------------------- /examples/read-parameterstore/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/service/ssm" 11 | "github.com/aws/aws-sdk-go-v2/service/ssm/types" 12 | "github.com/knadh/koanf/providers/parameterstore/v2" 13 | "github.com/knadh/koanf/v2" 14 | ) 15 | 16 | var k = koanf.New(".") 17 | 18 | func main() { 19 | // The configuration values are read from the environment variables. 20 | c, err := config.LoadDefaultConfig(context.TODO()) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | client := ssm.NewFromConfig(c) 25 | 26 | for k, v := range map[string]string{ 27 | "parent1": "alice", 28 | "parent2.child1": "bob", 29 | "parent2.child2.grandchild1": "carol", 30 | } { 31 | if _, err := client.PutParameter(context.TODO(), &ssm.PutParameterInput{ 32 | Name: aws.String(k), 33 | Value: aws.String(v), 34 | Type: types.ParameterTypeSecureString, 35 | Overwrite: aws.Bool(true), 36 | }); err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | 41 | // Get a parameter. 42 | if err := k.Load(parameterstore.ProviderWithClient(parameterstore.Config[ssm.GetParameterInput]{ 43 | Delim: ".", 44 | Input: ssm.GetParameterInput{Name: aws.String("parent1"), WithDecryption: aws.Bool(true)}, 45 | }, client), nil); err != nil { 46 | log.Fatalf("error loading config: %v", err) 47 | } 48 | fmt.Println(k.Sprint()) 49 | 50 | // Get parameters. 51 | if err := k.Load(parameterstore.ProviderWithClient(parameterstore.Config[ssm.GetParametersInput]{ 52 | Delim: ".", 53 | Input: ssm.GetParametersInput{Names: []string{"parent1", "parent2.child1"}, WithDecryption: aws.Bool(true)}, 54 | }, client), nil); err != nil { 55 | log.Fatalf("error loading config: %v", err) 56 | } 57 | fmt.Println(k.Sprint()) 58 | 59 | // Get parameters by path. 60 | if err := k.Load(parameterstore.ProviderWithClient(parameterstore.Config[ssm.GetParametersByPathInput]{ 61 | Delim: ".", 62 | Input: ssm.GetParametersByPathInput{Path: aws.String("/"), WithDecryption: aws.Bool(true)}, 63 | }, client), nil); err != nil { 64 | log.Fatalf("error loading config: %v", err) 65 | } 66 | fmt.Println(k.Sprint()) 67 | } 68 | -------------------------------------------------------------------------------- /examples/read-raw-bytes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/knadh/koanf/parsers/json" 7 | "github.com/knadh/koanf/providers/rawbytes" 8 | "github.com/knadh/koanf/v2" 9 | ) 10 | 11 | // Global koanf instance. Use . as the key path delimiter. This can be / or anything. 12 | var k = koanf.New(".") 13 | 14 | func main() { 15 | b := []byte(`{"type": "rawbytes", "parent1": {"child1": {"type": "rawbytes"}}}`) 16 | k.Load(rawbytes.Provider(b), json.Parser()) 17 | fmt.Println("type is = ", k.String("parent1.child1.type")) 18 | } 19 | -------------------------------------------------------------------------------- /examples/read-s3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/knadh/koanf/parsers/json" 9 | "github.com/knadh/koanf/providers/s3" 10 | "github.com/knadh/koanf/v2" 11 | ) 12 | 13 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 14 | var k = koanf.New(".") 15 | 16 | func main() { 17 | // Load JSON config from s3. 18 | if err := k.Load(s3.Provider(s3.Config{ 19 | AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"), 20 | SecretKey: os.Getenv("AWS_S3_SECRET_KEY"), 21 | Region: os.Getenv("AWS_S3_REGION"), 22 | Bucket: os.Getenv("AWS_S3_BUCKET"), 23 | ObjectKey: "mock/mock.json", 24 | }), json.Parser()); err != nil { 25 | log.Fatalf("error loading config: %v", err) 26 | } 27 | 28 | fmt.Println("parent's name is = ", k.String("parent1.name")) 29 | fmt.Println("parent's ID is = ", k.Int("parent1.id")) 30 | } 31 | -------------------------------------------------------------------------------- /examples/read-secretsmanager/README.md: -------------------------------------------------------------------------------- 1 | # AWS Secrets Manager Example 2 | 3 | ## Link 4 | [example](https://github.com/defensestation/koanf/blob/main/examples/read-secretsmanager/main.go) -------------------------------------------------------------------------------- /examples/read-struct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/knadh/koanf/providers/structs" 7 | "github.com/knadh/koanf/v2" 8 | ) 9 | 10 | var k = koanf.New(".") 11 | 12 | type parentStruct struct { 13 | Name string `koanf:"name"` 14 | ID int `koanf:"id"` 15 | Child1 childStruct `koanf:"child1"` 16 | } 17 | type childStruct struct { 18 | Name string `koanf:"name"` 19 | Type string `koanf:"type"` 20 | Empty map[string]string `koanf:"empty"` 21 | Grandchild1 grandchildStruct `koanf:"grandchild1"` 22 | } 23 | type grandchildStruct struct { 24 | Ids []int `koanf:"ids"` 25 | On bool `koanf:"on"` 26 | } 27 | type sampleStruct struct { 28 | Type string `koanf:"type"` 29 | Empty map[string]string `koanf:"empty"` 30 | Parent1 parentStruct `koanf:"parent1"` 31 | } 32 | 33 | func main() { 34 | s := sampleStruct{ 35 | Type: "json", 36 | Empty: make(map[string]string), 37 | Parent1: parentStruct{ 38 | Name: "parent1", 39 | ID: 1234, 40 | Child1: childStruct{ 41 | Name: "child1", 42 | Type: "json", 43 | Empty: make(map[string]string), 44 | Grandchild1: grandchildStruct{ 45 | Ids: []int{1, 2, 3}, 46 | On: true, 47 | }, 48 | }, 49 | }, 50 | } 51 | 52 | k.Load(structs.Provider(s, "koanf"), nil) 53 | 54 | fmt.Printf("name is = `%s`\n", k.String("parent1.child1.name")) 55 | } 56 | -------------------------------------------------------------------------------- /examples/read-vault/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/knadh/koanf/providers/vault/v2" 10 | "github.com/knadh/koanf/v2" 11 | ) 12 | 13 | // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. 14 | var k = koanf.New(".") 15 | 16 | func main() { 17 | provider, err := vault.Provider(vault.Config{ 18 | Address: os.Getenv("VAULT_ADDRESS"), 19 | Token: os.Getenv("VAULT_TOKEN"), 20 | Path: "secret/data/my-app", 21 | Timeout: 10 * time.Second, 22 | 23 | // If this is set to false, then `data` and `metadata` keys 24 | // from Vault are fetched. All config is then accessed as 25 | // k.String("data.YOUR_KEY") etc. instead of k.String("YOUR_KEY"). 26 | ExcludeMeta: true, 27 | }) 28 | if err != nil { 29 | log.Fatalf("Failed to instantiate vault provider: %v", err) 30 | } 31 | // Load mapped config from Vault storage. 32 | if err := k.Load(provider, nil); err != nil { 33 | log.Fatalf("error loading config: %v", err) 34 | } 35 | 36 | fmt.Println("database's host is = ", k.String("database.host")) 37 | fmt.Println("database's port is = ", k.Int("database.port")) 38 | } 39 | -------------------------------------------------------------------------------- /examples/unmarshal-flat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/knadh/koanf/parsers/json" 8 | "github.com/knadh/koanf/providers/file" 9 | "github.com/knadh/koanf/v2" 10 | ) 11 | 12 | // Global koanf instance. Use . as the key path delimiter. This can be / or anything. 13 | var k = koanf.New(".") 14 | 15 | func main() { 16 | // Load JSON config. 17 | if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil { 18 | log.Fatalf("error loading config: %v", err) 19 | } 20 | 21 | type rootFlat struct { 22 | Type string `koanf:"type"` 23 | Empty map[string]string `koanf:"empty"` 24 | Parent1Name string `koanf:"parent1.name"` 25 | Parent1ID int `koanf:"parent1.id"` 26 | Parent1Child1Name string `koanf:"parent1.child1.name"` 27 | Parent1Child1Type string `koanf:"parent1.child1.type"` 28 | Parent1Child1Empty map[string]string `koanf:"parent1.child1.empty"` 29 | Parent1Child1Grandchild1IDs []int `koanf:"parent1.child1.grandchild1.ids"` 30 | Parent1Child1Grandchild1On bool `koanf:"parent1.child1.grandchild1.on"` 31 | } 32 | 33 | // Unmarshal the whole root with FlatPaths: True. 34 | var o1 rootFlat 35 | k.UnmarshalWithConf("", &o1, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}) 36 | fmt.Println(o1) 37 | 38 | // Unmarshal a child structure of "parent1". 39 | type subFlat struct { 40 | Name string `koanf:"name"` 41 | ID int `koanf:"id"` 42 | Child1Name string `koanf:"child1.name"` 43 | Child1Type string `koanf:"child1.type"` 44 | Child1Empty map[string]string `koanf:"child1.empty"` 45 | Child1Grandchild1IDs []int `koanf:"child1.grandchild1.ids"` 46 | Child1Grandchild1On bool `koanf:"child1.grandchild1.on"` 47 | } 48 | 49 | var o2 subFlat 50 | k.UnmarshalWithConf("parent1", &o2, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}) 51 | fmt.Println(o2) 52 | } 53 | -------------------------------------------------------------------------------- /examples/unmarshal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/knadh/koanf/parsers/json" 8 | "github.com/knadh/koanf/providers/file" 9 | "github.com/knadh/koanf/v2" 10 | ) 11 | 12 | // Global koanf instance. Use . as the key path delimiter. This can be / or anything. 13 | var ( 14 | k = koanf.New(".") 15 | parser = json.Parser() 16 | ) 17 | 18 | func main() { 19 | // Load JSON config. 20 | if err := k.Load(file.Provider("mock/mock.json"), parser); err != nil { 21 | log.Fatalf("error loading config: %v", err) 22 | } 23 | 24 | // Structure to unmarshal nested conf to. 25 | type childStruct struct { 26 | Name string `koanf:"name"` 27 | Type string `koanf:"type"` 28 | Empty map[string]string `koanf:"empty"` 29 | GrandChild struct { 30 | Ids []int `koanf:"ids"` 31 | On bool `koanf:"on"` 32 | } `koanf:"grandchild1"` 33 | } 34 | 35 | var out childStruct 36 | 37 | // Quick unmarshal. 38 | k.Unmarshal("parent1.child1", &out) 39 | fmt.Println(out) 40 | 41 | // Unmarshal with advanced config. 42 | out = childStruct{} 43 | k.UnmarshalWithConf("parent1.child1", &out, koanf.UnmarshalConf{Tag: "koanf"}) 44 | fmt.Println(out) 45 | 46 | // Marshal the instance back to JSON. 47 | // The parser instance can be anything, eg: json.Parser(), 48 | // yaml.Parser() etc. 49 | b, _ := k.Marshal(parser) 50 | fmt.Println(string(b)) 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/go-viper/mapstructure/v2 v2.2.1 7 | github.com/knadh/koanf/maps v0.1.2 8 | github.com/mitchellh/copystructure v1.2.0 9 | ) 10 | 11 | require github.com/mitchellh/reflectwalk v1.0.2 // indirect 12 | 13 | retract v2.0.2 // Tagged as minor version, but contains breaking changes. 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 2 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 3 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 4 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 5 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 6 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 7 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 8 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 9 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24 2 | 3 | toolchain go1.24.2 4 | 5 | use ( 6 | . 7 | ./examples 8 | ./maps 9 | ./parsers/dotenv 10 | ./parsers/hcl 11 | ./parsers/hjson 12 | ./parsers/json 13 | ./parsers/kdl 14 | ./parsers/nestedtext 15 | ./parsers/toml 16 | ./parsers/yaml 17 | ./providers/appconfig 18 | ./providers/azkeyvault 19 | ./providers/basicflag 20 | ./providers/cliflagv2 21 | ./providers/cliflagv3 22 | ./providers/confmap 23 | ./providers/consul 24 | ./providers/env 25 | ./providers/etcd 26 | ./providers/file 27 | ./providers/fs 28 | ./providers/nats 29 | ./providers/parameterstore 30 | ./providers/posflag 31 | ./providers/rawbytes 32 | ./providers/s3 33 | ./providers/structs 34 | ./providers/vault 35 | ./tests 36 | ) 37 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package koanf 2 | 3 | // Provider represents a configuration provider. Providers can 4 | // read configuration from a source (file, HTTP etc.) 5 | type Provider interface { 6 | // ReadBytes returns the entire configuration as raw []bytes to be parsed. 7 | // with a Parser. 8 | ReadBytes() ([]byte, error) 9 | 10 | // Read returns the parsed configuration as a nested map[string]interface{}. 11 | // It is important to note that the string keys should not be flat delimited 12 | // keys like `parent.child.key`, but nested like `{parent: {child: {key: 1}}}`. 13 | Read() (map[string]interface{}, error) 14 | } 15 | 16 | // Parser represents a configuration format parser. 17 | type Parser interface { 18 | Unmarshal([]byte) (map[string]interface{}, error) 19 | Marshal(map[string]interface{}) ([]byte, error) 20 | } 21 | -------------------------------------------------------------------------------- /maps/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/maps 2 | 3 | go 1.18 4 | 5 | require github.com/mitchellh/copystructure v1.2.0 6 | 7 | require github.com/mitchellh/reflectwalk v1.0.2 // indirect 8 | -------------------------------------------------------------------------------- /maps/go.sum: -------------------------------------------------------------------------------- 1 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 2 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 3 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 4 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 5 | -------------------------------------------------------------------------------- /mock/mock.env: -------------------------------------------------------------------------------- 1 | lower=case 2 | UPPER=CASE 3 | MiXeD=CaSe 4 | COMMENT=AFTER # Comment 5 | # COMMENT 6 | MORE=vars 7 | quotedSpecial="j18120734xn2&*@#*&R#d1j23d*(*)" 8 | empty= 9 | -------------------------------------------------------------------------------- /mock/mock.hcl: -------------------------------------------------------------------------------- 1 | "bools" = [true, false, true] 2 | "empty" = {} 3 | "intbools" = [1, 0, 1] 4 | "orphan" = ["red", "blue", "orange"] 5 | "parent1" = { 6 | "boolmap" = { 7 | "notok3" = false 8 | "ok1" = true 9 | "ok2" = true 10 | } 11 | "child1" = { 12 | "empty" = {} 13 | "grandchild1" = { 14 | "ids" = [1, 2, 3] 15 | "on" = true 16 | } 17 | "name" = "child1" 18 | "type" = "hcl" 19 | } 20 | "floatmap" = { 21 | "key1" = 1.1 22 | "key2" = 1.2 23 | "key3" = 1.3 24 | } 25 | "id" = 1234 26 | "intmap" = { 27 | "key1" = 1 28 | "key2" = 1 29 | "key3" = 1 30 | } 31 | "name" = "parent1" 32 | "strmap" = { 33 | "key1" = "val1" 34 | "key2" = "val2" 35 | "key3" = "val3" 36 | } 37 | "strsmap" = { 38 | "key1" = ["val1", "val2", "val3"] 39 | "key2" = ["val4", "val5"] 40 | } 41 | } 42 | "parent2" = { 43 | "child2" = { 44 | "empty" = {} 45 | "grandchild2" = { 46 | "ids" = [4, 5, 6] 47 | "on" = true 48 | } 49 | "name" = "child2" 50 | } 51 | "id" = 5678 52 | "name" = "parent2" 53 | } 54 | "strbool" = "1" 55 | "strbools" = ["1", "t", "f"] 56 | "time" = "2019-01-01" 57 | "duration" = "3s" 58 | "negative_int" = -1234 59 | "type" = "hcl" 60 | -------------------------------------------------------------------------------- /mock/mock.hjson: -------------------------------------------------------------------------------- 1 | { 2 | # Analog of mock.json in hjson 3 | 4 | type: hjson 5 | parent1: { 6 | name: parent1 7 | id: 1234 8 | child1: { 9 | name: child1 10 | type: hjson 11 | grandchild1: { 12 | ids: [ 13 | 1, 14 | 2, 15 | 3, 16 | ] 17 | on: true 18 | } 19 | empty: {} 20 | } 21 | strmap: { 22 | key1: val1 23 | key2: val2 24 | key3: val3 25 | } 26 | strsmap: { 27 | key1: [ 28 | val1 29 | val2 30 | val3 31 | ] 32 | key2: [ 33 | val4 34 | val5 35 | ] 36 | } 37 | intmap: { 38 | key1: 1 39 | key2: 1 40 | key3: 1 41 | } 42 | floatmap: { 43 | key1: 1.1 44 | key2: 1.2 45 | key3: 1.3 46 | } 47 | boolmap: { 48 | ok1: true 49 | ok2: true 50 | notok3: false 51 | } 52 | } 53 | parent2: { 54 | name: parent2 55 | id: 5678 56 | child2: { 57 | name: child2 58 | grandchild2: { 59 | ids: [4, 5, 6] 60 | on: true 61 | } 62 | empty: {} 63 | } 64 | } 65 | orphan: [ 66 | red 67 | blue 68 | orange 69 | ] 70 | empty: {} 71 | bools: [ 72 | true, 73 | false, 74 | true, 75 | ] 76 | intbools: [ 77 | 1, 78 | 0, 79 | 1, 80 | ] 81 | strbools: [ 82 | "1", 83 | "t", 84 | "f" 85 | ] 86 | strbool: "1" 87 | time: "2019-01-01" 88 | duration: "3s" 89 | negative_int: -1234 90 | } 91 | -------------------------------------------------------------------------------- /mock/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "json", 3 | "parent1": { 4 | "name": "parent1", 5 | "id": 1234, 6 | "child1": { 7 | "name": "child1", 8 | "type": "json", 9 | "grandchild1": { 10 | "ids": [1,2,3], 11 | "on": true 12 | }, 13 | "empty": {} 14 | }, 15 | "strmap": { 16 | "key1": "val1", 17 | "key2": "val2", 18 | "key3": "val3" 19 | }, 20 | "strsmap": { 21 | "key1": ["val1", "val2", "val3"], 22 | "key2": ["val4", "val5"] 23 | }, 24 | "intmap": { 25 | "key1": 1, 26 | "key2": 1, 27 | "key3": 1 28 | }, 29 | "floatmap": { 30 | "key1": 1.1, 31 | "key2": 1.2, 32 | "key3": 1.3 33 | }, 34 | "boolmap": { 35 | "ok1": true, 36 | "ok2": true, 37 | "notok3": false 38 | } 39 | }, 40 | "parent2": { 41 | "name": "parent2", 42 | "id": 5678, 43 | "child2": { 44 | "name": "child2", 45 | "grandchild2": { 46 | "ids": [4,5,6], 47 | "on": true 48 | }, 49 | "empty": {} 50 | } 51 | }, 52 | "orphan": ["red", "blue", "orange"], 53 | "empty": {}, 54 | "bools": [true, false, true], 55 | "intbools": [1, 0, 1], 56 | "strbools": ["1", "t", "f"], 57 | "strbool": "1", 58 | "time": "2019-01-01", 59 | "duration": "3s", 60 | "negative_int": -1234 61 | } 62 | -------------------------------------------------------------------------------- /mock/mock.prop: -------------------------------------------------------------------------------- 1 | type=prop 2 | parent1.name=parent1 3 | parent1.id=1234 4 | parent1.child1.name=child1 5 | parent1.child1.type=prop 6 | parent1.child1.grandchild1.ids.0=1 7 | parent1.child1.grandchild1.ids.1=2 8 | parent1.child1.grandchild1.ids.2=3 9 | parent1.child1.grandchild1.on=true 10 | parent2.name=parent2 11 | parent2.id=5678 12 | parent2.child2.name=child2 13 | parent2.child2.grandchild2.ids.0=4 14 | parent2.child2.grandchild2.ids.1=5 15 | parent2.child2.grandchild2.ids.2=6 16 | parent2.child2.grandchild2.on=true 17 | orphan.0=red 18 | orphan.1=blue 19 | orphan.2=orange 20 | -------------------------------------------------------------------------------- /mock/mock.toml: -------------------------------------------------------------------------------- 1 | type = "toml" 2 | orphan = [ 3 | "red", 4 | "blue", 5 | "orange" 6 | ] 7 | bools = [ 8 | true, 9 | false, 10 | true 11 | ] 12 | intbools = [ 13 | 1.0, 14 | 0.0, 15 | 1.0 16 | ] 17 | strbools = [ 18 | "1", 19 | "t", 20 | "f" 21 | ] 22 | strbool = "1" 23 | time = "2019-01-01" 24 | duration = "3s" 25 | negative_int = -1234 26 | 27 | [parent1] 28 | name = "parent1" 29 | id = 1234.0 30 | 31 | [parent1.child1] 32 | name = "child1" 33 | type = "toml" 34 | 35 | [parent1.child1.grandchild1] 36 | ids = [ 37 | 1.0, 38 | 2.0, 39 | 3.0 40 | ] 41 | on = true 42 | 43 | [parent1.child1.empty] 44 | 45 | [parent1.strmap] 46 | key1 = "val1" 47 | key2 = "val2" 48 | key3 = "val3" 49 | 50 | [parent1.strsmap] 51 | key1 = [ 52 | "val1", 53 | "val2", 54 | "val3" 55 | ] 56 | key2 = [ 57 | "val4", 58 | "val5" 59 | ] 60 | 61 | [parent1.intmap] 62 | key1 = 1.0 63 | key2 = 1.0 64 | key3 = 1.0 65 | 66 | [parent1.floatmap] 67 | key1 = 1.1 68 | key2 = 1.2 69 | key3 = 1.3 70 | 71 | [parent1.boolmap] 72 | ok1 = true 73 | ok2 = true 74 | notok3 = false 75 | 76 | [parent2] 77 | name = "parent2" 78 | id = 5678.0 79 | 80 | [parent2.child2] 81 | name = "child2" 82 | 83 | [parent2.child2.grandchild2] 84 | ids = [ 85 | 4.0, 86 | 5.0, 87 | 6.0 88 | ] 89 | on = true 90 | 91 | [parent2.child2.empty] 92 | 93 | [empty] 94 | -------------------------------------------------------------------------------- /mock/mock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | type: yml 3 | parent1: 4 | name: parent1 5 | id: 1234 6 | child1: 7 | name: child1 8 | type: yml 9 | grandchild1: 10 | ids: 11 | - 1 12 | - 2 13 | - 3 14 | "on": true 15 | empty: {} 16 | strmap: 17 | key1: val1 18 | key2: val2 19 | key3: val3 20 | strsmap: 21 | key1: 22 | - val1 23 | - val2 24 | - val3 25 | key2: 26 | - val4 27 | - val5 28 | intmap: 29 | key1: 1 30 | key2: 1 31 | key3: 1 32 | floatmap: 33 | key1: 1.1 34 | key2: 1.2 35 | key3: 1.3 36 | boolmap: 37 | ok1: true 38 | ok2: true 39 | notok3: false 40 | parent2: 41 | name: parent2 42 | id: 5678 43 | child2: 44 | name: child2 45 | grandchild2: 46 | ids: 47 | - 4 48 | - 5 49 | - 6 50 | "on": true 51 | empty: {} 52 | orphan: 53 | - red 54 | - blue 55 | - orange 56 | empty: {} 57 | bools: 58 | - true 59 | - false 60 | - true 61 | intbools: 62 | - 1 63 | - 0 64 | - 1 65 | strbools: 66 | - "1" 67 | - t 68 | - f 69 | strbool: "1" 70 | time: "2019-01-01" 71 | duration: "3s" 72 | negative_int: -1234 73 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package koanf 2 | 3 | // options contains options to modify the behavior of Koanf.Load. 4 | type options struct { 5 | merge func(a, b map[string]interface{}) error 6 | } 7 | 8 | // newOptions creates a new options instance. 9 | func newOptions(opts []Option) *options { 10 | o := new(options) 11 | o.apply(opts) 12 | return o 13 | } 14 | 15 | // Option is a generic type used to modify the behavior of Koanf.Load. 16 | type Option func(*options) 17 | 18 | // apply the given options. 19 | func (o *options) apply(opts []Option) { 20 | for _, opt := range opts { 21 | opt(o) 22 | } 23 | } 24 | 25 | // WithMergeFunc is an option to modify the merge behavior of Koanf.Load. 26 | // If unset, the default merge function is used. 27 | // 28 | // The merge function is expected to merge map src into dest (left to right). 29 | func WithMergeFunc(merge func(src, dest map[string]interface{}) error) Option { 30 | return func(o *options) { 31 | o.merge = merge 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /parsers/dotenv/dotenv.go: -------------------------------------------------------------------------------- 1 | // Package dotenv implements a koanf.Parser that parses DOTENV bytes as conf maps. 2 | package dotenv 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/knadh/koanf/maps" 10 | ) 11 | 12 | // DotEnv implements a DOTENV parser. 13 | type DotEnv struct { 14 | delim string 15 | prefix string 16 | 17 | cb func(key string, value string) (string, interface{}) 18 | reverseCB map[string]string 19 | } 20 | 21 | // Parser returns a DOTENV Parser. 22 | func Parser() *DotEnv { 23 | return &DotEnv{} 24 | } 25 | 26 | // ParserEnv allows to make the DOTENV Parser behave like the env.Provider. 27 | func ParserEnv(prefix, delim string, cb func(s string) string) *DotEnv { 28 | return &DotEnv{ 29 | delim: delim, 30 | prefix: prefix, 31 | cb: func(key, value string) (string, interface{}) { 32 | return cb(key), value 33 | }, 34 | reverseCB: make(map[string]string), 35 | } 36 | } 37 | 38 | // ParserEnvWithValue allows to make the DOTENV Parser behave like the env.ProviderWithValue. 39 | func ParserEnvWithValue(prefix, delim string, cb func(key string, value string) (string, interface{})) *DotEnv { 40 | return &DotEnv{ 41 | delim: delim, 42 | prefix: prefix, 43 | cb: cb, 44 | reverseCB: make(map[string]string), 45 | } 46 | } 47 | 48 | // Unmarshal parses the given DOTENV bytes. 49 | func (p *DotEnv) Unmarshal(b []byte) (map[string]interface{}, error) { 50 | // Unmarshal DOTENV from []byte 51 | r, err := godotenv.Unmarshal(string(b)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // Convert a map[string]string to a map[string]interface{} 57 | mp := make(map[string]interface{}) 58 | for sourceKey, v := range r { 59 | if !strings.HasPrefix(sourceKey, p.prefix) { 60 | continue 61 | } 62 | 63 | if p.cb != nil { 64 | targetKey, value := p.cb(sourceKey, v) 65 | p.reverseCB[targetKey] = sourceKey 66 | mp[targetKey] = value 67 | } else { 68 | mp[sourceKey] = v 69 | } 70 | 71 | } 72 | 73 | if p.delim != "" { 74 | mp = maps.Unflatten(mp, p.delim) 75 | } 76 | return mp, nil 77 | } 78 | 79 | // Marshal marshals the given config map to DOTENV bytes. 80 | func (p *DotEnv) Marshal(o map[string]interface{}) ([]byte, error) { 81 | if p.delim != "" { 82 | o, _ = maps.Flatten(o, nil, p.delim) 83 | } 84 | 85 | // Convert a map[string]interface{} to a map[string]string 86 | mp := make(map[string]string) 87 | for targetKey, v := range o { 88 | if sourceKey, found := p.reverseCB[targetKey]; found { 89 | targetKey = sourceKey 90 | } 91 | 92 | mp[targetKey] = fmt.Sprint(v) 93 | } 94 | 95 | // Unmarshal to string 96 | out, err := godotenv.Marshal(mp) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | // Convert to []byte and return 102 | return []byte(out), nil 103 | } 104 | -------------------------------------------------------------------------------- /parsers/dotenv/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/dotenv 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/knadh/koanf/maps v0.1.2 8 | github.com/stretchr/testify v1.8.4 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/kr/pretty v0.2.1 // indirect 14 | github.com/kr/text v0.2.0 // indirect 15 | github.com/mitchellh/copystructure v1.2.0 // indirect 16 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /parsers/dotenv/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 5 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 6 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 7 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 8 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 9 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 12 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 13 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 14 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 15 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 16 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 17 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 21 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 24 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /parsers/hcl/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/hcl 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/hashicorp/hcl v1.0.0 7 | github.com/stretchr/testify v1.8.4 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.2.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /parsers/hcl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 5 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 6 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 7 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 15 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /parsers/hcl/hcl.go: -------------------------------------------------------------------------------- 1 | // Package hcl implements a koanf.Parser that parses Hashicorp 2 | // HCL bytes as conf maps. 3 | package hcl 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/hashicorp/hcl" 9 | ) 10 | 11 | // HCL implements a Hashicorp HCL parser. 12 | type HCL struct{ flattenSlices bool } 13 | 14 | // Parser returns an HCL Parser. 15 | // flattenSlices flattens HCL structures where maps turn into 16 | // lists of maps. Read more here: https://github.com/hashicorp/hcl/issues/162 17 | // It's recommended to turn this setting on. 18 | func Parser(flattenSlices bool) *HCL { 19 | return &HCL{flattenSlices: flattenSlices} 20 | } 21 | 22 | // Unmarshal parses the given HCL bytes. 23 | func (p *HCL) Unmarshal(b []byte) (map[string]interface{}, error) { 24 | o, err := hcl.Parse(string(b)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | var out map[string]interface{} 30 | if err := hcl.DecodeObject(&out, o); err != nil { 31 | return nil, err 32 | } 33 | if p.flattenSlices { 34 | flattenHCL(out) 35 | } 36 | return out, nil 37 | } 38 | 39 | // Marshal marshals the given config map to HCL bytes. 40 | func (p *HCL) Marshal(o map[string]interface{}) ([]byte, error) { 41 | return nil, errors.New("HCL marshalling is not supported") 42 | // TODO: Although this is the only way to do it, it's producing empty bytes. 43 | // Needs investigation. 44 | // The only way to generate HCL is from the HCL node structure. 45 | // Turn the map into JSON, then parse it with the HCL lib to create its 46 | // structure, and then, encode to HCL. 47 | // j, err := json.Marshal(o) 48 | // if err != nil { 49 | // return nil, err 50 | // } 51 | // tree, err := hcl.Parse(string(j)) 52 | // if err != nil { 53 | // return nil, err 54 | // } 55 | 56 | // var buf bytes.Buffer 57 | // out := bufio.NewWriter(&buf) 58 | // if err := printer.Fprint(out, tree.Node); err != nil { 59 | // return nil, err 60 | // } 61 | // return buf.Bytes(), err 62 | } 63 | 64 | // flattenHCL flattens an unmarshalled HCL structure where maps 65 | // turn into slices -- https://github.com/hashicorp/hcl/issues/162. 66 | func flattenHCL(mp map[string]interface{}) { 67 | for k, val := range mp { 68 | if v, ok := val.([]map[string]interface{}); ok { 69 | if len(v) == 1 { 70 | mp[k] = v[0] 71 | } 72 | } 73 | } 74 | for _, val := range mp { 75 | if v, ok := val.(map[string]interface{}); ok { 76 | flattenHCL(v) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /parsers/hcl/hcl_test.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestHCL_Unmarshal(t *testing.T) { 10 | 11 | hclParserWithFlatten := Parser(true) 12 | hclParserWithoutFlatten := Parser(false) 13 | 14 | testCases := []struct { 15 | name string 16 | input []byte 17 | output map[string]interface{} 18 | isErr bool 19 | function HCL 20 | }{ 21 | { 22 | name: "Empty HCL - With flatten", 23 | input: []byte(`{}`), 24 | function: *hclParserWithFlatten, 25 | output: map[string]interface{}{}, 26 | }, 27 | { 28 | name: "Empty HCL - Without flatten", 29 | input: []byte(`{}`), 30 | function: *hclParserWithoutFlatten, 31 | output: map[string]interface{}{}, 32 | }, 33 | { 34 | name: "Valid HCL - With flatten", 35 | input: []byte(`resource "aws_instance" "example" { 36 | count = 2 # meta-argument first 37 | ami = "abc123" 38 | instance_type = "t2.micro" 39 | lifecycle { # meta-argument block last 40 | create_before_destroy = true 41 | } 42 | }`), 43 | function: *hclParserWithFlatten, 44 | output: map[string]interface{}{ 45 | "resource": map[string]interface{}{ 46 | "aws_instance": map[string]interface{}{ 47 | "example": map[string]interface{}{ 48 | "ami": "abc123", 49 | "count": 2, 50 | "instance_type": "t2.micro", 51 | "lifecycle": map[string]interface{}{ 52 | "create_before_destroy": true, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | { 60 | name: "Valid HCL - Without flatten", 61 | input: []byte(`resource "aws_instance" "example" { 62 | count = 2 # meta-argument first 63 | ami = "abc123" 64 | instance_type = "t2.micro" 65 | lifecycle { # meta-argument block last 66 | create_before_destroy = true 67 | } 68 | }`), 69 | function: *hclParserWithoutFlatten, 70 | output: map[string]interface{}{ 71 | "resource": []map[string]interface{}{{ 72 | "aws_instance": []map[string]interface{}{{ 73 | "example": []map[string]interface{}{{ 74 | "ami": "abc123", 75 | "count": 2, 76 | "instance_type": "t2.micro", 77 | "lifecycle": []map[string]interface{}{{ 78 | "create_before_destroy": true}, 79 | }, 80 | }}, 81 | }}, 82 | }}, 83 | }, 84 | }, 85 | { 86 | name: "Invalid HCL - With missing parenthesis", 87 | input: []byte(`resource "aws_instance" "example" { 88 | ami = "abc123" 89 | `), 90 | function: *hclParserWithFlatten, 91 | isErr: true, 92 | }, 93 | } 94 | 95 | for _, tc := range testCases { 96 | t.Run(tc.name, func(t *testing.T) { 97 | out, err := tc.function.Unmarshal(tc.input) 98 | if tc.isErr { 99 | assert.NotNil(t, err) 100 | } else { 101 | assert.Nil(t, err) 102 | assert.Equal(t, tc.output, out) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestHCL_Marshal(t *testing.T) { 109 | 110 | hclParserWithFlatten := Parser(true) 111 | 112 | testCases := []struct { 113 | name string 114 | input map[string]interface{} 115 | isErr bool 116 | function HCL 117 | }{ 118 | { 119 | name: "Empty HCL", 120 | input: map[string]interface{}{}, 121 | isErr: true, 122 | function: *hclParserWithFlatten, 123 | }, 124 | { 125 | name: "Complex HCL", 126 | input: map[string]interface{}{ 127 | "resource": []map[string]interface{}{{ 128 | "aws_instance": []map[string]interface{}{{ 129 | "example": []map[string]interface{}{{ 130 | "ami": "abc123", 131 | "count": 2, 132 | "instance_type": "t2.micro", 133 | "lifecycle": []map[string]interface{}{{ 134 | "create_before_destroy": true}, 135 | }, 136 | }}, 137 | }}, 138 | }}, 139 | }, 140 | isErr: true, 141 | }, 142 | } 143 | 144 | for _, tc := range testCases { 145 | t.Run(tc.name, func(t *testing.T) { 146 | _, err := tc.function.Marshal(tc.input) 147 | if tc.isErr { 148 | assert.EqualError(t, err, "HCL marshalling is not supported") 149 | } else { 150 | assert.Nil(t, err) 151 | } 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /parsers/hjson/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/hjson 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/hjson/hjson-go/v4 v4.4.0 7 | github.com/stretchr/testify v1.8.4 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.2.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /parsers/hjson/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/hjson/hjson-go/v4 v4.4.0 h1:D/NPvqOCH6/eisTb5/ztuIS8GUvmpHaLOcNk1Bjr298= 5 | github.com/hjson/hjson-go/v4 v4.4.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= 6 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 7 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 15 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /parsers/hjson/hjson.go: -------------------------------------------------------------------------------- 1 | // Package hjson implements a koanf.Parser that parses HJSON bytes as conf maps. 2 | // Very similar to json. 3 | package hjson 4 | 5 | import ( 6 | "github.com/hjson/hjson-go/v4" 7 | ) 8 | 9 | // HJSON implements a HJSON parser. 10 | type HJSON struct{} 11 | 12 | // Parser returns a HJSON parser. 13 | func Parser() *HJSON { 14 | return &HJSON{} 15 | } 16 | 17 | // Unmarshal parses the given HJSON bytes. 18 | func (p *HJSON) Unmarshal(b []byte) (map[string]interface{}, error) { 19 | var out map[string]interface{} 20 | if err := hjson.Unmarshal(b, &out); err != nil { 21 | return nil, err 22 | } 23 | return out, nil 24 | } 25 | 26 | // Marshal marshals the given config map to HJSON bytes. 27 | func (p *HJSON) Marshal(o map[string]interface{}) ([]byte, error) { 28 | return hjson.Marshal(o) 29 | } 30 | -------------------------------------------------------------------------------- /parsers/json/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/json 2 | 3 | go 1.23.0 4 | 5 | require github.com/stretchr/testify v1.8.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/kr/pretty v0.2.1 // indirect 10 | github.com/kr/text v0.2.0 // indirect 11 | github.com/pmezard/go-difflib v1.0.0 // indirect 12 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /parsers/json/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 5 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 9 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 16 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /parsers/json/json.go: -------------------------------------------------------------------------------- 1 | // Package json implements a koanf.Parser that parses JSON bytes as conf maps. 2 | package json 3 | 4 | import ( 5 | "encoding/json" 6 | ) 7 | 8 | // JSON implements a JSON parser. 9 | type JSON struct{} 10 | 11 | // Parser returns a JSON Parser. 12 | func Parser() *JSON { 13 | return &JSON{} 14 | } 15 | 16 | // Unmarshal parses the given JSON bytes. 17 | func (p *JSON) Unmarshal(b []byte) (map[string]interface{}, error) { 18 | var out map[string]interface{} 19 | if err := json.Unmarshal(b, &out); err != nil { 20 | return nil, err 21 | } 22 | return out, nil 23 | } 24 | 25 | // Marshal marshals the given config map to JSON bytes. 26 | func (p *JSON) Marshal(o map[string]interface{}) ([]byte, error) { 27 | return json.Marshal(o) 28 | } 29 | -------------------------------------------------------------------------------- /parsers/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestJSON_Unmarshal(t *testing.T) { 9 | testCases := []struct { 10 | name string 11 | input []byte 12 | keys []string 13 | values []interface{} 14 | isErr bool 15 | }{ 16 | { 17 | name: "Empty JSON", 18 | input: []byte(`{}`), 19 | }, 20 | { 21 | name: "Valid JSON", 22 | input: []byte(`{ 23 | "key": "val", 24 | "name": "test", 25 | "number": 2 26 | }`), 27 | keys: []string{"key", "name", "number"}, 28 | values: []interface{}{"val", "test", 2.0}, 29 | }, 30 | { 31 | name: "Invalid JSON - missing curly brace", 32 | input: []byte(`{ 33 | "key": "val",`), 34 | isErr: true, 35 | }, 36 | { 37 | name: "Complex JSON - All types", 38 | input: []byte(`{ 39 | "array": [ 40 | 1, 41 | 2, 42 | 3 43 | ], 44 | "boolean": true, 45 | "color": "gold", 46 | "null": null, 47 | "number": 123, 48 | "object": { 49 | "a": "b", 50 | "c": "d" 51 | }, 52 | "string": "Hello World" 53 | }`), 54 | keys: []string{"array", "boolean", "color", "null", "number", "object", "string"}, 55 | values: []interface{}{[]interface{}{1.0, 2.0, 3.0}, 56 | true, 57 | "gold", 58 | nil, 59 | 123.0, 60 | map[string]interface{}{"a": "b", "c": "d"}, 61 | "Hello World"}, 62 | }, 63 | { 64 | name: "Invalid JSON - missing comma", 65 | input: []byte(`{ 66 | "boolean": true 67 | "number": 123 68 | }`), 69 | isErr: true, 70 | }, 71 | { 72 | name: "Invalid JSON - Redundant comma", 73 | input: []byte(`{ 74 | "number": 123, 75 | }`), 76 | isErr: true, 77 | }, 78 | } 79 | j := Parser() 80 | 81 | for _, tc := range testCases { 82 | t.Run(tc.name, func(t *testing.T) { 83 | out, err := j.Unmarshal(tc.input) 84 | if tc.isErr { 85 | assert.NotNil(t, err) 86 | } else { 87 | assert.Nil(t, err) 88 | for i, k := range tc.keys { 89 | v := out[k] 90 | assert.Equal(t, tc.values[i], v) 91 | } 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestJSON_Marshal(t *testing.T) { 98 | testCases := []struct { 99 | name string 100 | input map[string]interface{} 101 | output []byte 102 | isErr bool 103 | }{ 104 | { 105 | name: "Empty JSON", 106 | input: map[string]interface{}{}, 107 | output: []byte(`{}`), 108 | }, 109 | { 110 | name: "Valid JSON", 111 | input: map[string]interface{}{ 112 | "key": "val", 113 | "name": "test", 114 | "number": 2.0, 115 | }, 116 | output: []byte(`{"key":"val","name":"test","number":2}`), 117 | }, 118 | { 119 | name: "Complex JSON - All types", 120 | input: map[string]interface{}{ 121 | "array": []interface{}{1, 2, 3, 4, 5}, 122 | "boolean": true, 123 | "color": "gold", 124 | "null": nil, 125 | "number": 123, 126 | "object": map[string]interface{}{"a": "b", "c": "d"}, 127 | "string": "Hello World", 128 | }, 129 | output: []byte(`{"array":[1,2,3,4,5],"boolean":true,"color":"gold","null":null,"number":123,"object":{"a":"b","c":"d"},"string":"Hello World"}`), 130 | }, 131 | } 132 | 133 | j := Parser() 134 | 135 | for _, tc := range testCases { 136 | t.Run(tc.name, func(t *testing.T) { 137 | out, err := j.Marshal(tc.input) 138 | if tc.isErr { 139 | assert.NotNil(t, err) 140 | } else { 141 | assert.Nil(t, err) 142 | assert.Equal(t, tc.output, out) 143 | } 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /parsers/kdl/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/kdl 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/pmezard/go-difflib v1.0.0 // indirect 7 | github.com/stretchr/testify v1.8.4 8 | ) 9 | 10 | require github.com/sblinch/kdl-go v0.0.0-20240410000746-21754ba9ac55 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/kr/pretty v0.2.1 // indirect 15 | github.com/kr/text v0.2.0 // indirect 16 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /parsers/kdl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 5 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 9 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/sblinch/kdl-go v0.0.0-20240410000746-21754ba9ac55 h1:scyq0E9FvdGLX5lxAwjK0HebTM3Y7dG3tYrlXP+x+tk= 13 | github.com/sblinch/kdl-go v0.0.0-20240410000746-21754ba9ac55/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= 14 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 15 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /parsers/kdl/kdl.go: -------------------------------------------------------------------------------- 1 | // Package kdl implements a koanf.Parser that parses KDL bytes as conf maps. 2 | package kdl 3 | 4 | import ( 5 | kdl "github.com/sblinch/kdl-go" 6 | ) 7 | 8 | // KDL implements a KDL parser. 9 | type KDL struct{} 10 | 11 | // Parser returns a KDL Parser. 12 | func Parser() *KDL { 13 | return &KDL{} 14 | } 15 | 16 | // Unmarshal parses the given KDL bytes. 17 | func (p *KDL) Unmarshal(b []byte) (map[string]interface{}, error) { 18 | var o map[string]interface{} 19 | err := kdl.Unmarshal(b, &o) 20 | return o, err 21 | } 22 | 23 | // Marshal marshals the given config map to KDL bytes. 24 | func (p *KDL) Marshal(o map[string]interface{}) ([]byte, error) { 25 | return kdl.Marshal(o) 26 | } 27 | -------------------------------------------------------------------------------- /parsers/kdl/kdl_test.go: -------------------------------------------------------------------------------- 1 | package kdl 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestKDL_Unmarshal(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | input []byte 15 | keys []string 16 | values []interface{} 17 | isErr bool 18 | }{ 19 | { 20 | name: "Empty KDL", 21 | input: []byte(``), 22 | }, 23 | { 24 | name: "Valid KDL", 25 | input: []byte(`key "val" ; name "test" ; number 2.0`), 26 | keys: []string{"key", "name", "number"}, 27 | values: []interface{}{"val", "test", 2.0}, 28 | }, 29 | { 30 | name: "Invalid KDL - syntax error", 31 | input: []byte(`node1 key="val`), 32 | isErr: true, 33 | }, 34 | { 35 | name: "Complex KDL - Different types", 36 | input: []byte(` 37 | array 1.0 2.0 3.0 38 | boolean true 39 | color "gold" 40 | "null" null 41 | number 123 42 | object a="b" c="d" e=2.7 f=true 43 | string "Hello World" 44 | `), 45 | keys: []string{"array", "boolean", "color", "null", "number", "object", "string"}, 46 | values: []interface{}{[]interface{}{1.0, 2.0, 3.0}, 47 | true, 48 | "gold", 49 | nil, 50 | int64(123), 51 | map[string]interface{}{"a": "b", "c": "d", "e": 2.7, "f": true}, 52 | "Hello World"}, 53 | }, 54 | { 55 | name: "Invalid KDL - missing value", 56 | input: []byte(`node1 boolean=`), 57 | isErr: true, 58 | }, 59 | { 60 | name: "Complex KDL - Nested map", 61 | input: []byte(`key "value" 62 | "1" "skipped" 63 | map key="skipped" key="value" 64 | nested_map { 65 | map key="value" 17 { 66 | list "item1" "item2" "item3" 67 | mixup "y"=1 2 3 4 68 | first "first"=1 2 3 4 69 | child "test"=1 2 3 4 { "y" 5 ; "d" 6 ; } 70 | } 71 | } 72 | `), 73 | keys: []string{"key", "1", "map", "nested_map"}, 74 | values: []interface{}{ 75 | "value", 76 | "skipped", 77 | map[string]interface{}{ 78 | "key": "value", 79 | }, 80 | map[string]interface{}{ 81 | "map": map[string]interface{}{ 82 | "0": int64(17), 83 | "key": "value", 84 | "list": []interface{}{ 85 | "item1", 86 | "item2", 87 | "item3", 88 | }, 89 | "mixup": map[string]interface{}{ 90 | "y": int64(1), 91 | "0": int64(2), 92 | "1": int64(3), 93 | "2": int64(4), 94 | }, 95 | "first": map[string]interface{}{ 96 | "first": int64(1), 97 | "0": int64(2), 98 | "1": int64(3), 99 | "2": int64(4), 100 | }, 101 | "child": map[string]interface{}{ 102 | "test": int64(1), 103 | "0": int64(2), 104 | "1": int64(3), 105 | "2": int64(4), 106 | "y": int64(5), 107 | "d": int64(6), 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | } 114 | 115 | k := Parser() // Assuming Parser() is implemented for KDL 116 | 117 | for _, tc := range testCases { 118 | t.Run(tc.name, func(t *testing.T) { 119 | out, err := k.Unmarshal(tc.input) 120 | if tc.isErr { 121 | assert.NotNil(t, err) 122 | } else { 123 | assert.Nil(t, err) 124 | for i, k := range tc.keys { 125 | v := out[k] 126 | assert.Equal(t, tc.values[i], v) 127 | } 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func TestKDL_Marshal(t *testing.T) { 134 | testCases := []struct { 135 | name string 136 | input map[string]interface{} 137 | stringifiedOutput string 138 | isErr bool 139 | }{ 140 | { 141 | name: "Empty KDL", 142 | input: map[string]interface{}{}, 143 | stringifiedOutput: ``, 144 | }, 145 | { 146 | name: "Valid KDL", 147 | input: map[string]interface{}{ 148 | "key": "val", 149 | "name": "test", 150 | "number": 2.0, 151 | }, 152 | stringifiedOutput: `key "val" 153 | name "test" 154 | number 2.0 155 | `, 156 | }, 157 | { 158 | name: "Complex KDL - Different types", 159 | input: map[string]interface{}{ 160 | "null": nil, 161 | "boolean": true, 162 | "color": "gold", 163 | "number": int64(123), 164 | "string": "Hello World", 165 | // "array": []interface{}{1, 2, 3, 4, 5}, // https://github.com/sblinch/kdl-go/issues/3 166 | "object": map[string]interface{}{"a": "b", "c": "d"}, 167 | }, 168 | stringifiedOutput: `boolean true 169 | color "gold" 170 | number 123 171 | string "Hello World" 172 | object a="b" c="d" 173 | null null 174 | `, 175 | }, 176 | } 177 | 178 | k := Parser() // Assuming Parser() is implemented for KDL 179 | 180 | for _, tc := range testCases { 181 | t.Run(tc.name, func(t *testing.T) { 182 | out, err := k.Marshal(tc.input) 183 | if tc.isErr { 184 | assert.NotNil(t, err) 185 | } else { 186 | assert.Nil(t, err) 187 | assert.Equal(t, sortLines(tc.stringifiedOutput), sortLines(string(out))) 188 | } 189 | }) 190 | } 191 | } 192 | 193 | // kdl marshal is not guaranteed to produce the same output every time 194 | // so we sort the lines to compare the output. 195 | func sortLines(s string) string { 196 | lines := strings.Split(s, "\n") 197 | sort.Strings(lines) 198 | for i, l := range lines { 199 | if strings.HasPrefix(l, "object") { 200 | // object a="b" c="d" should be sorted to be able to compare and 201 | // remove flakiness. 202 | parts := strings.Split(l, " ") 203 | sort.Strings(parts[1:]) 204 | lines[i] = strings.Join(parts, " ") 205 | } 206 | } 207 | return strings.Join(lines, "\n") 208 | } 209 | -------------------------------------------------------------------------------- /parsers/nestedtext/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/nestedtext 2 | 3 | go 1.23.0 4 | 5 | require github.com/npillmayer/nestext v0.1.3 6 | -------------------------------------------------------------------------------- /parsers/nestedtext/go.sum: -------------------------------------------------------------------------------- 1 | github.com/npillmayer/nestext v0.1.3 h1:2dkbzJ5xMcyJW5b8wwrX+nnRNvf/Nn1KwGhIauGyE2E= 2 | github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= 3 | -------------------------------------------------------------------------------- /parsers/nestedtext/nestedtext.go: -------------------------------------------------------------------------------- 1 | // Package nestedtext implements a koanf Parser that parses NestedText bytes as conf maps. 2 | package nestedtext 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | 8 | "github.com/npillmayer/nestext" 9 | "github.com/npillmayer/nestext/ntenc" 10 | ) 11 | 12 | // NT implements a NestedText parser. 13 | type NT struct{} 14 | 15 | // Parser returns a NestedText Parser. 16 | func Parser() *NT { 17 | return &NT{} 18 | } 19 | 20 | // Unmarshal parses the given NestedText bytes. 21 | // 22 | // If the NT content does not reflect a dict (NT allows for top-level lists or strings as well), 23 | // the content will be wrapped into a dict with a single key named "nestedtext". 24 | func (p *NT) Unmarshal(b []byte) (map[string]interface{}, error) { 25 | r := bytes.NewReader(b) 26 | result, err := nestext.Parse(r, nestext.TopLevel("dict")) 27 | if err != nil { 28 | return nil, err 29 | } 30 | // Given option-parameter TopLevel, nestext.Parse is guaranteed to wrap the return value 31 | // in an appropriate type (dict = map[string]interface{} in this case), if necessary. 32 | // However, guard against type conversion failure anyway. 33 | rmap, ok := result.(map[string]interface{}) 34 | if !ok { 35 | return nil, errors.New("NestedText configuration expected to be a dict at top-level") 36 | } 37 | return rmap, nil 38 | } 39 | 40 | // Marshal marshals the given config map to NestedText bytes. 41 | func (p *NT) Marshal(m map[string]interface{}) ([]byte, error) { 42 | var buf bytes.Buffer 43 | _, err := ntenc.Encode(m, &buf) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return buf.Bytes(), nil 48 | } 49 | -------------------------------------------------------------------------------- /parsers/nestedtext/nt_test.go: -------------------------------------------------------------------------------- 1 | package nestedtext_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/knadh/koanf/parsers/nestedtext" 7 | ) 8 | 9 | func TestParser(t *testing.T) { 10 | // test input in NestedText format 11 | ntsource := `ports: 12 | - 6483 13 | - 8020 14 | - 9332 15 | timeout: 20 16 | ` 17 | // Test decoder 18 | nt := nestedtext.Parser() 19 | c, err := nt.Unmarshal([]byte(ntsource)) 20 | if err != nil { 21 | t.Fatal("Unmarshal of NestedText input failed") 22 | } 23 | timeout := c["timeout"] 24 | if timeout != "20" { 25 | t.Logf("config-tree: %#v", c) 26 | t.Errorf("expected timeout-parameter to be 20, is %q", timeout) 27 | } 28 | 29 | // test encoder 30 | out, err := nt.Marshal(c) 31 | if err != nil { 32 | t.Fatal("Marshal of config to NestedText failed") 33 | } 34 | if string(out) != ntsource { 35 | t.Logf("config-text: %q", string(out)) 36 | t.Errorf("expected output of Marshal(…) to equal input to Unmarshal(…); didn't") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /parsers/toml/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/toml/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/pelletier/go-toml/v2 v2.2.4 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /parsers/toml/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 4 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 8 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /parsers/toml/toml.go: -------------------------------------------------------------------------------- 1 | // Package toml implements a koanf.Parser that parses TOML bytes as conf maps. 2 | package toml 3 | 4 | import ( 5 | "github.com/pelletier/go-toml/v2" 6 | ) 7 | 8 | // TOML implements a TOML parser. 9 | type TOML struct{} 10 | 11 | // Parser returns a TOML Parser. 12 | func Parser() *TOML { 13 | return &TOML{} 14 | } 15 | 16 | // Unmarshal parses the given TOML bytes. 17 | func (p *TOML) Unmarshal(b []byte) (map[string]interface{}, error) { 18 | var outMap map[string]interface{} 19 | 20 | if err := toml.Unmarshal(b, &outMap); err != nil { 21 | return nil, err 22 | } 23 | 24 | return outMap, nil 25 | } 26 | 27 | // Marshal marshals the given config map to TOML bytes. 28 | func (p *TOML) Marshal(o map[string]interface{}) ([]byte, error) { 29 | out, err := toml.Marshal(&o) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return out, nil 35 | } 36 | -------------------------------------------------------------------------------- /parsers/toml/toml_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTOML_Unmarshal(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | input []byte 13 | output map[string]interface{} 14 | isErr bool 15 | }{ 16 | { 17 | name: "Empty TOML", 18 | input: []byte(``), 19 | output: map[string]interface{}(nil), 20 | }, 21 | { 22 | name: "Valid TOML", 23 | input: []byte(`key = "val" 24 | name = "test" 25 | number = 2 26 | `), 27 | output: map[string]interface{}{ 28 | "key": "val", 29 | "name": "test", 30 | "number": int64(2), 31 | }, 32 | }, 33 | { 34 | name: "Invalid TOML - missing end quotes", 35 | input: []byte(`key = "val`), 36 | isErr: true, 37 | }, 38 | { 39 | name: "Complex TOML - All types", 40 | input: []byte(`array = [ 1, 2, 3 ] 41 | boolean = true 42 | color = "gold" 43 | number = 123 44 | string = "Hello World" 45 | [object] 46 | a = "b" 47 | c = "d"`), 48 | output: map[string]interface{}{ 49 | "array": []interface{}{int64(1), int64(2), int64(3)}, 50 | "boolean": true, 51 | "color": "gold", 52 | "number": int64(123), 53 | "object": map[string]interface{}{"a": "b", "c": "d"}, 54 | "string": "Hello World", 55 | }, 56 | }, 57 | { 58 | name: "Invalid TOML - missing equal", 59 | input: []byte(`key "val"`), 60 | isErr: true, 61 | }, 62 | } 63 | 64 | tp := Parser() 65 | 66 | for _, tc := range testCases { 67 | t.Run(tc.name, func(t *testing.T) { 68 | out, err := tp.Unmarshal(tc.input) 69 | if tc.isErr { 70 | assert.NotNil(t, err) 71 | } else { 72 | assert.Nil(t, err) 73 | assert.Equal(t, tc.output, out) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestTOML_Marshal(t *testing.T) { 80 | testCases := []struct { 81 | name string 82 | input map[string]interface{} 83 | output []byte 84 | isErr bool 85 | }{ 86 | { 87 | name: "Empty TOML", 88 | input: map[string]interface{}{}, 89 | output: []byte(nil), 90 | }, 91 | { 92 | name: "Valid TOML", 93 | input: map[string]interface{}{ 94 | "key": "val", 95 | "name": "test", 96 | "number": 2.0, 97 | }, 98 | output: []byte(`key = 'val' 99 | name = 'test' 100 | number = 2.0 101 | `), 102 | }, 103 | { 104 | name: "Complex TOML - All types", 105 | input: map[string]interface{}{ 106 | "array": []interface{}{1, 2, 3, 4, 5}, 107 | "boolean": true, 108 | "color": "gold", 109 | "number": 123, 110 | "object": map[string]interface{}{"a": "b", "c": "d"}, 111 | "string": "Hello World", 112 | }, 113 | output: []byte(`array = [1, 2, 3, 4, 5] 114 | boolean = true 115 | color = 'gold' 116 | number = 123 117 | string = 'Hello World' 118 | 119 | [object] 120 | a = 'b' 121 | c = 'd' 122 | `), 123 | }, 124 | } 125 | 126 | tp := Parser() 127 | 128 | for _, tc := range testCases { 129 | t.Run(tc.name, func(t *testing.T) { 130 | out, err := tp.Marshal(tc.input) 131 | if tc.isErr { 132 | assert.NotNil(t, err) 133 | } else { 134 | assert.Nil(t, err) 135 | assert.Equal(t, tc.output, out) 136 | } 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /parsers/yaml/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/parsers/yaml 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.4 7 | gopkg.in/yaml.v3 v3.0.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.2.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /parsers/yaml/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 5 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 9 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 16 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /parsers/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | // Package yaml implements a koanf.Parser that parses YAML bytes as conf maps. 2 | package yaml 3 | 4 | import ( 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | // YAML implements a YAML parser. 9 | type YAML struct{} 10 | 11 | // Parser returns a YAML Parser. 12 | func Parser() *YAML { 13 | return &YAML{} 14 | } 15 | 16 | // Unmarshal parses the given YAML bytes. 17 | func (p *YAML) Unmarshal(b []byte) (map[string]interface{}, error) { 18 | var out map[string]interface{} 19 | if err := yaml.Unmarshal(b, &out); err != nil { 20 | return nil, err 21 | } 22 | 23 | return out, nil 24 | } 25 | 26 | // Marshal marshals the given config map to YAML bytes. 27 | func (p *YAML) Marshal(o map[string]interface{}) ([]byte, error) { 28 | return yaml.Marshal(o) 29 | } 30 | -------------------------------------------------------------------------------- /parsers/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestYAML_Unmarshal(t *testing.T) { 10 | 11 | testCases := []struct { 12 | name string 13 | input []byte 14 | keys []string 15 | values []interface{} 16 | isErr bool 17 | }{ 18 | { 19 | name: "Empty YAML", 20 | input: []byte(`{}`), 21 | }, 22 | { 23 | name: "Valid YAML", 24 | input: []byte(`key: val 25 | name: test 26 | number: 2`), 27 | keys: []string{"key", "name", "number"}, 28 | values: []interface{}{"val", "test", 2}, 29 | }, 30 | { 31 | name: "Invalid YAML - wrong indentation", 32 | input: []byte(`key: val 33 | name: test 34 | number: 2`), 35 | isErr: true, 36 | }, 37 | { 38 | name: "Complex YAML - All types", 39 | input: []byte(`--- 40 | array: 41 | - 1 42 | - 2 43 | - 3 44 | boolean: true 45 | color: gold 46 | 'null': 47 | number: 123 48 | object: 49 | a: b 50 | c: d 51 | string: Hello World`), 52 | keys: []string{"array", "boolean", "color", "null", "number", "object", "string"}, 53 | values: []interface{}{[]interface{}{1, 2, 3}, 54 | true, 55 | "gold", 56 | nil, 57 | 123, 58 | map[string]interface{}{"a": "b", "c": "d"}, 59 | "Hello World"}, 60 | }, 61 | { 62 | name: "Valid YAML - With comments", 63 | input: []byte(`--- 64 | key: #Here is a single-line comment 65 | - value line 5 66 | #Here is a 67 | #multi-line comment 68 | - value line 13`), 69 | keys: []string{"key"}, 70 | values: []interface{}{[]interface{}{"value line 5", "value line 13"}}, 71 | }, 72 | } 73 | 74 | y := Parser() 75 | 76 | for _, tc := range testCases { 77 | t.Run(tc.name, func(t *testing.T) { 78 | out, err := y.Unmarshal(tc.input) 79 | if tc.isErr { 80 | assert.NotNil(t, err) 81 | } else { 82 | assert.Nil(t, err) 83 | for i, k := range tc.keys { 84 | v := out[k] 85 | assert.Equal(t, tc.values[i], v) 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestYAML_Marshal(t *testing.T) { 93 | testCases := []struct { 94 | name string 95 | input map[string]interface{} 96 | output []byte 97 | isErr bool 98 | }{ 99 | { 100 | name: "Empty YAML", 101 | input: map[string]interface{}{}, 102 | output: []byte(`{} 103 | `), 104 | }, 105 | { 106 | name: "Valid YAML", 107 | input: map[string]interface{}{ 108 | "key": "val", 109 | "name": "test", 110 | "number": 2, 111 | }, 112 | output: []byte(`key: val 113 | name: test 114 | number: 2 115 | `), 116 | }, 117 | { 118 | name: "Complex YAML - All types", 119 | input: map[string]interface{}{ 120 | "array": []interface{}{1, 2, 3, 4, 5}, 121 | "boolean": true, 122 | "color": "gold", 123 | "null": nil, 124 | "number": 123, 125 | "object": map[string]interface{}{"a": "b", "c": "d"}, 126 | "string": "Hello World", 127 | }, 128 | output: []byte(`array: 129 | - 1 130 | - 2 131 | - 3 132 | - 4 133 | - 5 134 | boolean: true 135 | color: gold 136 | "null": null 137 | number: 123 138 | object: 139 | a: b 140 | c: d 141 | string: Hello World 142 | `), 143 | }, 144 | } 145 | 146 | y := Parser() 147 | 148 | for _, tc := range testCases { 149 | t.Run(tc.name, func(t *testing.T) { 150 | out, err := y.Marshal(tc.input) 151 | if tc.isErr { 152 | assert.NotNil(t, err) 153 | } else { 154 | assert.Nil(t, err) 155 | assert.Equal(t, tc.output, out) 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /providers/appconfig/appconfig.go: -------------------------------------------------------------------------------- 1 | // Package appconfig implements a koanf.Provider for AWS AppConfig 2 | // and provides it to koanf to be parsed by a koanf.Parser. 3 | package appconfig 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/credentials" 13 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 14 | "github.com/aws/aws-sdk-go-v2/service/appconfig" 15 | "github.com/aws/aws-sdk-go-v2/service/sts" 16 | ) 17 | 18 | // Config holds the AWS AppConfig Configuration. 19 | type Config struct { 20 | // The AWS AppConfig Application to get. Specify either the application 21 | // name or the application ID. 22 | Application string 23 | 24 | // The Client ID for the AppConfig. Enables AppConfig to deploy the 25 | // configuration in intervals, as defined in the deployment strategy. 26 | ClientID string 27 | 28 | // The AppConfig configuration to fetch. Specify either the configuration 29 | // name or the configuration ID. 30 | Configuration string 31 | 32 | // The AppConfig environment to get. Specify either the environment 33 | // name or the environment ID. 34 | Environment string 35 | 36 | // The AppConfig Configuration Version to fetch. Specifying a ClientConfigurationVersion 37 | // ensures that the configuration is only fetched if it is updated. If not specified, 38 | // the latest available configuration is fetched always. 39 | // Setting this to the latest configuration version will return an empty slice of bytes. 40 | ClientConfigurationVersion string 41 | 42 | // The AWS Access Key ID to use. This value is fetched from the environment 43 | // if not specified. 44 | AWSAccessKeyID string 45 | 46 | // The AWS Secret Access Key to use. This value is fetched from the environment 47 | // if not specified. 48 | AWSSecretAccessKey string 49 | 50 | // The AWS IAM Role ARN to use. Useful for access requiring IAM AssumeRole. 51 | AWSRoleARN string 52 | 53 | // The AWS Region to use. This value is fetched from the environment if not specified. 54 | AWSRegion string 55 | 56 | // Time interval at which the watcher will refresh the configuration. 57 | // Defaults to 60 seconds. 58 | WatchInterval time.Duration 59 | } 60 | 61 | // AppConfig implements an AWS AppConfig provider. 62 | type AppConfig struct { 63 | client *appconfig.Client 64 | config Config 65 | input appconfig.GetConfigurationInput 66 | } 67 | 68 | // Provider returns an AWS AppConfig provider. 69 | func Provider(cfg Config) (*AppConfig, error) { 70 | c, err := config.LoadDefaultConfig(context.TODO()) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if cfg.AWSRegion != "" { 76 | c.Region = cfg.AWSRegion 77 | } 78 | 79 | // Check if AWS Access Key ID and Secret Key are specified. 80 | if cfg.AWSAccessKeyID != "" && cfg.AWSSecretAccessKey != "" { 81 | c.Credentials = credentials.NewStaticCredentialsProvider(cfg.AWSAccessKeyID, cfg.AWSSecretAccessKey, "") 82 | } 83 | 84 | // Check if AWS Role ARN is present. 85 | if cfg.AWSRoleARN != "" { 86 | stsSvc := sts.NewFromConfig(c) 87 | credentials := stscreds.NewAssumeRoleProvider(stsSvc, cfg.AWSRoleARN) 88 | c.Credentials = aws.NewCredentialsCache(credentials) 89 | } 90 | client := appconfig.NewFromConfig(c) 91 | 92 | return &AppConfig{client: client, config: cfg}, nil 93 | } 94 | 95 | // ProviderWithClient returns an AWS AppConfig provider 96 | // using an existing AWS appconfig client. 97 | func ProviderWithClient(cfg Config, client *appconfig.Client) *AppConfig { 98 | return &AppConfig{client: client, config: cfg} 99 | } 100 | 101 | // ReadBytes returns the raw bytes for parsing. 102 | func (ac *AppConfig) ReadBytes() ([]byte, error) { 103 | ac.input = appconfig.GetConfigurationInput{ 104 | Application: &ac.config.Application, 105 | ClientId: &ac.config.ClientID, 106 | Configuration: &ac.config.Configuration, 107 | Environment: &ac.config.Environment, 108 | } 109 | if ac.config.ClientConfigurationVersion != "" { 110 | ac.input.ClientConfigurationVersion = &ac.config.ClientConfigurationVersion 111 | } 112 | 113 | conf, err := ac.client.GetConfiguration(context.TODO(), &ac.input) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | // Set the response configuration version as the current configuration version. 119 | // Useful for Watch(). 120 | ac.input.ClientConfigurationVersion = conf.ConfigurationVersion 121 | 122 | return conf.Content, nil 123 | } 124 | 125 | // Read is not supported by the appconfig provider. 126 | func (ac *AppConfig) Read() (map[string]interface{}, error) { 127 | return nil, errors.New("appconfig provider does not support this method") 128 | } 129 | 130 | // Watch polls AWS AppConfig for configuration updates. 131 | func (ac *AppConfig) Watch(cb func(event interface{}, err error)) error { 132 | if ac.config.WatchInterval == 0 { 133 | // Set default watch interval to 60 seconds. 134 | ac.config.WatchInterval = 60 * time.Second 135 | } 136 | 137 | go func() { 138 | loop: 139 | for { 140 | conf, err := ac.client.GetConfiguration(context.TODO(), &ac.input) 141 | if err != nil { 142 | cb(nil, err) 143 | break loop 144 | } 145 | 146 | // Check if the configuration has been updated. 147 | if len(conf.Content) == 0 { 148 | // Configuration is not updated and we have the latest version. 149 | // Sleep for WatchInterval and retry watcher. 150 | time.Sleep(ac.config.WatchInterval) 151 | continue 152 | } 153 | 154 | // Trigger event. 155 | cb(nil, nil) 156 | } 157 | }() 158 | 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /providers/appconfig/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/appconfig/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.36.3 7 | github.com/aws/aws-sdk-go-v2/config v1.29.14 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 9 | github.com/aws/aws-sdk-go-v2/service/appconfig v1.37.3 10 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 11 | ) 12 | 13 | require ( 14 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 22 | github.com/aws/smithy-go v1.22.3 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /providers/appconfig/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 2 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 3 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 4 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 15 | github.com/aws/aws-sdk-go-v2/service/appconfig v1.37.3 h1:4B5MufJNLfzoUzad3nqndhk/guoUxQJ/LzPOS8WoZWk= 16 | github.com/aws/aws-sdk-go-v2/service/appconfig v1.37.3/go.mod h1:CN/8VG7LSDuminHk8uUcxsdlAvbiLSwkK46K21F0fuA= 17 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 27 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 28 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 29 | -------------------------------------------------------------------------------- /providers/azkeyvault/azkeyvault.go: -------------------------------------------------------------------------------- 1 | package azkeyvault 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" 8 | "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" 9 | "github.com/knadh/koanf/maps" 10 | ) 11 | 12 | type Config struct { 13 | // KeyVaultUrl key vault url 14 | KeyVaultUrl string 15 | 16 | // Delim is the delimiter to use 17 | // when specifying config key paths, for instance a . for `parent.child.key` 18 | // or a / for `parent/child/key`. 19 | Delim string 20 | 21 | // TokenCredential Token credential to connect to the Azure Keyvault. 22 | // It can be created using client Id with client secret or client certificate. 23 | TokenCredential azcore.TokenCredential 24 | 25 | // If FlatPaths is true, then the loaded configuration is not split into 26 | // hierarchical maps based on the delimiter. The keys including the delimiter, 27 | // eg: app.db.name stays as-is in the confmap. 28 | FlatPaths bool 29 | } 30 | 31 | type AzureKeyVault struct { 32 | kvclient *azsecrets.Client 33 | config Config 34 | } 35 | 36 | func Provider(config Config) (*AzureKeyVault, error) { 37 | kvClient, err := azsecrets.NewClient(config.KeyVaultUrl, config.TokenCredential, nil) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &AzureKeyVault{kvclient: kvClient, config: config}, nil 43 | } 44 | 45 | func (kv *AzureKeyVault) ReadBytes() ([]byte, error) { 46 | return nil, errors.New("azure key vault provider does not support this method") 47 | } 48 | 49 | func (kv *AzureKeyVault) Read() (map[string]interface{}, error) { 50 | ctx := context.Background() 51 | secrets := make(map[string]interface{}) 52 | 53 | // Get all the secrets from the KV 54 | pager := kv.kvclient.NewListSecretPropertiesPager(nil) 55 | for pager.More() { 56 | page, err := pager.NextPage(ctx) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | for _, secret := range page.Value { 62 | sName := secret.ID.Name() 63 | SValue, err := kv.kvclient.GetSecret(ctx, sName, "", nil) 64 | 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to get the secret value for %s, Error: %w", sName, err) 67 | } 68 | 69 | secrets[sName] = *SValue.Value 70 | } 71 | } 72 | 73 | if kv.config.Delim != "" && !kv.config.FlatPaths { 74 | data := maps.Unflatten(secrets, kv.config.Delim) 75 | return data, nil 76 | } 77 | 78 | return secrets, nil 79 | } 80 | -------------------------------------------------------------------------------- /providers/azkeyvault/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/azkeyvault 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 7 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 8 | github.com/knadh/koanf/maps v0.1.2 9 | ) 10 | 11 | require ( 12 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 13 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect 14 | github.com/mitchellh/copystructure v1.2.0 // indirect 15 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 16 | golang.org/x/net v0.39.0 // indirect 17 | golang.org/x/text v0.24.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /providers/azkeyvault/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 3 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= 4 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= 5 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= 6 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 7 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= 8 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= 9 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= 10 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= 11 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 12 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 16 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 17 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 18 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 19 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 20 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 21 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 22 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 23 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 24 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 25 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 26 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 27 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 28 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 32 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 34 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 35 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 36 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 37 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 38 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 39 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 40 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /providers/basicflag/basicflag.go: -------------------------------------------------------------------------------- 1 | // Package basicflag implements a koanf.Provider that reads commandline 2 | // parameters as conf maps using the Go's flag package. 3 | package basicflag 4 | 5 | import ( 6 | "errors" 7 | "flag" 8 | 9 | "github.com/knadh/koanf/maps" 10 | ) 11 | 12 | // Opt represents optional options (yup) passed to the provider. 13 | type Opt struct { 14 | KeyMap KoanfIntf 15 | } 16 | 17 | // KoanfIntf is an interface that represents a small subset of methods 18 | // used by this package from Koanf{}. 19 | type KoanfIntf interface { 20 | Exists(string) bool 21 | } 22 | 23 | // Pflag implements a pflag command line provider. 24 | type Pflag struct { 25 | delim string 26 | flagset *flag.FlagSet 27 | cb func(key string, value string) (string, interface{}) 28 | opt *Opt 29 | } 30 | 31 | // Provider returns a commandline flags provider that returns 32 | // a nested map[string]interface{} of environment variable where the 33 | // nesting hierarchy of keys are defined by delim. For instance, the 34 | // delim "." will convert the key `parent.child.key: 1` 35 | // to `{parent: {child: {key: 1}}}`. 36 | // 37 | // It takes an optional (but recommended) Opt{} argument containing a Koanf instance. 38 | // It checks if the defined flags have been set by other providers (e.g., a config file). 39 | // If not, default flag values are merged. If they exist, flag values are merged only if 40 | // explicitly set in the command line. The function is variadic to maintain backward compatibility. 41 | // See https://github.com/knadh/koanf/issues/255 42 | func Provider(f *flag.FlagSet, delim string, opt ...*Opt) *Pflag { 43 | pf := &Pflag{ 44 | flagset: f, 45 | delim: delim, 46 | } 47 | 48 | if len(opt) > 0 { 49 | pf.opt = opt[0] 50 | } 51 | 52 | return pf 53 | } 54 | 55 | // ProviderWithValue works exactly the same as Provider except the callback 56 | // takes a (key, value) with the variable name and value and allows you 57 | // to modify both. This is useful for cases where you may want to return 58 | // other types like a string slice instead of just a string. 59 | // 60 | // It takes an optional Opt{} (but recommended) argument with a Koanf instance (opt.KeyMap) to see if 61 | // the flags defined have been set from other providers, for instance, 62 | // a config file. If they are not, then the default values of the flags 63 | // are merged. If they do exist, the flag values are not merged but only 64 | // the values that have been explicitly set in the command line are merged. 65 | // It is a variadic function as a hack to ensure backwards compatibility with the 66 | // function definition. 67 | // See https://github.com/knadh/koanf/issues/255 68 | func ProviderWithValue(f *flag.FlagSet, delim string, cb func(key string, value string) (string, interface{}), ko ...KoanfIntf) *Pflag { 69 | pf := &Pflag{ 70 | flagset: f, 71 | delim: delim, 72 | cb: cb, 73 | } 74 | 75 | if len(ko) > 0 { 76 | pf.opt = &Opt{ 77 | KeyMap: ko[0], 78 | } 79 | } 80 | return pf 81 | } 82 | 83 | // Read reads the flag variables and returns a nested conf map. 84 | func (p *Pflag) Read() (map[string]interface{}, error) { 85 | var changed map[string]struct{} 86 | 87 | // Prepare a map of flags that have been explicitly set by the user as aa KeyMap instance of Koanf 88 | // has been provided. 89 | if p.opt != nil && p.opt.KeyMap != nil { 90 | changed = map[string]struct{}{} 91 | 92 | p.flagset.Visit(func(f *flag.Flag) { 93 | key := f.Name 94 | if p.cb != nil { 95 | key, _ = p.cb(f.Name, "") 96 | } 97 | if key == "" { 98 | return 99 | } 100 | 101 | changed[key] = struct{}{} 102 | }) 103 | } 104 | 105 | mp := make(map[string]interface{}) 106 | p.flagset.VisitAll(func(f *flag.Flag) { 107 | var ( 108 | key = f.Name 109 | val interface{} = f.Value.String() 110 | ) 111 | if p.cb != nil { 112 | k, v := p.cb(f.Name, f.Value.String()) 113 | // If the callback blanked the key, it should be omitted 114 | if k == "" { 115 | return 116 | } 117 | 118 | key = k 119 | val = v 120 | } 121 | 122 | // If the default value of the flag was never changed by the user, 123 | // it should not override the value in the conf map (if it exists in the first place). 124 | if changed != nil { 125 | if _, ok := changed[key]; !ok { 126 | if p.opt.KeyMap.Exists(key) { 127 | return 128 | } 129 | } 130 | } 131 | 132 | mp[key] = val 133 | }) 134 | return maps.Unflatten(mp, p.delim), nil 135 | } 136 | 137 | // ReadBytes is not supported by the basicflag koanf. 138 | func (p *Pflag) ReadBytes() ([]byte, error) { 139 | return nil, errors.New("basicflag provider does not support this method") 140 | } 141 | -------------------------------------------------------------------------------- /providers/basicflag/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/basicflag 2 | 3 | go 1.23.0 4 | 5 | require github.com/knadh/koanf/maps v0.1.2 6 | 7 | require ( 8 | github.com/mitchellh/copystructure v1.2.0 // indirect 9 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /providers/basicflag/go.sum: -------------------------------------------------------------------------------- 1 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 2 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 3 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 4 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 5 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 6 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 7 | -------------------------------------------------------------------------------- /providers/cliflagv2/cliflagv2.go: -------------------------------------------------------------------------------- 1 | // Package cliflagv2 implements a koanf.Provider that reads commandline 2 | // parameters as conf maps using urfave/cli/v2 flag. 3 | package cliflagv2 4 | 5 | import ( 6 | "errors" 7 | "slices" 8 | "strings" 9 | 10 | "github.com/knadh/koanf/maps" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | // CliFlag implements a cli.Flag command line provider. 15 | type CliFlag struct { 16 | ctx *cli.Context 17 | delim string 18 | config *Config 19 | } 20 | 21 | type Config struct { 22 | Defaults []string 23 | } 24 | 25 | // Provider returns a commandline flags provider that returns 26 | // a nested map[string]interface{} of environment variable where the 27 | // nesting hierarchy of keys are defined by delim. For instance, the 28 | // delim "." will convert the key `parent.child.key: 1` 29 | // to `{parent: {child: {key: 1}}}`. 30 | func Provider(f *cli.Context, delim string) *CliFlag { 31 | return &CliFlag{ 32 | ctx: f, 33 | delim: delim, 34 | config: &Config{ 35 | Defaults: []string{}, 36 | }, 37 | } 38 | } 39 | 40 | // ProviderWithConfig returns a commandline flags provider with a 41 | // Configuration struct attached. 42 | func ProviderWithConfig(f *cli.Context, delim string, config *Config) *CliFlag { 43 | return &CliFlag{ 44 | ctx: f, 45 | delim: delim, 46 | config: config, 47 | } 48 | 49 | } 50 | 51 | // ReadBytes is not supported by the cliflagv2 provider. 52 | func (p *CliFlag) ReadBytes() ([]byte, error) { 53 | return nil, errors.New("cliflagv2 provider does not support this method") 54 | } 55 | 56 | // Read reads the flag variables and returns a nested conf map. 57 | func (p *CliFlag) Read() (map[string]interface{}, error) { 58 | out := make(map[string]interface{}) 59 | 60 | // Get command lineage (from root to current command) 61 | lineage := p.ctx.Lineage() 62 | if len(lineage) > 0 { 63 | // Build command path and process flags for each level 64 | var cmdPath []string 65 | for i := len(lineage) - 1; i >= 0; i-- { 66 | cmd := lineage[i] 67 | if cmd.Command == nil { 68 | continue 69 | } 70 | cmdPath = append(cmdPath, cmd.Command.Name) 71 | prefix := strings.Join(cmdPath, p.delim) 72 | p.processFlags(cmd.Command.Flags, prefix, out) 73 | } 74 | } 75 | 76 | if p.delim == "" { 77 | return out, nil 78 | } 79 | 80 | return maps.Unflatten(out, p.delim), nil 81 | } 82 | 83 | func (p *CliFlag) processFlags(flags []cli.Flag, prefix string, out map[string]interface{}) { 84 | for _, flag := range flags { 85 | name := flag.Names()[0] 86 | if p.ctx.IsSet(name) || slices.Contains(p.config.Defaults, name) { 87 | value := p.getFlagValue(name) 88 | if value != nil { 89 | // Build the full path for the flag 90 | fullPath := name 91 | if prefix != "global" { 92 | fullPath = prefix + p.delim + name 93 | } 94 | 95 | p.setNestedValue(fullPath, value, out) 96 | } 97 | } 98 | } 99 | } 100 | 101 | // setNestedValue sets a value in the nested configuration structure 102 | func (p *CliFlag) setNestedValue(path string, value interface{}, out map[string]interface{}) { 103 | parts := strings.Split(path, p.delim) 104 | current := out 105 | 106 | // Navigate/create the nested structure 107 | for i := 0; i < len(parts)-1; i++ { 108 | if _, exists := current[parts[i]]; !exists { 109 | current[parts[i]] = make(map[string]interface{}) 110 | } 111 | current = current[parts[i]].(map[string]interface{}) 112 | } 113 | 114 | // Set the final value 115 | current[parts[len(parts)-1]] = value 116 | } 117 | 118 | // getFlagValue extracts the typed value from the flag. 119 | func (p *CliFlag) getFlagValue(name string) interface{} { 120 | // Find the flag definition 121 | flag := p.findFlag(name) 122 | if flag == nil { 123 | return nil 124 | } 125 | 126 | // Use type switch to get the appropriate value 127 | switch flag.(type) { 128 | case *cli.StringFlag: 129 | return p.ctx.String(name) 130 | case *cli.StringSliceFlag: 131 | return p.ctx.StringSlice(name) 132 | case *cli.IntFlag: 133 | return p.ctx.Int(name) 134 | case *cli.Int64Flag: 135 | return p.ctx.Int64(name) 136 | case *cli.IntSliceFlag: 137 | return p.ctx.IntSlice(name) 138 | case *cli.Float64Flag: 139 | return p.ctx.Float64(name) 140 | case *cli.Float64SliceFlag: 141 | return p.ctx.Float64Slice(name) 142 | case *cli.BoolFlag: 143 | return p.ctx.Bool(name) 144 | case *cli.DurationFlag: 145 | return p.ctx.Duration(name) 146 | case *cli.TimestampFlag: 147 | return p.ctx.Timestamp(name) 148 | case *cli.PathFlag: 149 | return p.ctx.Path(name) 150 | default: 151 | return p.ctx.Generic(name) 152 | } 153 | } 154 | 155 | // findFlag looks up a flag by name in both global and command-specific flags 156 | func (p *CliFlag) findFlag(name string) cli.Flag { 157 | // Check global flags 158 | for _, f := range p.ctx.App.Flags { 159 | for _, n := range f.Names() { 160 | if n == name { 161 | return f 162 | } 163 | } 164 | } 165 | 166 | // Check command-specific flags if we're in a command 167 | if p.ctx.Command != nil { 168 | for _, f := range p.ctx.Command.Flags { 169 | for _, n := range f.Names() { 170 | if n == name { 171 | return f 172 | } 173 | } 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /providers/cliflagv2/cliflagv2_test.go: -------------------------------------------------------------------------------- 1 | package cliflagv2 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/knadh/koanf/v2" 9 | "github.com/stretchr/testify/require" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func TestCliFlag(t *testing.T) { 14 | cliApp := cli.App{ 15 | Name: "testing", 16 | Action: func(ctx *cli.Context) error { 17 | p := Provider(ctx, ".") 18 | x, err := p.Read() 19 | require.NoError(t, err) 20 | require.NotEmpty(t, x) 21 | 22 | fmt.Printf("x: %v\n", x) 23 | 24 | k := koanf.New(".") 25 | err = k.Load(p, nil) 26 | 27 | fmt.Printf("k.All(): %v\n", k.All()) 28 | 29 | return nil 30 | }, 31 | Flags: []cli.Flag{ 32 | cli.HelpFlag, 33 | cli.VersionFlag, 34 | &cli.StringFlag{ 35 | Name: "test", 36 | Usage: "test flag", 37 | Value: "test", 38 | Aliases: []string{"t"}, 39 | EnvVars: []string{"TEST_FLAG"}, 40 | }, 41 | }, 42 | Commands: []*cli.Command{ 43 | { 44 | Name: "x", 45 | Description: "yeah yeah testing", 46 | Action: func(ctx *cli.Context) error { 47 | p := Provider(ctx, ".") 48 | x, err := p.Read() 49 | require.NoError(t, err) 50 | require.NotEmpty(t, x) 51 | fmt.Printf("x: %s\n", x) 52 | 53 | k := koanf.New(".") 54 | err = k.Load(p, nil) 55 | 56 | fmt.Printf("k.All(): %v\n", k.All()) 57 | 58 | require.Equal(t, k.String("testing.x.lol"), "dsf") 59 | return nil 60 | }, 61 | Flags: []cli.Flag{ 62 | cli.HelpFlag, 63 | cli.VersionFlag, 64 | &cli.StringFlag{ 65 | Name: "lol", 66 | Usage: "test flag", 67 | Value: "test", 68 | Required: true, 69 | EnvVars: []string{"TEST_FLAG"}, 70 | }, 71 | }, 72 | }, 73 | }, 74 | } 75 | 76 | x := append([]string{"testing", "--test", "gf", "x", "--lol", "dsf"}, os.Args...) 77 | err := cliApp.Run(append(x, os.Environ()...)) 78 | require.NoError(t, err) 79 | } 80 | -------------------------------------------------------------------------------- /providers/cliflagv2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/cliflagv2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/knadh/koanf/maps v0.1.2 7 | github.com/knadh/koanf/v2 v2.1.2 8 | github.com/stretchr/testify v1.9.0 9 | github.com/urfave/cli/v2 v2.27.6 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 16 | github.com/mitchellh/copystructure v1.2.0 // indirect 17 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 20 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /providers/cliflagv2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 6 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 7 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 8 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 9 | github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= 10 | github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= 11 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 12 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 13 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 14 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 18 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 19 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 20 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= 22 | github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 23 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 24 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /providers/cliflagv3/cliflagv3.go: -------------------------------------------------------------------------------- 1 | // Package cliflagv3 implements a koanf.Provider that reads commandline 2 | // parameters as conf maps using urfave/cli/v3 flag. 3 | package cliflagv3 4 | 5 | import ( 6 | "errors" 7 | "slices" 8 | "strings" 9 | 10 | "github.com/knadh/koanf/maps" 11 | "github.com/urfave/cli/v3" 12 | ) 13 | 14 | // CliFlag implements a cli.Flag command line provider. 15 | type CliFlag struct { 16 | cmd *cli.Command 17 | delim string 18 | config *Config 19 | } 20 | 21 | type Config struct { 22 | Defaults []string 23 | } 24 | 25 | // Provider returns a commandline flags provider that returns 26 | // a nested map[string]interface{} of environment variable where the 27 | // nesting hierarchy of keys are defined by delim. For instance, the 28 | // delim "." will convert the key `parent.child.key: 1` 29 | // to `{parent: {child: {key: 1}}}`. 30 | func Provider(f *cli.Command, delim string) *CliFlag { 31 | return &CliFlag{ 32 | cmd: f, 33 | delim: delim, 34 | config: &Config{ 35 | Defaults: []string{}, 36 | }, 37 | } 38 | } 39 | 40 | // ProviderWithConfig returns a commandline flags provider with a 41 | // Configuration struct attached. 42 | func ProviderWithConfig(f *cli.Command, delim string, config *Config) *CliFlag { 43 | return &CliFlag{ 44 | cmd: f, 45 | delim: delim, 46 | config: config, 47 | } 48 | } 49 | 50 | // ReadBytes is not supported by the cliflagv3 provider. 51 | func (p *CliFlag) ReadBytes() ([]byte, error) { 52 | return nil, errors.New("cliflagv3 provider does not support this method") 53 | } 54 | 55 | // Read reads the flag variables and returns a nested conf map. 56 | func (p *CliFlag) Read() (map[string]interface{}, error) { 57 | out := make(map[string]interface{}) 58 | 59 | // Get command lineage (from root to current command) 60 | lineage := p.cmd.Lineage() 61 | if len(lineage) > 0 { 62 | // Build command path and process flags for each level 63 | var cmdPath []string 64 | for i := len(lineage) - 1; i >= 0; i-- { 65 | cmd := lineage[i] 66 | cmdPath = append(cmdPath, cmd.Name) 67 | prefix := strings.Join(cmdPath, p.delim) 68 | p.processFlags(cmd.Flags, prefix, out) 69 | } 70 | } 71 | 72 | if p.delim == "" { 73 | return out, nil 74 | } 75 | 76 | return maps.Unflatten(out, p.delim), nil 77 | } 78 | 79 | func (p *CliFlag) processFlags(flags []cli.Flag, prefix string, out map[string]interface{}) { 80 | for _, flag := range flags { 81 | name := flag.Names()[0] 82 | if p.cmd.IsSet(name) || slices.Contains(p.config.Defaults, name) { 83 | value := p.getFlagValue(name) 84 | if value != nil { 85 | // Build the full path for the flag 86 | fullPath := name 87 | if prefix != "global" { 88 | fullPath = prefix + p.delim + name 89 | } 90 | 91 | p.setNestedValue(fullPath, value, out) 92 | } 93 | } 94 | } 95 | } 96 | 97 | // setNestedValue sets a value in the nested configuration structure 98 | func (p *CliFlag) setNestedValue(path string, value interface{}, out map[string]interface{}) { 99 | parts := strings.Split(path, p.delim) 100 | current := out 101 | 102 | // Navigate/create the nested structure 103 | for i := 0; i < len(parts)-1; i++ { 104 | if _, exists := current[parts[i]]; !exists { 105 | current[parts[i]] = make(map[string]interface{}) 106 | } 107 | current = current[parts[i]].(map[string]interface{}) 108 | } 109 | 110 | // Set the final value 111 | current[parts[len(parts)-1]] = value 112 | } 113 | 114 | // getFlagValue extracts the typed value from the flag. 115 | func (p *CliFlag) getFlagValue(name string) interface{} { 116 | // Find the flag definition 117 | flag := p.findFlag(name) 118 | if flag == nil { 119 | return nil 120 | } 121 | return flag.Get() 122 | } 123 | 124 | // findFlag looks up a flag by name 125 | func (p *CliFlag) findFlag(name string) cli.Flag { 126 | // Check global flags 127 | for _, f := range p.cmd.Flags { 128 | if slices.Contains(f.Names(), name) { 129 | return f 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /providers/cliflagv3/cliflagv3_test.go: -------------------------------------------------------------------------------- 1 | package cliflagv3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/knadh/koanf/v2" 10 | "github.com/stretchr/testify/require" 11 | "github.com/urfave/cli/v3" 12 | ) 13 | 14 | func TestCliFlag(t *testing.T) { 15 | cliApp := cli.Command{ 16 | Name: "testing", 17 | Action: func(ctx context.Context, cmd *cli.Command) error { 18 | p := Provider(cmd, ".") 19 | x, err := p.Read() 20 | require.NoError(t, err) 21 | require.NotEmpty(t, x) 22 | 23 | fmt.Printf("x: %v\n", x) 24 | 25 | k := koanf.New(".") 26 | err = k.Load(p, nil) 27 | 28 | fmt.Printf("k.All(): %v\n", k.All()) 29 | 30 | return nil 31 | }, 32 | Flags: []cli.Flag{ 33 | cli.HelpFlag, 34 | cli.VersionFlag, 35 | &cli.StringFlag{ 36 | Name: "test", 37 | Usage: "test flag", 38 | Value: "test", 39 | Aliases: []string{"t"}, 40 | Sources: cli.EnvVars("TEST_FLAG"), 41 | }, 42 | }, 43 | Commands: []*cli.Command{ 44 | { 45 | Name: "x", 46 | Description: "yeah yeah testing", 47 | Action: func(ctx context.Context, cmd *cli.Command) error { 48 | p := Provider(cmd, ".") 49 | x, err := p.Read() 50 | require.NoError(t, err) 51 | require.NotEmpty(t, x) 52 | fmt.Printf("x: %s\n", x) 53 | 54 | k := koanf.New(".") 55 | err = k.Load(p, nil) 56 | 57 | fmt.Printf("k.All(): %v\n", k.All()) 58 | 59 | require.Equal(t, k.String("testing.x.lol"), "dsf") 60 | return nil 61 | }, 62 | Flags: []cli.Flag{ 63 | cli.HelpFlag, 64 | cli.VersionFlag, 65 | &cli.StringFlag{ 66 | Name: "lol", 67 | Usage: "test flag", 68 | Value: "test", 69 | Required: true, 70 | Sources: cli.EnvVars("TEST_FLAG"), 71 | }, 72 | }, 73 | }, 74 | }, 75 | } 76 | 77 | x := []string{"testing", "--test", "gf", "x", "--lol", "dsf"} 78 | err := cliApp.Run(context.Background(), append(x, os.Environ()...)) 79 | require.NoError(t, err) 80 | } 81 | -------------------------------------------------------------------------------- /providers/cliflagv3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/cliflagv3 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/knadh/koanf/maps v0.1.2 7 | github.com/knadh/koanf/v2 v2.1.2 8 | github.com/stretchr/testify v1.10.0 9 | github.com/urfave/cli/v3 v3.3.3 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 15 | github.com/mitchellh/copystructure v1.2.0 // indirect 16 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /providers/cliflagv3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 4 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 5 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 6 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 7 | github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= 8 | github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= 9 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 10 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 11 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 12 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 18 | github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /providers/confmap/confmap.go: -------------------------------------------------------------------------------- 1 | // Package confmap implements a koanf.Provider that takes nested 2 | // and flat map[string]interface{} config maps and provides them 3 | // to koanf. 4 | package confmap 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/knadh/koanf/maps" 10 | ) 11 | 12 | // Confmap implements a raw map[string]interface{} provider. 13 | type Confmap struct { 14 | mp map[string]interface{} 15 | } 16 | 17 | // Provider returns a confmap Provider that takes a flat or nested 18 | // map[string]interface{}. If a delim is provided, it indicates that the 19 | // keys are flat and the map needs to be unflattened by delim. 20 | func Provider(mp map[string]interface{}, delim string) *Confmap { 21 | cp := maps.Copy(mp) 22 | maps.IntfaceKeysToStrings(cp) 23 | if delim != "" { 24 | cp = maps.Unflatten(cp, delim) 25 | } 26 | return &Confmap{mp: cp} 27 | } 28 | 29 | // ReadBytes is not supported by the confmap provider. 30 | func (e *Confmap) ReadBytes() ([]byte, error) { 31 | return nil, errors.New("confmap provider does not support this method") 32 | } 33 | 34 | // Read returns the loaded map[string]interface{}. 35 | func (e *Confmap) Read() (map[string]interface{}, error) { 36 | return e.mp, nil 37 | } 38 | -------------------------------------------------------------------------------- /providers/confmap/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/confmap 2 | 3 | go 1.23.0 4 | 5 | require github.com/knadh/koanf/maps v0.1.2 6 | 7 | require ( 8 | github.com/mitchellh/copystructure v1.2.0 // indirect 9 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /providers/confmap/go.sum: -------------------------------------------------------------------------------- 1 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 2 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 3 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 4 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 5 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 6 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 7 | -------------------------------------------------------------------------------- /providers/consul/consul.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/hashicorp/consul/api" 8 | "github.com/hashicorp/consul/api/watch" 9 | ) 10 | 11 | // Config represents the Consul client configuration. 12 | type Config struct { 13 | // Path of the key to read. If Recurse is true, this is treated 14 | // as a prefix. 15 | Key string 16 | 17 | // https://www.consul.io/api-docs/kv#read-key 18 | // If recurse is true, Consul returns an array of keys. 19 | // It specifies if the lookup should be recursive and treat 20 | // Key as a prefix instead of a literal match. 21 | // This is analogous to: consul kv get -recurse key 22 | Recurse bool 23 | 24 | // Gets additional metadata about the key in addition to the value such 25 | // as the ModifyIndex and any flags that may have been set on the key. 26 | // This is analogous to: consul kv get -detailed key 27 | Detailed bool 28 | 29 | // Consul client config 30 | Cfg *api.Config 31 | } 32 | 33 | // Consul implements the Consul provider. 34 | type Consul struct { 35 | client *api.Client 36 | cfg Config 37 | } 38 | 39 | // Provider returns an instance of the Consul provider. 40 | func Provider(cfg Config) (*Consul, error) { 41 | c, err := api.NewClient(cfg.Cfg) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &Consul{client: c, cfg: cfg}, nil 47 | } 48 | 49 | // ReadBytes is not supported by the Consul provider. 50 | func (c *Consul) ReadBytes() ([]byte, error) { 51 | return nil, errors.New("consul provider does not support this method") 52 | } 53 | 54 | // Read reads configuration from the Consul provider. 55 | func (c *Consul) Read() (map[string]interface{}, error) { 56 | var ( 57 | mp = make(map[string]interface{}) 58 | kv = c.client.KV() 59 | ) 60 | 61 | if c.cfg.Recurse { 62 | pairs, _, err := kv.List(c.cfg.Key, nil) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // Detailed information can be obtained using standard koanf flattened delimited keys: 68 | // For example: 69 | // "parent1.CreateIndex" 70 | // "parent1.Flags" 71 | // "parent1.LockIndex" 72 | // "parent1.ModifyIndex" 73 | // "parent1.Session" 74 | // "parent1.Value" 75 | if c.cfg.Detailed { 76 | for _, pair := range pairs { 77 | m := make(map[string]interface{}) 78 | m["CreateIndex"] = fmt.Sprintf("%d", pair.CreateIndex) 79 | m["Flags"] = fmt.Sprintf("%d", pair.Flags) 80 | m["LockIndex"] = fmt.Sprintf("%d", pair.LockIndex) 81 | m["ModifyIndex"] = fmt.Sprintf("%d", pair.ModifyIndex) 82 | 83 | if pair.Session == "" { 84 | m["Session"] = "-" 85 | } else { 86 | m["Session"] = fmt.Sprintf("%s", pair.Session) 87 | } 88 | 89 | m["Value"] = string(pair.Value) 90 | 91 | mp[pair.Key] = m 92 | } 93 | } else { 94 | for _, pair := range pairs { 95 | mp[pair.Key] = string(pair.Value) 96 | } 97 | } 98 | 99 | return mp, nil 100 | } 101 | 102 | pair, _, err := kv.Get(c.cfg.Key, nil) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | if c.cfg.Detailed { 108 | m := make(map[string]interface{}) 109 | m["CreateIndex"] = fmt.Sprintf("%d", pair.CreateIndex) 110 | m["Flags"] = fmt.Sprintf("%d", pair.Flags) 111 | m["LockIndex"] = fmt.Sprintf("%d", pair.LockIndex) 112 | m["ModifyIndex"] = fmt.Sprintf("%d", pair.ModifyIndex) 113 | 114 | if pair.Session == "" { 115 | m["Session"] = "-" 116 | } else { 117 | m["Session"] = fmt.Sprintf("%s", pair.Session) 118 | } 119 | 120 | m["Value"] = string(pair.Value) 121 | 122 | mp[pair.Key] = m 123 | } else { 124 | mp[pair.Key] = string(pair.Value) 125 | } 126 | 127 | return mp, nil 128 | } 129 | 130 | // Watch watches for changes in the Consul API and triggers a callback. 131 | func (c *Consul) Watch(cb func(event interface{}, err error)) error { 132 | p := make(map[string]interface{}) 133 | 134 | if c.cfg.Recurse { 135 | p["type"] = "keyprefix" 136 | p["prefix"] = c.cfg.Key 137 | } else { 138 | p["type"] = "key" 139 | p["key"] = c.cfg.Key 140 | } 141 | 142 | plan, err := watch.Parse(p) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | plan.Handler = func(_ uint64, val interface{}) { 148 | cb(val, nil) 149 | } 150 | 151 | go func() { 152 | plan.Run(c.cfg.Cfg.Address) 153 | }() 154 | 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /providers/consul/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/consul/v2 2 | 3 | go 1.23.0 4 | 5 | require github.com/hashicorp/consul/api v1.32.0 6 | 7 | require ( 8 | github.com/armon/go-metrics v0.4.1 // indirect 9 | github.com/fatih/color v1.18.0 // indirect 10 | github.com/hashicorp/errwrap v1.1.0 // indirect 11 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 12 | github.com/hashicorp/go-hclog v1.6.3 // indirect 13 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 14 | github.com/hashicorp/go-metrics v0.5.4 // indirect 15 | github.com/hashicorp/go-multierror v1.1.1 // indirect 16 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 17 | github.com/hashicorp/golang-lru v1.0.2 // indirect 18 | github.com/hashicorp/serf v0.10.2 // indirect 19 | github.com/mattn/go-colorable v0.1.14 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/mitchellh/go-homedir v1.1.0 // indirect 22 | github.com/mitchellh/mapstructure v1.5.0 // indirect 23 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 24 | golang.org/x/net v0.38.0 // indirect 25 | golang.org/x/sys v0.32.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /providers/env/env.go: -------------------------------------------------------------------------------- 1 | // Package env implements a koanf.Provider that reads environment 2 | // variables as conf maps. 3 | package env 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "strings" 9 | 10 | "github.com/knadh/koanf/maps" 11 | ) 12 | 13 | // Env implements an environment variables provider. 14 | type Env struct { 15 | prefix string 16 | delim string 17 | cb func(key string, value string) (string, interface{}) 18 | } 19 | 20 | // Provider returns an environment variables provider that returns 21 | // a nested map[string]interface{} of environment variable where the 22 | // nesting hierarchy of keys is defined by delim. For instance, the 23 | // delim "." will convert the key `parent.child.key: 1` 24 | // to `{parent: {child: {key: 1}}}`. 25 | // 26 | // If prefix is specified (case-sensitive), only the env vars with 27 | // the prefix are captured. cb is an optional callback that takes 28 | // a string and returns a string (the env variable name) in case 29 | // transformations have to be applied, for instance, to lowercase 30 | // everything, strip prefixes and replace _ with . etc. 31 | // If the callback returns an empty string, the variable will be 32 | // ignored. 33 | func Provider(prefix, delim string, cb func(s string) string) *Env { 34 | e := &Env{ 35 | prefix: prefix, 36 | delim: delim, 37 | } 38 | if cb != nil { 39 | e.cb = func(key string, value string) (string, interface{}) { 40 | return cb(key), value 41 | } 42 | } 43 | return e 44 | } 45 | 46 | // ProviderWithValue works exactly the same as Provider except the callback 47 | // takes a (key, value) with the variable name and value and allows you 48 | // to modify both. This is useful for cases where you may want to return 49 | // other types like a string slice instead of just a string. 50 | func ProviderWithValue(prefix, delim string, cb func(key string, value string) (string, interface{})) *Env { 51 | return &Env{ 52 | prefix: prefix, 53 | delim: delim, 54 | cb: cb, 55 | } 56 | } 57 | 58 | // ReadBytes is not supported by the env provider. 59 | func (e *Env) ReadBytes() ([]byte, error) { 60 | return nil, errors.New("env provider does not support this method") 61 | } 62 | 63 | // Read reads all available environment variables into a key:value map 64 | // and returns it. 65 | func (e *Env) Read() (map[string]interface{}, error) { 66 | // Collect the environment variable keys. 67 | var keys []string 68 | for _, k := range os.Environ() { 69 | if e.prefix != "" { 70 | if strings.HasPrefix(k, e.prefix) { 71 | keys = append(keys, k) 72 | } 73 | } else { 74 | keys = append(keys, k) 75 | } 76 | } 77 | 78 | mp := make(map[string]interface{}) 79 | for _, k := range keys { 80 | parts := strings.SplitN(k, "=", 2) 81 | 82 | // If there's a transformation callback, 83 | // run it through every key/value. 84 | if e.cb != nil { 85 | key, value := e.cb(parts[0], parts[1]) 86 | // If the callback blanked the key, it should be omitted 87 | if key == "" { 88 | continue 89 | } 90 | mp[key] = value 91 | } else { 92 | mp[parts[0]] = parts[1] 93 | } 94 | 95 | } 96 | 97 | if e.delim != "" { 98 | return maps.Unflatten(mp, e.delim), nil 99 | } 100 | 101 | return mp, nil 102 | } 103 | -------------------------------------------------------------------------------- /providers/env/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/env 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/knadh/koanf/maps v0.1.2 7 | github.com/stretchr/testify v1.8.4 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.2.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/mitchellh/copystructure v1.2.0 // indirect 15 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /providers/env/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 5 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 6 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 7 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 13 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 14 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 15 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 19 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 22 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /providers/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | clientv3 "go.etcd.io/etcd/client/v3" 9 | ) 10 | 11 | type Config struct { 12 | // etcd endpoints 13 | Endpoints []string 14 | 15 | // timeout 16 | DialTimeout time.Duration 17 | 18 | // prefix request option 19 | Prefix bool 20 | 21 | // limit request option 22 | Limit bool 23 | 24 | // number of limited pairs 25 | NLimit int64 26 | 27 | // key, key with prefix, etc. 28 | Key string 29 | } 30 | 31 | // Etcd implements the etcd config provider. 32 | type Etcd struct { 33 | client *clientv3.Client 34 | cfg Config 35 | } 36 | 37 | // Provider returns a provider that takes etcd config. 38 | func Provider(cfg Config) (*Etcd, error) { 39 | eCfg := clientv3.Config{ 40 | Endpoints: cfg.Endpoints, 41 | DialTimeout: cfg.DialTimeout, 42 | } 43 | 44 | c, err := clientv3.New(eCfg) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return &Etcd{client: c, cfg: cfg}, nil 50 | } 51 | 52 | // ReadBytes is not supported by etcd provider. 53 | func (e *Etcd) ReadBytes() ([]byte, error) { 54 | return nil, errors.New("etcd provider does not support this method") 55 | } 56 | 57 | // Read returns a nested config map. 58 | func (e *Etcd) Read() (map[string]interface{}, error) { 59 | ctx, cancel := context.WithTimeout(context.Background(), e.cfg.DialTimeout) 60 | defer cancel() 61 | 62 | var resp *clientv3.GetResponse 63 | if e.cfg.Prefix { 64 | if e.cfg.Limit { 65 | r, err := e.client.Get(ctx, e.cfg.Key, clientv3.WithPrefix(), clientv3.WithLimit(e.cfg.NLimit)) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | resp = r 71 | } else { 72 | r, err := e.client.Get(ctx, e.cfg.Key, clientv3.WithPrefix()) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | resp = r 78 | } 79 | } else { 80 | r, err := e.client.Get(ctx, e.cfg.Key) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | resp = r 86 | } 87 | 88 | mp := make(map[string]interface{}, len(resp.Kvs)) 89 | for _, r := range resp.Kvs { 90 | mp[string(r.Key)] = string(r.Value) 91 | } 92 | 93 | return mp, nil 94 | } 95 | 96 | func (e *Etcd) Watch(cb func(event interface{}, err error)) error { 97 | var w clientv3.WatchChan 98 | 99 | go func() { 100 | if e.cfg.Prefix { 101 | w = e.client.Watch(context.Background(), e.cfg.Key, clientv3.WithPrefix()) 102 | } else { 103 | w = e.client.Watch(context.Background(), e.cfg.Key) 104 | } 105 | 106 | for wresp := range w { 107 | for _, ev := range wresp.Events { 108 | cb(ev, nil) 109 | } 110 | } 111 | }() 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /providers/etcd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/etcd/v2 2 | 3 | go 1.23.0 4 | 5 | require go.etcd.io/etcd/client/v3 v3.5.21 6 | 7 | require ( 8 | github.com/coreos/go-semver v0.3.1 // indirect 9 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 10 | github.com/gogo/protobuf v1.3.2 // indirect 11 | github.com/golang/protobuf v1.5.4 // indirect 12 | go.etcd.io/etcd/api/v3 v3.5.21 // indirect 13 | go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect 14 | go.uber.org/multierr v1.11.0 // indirect 15 | go.uber.org/zap v1.27.0 // indirect 16 | golang.org/x/net v0.39.0 // indirect 17 | golang.org/x/sys v0.32.0 // indirect 18 | golang.org/x/text v0.24.0 // indirect 19 | google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect 20 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect 21 | google.golang.org/grpc v1.71.1 // indirect 22 | google.golang.org/protobuf v1.36.6 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /providers/file/file.go: -------------------------------------------------------------------------------- 1 | // Package file implements a koanf.Provider that reads raw bytes 2 | // from files on disk to be used with a koanf.Parser to parse 3 | // into conf maps. 4 | package file 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/fsnotify/fsnotify" 15 | ) 16 | 17 | // File implements a File provider. 18 | type File struct { 19 | path string 20 | w *fsnotify.Watcher 21 | 22 | // Using Go 1.18 atomic functions for backwards compatibility. 23 | isWatching uint32 24 | isUnwatched uint32 25 | } 26 | 27 | // Provider returns a file provider. 28 | func Provider(path string) *File { 29 | return &File{path: filepath.Clean(path)} 30 | } 31 | 32 | // ReadBytes reads the contents of a file on disk and returns the bytes. 33 | func (f *File) ReadBytes() ([]byte, error) { 34 | return os.ReadFile(f.path) 35 | } 36 | 37 | // Read is not supported by the file provider. 38 | func (f *File) Read() (map[string]interface{}, error) { 39 | return nil, errors.New("file provider does not support this method") 40 | } 41 | 42 | // Watch watches the file and triggers a callback when it changes. It is a 43 | // blocking function that internally spawns a goroutine to watch for changes. 44 | func (f *File) Watch(cb func(event interface{}, err error)) error { 45 | // If a watcher already exists, return an error. 46 | if atomic.LoadUint32(&f.isWatching) == 1 { 47 | return errors.New("file is already being watched") 48 | } 49 | 50 | // Resolve symlinks and save the original path so that changes to symlinks 51 | // can be detected. 52 | realPath, err := filepath.EvalSymlinks(f.path) 53 | if err != nil { 54 | return err 55 | } 56 | realPath = filepath.Clean(realPath) 57 | 58 | // Although only a single file is being watched, fsnotify has to watch 59 | // the whole parent directory to pick up all events such as symlink changes. 60 | fDir, _ := filepath.Split(f.path) 61 | 62 | f.w, err = fsnotify.NewWatcher() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | atomic.StoreUint32(&f.isWatching, 1) 68 | 69 | var ( 70 | lastEvent string 71 | lastEventTime time.Time 72 | ) 73 | 74 | go func() { 75 | loop: 76 | for { 77 | select { 78 | case event, ok := <-f.w.Events: 79 | if !ok { 80 | // Only throw an error if it was not an explicit unwatch. 81 | if atomic.LoadUint32(&f.isUnwatched) == 0 { 82 | cb(nil, errors.New("fsnotify watch channel closed")) 83 | } 84 | 85 | break loop 86 | } 87 | 88 | // Use a simple timer to buffer events as certain events fire 89 | // multiple times on some platforms. 90 | if event.String() == lastEvent && time.Since(lastEventTime) < time.Millisecond*5 { 91 | continue 92 | } 93 | lastEvent = event.String() 94 | lastEventTime = time.Now() 95 | 96 | evFile := filepath.Clean(event.Name) 97 | 98 | // Resolve symlink to get the real path, in case the symlink's 99 | // target has changed. 100 | curPath, err := filepath.EvalSymlinks(f.path) 101 | if err != nil { 102 | cb(nil, err) 103 | break loop 104 | } 105 | curPath = filepath.Clean(curPath) 106 | 107 | onWatchedFile := evFile == realPath || evFile == f.path 108 | 109 | // Since the event is triggered on a directory, is this 110 | // a create or write on the file being watched? 111 | // 112 | // Or has the real path of the file being watched changed? 113 | // 114 | // If either of the above are true, trigger the callback. 115 | if event.Has(fsnotify.Create|fsnotify.Write) && (onWatchedFile || 116 | (curPath != "" && curPath != realPath)) { 117 | realPath = curPath 118 | 119 | // Trigger event. 120 | cb(nil, nil) 121 | } else if onWatchedFile && event.Has(fsnotify.Remove) { 122 | cb(nil, fmt.Errorf("file %s was removed", event.Name)) 123 | break loop 124 | } 125 | 126 | // There's an error. 127 | case err, ok := <-f.w.Errors: 128 | if !ok { 129 | // Only throw an error if it was not an explicit unwatch. 130 | if atomic.LoadUint32(&f.isUnwatched) == 0 { 131 | cb(nil, errors.New("fsnotify err channel closed")) 132 | } 133 | 134 | break loop 135 | } 136 | 137 | // Pass the error to the callback. 138 | cb(nil, err) 139 | break loop 140 | } 141 | } 142 | 143 | atomic.StoreUint32(&f.isWatching, 0) 144 | atomic.StoreUint32(&f.isUnwatched, 0) 145 | f.w.Close() 146 | }() 147 | 148 | // Watch the directory for changes. 149 | return f.w.Add(fDir) 150 | } 151 | 152 | // Unwatch stops watching the files and closes fsnotify watcher. 153 | func (f *File) Unwatch() error { 154 | atomic.StoreUint32(&f.isUnwatched, 1) 155 | return f.w.Close() 156 | } 157 | -------------------------------------------------------------------------------- /providers/file/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/file 2 | 3 | go 1.23.0 4 | 5 | require github.com/fsnotify/fsnotify v1.9.0 6 | 7 | require golang.org/x/sys v0.32.0 // indirect 8 | -------------------------------------------------------------------------------- /providers/file/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 2 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 3 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 4 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 5 | -------------------------------------------------------------------------------- /providers/fs/fs.go: -------------------------------------------------------------------------------- 1 | // Package fs implements a koanf.Provider that reads raw bytes 2 | // from given fs.FS to be used with a koanf.Parser to parse 3 | // into conf maps. 4 | 5 | //go:build go1.16 6 | // +build go1.16 7 | 8 | package fs 9 | 10 | import ( 11 | "errors" 12 | "io" 13 | "io/fs" 14 | ) 15 | 16 | // FS implements an fs.FS provider. 17 | type FS struct { 18 | fs fs.FS 19 | path string 20 | } 21 | 22 | // Provider returns an fs.FS provider. 23 | func Provider(fs fs.FS, filepath string) *FS { 24 | return &FS{fs: fs, path: filepath} 25 | } 26 | 27 | // ReadBytes reads the contents of given filepath from fs.FS and returns the bytes. 28 | func (f *FS) ReadBytes() ([]byte, error) { 29 | fd, err := f.fs.Open(f.path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer fd.Close() 34 | 35 | return io.ReadAll(fd) 36 | } 37 | 38 | // Read is not supported by the fs.FS provider. 39 | func (f *FS) Read() (map[string]interface{}, error) { 40 | return nil, errors.New("fs.FS provider does not support this method") 41 | } 42 | -------------------------------------------------------------------------------- /providers/fs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/fs 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /providers/nats/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/nats 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/knadh/koanf/v2 v2.0.0 7 | github.com/nats-io/nats-server/v2 v2.10.27 8 | github.com/nats-io/nats.go v1.41.1 9 | github.com/stretchr/testify v1.8.4 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/klauspost/compress v1.18.0 // indirect 15 | github.com/knadh/koanf/maps v0.1.1 // indirect 16 | github.com/kr/pretty v0.2.1 // indirect 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/minio/highwayhash v1.0.3 // indirect 19 | github.com/mitchellh/copystructure v1.2.0 // indirect 20 | github.com/mitchellh/mapstructure v1.5.0 // indirect 21 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 22 | github.com/nats-io/jwt/v2 v2.7.3 // indirect 23 | github.com/nats-io/nkeys v0.4.10 // indirect 24 | github.com/nats-io/nuid v1.0.1 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | golang.org/x/crypto v0.37.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/time v0.10.0 // indirect 29 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /providers/nats/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 5 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 6 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= 7 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 8 | github.com/knadh/koanf/v2 v2.0.0 h1:XPQ5ilNnwnNaHrfQ1YpTVhUAjcGHnEKA+lRpipQv02Y= 9 | github.com/knadh/koanf/v2 v2.0.0/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= 10 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 11 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 12 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 15 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 16 | github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= 17 | github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= 18 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 19 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 20 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 21 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 22 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 23 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 24 | github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= 25 | github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= 26 | github.com/nats-io/nats-server/v2 v2.10.27 h1:A/i3JqtrP897UHc2/Jia/mqaXkqj9+HGdpz+R0mC+sM= 27 | github.com/nats-io/nats-server/v2 v2.10.27/go.mod h1:SGzoWGU8wUVnMr/HJhEMv4R8U4f7hF4zDygmRxpNsvg= 28 | github.com/nats-io/nats.go v1.41.1 h1:lCc/i5x7nqXbspxtmXaV4hRguMPHqE/kYltG9knrCdU= 29 | github.com/nats-io/nats.go v1.41.1/go.mod h1:mzHiutcAdZrg6WLfYVKXGseqqow2fWmwlTEUOHsI4jY= 30 | github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc= 31 | github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U= 32 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 33 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 37 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 38 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 39 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 40 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 42 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 43 | golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= 44 | golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 47 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /providers/nats/nats.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/knadh/koanf/maps" 10 | "github.com/nats-io/nats.go" 11 | ) 12 | 13 | type Config struct { 14 | // nats endpoint (comma separated urls are possible, eg "nats://one, nats://two"). 15 | URL string 16 | 17 | // Optional NATS options: nats.Connect(url, ...options) 18 | Options []nats.Option 19 | 20 | // Optional JetStream options: nc.JetStream(...options) 21 | JetStreamOptions []nats.JSOpt 22 | 23 | // Bucket is the Nats KV bucket. 24 | Bucket string 25 | 26 | // Prefix (optional). 27 | Prefix string 28 | 29 | // If true, keys will be unflattened by delimiter "." into a nested map 30 | // So, "a.b.c" results in {"a": {"b": {"c": "value" }}} 31 | // Prefix will be included 32 | Unflatten bool 33 | } 34 | 35 | // Nats implements the nats config provider. 36 | type Nats struct { 37 | kv nats.KeyValue 38 | cfg Config 39 | } 40 | 41 | // Provider returns a provider that takes nats config. 42 | func Provider(cfg Config) (*Nats, error) { 43 | nc, err := nats.Connect(cfg.URL, cfg.Options...) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | js, err := nc.JetStream(cfg.JetStreamOptions...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | kv, err := js.KeyValue(cfg.Bucket) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &Nats{kv: kv, cfg: cfg}, nil 59 | } 60 | 61 | // ReadBytes is not supported by nats provider. 62 | func (n *Nats) ReadBytes() ([]byte, error) { 63 | return nil, errors.New("nats provider does not support this method") 64 | } 65 | 66 | // Read returns a nested config map. 67 | func (n *Nats) Read() (map[string]interface{}, error) { 68 | keys, err := n.kv.Keys() 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | mp := make(map[string]interface{}) 74 | for _, key := range keys { 75 | if !strings.HasPrefix(key, n.cfg.Prefix) { 76 | continue 77 | } 78 | res, err := n.kv.Get(key) 79 | if err != nil { 80 | return nil, err 81 | } 82 | mp[res.Key()] = string(res.Value()) 83 | } 84 | if n.cfg.Unflatten { 85 | return maps.Unflatten(mp, "."), nil 86 | } 87 | 88 | return mp, nil 89 | } 90 | 91 | func (n *Nats) Watch(cb func(event interface{}, err error)) error { 92 | w, err := n.kv.Watch(fmt.Sprintf("%s.*", n.cfg.Prefix)) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | start := time.Now() 98 | go func(watcher nats.KeyWatcher) { 99 | for update := range watcher.Updates() { 100 | // ignore nil events and only callback when the event is new (nats always sends one "old" event) 101 | if update != nil && update.Created().After(start) { 102 | cb(update, nil) 103 | } 104 | } 105 | }(w) 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /providers/nats/nats_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.19 2 | // +build go1.19 3 | 4 | package nats 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/knadh/koanf/v2" 11 | "github.com/nats-io/nats.go" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestNats(t *testing.T) { 16 | k := koanf.NewWithConf(koanf.Conf{}) 17 | 18 | nc, err := nats.Connect(testNatsURL) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer nc.Drain() 23 | 24 | js, err := nc.JetStream() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | kv, err := js.CreateKeyValue(&nats.KeyValueConfig{ 29 | Bucket: "test", 30 | }) 31 | _, err = kv.Put("some.test.color", []byte("blue")) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | provider, err := Provider(Config{ 37 | URL: testNatsURL, 38 | Bucket: "test", 39 | Prefix: "some.test", 40 | }) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | err = k.Load(provider, nil) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | assert.Equal(t, k.Keys(), []string{"some.test.color"}) 50 | assert.Equal(t, k.Get("some.test.color"), "blue") 51 | 52 | err = provider.Watch(func(event interface{}, err error) { 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | err = k.Load(provider, nil) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | }) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | go func() { 67 | _, err := kv.Put("some.test.color", []byte("yellow")) 68 | if err != nil { 69 | t.Error(err) 70 | return 71 | } 72 | }() 73 | 74 | time.Sleep(100 * time.Millisecond) 75 | 76 | assert.Equal(t, k.Get("some.test.color"), "yellow") 77 | } 78 | -------------------------------------------------------------------------------- /providers/nats/testrunner_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.19 2 | // +build go1.19 3 | 4 | package nats 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/nats-io/nats-server/v2/logger" 13 | "github.com/nats-io/nats-server/v2/server" 14 | ) 15 | 16 | var testNatsURL string 17 | 18 | func TestMain(m *testing.M) { 19 | gnatsd, err := server.NewServer(&server.Options{ 20 | Port: server.RANDOM_PORT, 21 | JetStream: true, 22 | }) 23 | if err != nil { 24 | log.Fatal("failed to create gnatsd server") 25 | } 26 | gnatsd.SetLogger( 27 | logger.NewStdLogger(false, false, false, false, false), 28 | false, 29 | false, 30 | ) 31 | go gnatsd.Start() 32 | defer gnatsd.Shutdown() 33 | 34 | if !gnatsd.ReadyForConnections(time.Second) { 35 | log.Fatal("failed to start the gnatsd server") 36 | } 37 | testNatsURL = "nats://" + gnatsd.Addr().String() 38 | 39 | os.Exit(m.Run()) 40 | } 41 | -------------------------------------------------------------------------------- /providers/parameterstore/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/parameterstore/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.36.3 7 | github.com/aws/aws-sdk-go-v2/config v1.29.14 8 | github.com/aws/aws-sdk-go-v2/service/ssm v1.58.2 9 | github.com/aws/smithy-go v1.22.3 10 | github.com/knadh/koanf/maps v0.1.2 11 | github.com/knadh/koanf/v2 v2.0.1 12 | github.com/stretchr/testify v1.8.4 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/kr/pretty v0.2.1 // indirect 28 | github.com/kr/text v0.2.0 // indirect 29 | github.com/mitchellh/copystructure v1.2.0 // indirect 30 | github.com/mitchellh/mapstructure v1.5.0 // indirect 31 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /providers/parameterstore/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 2 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 3 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 4 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 15 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 16 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 17 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 18 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 19 | github.com/aws/aws-sdk-go-v2/service/ssm v1.58.2 h1:uXy3QGAw3xv0RS+OlbeMEAnOA3vFFsf7yvjUswV6N/k= 20 | github.com/aws/aws-sdk-go-v2/service/ssm v1.58.2/go.mod h1:PUWUl5MDiYNQkUHN9Pyd9kgtA/YhbxnSnHP+yQqzrM8= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 27 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 28 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 29 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 33 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 34 | github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= 35 | github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= 36 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 37 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 41 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 42 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 43 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 44 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 45 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 46 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 47 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 51 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 54 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 56 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | -------------------------------------------------------------------------------- /providers/parameterstore/parameterstore.go: -------------------------------------------------------------------------------- 1 | // Package parameterstore implements a koanf.Provider for AWS Systems Manager Parameter Store. 2 | package parameterstore 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | 8 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 9 | "github.com/aws/aws-sdk-go-v2/service/ssm" 10 | "github.com/knadh/koanf/maps" 11 | ) 12 | 13 | // Input is a constraint that permits any input type 14 | // to get parameters from AWS Systems Manager Parameter Store. 15 | type Input interface { 16 | ssm.GetParameterInput | ssm.GetParametersInput | ssm.GetParametersByPathInput 17 | } 18 | 19 | // Config represents a ParameterStore provider configuration. 20 | type Config[T Input] struct { 21 | // Delim is the delimiter to use 22 | // when specifying config key paths, for instance a . for `parent.child.key` 23 | // or a / for `parent/child/key`. 24 | Delim string 25 | 26 | // Input is the input to get parameters. 27 | Input T 28 | 29 | // OptFns is the additional functional options to get parameters. 30 | OptFns []func(*ssm.Options) 31 | 32 | // Callback is an optional callback that takes a (key, value) 33 | // with the variable name and value and allows you to modify both. 34 | // If the callback returns an empty key, the variable will be ignored. 35 | Callback func(key, value string) (string, interface{}) 36 | } 37 | 38 | // ParameterStore implements an AWS Systems Manager Parameter Store provider. 39 | type ParameterStore[T Input] struct { 40 | client *ssm.Client 41 | config Config[T] 42 | } 43 | 44 | // Provider returns a ParameterStore provider. 45 | // The AWS Systems Manager Client is configured via environment variables. 46 | // The configuration values are read from the environment variables. 47 | // - AWS_REGION 48 | // - AWS_ACCESS_KEY_ID 49 | // - AWS_SECRET_ACCESS_KEY 50 | // - AWS_SESSION_TOKEN 51 | func Provider[T Input](config Config[T]) (*ParameterStore[T], error) { 52 | c, err := awsconfig.LoadDefaultConfig(context.TODO()) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return ProviderWithClient[T](config, ssm.NewFromConfig(c)), nil 57 | } 58 | 59 | // ProviderWithClient returns a ParameterStore provider 60 | // using an existing AWS Systems Manager client. 61 | func ProviderWithClient[T Input](config Config[T], client *ssm.Client) *ParameterStore[T] { 62 | return &ParameterStore[T]{ 63 | client: client, 64 | config: config, 65 | } 66 | } 67 | 68 | // ReadBytes is not supported by the ParameterStore provider. 69 | func (ps *ParameterStore[T]) ReadBytes() ([]byte, error) { 70 | return nil, errors.New("parameterstore provider does not support this method") 71 | } 72 | 73 | // Read returns a nested config map. 74 | func (ps *ParameterStore[T]) Read() (map[string]interface{}, error) { 75 | var ( 76 | mp = make(map[string]interface{}) 77 | ) 78 | switch input := interface{}(ps.config.Input).(type) { 79 | case ssm.GetParameterInput: 80 | output, err := ps.client.GetParameter(context.TODO(), &input, ps.config.OptFns...) 81 | if err != nil { 82 | return nil, err 83 | } 84 | // If there's a transformation callback, run it. 85 | if ps.config.Callback != nil { 86 | name, value := ps.config.Callback(*output.Parameter.Name, *output.Parameter.Value) 87 | // If the callback blanked the key, it should be omitted. 88 | if name == "" { 89 | break 90 | } 91 | mp[name] = value 92 | } else { 93 | mp[*output.Parameter.Name] = *output.Parameter.Value 94 | } 95 | case ssm.GetParametersInput: 96 | output, err := ps.client.GetParameters(context.TODO(), &input, ps.config.OptFns...) 97 | if err != nil { 98 | return nil, err 99 | } 100 | for _, p := range output.Parameters { 101 | // If there's a transformation callback, run it. 102 | if ps.config.Callback != nil { 103 | name, value := ps.config.Callback(*p.Name, *p.Value) 104 | // If the callback blanked the key, it should be omitted. 105 | if name == "" { 106 | break 107 | } 108 | mp[name] = value 109 | } else { 110 | mp[*p.Name] = *p.Value 111 | } 112 | } 113 | case ssm.GetParametersByPathInput: 114 | var nextToken *string 115 | for { 116 | input.NextToken = nextToken 117 | output, err := ps.client.GetParametersByPath(context.TODO(), &input, ps.config.OptFns...) 118 | if err != nil { 119 | return nil, err 120 | } 121 | for _, p := range output.Parameters { 122 | // If there's a transformation callback, run it. 123 | if ps.config.Callback != nil { 124 | name, value := ps.config.Callback(*p.Name, *p.Value) 125 | // If the callback blanked the key, it should be omitted. 126 | if name == "" { 127 | break 128 | } 129 | mp[name] = value 130 | } else { 131 | mp[*p.Name] = *p.Value 132 | } 133 | } 134 | if output.NextToken == nil { 135 | break 136 | } 137 | nextToken = output.NextToken 138 | } 139 | } 140 | // Unflatten only when a delimiter is specified. 141 | if ps.config.Delim != "" { 142 | mp = maps.Unflatten(mp, ps.config.Delim) 143 | } 144 | return mp, nil 145 | } 146 | -------------------------------------------------------------------------------- /providers/posflag/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/posflag 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/knadh/koanf/maps v0.1.2 7 | github.com/spf13/pflag v1.0.6 8 | ) 9 | 10 | require ( 11 | github.com/mitchellh/copystructure v1.2.0 // indirect 12 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /providers/posflag/go.sum: -------------------------------------------------------------------------------- 1 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 2 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 3 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 4 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 5 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 6 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 7 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 8 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | -------------------------------------------------------------------------------- /providers/rawbytes/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/rawbytes 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /providers/rawbytes/rawbytes.go: -------------------------------------------------------------------------------- 1 | // Package rawbytes implements a koanf.Provider that takes a []byte slice 2 | // and provides it to koanf to be parsed by a koanf.Parser. 3 | package rawbytes 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | // RawBytes implements a raw bytes provider. 10 | type RawBytes struct { 11 | b []byte 12 | } 13 | 14 | // Provider returns a provider that takes a raw []byte slice to be parsed 15 | // by a koanf.Parser parser. This should be a nested conf map, like the 16 | // contents of a raw JSON config file. 17 | func Provider(b []byte) *RawBytes { 18 | r := &RawBytes{b: make([]byte, len(b))} 19 | copy(r.b[:], b) 20 | return r 21 | } 22 | 23 | // ReadBytes returns the raw bytes for parsing. 24 | func (r *RawBytes) ReadBytes() ([]byte, error) { 25 | return r.b, nil 26 | } 27 | 28 | // Read is not supported by rawbytes provider. 29 | func (r *RawBytes) Read() (map[string]interface{}, error) { 30 | return nil, errors.New("rawbytes provider does not support this method") 31 | } 32 | -------------------------------------------------------------------------------- /providers/s3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/s3 2 | 3 | go 1.23.0 4 | 5 | require github.com/rhnvrm/simples3 v0.8.4 6 | -------------------------------------------------------------------------------- /providers/s3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/rhnvrm/simples3 v0.8.4 h1:w3lhMtL7Cqpi5T61gW03pPFCTHHMwtHCwczUowmLCvc= 2 | github.com/rhnvrm/simples3 v0.8.4/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= 3 | -------------------------------------------------------------------------------- /providers/s3/s3.go: -------------------------------------------------------------------------------- 1 | // Package s3 implements a koanf.Provider that takes a []byte slice 2 | // and provides it to koanf to be parsed by a koanf.Parser. 3 | package s3 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | 9 | "github.com/rhnvrm/simples3" 10 | ) 11 | 12 | // Config for the provider. 13 | type Config struct { 14 | // AWS Access Key 15 | AccessKey string 16 | 17 | // AWS Secret Key 18 | SecretKey string 19 | 20 | // AWS region 21 | Region string 22 | 23 | // Bucket Name 24 | Bucket string 25 | 26 | // Object Key 27 | ObjectKey string 28 | 29 | // Optional: Custom S3 compatible endpoint 30 | Endpoint string 31 | } 32 | 33 | // S3 implements a s3 provider. 34 | type S3 struct { 35 | s3 *simples3.S3 36 | cfg Config 37 | } 38 | 39 | // Provider returns a provider that takes a simples3 config. 40 | func Provider(cfg Config) *S3 { 41 | s3 := simples3.New(cfg.Region, cfg.AccessKey, cfg.SecretKey) 42 | s3.SetEndpoint(cfg.Endpoint) 43 | 44 | return &S3{s3: s3, cfg: cfg} 45 | } 46 | 47 | // ReadBytes reads the contents of a file on s3 and returns the bytes. 48 | func (r *S3) ReadBytes() ([]byte, error) { 49 | resp, err := r.s3.FileDownload(simples3.DownloadInput{ 50 | Bucket: r.cfg.Bucket, 51 | ObjectKey: r.cfg.ObjectKey, 52 | }) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | defer resp.Close() 58 | 59 | data, err := io.ReadAll(resp) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return data, nil 65 | } 66 | 67 | // Read is not supported for s3 provider. 68 | func (r *S3) Read() (map[string]interface{}, error) { 69 | return nil, errors.New("s3 provider does not support this method") 70 | } 71 | -------------------------------------------------------------------------------- /providers/structs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/structs 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/fatih/structs v1.1.0 7 | github.com/knadh/koanf/maps v0.1.2 8 | ) 9 | 10 | require ( 11 | github.com/mitchellh/copystructure v1.2.0 // indirect 12 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /providers/structs/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 2 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 3 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 4 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 5 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 6 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 7 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 8 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 9 | -------------------------------------------------------------------------------- /providers/structs/structs.go: -------------------------------------------------------------------------------- 1 | // Package structs implements a koanf.Provider that takes a struct and tag 2 | // and returns a nested config map (using fatih/structs) to provide it to koanf. 3 | package structs 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/fatih/structs" 9 | 10 | "github.com/knadh/koanf/maps" 11 | ) 12 | 13 | // Structs implements a structs provider. 14 | type Structs struct { 15 | s interface{} 16 | tag string 17 | delim string 18 | } 19 | 20 | // Provider returns a provider that takes a struct and a struct tag 21 | // and uses structs to parse and provide it to koanf. 22 | func Provider(s interface{}, tag string) *Structs { 23 | return &Structs{s: s, tag: tag} 24 | } 25 | 26 | // ProviderWithDelim returns a provider that takes a struct and a struct tag 27 | // along with a delim and uses structs to parse and provide it to koanf. 28 | func ProviderWithDelim(s interface{}, tag, delim string) *Structs { 29 | return &Structs{s: s, tag: tag, delim: delim} 30 | } 31 | 32 | // ReadBytes is not supported by the structs provider. 33 | func (s *Structs) ReadBytes() ([]byte, error) { 34 | return nil, errors.New("structs provider does not support this method") 35 | } 36 | 37 | // Read reads the struct and returns a nested config map. 38 | func (s *Structs) Read() (map[string]interface{}, error) { 39 | ns := structs.New(s.s) 40 | ns.TagName = s.tag 41 | 42 | out := ns.Map() 43 | 44 | if s.delim != "" { 45 | out = maps.Unflatten(out, s.delim) 46 | } 47 | 48 | return out, nil 49 | } 50 | -------------------------------------------------------------------------------- /providers/structs/structs_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type parentStruct struct { 9 | Name string `koanf:"name"` 10 | ID int `koanf:"id"` 11 | Child1 childStruct `koanf:"child1"` 12 | } 13 | type childStruct struct { 14 | Name string `koanf:"name"` 15 | Type string `koanf:"type"` 16 | Empty map[string]string `koanf:"empty"` 17 | Grandchild1 grandchildStruct `koanf:"grandchild1"` 18 | } 19 | type grandchildStruct struct { 20 | Ids []int `koanf:"ids"` 21 | On bool `koanf:"on"` 22 | } 23 | type testStruct struct { 24 | Type string `koanf:"type"` 25 | Empty map[string]string `koanf:"empty"` 26 | Parent1 parentStruct `koanf:"parent1"` 27 | } 28 | 29 | type testStructWithDelim struct { 30 | Endpoint string `koanf:"conf_endpoint"` 31 | Username string `koanf:"conf_creds.username"` 32 | Password string `koanf:"conf_creds.password"` 33 | } 34 | 35 | func TestStructs_Read(t *testing.T) { 36 | type fields struct { 37 | s interface{} 38 | tag string 39 | delim string 40 | } 41 | tests := []struct { 42 | name string 43 | fields fields 44 | want map[string]interface{} 45 | wantErr bool 46 | }{ 47 | { 48 | name: "read", 49 | fields: fields{ 50 | s: testStruct{ 51 | Type: "json", 52 | Empty: make(map[string]string), 53 | Parent1: parentStruct{ 54 | Name: "parent1", 55 | ID: 1234, 56 | Child1: childStruct{ 57 | Name: "child1", 58 | Type: "json", 59 | Empty: make(map[string]string), 60 | Grandchild1: grandchildStruct{ 61 | Ids: []int{1, 2, 3}, 62 | On: true, 63 | }, 64 | }, 65 | }, 66 | }, 67 | tag: "koanf", 68 | }, 69 | want: map[string]interface{}{ 70 | "type": "json", 71 | "empty": map[string]string{}, 72 | "parent1": map[string]interface{}{ 73 | "child1": map[string]interface{}{ 74 | "empty": map[string]string{}, 75 | "type": "json", 76 | "name": "child1", 77 | "grandchild1": map[string]interface{}{ 78 | "on": true, 79 | "ids": []int{1, 2, 3}, 80 | }, 81 | }, 82 | "name": "parent1", 83 | "id": 1234, 84 | }, 85 | }, 86 | wantErr: false, 87 | }, 88 | { 89 | name: "read delim struct", 90 | fields: fields{ 91 | s: testStructWithDelim{ 92 | Endpoint: "test_endpoint", 93 | Username: "test_username", 94 | Password: "test_password", 95 | }, 96 | tag: "koanf", 97 | delim: ".", 98 | }, 99 | want: map[string]interface{}{ 100 | "conf_creds": map[string]interface{}{ 101 | "password": "test_password", 102 | "username": "test_username", 103 | }, 104 | "conf_endpoint": "test_endpoint", 105 | }, 106 | wantErr: false, 107 | }, 108 | } 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | s := &Structs{ 112 | s: tt.fields.s, 113 | tag: tt.fields.tag, 114 | delim: tt.fields.delim, 115 | } 116 | got, err := s.Read() 117 | if (err != nil) != tt.wantErr { 118 | t.Errorf("Structs.Read() error = %v, wantErr %v", err, tt.wantErr) 119 | return 120 | } 121 | if !reflect.DeepEqual(got, tt.want) { 122 | t.Errorf("Structs.Read() = \n%#v\nwant \n%#v\n", got, tt.want) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /providers/vault/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/providers/vault/v2 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/hashicorp/vault/api v1.16.0 9 | github.com/knadh/koanf/maps v0.1.2 10 | ) 11 | 12 | require ( 13 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 14 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 17 | github.com/hashicorp/go-multierror v1.1.1 // indirect 18 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 19 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 20 | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect 21 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 22 | github.com/hashicorp/go-sockaddr v1.0.7 // indirect 23 | github.com/hashicorp/hcl v1.0.0 // indirect 24 | github.com/mitchellh/copystructure v1.2.0 // indirect 25 | github.com/mitchellh/go-homedir v1.1.0 // indirect 26 | github.com/mitchellh/mapstructure v1.5.0 // indirect 27 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 28 | github.com/ryanuber/go-glob v1.0.0 // indirect 29 | golang.org/x/net v0.39.0 // indirect 30 | golang.org/x/text v0.24.0 // indirect 31 | golang.org/x/time v0.11.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /providers/vault/vault.go: -------------------------------------------------------------------------------- 1 | // Package vault implements a koanf.Provider for Hashicorp Vault KV secrets engine 2 | // and provides it to koanf to be parsed by a koanf.Parser. 3 | package vault 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/hashicorp/vault/api" 12 | "github.com/knadh/koanf/maps" 13 | ) 14 | 15 | type Config struct { 16 | // Vault server address 17 | Address string 18 | 19 | // AuthMethod the Vault auth method https://developer.hashicorp.com/vault/docs/auth 20 | AuthMethod api.AuthMethod 21 | 22 | // Vault static token 23 | Token string 24 | 25 | // Secret data path 26 | Path string 27 | 28 | // If FlatPaths is true, then the loaded configuration is not split into 29 | // hierarchical maps based on the delimiter. The keys including the delimiter, 30 | // eg: app.db.name stays as-is in the confmap. 31 | FlatPaths bool 32 | 33 | // Delim is the delimiter to use 34 | // when specifying config key paths, for instance a . for `parent.child.key` 35 | // or a / for `parent/child/key`. 36 | Delim string 37 | 38 | // Internal HTTP client timeout 39 | Timeout time.Duration 40 | 41 | // Transport the optional HTTP client transport allows you to 42 | // customize the settings like InsecureSkipVerify 43 | Transport *http.Transport 44 | 45 | // ExcludeMeta states whether the secret should be returned with its metadata. 46 | // If ExcludeMeta is true, no metadata will be returned, and the data can be 47 | // accessed as `k.String("key")`. If set to false, the value for data `key` 48 | // and the metadata `version` can be accessed as `k.String("data.key")` and 49 | // `k.Int("metadata.version")`. 50 | ExcludeMeta bool 51 | } 52 | 53 | type Vault struct { 54 | client *api.Client 55 | cfg Config 56 | } 57 | 58 | // Provider returns a provider that takes a Vault config. 59 | func Provider(cfg Config) (*Vault, error) { 60 | httpClient := &http.Client{Timeout: cfg.Timeout, Transport: cfg.Transport} 61 | client, err := api.NewClient(&api.Config{Address: cfg.Address, HttpClient: httpClient}) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if cfg.AuthMethod != nil { 66 | if _, err := client.Auth().Login(context.Background(), cfg.AuthMethod); err != nil { 67 | return nil, err 68 | } 69 | } else { 70 | client.SetToken(cfg.Token) 71 | } 72 | 73 | return &Vault{client: client, cfg: cfg}, nil 74 | } 75 | 76 | // ReadBytes is not supported by the vault provider. 77 | func (r *Vault) ReadBytes() ([]byte, error) { 78 | return nil, errors.New("vault provider does not support this method") 79 | } 80 | 81 | // Read fetches the configuration from the source and returns a nested config map. 82 | func (r *Vault) Read() (map[string]interface{}, error) { 83 | secret, err := r.client.Logical().Read(r.cfg.Path) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if secret == nil { 89 | return nil, errors.New("vault provider fetched no data") 90 | } 91 | 92 | s := secret.Data 93 | if r.cfg.ExcludeMeta { 94 | s = secret.Data["data"].(map[string]interface{}) 95 | } 96 | 97 | // Unflatten only when a delimiter is specified 98 | if !r.cfg.FlatPaths && r.cfg.Delim != "" { 99 | data := maps.Unflatten(s, r.cfg.Delim) 100 | 101 | return data, nil 102 | } 103 | 104 | return s, nil 105 | } 106 | -------------------------------------------------------------------------------- /tests/fs_test.go: -------------------------------------------------------------------------------- 1 | package koanf_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "testing/fstest" 8 | "time" 9 | 10 | "github.com/knadh/koanf/parsers/json" 11 | "github.com/knadh/koanf/providers/fs" 12 | "github.com/knadh/koanf/v2" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestFSProvider(t *testing.T) { 18 | var ( 19 | assert = assert.New(t) 20 | ) 21 | 22 | cases := []Case{ 23 | {koanf: koanf.New("."), file: "mock.json", parser: json.Parser(), typeName: "json"}, 24 | } 25 | 26 | // load file system 27 | testFS := os.DirFS("../mock") 28 | 29 | for _, c := range cases { 30 | // Test fs.FS before setting up koanf 31 | err := fstest.TestFS(testFS, c.file) 32 | require.NoError(t, err, "failed asserting file existence in fs.FS") 33 | 34 | // koanf setup 35 | p := fs.Provider(testFS, c.file) 36 | err = c.koanf.Load(p, c.parser) 37 | require.NoError(t, err, fmt.Sprintf("error loading: %v", c.file)) 38 | 39 | // Type. 40 | require.Equal(t, c.typeName, c.koanf.Get("type")) 41 | 42 | assert.Equal(nil, c.koanf.Get("xxx")) 43 | assert.Equal(make(map[string]interface{}), c.koanf.Get("empty")) 44 | 45 | // Int. 46 | assert.Equal(int64(0), c.koanf.Int64("xxxx")) 47 | assert.Equal(int64(1234), c.koanf.Int64("parent1.id")) 48 | 49 | assert.Equal(int(0), c.koanf.Int("xxxx")) 50 | assert.Equal(int(1234), c.koanf.Int("parent1.id")) 51 | 52 | assert.Equal([]int64{}, c.koanf.Int64s("xxxx")) 53 | assert.Equal([]int64{1, 2, 3}, c.koanf.Int64s("parent1.child1.grandchild1.ids")) 54 | 55 | assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Int64Map("parent1.intmap")) 56 | assert.Equal(map[string]int64{}, c.koanf.Int64Map("parent1.boolmap")) 57 | assert.Equal(map[string]int64{}, c.koanf.Int64Map("xxxx")) 58 | assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Int64Map("parent1.floatmap")) 59 | 60 | assert.Equal([]int{1, 2, 3}, c.koanf.Ints("parent1.child1.grandchild1.ids")) 61 | assert.Equal([]int{}, c.koanf.Ints("xxxx")) 62 | 63 | assert.Equal(map[string]int{"key1": 1, "key2": 1, "key3": 1}, c.koanf.IntMap("parent1.intmap")) 64 | assert.Equal(map[string]int{}, c.koanf.IntMap("parent1.boolmap")) 65 | assert.Equal(map[string]int{}, c.koanf.IntMap("xxxx")) 66 | 67 | // Float. 68 | assert.Equal(float64(0), c.koanf.Float64("xxx")) 69 | assert.Equal(float64(1234), c.koanf.Float64("parent1.id")) 70 | 71 | assert.Equal([]float64{}, c.koanf.Float64s("xxxx")) 72 | assert.Equal([]float64{1, 2, 3}, c.koanf.Float64s("parent1.child1.grandchild1.ids")) 73 | 74 | assert.Equal(map[string]float64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Float64Map("parent1.intmap")) 75 | assert.Equal(map[string]float64{"key1": 1.1, "key2": 1.2, "key3": 1.3}, c.koanf.Float64Map("parent1.floatmap")) 76 | assert.Equal(map[string]float64{}, c.koanf.Float64Map("parent1.boolmap")) 77 | assert.Equal(map[string]float64{}, c.koanf.Float64Map("xxxx")) 78 | 79 | // String and bytes. 80 | assert.Equal([]byte{}, c.koanf.Bytes("xxxx")) 81 | assert.Equal([]byte("parent1"), c.koanf.Bytes("parent1.name")) 82 | 83 | assert.Equal("", c.koanf.String("xxxx")) 84 | assert.Equal("parent1", c.koanf.String("parent1.name")) 85 | 86 | assert.Equal([]string{}, c.koanf.Strings("xxxx")) 87 | assert.Equal([]string{"red", "blue", "orange"}, c.koanf.Strings("orphan")) 88 | 89 | assert.Equal(map[string]string{"key1": "val1", "key2": "val2", "key3": "val3"}, c.koanf.StringMap("parent1.strmap")) 90 | assert.Equal(map[string][]string{"key1": {"val1", "val2", "val3"}, "key2": {"val4", "val5"}}, c.koanf.StringsMap("parent1.strsmap")) 91 | assert.Equal(map[string]string{}, c.koanf.StringMap("xxxx")) 92 | assert.Equal(map[string]string{}, c.koanf.StringMap("parent1.intmap")) 93 | 94 | // Bools. 95 | assert.Equal(false, c.koanf.Bool("xxxx")) 96 | assert.Equal(false, c.koanf.Bool("type")) 97 | assert.Equal(true, c.koanf.Bool("parent1.child1.grandchild1.on")) 98 | assert.Equal(true, c.koanf.Bool("strbool")) 99 | 100 | assert.Equal([]bool{}, c.koanf.Bools("xxxx")) 101 | assert.Equal([]bool{true, false, true}, c.koanf.Bools("bools")) 102 | assert.Equal([]bool{true, false, true}, c.koanf.Bools("intbools")) 103 | assert.Equal([]bool{true, true, false}, c.koanf.Bools("strbools")) 104 | 105 | assert.Equal(map[string]bool{"ok1": true, "ok2": true, "notok3": false}, c.koanf.BoolMap("parent1.boolmap")) 106 | assert.Equal(map[string]bool{"key1": true, "key2": true, "key3": true}, c.koanf.BoolMap("parent1.intmap")) 107 | assert.Equal(map[string]bool{}, c.koanf.BoolMap("xxxx")) 108 | 109 | // Others. 110 | assert.Equal(time.Duration(1234), c.koanf.Duration("parent1.id")) 111 | assert.Equal(time.Duration(0), c.koanf.Duration("xxxx")) 112 | assert.Equal(time.Second*3, c.koanf.Duration("duration")) 113 | 114 | assert.Equal(time.Time{}, c.koanf.Time("xxxx", "2006-01-02")) 115 | assert.Equal(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), c.koanf.Time("time", "2006-01-02")) 116 | 117 | assert.Equal([]string{}, c.koanf.MapKeys("xxxx"), "map keys mismatch") 118 | assert.Equal([]string{"bools", "duration", "empty", "intbools", "negative_int", "orphan", "parent1", "parent2", "strbool", "strbools", "time", "type"}, 119 | c.koanf.MapKeys(""), "map keys mismatch") 120 | assert.Equal([]string{"key1", "key2", "key3"}, c.koanf.MapKeys("parent1.strmap"), "map keys mismatch") 121 | 122 | // Attempt to parse int=1234 as a Unix timestamp. 123 | assert.Equal(time.Date(1970, 1, 1, 0, 20, 34, 0, time.UTC), c.koanf.Time("parent1.id", "").UTC()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knadh/koanf/koanf_tests 2 | 3 | go 1.23.0 4 | 5 | replace ( 6 | github.com/knadh/koanf/maps => ../maps 7 | github.com/knadh/koanf/parsers/dotenv => ../parsers/dotenv 8 | github.com/knadh/koanf/parsers/hcl => ../parsers/hcl 9 | github.com/knadh/koanf/parsers/hjson => ../parsers/hjson 10 | github.com/knadh/koanf/parsers/json => ../parsers/json 11 | github.com/knadh/koanf/parsers/toml => ../parsers/toml 12 | github.com/knadh/koanf/parsers/yaml => ../parsers/yaml 13 | github.com/knadh/koanf/providers/basicflag => ../providers/basicflag 14 | github.com/knadh/koanf/providers/confmap => ../providers/confmap 15 | github.com/knadh/koanf/providers/env => ../providers/env 16 | github.com/knadh/koanf/providers/file => ../providers/file 17 | github.com/knadh/koanf/providers/fs => ../providers/fs 18 | github.com/knadh/koanf/providers/posflag => ../providers/posflag 19 | github.com/knadh/koanf/providers/rawbytes => ../providers/rawbytes 20 | github.com/knadh/koanf/providers/structs => ../providers/structs 21 | github.com/knadh/koanf/v2 => ../ 22 | ) 23 | 24 | require ( 25 | github.com/knadh/koanf/maps v0.1.2 26 | github.com/knadh/koanf/parsers/dotenv v0.0.0-00010101000000-000000000000 27 | github.com/knadh/koanf/parsers/hcl v0.0.0-00010101000000-000000000000 28 | github.com/knadh/koanf/parsers/hjson v0.0.0-00010101000000-000000000000 29 | github.com/knadh/koanf/parsers/json v0.0.0-00010101000000-000000000000 30 | github.com/knadh/koanf/parsers/toml v0.0.0-00010101000000-000000000000 31 | github.com/knadh/koanf/parsers/yaml v0.0.0-00010101000000-000000000000 32 | github.com/knadh/koanf/providers/basicflag v0.0.0-00010101000000-000000000000 33 | github.com/knadh/koanf/providers/confmap v0.0.0-00010101000000-000000000000 34 | github.com/knadh/koanf/providers/env v0.0.0-00010101000000-000000000000 35 | github.com/knadh/koanf/providers/file v0.0.0-00010101000000-000000000000 36 | github.com/knadh/koanf/providers/fs v0.0.0-00010101000000-000000000000 37 | github.com/knadh/koanf/providers/posflag v0.0.0-00010101000000-000000000000 38 | github.com/knadh/koanf/providers/rawbytes v0.0.0-00010101000000-000000000000 39 | github.com/knadh/koanf/providers/structs v0.0.0-00010101000000-000000000000 40 | github.com/knadh/koanf/v2 v2.0.0-00010101000000-000000000000 41 | github.com/spf13/pflag v1.0.5 42 | github.com/stretchr/testify v1.9.0 43 | ) 44 | 45 | require ( 46 | github.com/davecgh/go-spew v1.1.1 // indirect 47 | github.com/fatih/structs v1.1.0 // indirect 48 | github.com/fsnotify/fsnotify v1.7.0 // indirect 49 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 50 | github.com/hashicorp/hcl v1.0.0 // indirect 51 | github.com/hjson/hjson-go/v4 v4.3.0 // indirect 52 | github.com/joho/godotenv v1.5.1 // indirect 53 | github.com/mitchellh/copystructure v1.2.0 // indirect 54 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 55 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 56 | github.com/pmezard/go-difflib v1.0.0 // indirect 57 | golang.org/x/sys v0.21.0 // indirect 58 | gopkg.in/yaml.v3 v3.0.1 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /tests/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 5 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 6 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 7 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 8 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 9 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 10 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 11 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 12 | github.com/hjson/hjson-go/v4 v4.3.0 h1:dyrzJdqqFGhHt+FSrs5n9s6b0fPM8oSJdWo+oS3YnJw= 13 | github.com/hjson/hjson-go/v4 v4.3.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= 14 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 15 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 16 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 19 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 20 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 21 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 22 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 23 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 27 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 30 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 31 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 32 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 34 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 35 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 36 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 37 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 38 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /tests/posflag_test.go: -------------------------------------------------------------------------------- 1 | package koanf_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/knadh/koanf/providers/confmap" 8 | "github.com/knadh/koanf/providers/posflag" 9 | "github.com/knadh/koanf/v2" 10 | "github.com/spf13/pflag" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func posflagCallback(key string, value string) (string, interface{}) { 15 | return strings.ReplaceAll(key, "-", "_"), value 16 | } 17 | 18 | func TestLoad(t *testing.T) { 19 | assert := func(t *testing.T, k *koanf.Koanf) { 20 | require.Equal(t, k.String("key.one-example"), "val1") 21 | require.Equal(t, k.String("key.two_example"), "val2") 22 | require.Equal(t, k.Strings("key.strings"), []string{"1", "2", "3"}) 23 | require.Equal(t, k.Int("key.int"), 123) 24 | require.Equal(t, k.Ints("key.ints"), []int{1, 2, 3}) 25 | require.Equal(t, k.Float64("key.float"), 123.123) 26 | } 27 | 28 | fs := &pflag.FlagSet{} 29 | fs.String("key.one-example", "val1", "") 30 | fs.String("key.two_example", "val2", "") 31 | fs.StringSlice("key.strings", []string{"1", "2", "3"}, "") 32 | fs.Int("key.int", 123, "") 33 | fs.IntSlice("key.ints", []int{1, 2, 3}, "") 34 | fs.Float64("key.float", 123.123, "") 35 | 36 | k := koanf.New(".") 37 | require.Nil(t, k.Load(posflag.Provider(fs, ".", k), nil)) 38 | assert(t, k) 39 | 40 | // Test load with a custom flag callback. 41 | k = koanf.New(".") 42 | p := posflag.ProviderWithFlag(fs, ".", k, func(f *pflag.Flag) (string, interface{}) { 43 | return f.Name, posflag.FlagVal(fs, f) 44 | }) 45 | require.Nil(t, k.Load(p, nil), nil) 46 | assert(t, k) 47 | 48 | // Test load with a custom key, val callback. 49 | k = koanf.New(".") 50 | p = posflag.ProviderWithValue(fs, ".", k, func(key, val string) (string, interface{}) { 51 | if key == "key.float" { 52 | return "", val 53 | } 54 | return key, val 55 | }) 56 | require.Nil(t, k.Load(p, nil), nil) 57 | require.Equal(t, k.String("key.one-example"), "val1") 58 | require.Equal(t, k.String("key.two_example"), "val2") 59 | require.Equal(t, k.String("key.int"), "123") 60 | require.Equal(t, k.String("key.ints"), "[1,2,3]") 61 | require.Equal(t, k.String("key.float"), "") 62 | } 63 | 64 | func TestIssue90(t *testing.T) { 65 | exampleKeys := map[string]interface{}{ 66 | "key.one_example": "a struct value", 67 | "key.two_example": "b struct value", 68 | } 69 | 70 | fs := &pflag.FlagSet{} 71 | fs.String("key.one-example", "a posflag value", "") 72 | fs.String("key.two_example", "a posflag value", "") 73 | 74 | k := koanf.New(".") 75 | 76 | err := k.Load(confmap.Provider(exampleKeys, "."), nil) 77 | require.Nil(t, err) 78 | 79 | err = k.Load(posflag.ProviderWithValue(fs, ".", k, posflagCallback), nil) 80 | require.Nil(t, err) 81 | 82 | require.Equal(t, exampleKeys, k.All()) 83 | } 84 | 85 | func TestIssue100(t *testing.T) { 86 | var err error 87 | f := &pflag.FlagSet{} 88 | f.StringToString("string", map[string]string{"k": "v"}, "") 89 | f.StringToInt("int", map[string]int{"k": 1}, "") 90 | f.StringToInt64("int64", map[string]int64{"k": 2}, "") 91 | 92 | k := koanf.New(".") 93 | 94 | err = k.Load(posflag.Provider(f, ".", k), nil) 95 | require.Nil(t, err) 96 | 97 | type Maps struct { 98 | String map[string]string 99 | Int map[string]int 100 | Int64 map[string]int64 101 | } 102 | maps := new(Maps) 103 | 104 | err = k.Unmarshal("", maps) 105 | require.Nil(t, err) 106 | 107 | require.Equal(t, map[string]string{"k": "v"}, maps.String) 108 | require.Equal(t, map[string]int{"k": 1}, maps.Int) 109 | require.Equal(t, map[string]int64{"k": 2}, maps.Int64) 110 | } 111 | -------------------------------------------------------------------------------- /tests/textmarshal_test.go: -------------------------------------------------------------------------------- 1 | package koanf_test 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/knadh/koanf/providers/env" 10 | "github.com/knadh/koanf/providers/structs" 11 | "github.com/knadh/koanf/v2" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestTextUnmarshalStringFixed(t *testing.T) { 16 | defer func() { 17 | assert.Nil(t, recover()) 18 | }() 19 | 20 | type targetStruct struct { 21 | LogFormatPointer LogFormatPointer // default should map to json 22 | LogFormatValue LogFormatValue // default should map to json 23 | } 24 | 25 | target := &targetStruct{"text_custom", "text_custom"} 26 | before := *target 27 | 28 | var ( 29 | bptr interface{} = &(target.LogFormatPointer) 30 | cptr interface{} = target.LogFormatValue 31 | ) 32 | _, ok := (bptr).(encoding.TextMarshaler) 33 | assert.True(t, ok) 34 | 35 | _, ok = (cptr).(encoding.TextMarshaler) 36 | assert.True(t, ok) 37 | 38 | k := koanf.New(".") 39 | k.Load(structs.Provider(target, "koanf"), nil) 40 | 41 | k.Load(env.Provider("", ".", func(s string) string { 42 | return strings.Replace(strings.ToLower(s), "_", ".", -1) 43 | }), nil) 44 | 45 | // default values 46 | err := k.Unmarshal("", &target) 47 | assert.NoError(t, err) 48 | assert.Equal(t, &before, target) 49 | } 50 | 51 | // LogFormatValue is a custom string type that implements the TextUnmarshaler interface 52 | // Additionally it implements the TextMarshaler interface (value receiver) 53 | type LogFormatValue string 54 | 55 | // pointer receiver 56 | func (c *LogFormatValue) UnmarshalText(data []byte) error { 57 | switch strings.ToLower(string(data)) { 58 | case "", "json": 59 | *c = "json_custom" 60 | case "text": 61 | *c = "text_custom" 62 | default: 63 | return fmt.Errorf("invalid log format: %s", string(data)) 64 | } 65 | return nil 66 | } 67 | 68 | // value receiver 69 | func (c LogFormatValue) MarshalText() ([]byte, error) { 70 | //overcomplicated custom internal string representation 71 | switch c { 72 | case "", "json_custom": 73 | return []byte("json"), nil 74 | case "text_custom": 75 | return []byte("text"), nil 76 | } 77 | return nil, fmt.Errorf("invalid internal string representation: %q", c) 78 | } 79 | 80 | // LogFormatPointer is a custom string type that implements the TextUnmarshaler interface 81 | // Additionally it implements the TextMarshaler interface (pointer receiver) 82 | type LogFormatPointer string 83 | 84 | // pointer receiver 85 | func (c *LogFormatPointer) UnmarshalText(data []byte) error { 86 | switch strings.ToLower(string(data)) { 87 | case "", "json": 88 | *c = "json_custom" 89 | case "text": 90 | *c = "text_custom" 91 | default: 92 | return fmt.Errorf("invalid log format: %s", string(data)) 93 | } 94 | return nil 95 | } 96 | 97 | // also pointer receiver 98 | func (c *LogFormatPointer) MarshalText() ([]byte, error) { 99 | //overcomplicated custom internal string representation 100 | switch *c { 101 | case "", "json_custom": 102 | return []byte("json"), nil 103 | case "text_custom": 104 | return []byte("text"), nil 105 | } 106 | return nil, fmt.Errorf("invalid internal string representation: %q", *c) 107 | } 108 | --------------------------------------------------------------------------------