Repository: spf13/viper Branch: master Commit: 528f7416c4b5 Files: 63 Total size: 272.1 KB Directory structure: gitextract_5z5ozi0n/ ├── .editorconfig ├── .envrc ├── .github/ │ ├── .editorconfig │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── PULL_REQUEST_TEMPLATES.md │ ├── dependabot.yaml │ ├── octoslash/ │ │ ├── policies/ │ │ │ ├── collaborator.cedar │ │ │ └── triager.cedar │ │ └── principals.json │ ├── release.yml │ └── workflows/ │ ├── checks.yaml │ ├── ci.yaml │ ├── octoslash.yaml │ └── stale.yaml ├── .gitignore ├── .golangci.yaml ├── .yamlignore ├── .yamllint.yaml ├── LICENSE ├── Makefile ├── README.md ├── TROUBLESHOOTING.md ├── UPGRADE.md ├── encoding.go ├── encoding_test.go ├── errors.go ├── experimental.go ├── file.go ├── finder.go ├── finder_example_test.go ├── finder_test.go ├── flags.go ├── flags_test.go ├── flake.nix ├── go.mod ├── go.sum ├── internal/ │ ├── encoding/ │ │ ├── dotenv/ │ │ │ ├── codec.go │ │ │ ├── codec_test.go │ │ │ └── map_utils.go │ │ ├── json/ │ │ │ ├── codec.go │ │ │ └── codec_test.go │ │ ├── toml/ │ │ │ ├── codec.go │ │ │ └── codec_test.go │ │ └── yaml/ │ │ ├── codec.go │ │ └── codec_test.go │ ├── features/ │ │ ├── bind_struct.go │ │ ├── bind_struct_default.go │ │ ├── finder.go │ │ └── finder_default.go │ └── testutil/ │ └── filepath.go ├── logger.go ├── overrides_test.go ├── remote/ │ ├── go.mod │ ├── go.sum │ └── remote.go ├── remote.go ├── util.go ├── util_test.go ├── viper.go ├── viper_test.go └── viper_yaml_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.go] indent_style = tab [{Makefile,*.mk}] indent_style = tab [*.nix] indent_size = 2 [.golangci.yaml] indent_size = 2 ================================================ FILE: .envrc ================================================ if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" fi use flake . --impure ================================================ FILE: .github/.editorconfig ================================================ [{*.yml,*.yaml}] indent_size = 2 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: 🐛 Bug report description: Report a bug to help us improve Viper labels: [kind/bug] body: - type: markdown attributes: value: | Thank you for submitting a bug report! Please fill out the template below to make it easier to debug your problem. If you are not sure if it is a bug or not, you can contact us via the available [support channels](https://github.com/spf13/viper/issues/new/choose). - type: checkboxes attributes: label: Preflight Checklist description: Please ensure you've completed all of the following. options: - label: I have searched the [issue tracker](https://www.github.com/spf13/viper/issues) for an issue that matches the one I want to file, without success. required: true - label: I am not looking for support or already pursued the available [support channels](https://github.com/spf13/viper/issues/new/choose) without success. required: true - label: I have checked the [troubleshooting guide](https://github.com/spf13/viper/blob/master/TROUBLESHOOTING.md) for my problem, without success. required: true - type: input attributes: label: Viper Version description: What version of Viper are you using? placeholder: 1.8.1 validations: required: true - type: input attributes: label: Go Version description: What version of Go are you using? placeholder: "1.16" validations: required: true - type: dropdown attributes: label: Config Source description: What sources do you load configuration from? options: - Manual set - Flags - Environment variables - Files - Remove K/V stores - Defaults multiple: true validations: required: true - type: dropdown attributes: label: Format description: Which file formats do you use? options: - JSON - YAML - TOML - Dotenv - HCL - Java properties - INI - Other (specify below) multiple: true - type: input attributes: label: Repl.it link description: Complete example on Repl.it reproducing the issue. [Here](https://repl.it/@sagikazarmark/Viper-example) is an example you can use. placeholder: https://repl.it/@sagikazarmark/Viper-example - type: textarea attributes: label: Code reproducing the issue description: Please provide a Repl.it link if possible. render: go placeholder: | package main import ( "github.com/spf13/viper" ) func main() { v := viper.New() // ... var config Config err = v.Unmarshal(&config) if err != nil { panic(err) } } - type: textarea attributes: label: Expected Behavior description: A clear and concise description of what you expected to happen. validations: required: true - type: textarea attributes: label: Actual Behavior description: A clear description of what actually happens. validations: required: true - type: textarea attributes: label: Steps To Reproduce description: Steps to reproduce the behavior if it is not self-explanatory. placeholder: | 1. In this environment... 2. With this config... 3. Run '...' 4. See error... - type: textarea attributes: label: Additional Information description: Links? References? Anything that will give us more context about the issue that you are encountering! ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: ❓ Ask a question url: https://github.com/spf13/viper/discussions/new?category=q-a about: Ask and discuss questions with other Viper community members - name: 📓 Reference url: https://pkg.go.dev/mod/github.com/spf13/viper about: Check the Go code reference - name: 💬 Slack channel url: https://gophers.slack.com/messages/viper about: Please ask and answer questions here ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: 🎉 Feature request description: Suggest an idea for Viper labels: [kind/enhancement] body: - type: markdown attributes: value: | Thank you for submitting a feature request! Please describe what you would like to change/add and why in detail by filling out the template below. If you are not sure if your request fits into Viper, you can contact us via the available [support channels](https://github.com/spf13/viper/issues/new/choose). - type: checkboxes attributes: label: Preflight Checklist description: Please ensure you've completed all of the following. options: - label: I have searched the [issue tracker](https://www.github.com/spf13/viper/issues) for an issue that matches the one I want to file, without success. required: true - type: textarea attributes: label: Problem Description description: A clear and concise description of the problem you are seeking to solve with this feature request. validations: required: true - type: textarea attributes: label: Proposed Solution description: A clear and concise description of what would you like to happen. validations: required: true - type: textarea attributes: label: Alternatives Considered description: A clear and concise description of any alternative solutions or features you've considered. - type: textarea attributes: label: Additional Information description: Add any other context about the problem here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATES.md ================================================ **Overview**: **What problem does it solve?**: **Special notes for a reviewer**: ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: gomod directory: / labels: - area/dependencies schedule: interval: daily - package-ecosystem: github-actions directory: / labels: - area/dependencies schedule: interval: daily ================================================ FILE: .github/octoslash/policies/collaborator.cedar ================================================ permit( principal in Role::"Collaborator", action, resource ); ================================================ FILE: .github/octoslash/policies/triager.cedar ================================================ permit( principal in Role::"Triager", action in [Action::"close", Action::"add-label", Action::"remove-label", Action::"self-assign", Action::"self-unassign"], resource ); ================================================ FILE: .github/octoslash/principals.json ================================================ [ { "uid": { "type": "User", "id": "1226384" }, "attrs": { "login": "sagikazarmark" }, "parents": [{ "type": "Role", "id": "Collaborator" }] }, { "uid": { "type": "User", "id": "805695" }, "attrs": { "login": "spacez320" }, "parents": [{ "type": "Role", "id": "Triager" }] } ] ================================================ FILE: .github/release.yml ================================================ changelog: exclude: labels: - release-note/ignore categories: - title: Exciting New Features 🎉 labels: - kind/feature - release-note/new-feature - title: Enhancements 🚀 labels: - kind/enhancement - release-note/enhancement - title: Bug Fixes 🐛 labels: - kind/bug - release-note/bug-fix - title: Breaking Changes 🛠 labels: - release-note/breaking-change - title: Deprecations ❌ labels: - release-note/deprecation - title: Dependency Updates ⬆️ labels: - area/dependencies - release-note/dependency-update - title: Other Changes labels: - "*" ================================================ FILE: .github/workflows/checks.yaml ================================================ name: PR Checks on: pull_request: types: [opened, labeled, unlabeled, synchronize] permissions: pull-requests: read jobs: release-label: name: Release note label runs-on: ubuntu-latest steps: - name: Check minimum labels uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5 with: mode: minimum count: 1 labels: | release-note/ignore kind/feature release-note/new-feature kind/enhancement release-note/enhancement kind/bug release-note/bug-fix release-note/breaking-change release-note/deprecation area/dependencies release-note/dependency-update release-note/misc ================================================ FILE: .github/workflows/ci.yaml ================================================ name: CI on: push: branches: [master] pull_request: permissions: contents: read jobs: build: name: Build runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - goos: js goarch: wasm - goos: aix goarch: ppc64 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.25" - name: Build run: go build . env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} test: name: Test runs-on: ${{ matrix.os }} strategy: # Fail fast is disabled because there are Go version specific features and tests # that should be able to fail independently. fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] go: ["1.23", "1.24", "1.25"] tags: ["", "viper_finder", "viper_bind_struct"] steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: ${{ matrix.go }} - name: Test run: go test -race -v -tags '${{ matrix.tags }}' -shuffle=on ./... if: runner.os != 'Windows' - name: Test (without race detector) run: go test -v -tags '${{ matrix.tags }}' -shuffle=on ./... if: runner.os == 'Windows' lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.25" - name: Lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: version: v2.4.0 dev: name: Developer environment runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Nix uses: cachix/install-nix-action@3715ab1a11cac9e991980d7b4a28d80c7ebdd8f9 # v27 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - name: Check run: nix flake check --impure - name: Dev shell run: nix develop --impure dependency-review: name: Dependency review runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Dependency Review uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 ================================================ FILE: .github/workflows/octoslash.yaml ================================================ name: Octoslash on: issue_comment permissions: issues: write pull-requests: write jobs: run: name: Run runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Octoslash uses: sagikazarmark/octoslash-action@v0 ================================================ FILE: .github/workflows/stale.yaml ================================================ name: "Close stale issues" on: schedule: - cron: "0 3 * * *" permissions: actions: write issues: write # contents: write # only for delete-branch option # pull-requests: write jobs: stale: name: Stale runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: days-before-stale: 30 days-before-close: 5 days-before-pr-stale: -1 # Ignore all PRs stale-issue-message: "Issues with no activity for 30 days are marked stale and subject to being closed." close-issue-message: "This issue has been closed for inactivity. Please re-open if this was a mistake." exempt-issue-labels: "lifecycle/keep" stale-issue-label: "lifecycle/stale" operations-per-run: 500 # Must respect GitHub API limits debug-only: false # Set this to 'true' to disable stale issue management ================================================ FILE: .gitignore ================================================ /.devenv/ /.direnv/ /.idea/ /.pre-commit-config.yaml /bin/ /build/ /var/ /vendor/ ================================================ FILE: .golangci.yaml ================================================ version: "2" run: timeout: 5m linters: enable: - bodyclose - dogsled - dupl - durationcheck - exhaustive - gocritic - godot - gomoddirectives - goprintffuncname - govet - importas - ineffassign - makezero - misspell - nakedret - nilerr - noctx - nolintlint - prealloc - predeclared - revive - rowserrcheck - sqlclosecheck - staticcheck - tparallel - unconvert - unparam - unused - wastedassign - whitespace # fixme # - cyclop # - errcheck # - errorlint # - exhaustivestruct # - forbidigo # - forcetypeassert # - gochecknoglobals # - gochecknoinits # - gocognit # - goconst # - gocyclo # - gosec # - gosimple # - ifshort # - lll # - nlreturn # - paralleltest # - scopelint # - thelper # - wrapcheck # unused # - depguard # - goheader # - gomodguard # don't enable: # - asciicheck # - funlen # - godox # - goerr113 # - gomnd # - interfacer # - maligned # - nestif # - testpackage # - wsl exclusions: rules: - linters: - errcheck - noctx path: _test.go presets: - std-error-handling settings: revive: rules: - name: blank-imports - name: context-as-argument arguments: - allowTypesBefore: "*testing.T" - name: context-keys-type - name: dot-imports - name: empty-block - name: error-naming - name: error-return - name: error-strings - name: errorf - name: exported arguments: - "sayRepetitiveInsteadOfStutters" # make error messages clearer - name: increment-decrement - name: indent-error-flow - name: package-comments disabled: true # disable package comments rule for now - name: range - name: redefines-builtin-id - name: superfluous-else - name: time-naming - name: unexported-return - name: unreachable-code - name: unused-parameter - name: var-declaration - name: var-naming # TODO: add more revive rules misspell: locale: US nolintlint: allow-unused: false # report any unused nolint directives require-specific: false # don't require nolint directives to be specific about which linter is being skipped gocritic: # Enable multiple checks by tags. See "Tags" section in https://github.com/go-critic/go-critic#usage. enabled-tags: - diagnostic - experimental - opinionated - style disabled-checks: - importShadow - unnamedResult formatters: enable: - gci - gofmt - gofumpt - goimports # - golines settings: gci: sections: - standard - default - localmodule ================================================ FILE: .yamlignore ================================================ # TODO: FIXME /.github/ ================================================ FILE: .yamllint.yaml ================================================ ignore-from-file: [.gitignore, .yamlignore] extends: default rules: line-length: disable ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Steve Francia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html OS = $(shell uname | tr A-Z a-z) export PATH := $(abspath bin/):${PATH} # Build variables BUILD_DIR ?= build export CGO_ENABLED ?= 0 export GOOS = $(shell go env GOOS) ifeq (${VERBOSE}, 1) ifeq ($(filter -v,${GOARGS}),) GOARGS += -v endif TEST_FORMAT = short-verbose endif # Dependency versions GOTESTSUM_VERSION = 1.9.0 GOLANGCI_VERSION = 1.53.3 # Add the ability to override some variables # Use with care -include override.mk .PHONY: clear clear: ## Clear the working area and the project rm -rf bin/ .PHONY: check check: test lint ## Run tests and linters TEST_PKGS ?= ./... .PHONY: test test: TEST_FORMAT ?= short test: SHELL = /bin/bash test: export CGO_ENABLED=1 test: bin/gotestsum ## Run tests @mkdir -p ${BUILD_DIR} bin/gotestsum --no-summary=skipped --junitfile ${BUILD_DIR}/coverage.xml --format ${TEST_FORMAT} -- -race -coverprofile=${BUILD_DIR}/coverage.txt -covermode=atomic $(filter-out -v,${GOARGS}) $(if ${TEST_PKGS},${TEST_PKGS},./...) .PHONY: lint lint: lint-go lint-yaml lint: ## Run linters .PHONY: lint-go lint-go: golangci-lint run $(if ${CI},--out-format github-actions,) .PHONY: lint-yaml lint-yaml: yamllint $(if ${CI},-f github,) --no-warnings . .PHONY: fmt fmt: ## Format code golangci-lint run --fix deps: bin/golangci-lint bin/gotestsum yamllint deps: ## Install dependencies bin/gotestsum: @mkdir -p bin curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_${OS}_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum && chmod +x ./bin/gotestsum bin/golangci-lint: @mkdir -p bin curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- v${GOLANGCI_VERSION} .PHONY: yamllint yamllint: pip3 install --user yamllint # Add custom targets here -include custom.mk .PHONY: list list: ## List all make targets @${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort .PHONY: help .DEFAULT_GOAL := help help: @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' # Variable outputting/exporting rules var-%: ; @echo $($*) varexport-%: ; @echo $*=$($*) ================================================ FILE: README.md ================================================ > ## Viper v2 Feedback > > Viper is heading towards v2 and we would love to hear what _**you**_ would > like to see in it. Share your thoughts here: > https://forms.gle/R6faU74qPRPAzchZ9 > > **Thank you!** ![viper logo](https://github.com/user-attachments/assets/acae9193-2974-41f3-808d-2d433f5ada5e) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#configuration) [![run on repl.it](https://repl.it/badge/github/sagikazarmark/Viper-example)](https://repl.it/@sagikazarmark/Viper-example#main.go) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spf13/viper/ci.yaml?branch=master&style=flat-square)](https://github.com/spf13/viper/actions?query=workflow%3ACI) [![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Go Report Card](https://goreportcard.com/badge/github.com/spf13/viper?style=flat-square)](https://goreportcard.com/report/github.com/spf13/viper) ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.23-61CFDD.svg?style=flat-square) [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/spf13/viper)](https://pkg.go.dev/mod/github.com/spf13/viper) **Go configuration with fangs!** Many Go projects are built using Viper including: * [Hugo](http://gohugo.io) * [EMC RexRay](http://rexray.readthedocs.org/en/stable/) * [Imgur’s Incus](https://github.com/Imgur/incus) * [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack) * [Docker Notary](https://github.com/docker/Notary) * [BloomApi](https://www.bloomapi.com/) * [doctl](https://github.com/digitalocean/doctl) * [Clairctl](https://github.com/jgsqware/clairctl) * [Mercure](https://mercure.rocks) * [Meshery](https://github.com/meshery/meshery) * [Bearer](https://github.com/bearer/bearer) * [Coder](https://github.com/coder/coder) * [Vitess](https://vitess.io/) ## Install ```shell go get github.com/spf13/viper ``` > **NOTE** Viper uses [Go Modules](https://go.dev/wiki/Modules) to manage dependencies. ## Why use Viper? Viper is a complete configuration solution for Go applications including [12-Factor apps](https://12factor.net/#the_twelve_factors). It is designed to work within any application, and can handle all types of configuration needs and formats. It supports: * setting defaults * setting explicit values * reading config files * dynamic discovery of config files across multiple locations * reading from environment variables * reading from remote systems (e.g. Etcd or Consul) * reading from command line flags * reading from buffers * live watching and updating configuration * aliasing configuration keys for easy refactoring Viper can be thought of as a registry for all of your applications' configuration needs. ## Putting Values in Viper Viper can read from multiple configuration sources and merges them together into one set of configuration keys and values. Viper uses the following precedence for merging: * explicit call to `Set` * flags * environment variables * config files * external key/value stores * defaults > **NOTE** Viper configuration keys are case insensitive. ### Reading Config Files Viper requires minimal configuration to load config files. Viper currently supports: * JSON * TOML * YAML * INI * envfile * Java Propeties A single Viper instance only supports a single configuration file, but multiple paths may be searched for one. Here is an example of how to use Viper to search for and read a configuration file. At least one path should be provided where a configuration file is expected. ```go // Name of the config file without an extension (Viper will intuit the type // from an extension on the actual file) viper.SetConfigName("config") // Add search paths to find the file viper.AddConfigPath("/etc/appname/") viper.AddConfigPath("$HOME/.appname") viper.AddConfigPath(".") // Find and read the config file err := viper.ReadInConfig() // Handle errors if err != nil { panic(fmt.Errorf("fatal error config file: %w", err)) } ``` You can handle the specific case where no config file is found. ```go var fileLookupError viper.FileLookupError if err := viper.ReadInConfig(); err != nil { if errors.As(err, &fileLookupError) { // Indicates an explicitly set config file is not found (such as with // using `viper.SetConfigFile`) or that no config file was found in // any search path (such as when using `viper.AddConfigPath`) } else { // Config file was found but another error was produced } } // Config file found and successfully parsed ``` > **NOTE (since 1.6)** You can also have a file without an extension and > specify the format programmatically, which is useful for files that naturally > have no extension (e.g., `.bashrc`). ### Writing Config Files At times you may want to store all configuration modifications made during run time. ```go // Writes current config to the path set by `AddConfigPath` and `SetConfigName` viper.WriteConfig() viper.SafeWriteConfig() // Like the above, but will error if the config file exists // Writes current config to a specific place viper.WriteConfigAs("/path/to/my/.config") // Will error since it has already been written viper.SafeWriteConfigAs("/path/to/my/.config") viper.SafeWriteConfigAs("/path/to/my/.other_config") ``` As a rule of the thumb, methods prefixed with `Safe` won't overwrite any existing file, while other methods will. ### Watching and Re-reading Config Files Gone are the days of needing to restart a server to have a config take effect--Viper powered applications can read an update to a config file while running and not miss a beat. It's also possible to provide a function for Viper to run each time a change occurs. ```go // All config paths must be defined prior to calling `WatchConfig()` viper.AddConfigPath("$HOME/.appname") viper.OnConfigChange(func(e fsnotify.Event) { fmt.Println("Config file changed:", e.Name) }) viper.WatchConfig() ``` ### Reading Config from `io.Reader` Viper predefines many configuration sources but you can also implement your own required configuration source. ```go viper.SetConfigType("yaml") var yamlExample = []byte(` hacker: true hobbies: - skateboarding - snowboarding - go name: steve `) viper.ReadConfig(bytes.NewBuffer(yamlExample)) viper.Get("name") // "steve" ``` ### Setting Defaults A good configuration system will support default values, which are used if a key hasn't been set in some other way. Examples: ```go viper.SetDefault("ContentDir", "content") viper.SetDefault("LayoutDir", "layouts") viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"}) ``` ### Setting Overrides Viper allows explict setting of configuration, such as from your own application logic. ```go viper.Set("verbose", true) viper.Set("host.port", 5899) // Set an embedded key ``` ### Registering and Using Aliases Aliases permit a single value to be referenced by multiple keys ```go viper.RegisterAlias("loud", "Verbose") viper.Set("verbose", true) // Same result as next line viper.Set("loud", true) // Same result as prior line viper.GetBool("loud") // true viper.GetBool("verbose") // true ``` ### Working with Environment Variables Viper has full support for environment variables. > **NOTE** Unlike other configuration sources, environment variables are case > sensitive. ```go // Tells Viper to use this prefix when reading environment variables viper.SetEnvPrefix("spf") // Viper will look for "SPF_ID", automatically uppercasing the prefix and key viper.BindEnv("id") // Alternatively, we can search for any environment variable prefixed and load // them in viper.AutomaticEnv() os.Setenv("SPF_ID", "13") id := viper.Get("id") // 13 ``` * By default, empty environment variables are considered unset and will fall back to the next configuration source, unless `AllowEmptyEnv` is used. * Viper does not "cache" environment variables--the value will be read each time it is accessed. * `SetEnvKeyReplacer` and `EnvKeyReplacer` allow you to rewrite environment variable keys, which is useful to merge SCREAMING_SNAKE_CASE environment variables with kebab-cased configuration values from other sources. ### Working with Flags Viper has the ability to bind to flags. Specifically, Viper supports [pflag](https://github.com/spf13/pflag/) as used in the [Cobra](https://github.com/spf13/cobra) library. Like environment variables, the value is not set when the binding method is called, but when it is accessed. For individual flags, the `BindPFlag` method provides this functionality. ```go serverCmd.Flags().Int("port", 1138, "Port to run Application server on") viper.BindPFlag("port", serverCmd.Flags().Lookup("port")) ``` You can also bind an existing set of pflags. ```go pflag.Int("flagname", 1234, "help message for flagname") pflag.Parse() viper.BindPFlags(pflag.CommandLine) i := viper.GetInt("flagname") // Retrieve values from viper instead of pflag ``` The standard library [flag](https://golang.org/pkg/flag/) package is not directly supported, but may be parsed through pflag. ```go package main import ( "flag" "github.com/spf13/pflag" ) func main() { // Using standard library "flag" package flag.Int("flagname", 1234, "help message for flagname") // Pass standard library flags to pflag pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() // Viper takes over viper.BindPFlags(pflag.CommandLine) } ``` Use of pflag may be avoided entirely by implementing the `FlagValue` and `FlagValueSet` interfaces. ```go // Implementing FlagValue type myFlag struct {} func (f myFlag) HasChanged() bool { return false } func (f myFlag) Name() string { return "my-flag-name" } func (f myFlag) ValueString() string { return "my-flag-value" } func (f myFlag) ValueType() string { return "string" } viper.BindFlagValue("my-flag-name", myFlag{}) // Implementing FlagValueSet type myFlagSet struct { flags []myFlag } func (f myFlagSet) VisitAll(fn func(FlagValue)) { for _, flag := range flags { fn(flag) } } fSet := myFlagSet{ flags: []myFlag{myFlag{}, myFlag{}}, } viper.BindFlagValues("my-flags", fSet) ``` ### Remote Key/Value Store Support To enable remote support in Viper, do a blank import of the `viper/remote` package. ```go import _ "github.com/spf13/viper/remote" ``` Viper supports the following remote key/value stores. Examples for each are provided below. * Etcd and Etcd3 * Consul * Firestore * NATS Viper will read a config string retrieved from a path in a key/value store. Viper supports multiple hosts separated by `;`. For example: `http://127.0.0.1:4001;http://127.0.0.1:4002`. #### Encryption Viper uses [crypt](https://github.com/sagikazarmark/crypt) to retrieve configuration from the key/value store, which means that you can store your configuration values encrypted and have them automatically decrypted if you have the correct GPG keyring. Encryption is optional. Crypt has a command-line helper that you can use to put configurations in your key/value store. ```bash $ go get github.com/sagikazarmark/crypt/bin/crypt $ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json $ crypt get -plaintext /config/hugo.json ``` See the Crypt documentation for examples of how to set encrypted values, or how to use Consul. ### Remote Key/Value Store Examples (Unencrypted) #### etcd ```go viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json") viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" err := viper.ReadRemoteConfig() ``` #### etcd3 ```go viper.AddRemoteProvider("etcd3", "http://127.0.0.1:4001","/config/hugo.json") viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" err := viper.ReadRemoteConfig() ``` #### Consul Given a Consul key `MY_CONSUL_KEY` with the value: ```json { "port": 8080, "hostname": "myhostname.com" } ``` ```go viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY") viper.SetConfigType("json") // Need to explicitly set this to json err := viper.ReadRemoteConfig() fmt.Println(viper.Get("port")) // 8080 ``` #### Firestore ```go viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document") viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml" err := viper.ReadRemoteConfig() ``` Of course, you're allowed to use `SecureRemoteProvider` also. #### NATS ```go viper.AddRemoteProvider("nats", "nats://127.0.0.1:4222", "myapp.config") viper.SetConfigType("json") err := viper.ReadRemoteConfig() ``` ### Remote Key/Value Store Examples (Encrypted) ```go viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg") viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" err := viper.ReadRemoteConfig() ``` ### Watching Key/Value Store Changes ```go // Alternatively, you can create a new viper instance var runtime_viper = viper.New() runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml") runtime_viper.SetConfigType("yaml") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv" // Read from remote config the first time err := runtime_viper.ReadRemoteConfig() // Unmarshal config runtime_viper.Unmarshal(&runtime_conf) // Open a goroutine to watch remote changes forever go func(){ for { time.Sleep(time.Second * 5) // delay after each request // Currently, only tested with Etcd support err := runtime_viper.WatchRemoteConfig() if err != nil { log.Errorf("unable to read remote config: %v", err) continue } // Unmarshal new config into our runtime config struct runtime_viper.Unmarshal(&runtime_conf) } }() ``` ## Getting Values From Viper The simplest way to retrieve configuration values from Viper is to use `Get*` functions. `Get` will return an any type, but specific types may be retrieved with `Get` functions. Note that each `Get*` function will return a zero value if it’s key is not found. To check if a key exists, use the `IsSet` method. Nested keys use `.` as a delimiter and numbers for array indexes. Given the following configuration: ```jsonc { "datastore": { "metric": { "host": "127.0.0.1", "ports": [ 5799, 6029 ] } } } ``` ```go GetString("datastore.metric.host") // "127.0.0.1" GetInt("host.ports.1") // 6029 ``` > **NOTE** Viper _does not_ deep merge configuration values. Complex values > that are overridden will be entirely replaced. If there exists a key that matches the delimited key path, its value will be returned instead. ```jsonc { "datastore.metric.host": "0.0.0.0", "datastore": { "metric": { "host": "127.0.0.1" } } } ``` ```go GetString("datastore.metric.host") // "0.0.0.0" ``` ### Configuration Subsets It's often useful to extract a subset of configuration (e.g., when developing a reusable module which should accept specific sections of configuration). ```yaml cache: cache1: item-size: 64 max-items: 100 cache2: item-size: 80 max-items: 200 ``` ```go func NewCache(v *Viper) *Cache { return &Cache{ ItemSize: v.GetInt("item-size"), MaxItems: v.GetInt("max-items"), } } cache1Config := viper.Sub("cache.cache1") if cache1Config == nil { // Sub returns nil if the key cannot be found panic("cache configuration not found") } cache1 := NewCache(cache1Config) ``` ### Unmarshaling You also have the option of unmarshaling configuration to a struct, map, etc., using `Unmarshal*` methods. ```go type config struct { Port int Name string PathMap string `mapstructure:"path_map"` } var C config err := viper.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } ``` If you want to unmarshal configuration where the keys themselves contain `.` (the default key delimiter), you can change the delimiter. ```go v := viper.NewWithOptions(viper.KeyDelimiter("::")) v.SetDefault("chart::values", map[string]any{ "ingress": map[string]any{ "annotations": map[string]any{ "traefik.frontend.rule.type": "PathPrefix", "traefik.ingress.kubernetes.io/ssl-redirect": "true", }, }, }) type config struct { Chart struct{ Values map[string]any } } var C config v.Unmarshal(&C) ``` Viper also supports unmarshaling into embedded structs. ```go /* Example config: module: enabled: true token: 89h3f98hbwf987h3f98wenf89ehf */ type config struct { Module struct { Enabled bool moduleConfig `mapstructure:",squash"` } } type moduleConfig struct { Token string } var C config err := viper.Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } ``` Viper uses [github.com/go-viper/mapstructure](https://github.com/go-viper/mapstructure) under the hood for unmarshaling values which uses `mapstructure` tags, by default. ### Marshalling to String You may need to marshal all the settings held in Viper into a string. You can use your favorite format's marshaller with the config returned by `AllSettings`. ```go import ( yaml "go.yaml.in/yaml/v3" ) func yamlStringSettings() string { c := viper.AllSettings() bs, err := yaml.Marshal(c) if err != nil { log.Fatalf("unable to marshal config to YAML: %v", err) } return string(bs) } ``` ### Decoding Custom Formats A frequently requested feature is adding more value formats and decoders (for example; parsing character delimited strings into slices. This is already available in Viper using mapstructure decode hooks. Read more in [this blog post](https://sagikazarmark.hu/blog/decoding-custom-formats-with-viper/). ## FAQ ### Why is it called “Viper”? Viper is designed to be a [companion](http://en.wikipedia.org/wiki/Viper_(G.I._Joe)) to [Cobra](https://github.com/spf13/cobra). While both can operate completely independently, together they make a powerful pair to handle much of your application foundation needs. ### I found a bug or want a feature, should I file an issue or a PR? Yes, but there are two things to be aware of. 1. The Viper project is currently prioritizing backwards compatibility and stability over features. 2. Features may be deferred until Viper 2 forms. ### Can multiple Viper instances be used? **tl;dr:** Yes. Each will have its own unique configuration and can read from a different configuration source. All of the functions that the Viper package supports are mirrored as methods on a Viper instance. ```go x := viper.New() y := viper.New() x.SetDefault("ContentDir", "content") y.SetDefault("ContentDir", "foobar") ``` ### Should Viper be a global singleton or passed around? The best practice is to initialize a Viper instance and pass that around when necessary. Viper comes with a global instance (singleton) out of the box. Although it makes setting up configuration easy, using it is generally discouraged as it makes testing harder and can lead to unexpected behavior. The global instance may be deprecated in the future. See [#1855](https://github.com/spf13/viper/issues/1855) for more details. ### Does Viper support case sensitive keys? **tl;dr:** No. Viper merges configuration from various sources, many of which are either case insensitive or use different casing than other sources (e.g., env vars). In order to provide the best experience when using multiple sources, all keys are made case insensitive. There has been several attempts to implement case sensitivity, but unfortunately it's not trivial. We might take a stab at implementing it in [Viper v2](https://github.com/spf13/viper/issues/772), but despite the initial noise, it does not seem to be requested that much. You can vote for case sensitivity by filling out this feedback form: https://forms.gle/R6faU74qPRPAzchZ9. ### Is it safe to concurrently read and write to a Viper instance? No, you will need to synchronize access to Viper yourself (for example by using the `sync` package). Concurrent reads and writes can cause a panic. ## Troubleshooting See [TROUBLESHOOTING.md](TROUBLESHOOTING.md). ## Development **For an optimal developer experience, it is recommended to install [Nix](https://nixos.org/download.html) and [direnv](https://direnv.net/docs/installation.html).** _Alternatively, install [Go](https://go.dev/dl/) on your computer then run `make deps` to install the rest of the dependencies._ Run the test suite: ```shell make test ``` Run linters: ```shell make lint # pass -j option to run them in parallel ``` Some linter violations can automatically be fixed: ```shell make fmt ``` ## License The project is licensed under the [MIT License](LICENSE). ================================================ FILE: TROUBLESHOOTING.md ================================================ # Troubleshooting ## Unmarshaling doesn't work The most common reason for this issue is improper use of struct tags (eg. `yaml` or `json`). Viper uses [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) under the hood for unmarshaling values which uses `mapstructure` tags by default. Please refer to the library's documentation for using other struct tags. ## Cannot find package Viper installation seems to fail a lot lately with the following (or a similar) error: ``` cannot find package "github.com/hashicorp/hcl/tree/hcl1" in any of: /usr/local/Cellar/go/1.15.7_1/libexec/src/github.com/hashicorp/hcl/tree/hcl1 (from $GOROOT) /Users/user/go/src/github.com/hashicorp/hcl/tree/hcl1 (from $GOPATH) ``` As the error message suggests, Go tries to look up dependencies in `GOPATH` mode (as it's commonly called) from the `GOPATH`. Viper opted to use [Go Modules](https://go.dev/wiki/Modules) to manage its dependencies. While in many cases the two methods are interchangeable, once a dependency releases new (major) versions, `GOPATH` mode is no longer able to decide which version to use, so it'll either use one that's already present or pick a version (usually the `master` branch). The solution is easy: switch to using Go Modules. Please refer to the [wiki](https://go.dev/wiki/Modules) on how to do that. **tl;dr* `export GO111MODULE=on` ## Unquoted 'y' and 'n' characters get replaced with _true_ and _false_ when reading a YAML file This is a YAML 1.1 feature according to [go-yaml/yaml#740](https://github.com/go-yaml/yaml/issues/740). Potential solutions are: 1. Quoting values resolved as boolean 1. Upgrading to YAML v3 (for the time being this is possible by passing the `viper_yaml3` tag to your build) ================================================ FILE: UPGRADE.md ================================================ # Update Log **This document details any major updates required to use new features or improvements in Viper.** ## v1.20.x ### New file searching API Viper now includes a new file searching API that allows users to customize how Viper looks for config files. Viper accepts a custom [`Finder`](https://pkg.go.dev/github.com/spf13/viper#Finder) interface implementation: ```go // Finder looks for files and directories in an [afero.Fs] filesystem. type Finder interface { Find(fsys afero.Fs) ([]string, error) } ``` It is supposed to return a list of paths to config files. The default implementation uses [github.com/sagikazarmark/locafero](https://github.com/sagikazarmark/locafero) under the hood. You can supply your own implementation using `WithFinder`: ```go v := viper.NewWithOptions( viper.WithFinder(&MyFinder{}), ) ``` For more information, check out the [Finder examples](https://pkg.go.dev/github.com/spf13/viper#Finder) and the [documentation](https://pkg.go.dev/github.com/sagikazarmark/locafero) for the locafero package. ### New encoding API Viper now allows customizing the encoding layer by providing an API for encoding and decoding configuration data: ```go // Encoder encodes Viper's internal data structures into a byte representation. // It's primarily used for encoding a map[string]any into a file format. type Encoder interface { Encode(v map[string]any) ([]byte, error) } // Decoder decodes the contents of a byte slice into Viper's internal data structures. // It's primarily used for decoding contents of a file into a map[string]any. type Decoder interface { Decode(b []byte, v map[string]any) error } // Codec combines [Encoder] and [Decoder] interfaces. type Codec interface { Encoder Decoder } ``` By default, Viper includes the following codecs: - JSON - TOML - YAML - Dotenv The rest of the codecs are moved to [github.com/go-viper/encoding](https://github.com/go-viper/encoding) Customizing the encoding layer is possible by providing a custom registry of codecs: - [Encoder](https://pkg.go.dev/github.com/spf13/viper#Encoder) -> [EncoderRegistry](https://pkg.go.dev/github.com/spf13/viper#EncoderRegistry) - [Decoder](https://pkg.go.dev/github.com/spf13/viper#Decoder) -> [DecoderRegistry](https://pkg.go.dev/github.com/spf13/viper#DecoderRegistry) - [Codec](https://pkg.go.dev/github.com/spf13/viper#Codec) -> [CodecRegistry](https://pkg.go.dev/github.com/spf13/viper#CodecRegistry) You can supply the registry of codecs to Viper using the appropriate `With*Registry` function: ```go codecRegistry := viper.NewCodecRegistry() codecRegistry.RegisterCodec("myformat", &MyCodec{}) v := viper.NewWithOptions( viper.WithCodecRegistry(codecRegistry), ) ``` ### BREAKING: "github.com/mitchellh/mapstructure" depedency replaced The original [mapstructure](https://github.com/mitchellh/mapstructure) has been [archived](https://github.com/mitchellh/mapstructure/issues/349) and was replaced with a [fork](https://github.com/go-viper/mapstructure) maintained by Viper ([#1723](https://github.com/spf13/viper/pull/1723)). As a result, the package import path needs to be changed in cases where `mapstructure` is directly referenced in your code. For example, when providing a custom decoder config: ```go err := viper.Unmarshal(&appConfig, func(config *mapstructure.DecoderConfig) { config.TagName = "yaml" }) ``` The change is fairly straightforward, just replace all occurrences of the import path `github.com/mitchellh/mapstructure` with `github.com/go-viper/mapstructure/v2`: ```diff - import "github.com/mitchellh/mapstructure" + import "github.com/go-viper/mapstructure/v2" ``` ### BREAKING: HCL, Java properties, INI removed from core In order to reduce third-party dependencies, Viper dropped support for the following formats from the core: - HCL - Java properties - INI You can still use these formats though by importing them from [github.com/go-viper/encoding](https://github.com/go-viper/encoding): ```go import ( "github.com/go-viper/encoding/hcl" "github.com/go-viper/encoding/javaproperties" "github.com/go-viper/encoding/ini" ) codecRegistry := viper.NewCodecRegistry() { codec := hcl.Codec{} codecRegistry.RegisterCodec("hcl", codec) codecRegistry.RegisterCodec("tfvars", codec) } { codec := &javaproperties.Codec{} codecRegistry.RegisterCodec("properties", codec) codecRegistry.RegisterCodec("props", codec) codecRegistry.RegisterCodec("prop", codec) } codecRegistry.RegisterCodec("ini", ini.Codec{}) v := viper.NewWithOptions( viper.WithCodecRegistry(codecRegistry), ) ``` ================================================ FILE: encoding.go ================================================ package viper import ( "errors" "strings" "sync" "github.com/spf13/viper/internal/encoding/dotenv" "github.com/spf13/viper/internal/encoding/json" "github.com/spf13/viper/internal/encoding/toml" "github.com/spf13/viper/internal/encoding/yaml" ) // Encoder encodes Viper's internal data structures into a byte representation. // It's primarily used for encoding a map[string]any into a file format. type Encoder interface { Encode(v map[string]any) ([]byte, error) } // Decoder decodes the contents of a byte slice into Viper's internal data structures. // It's primarily used for decoding contents of a file into a map[string]any. type Decoder interface { Decode(b []byte, v map[string]any) error } // Codec combines [Encoder] and [Decoder] interfaces. type Codec interface { Encoder Decoder } // TODO: consider adding specific errors for not found scenarios // EncoderRegistry returns an [Encoder] for a given format. // // Format is case-insensitive. // // [EncoderRegistry] returns an error if no [Encoder] is registered for the format. type EncoderRegistry interface { Encoder(format string) (Encoder, error) } // DecoderRegistry returns an [Decoder] for a given format. // // Format is case-insensitive. // // [DecoderRegistry] returns an error if no [Decoder] is registered for the format. type DecoderRegistry interface { Decoder(format string) (Decoder, error) } // CodecRegistry combines [EncoderRegistry] and [DecoderRegistry] interfaces. type CodecRegistry interface { EncoderRegistry DecoderRegistry } // WithEncoderRegistry sets a custom [EncoderRegistry]. func WithEncoderRegistry(r EncoderRegistry) Option { return optionFunc(func(v *Viper) { if r == nil { return } v.encoderRegistry = r }) } // WithDecoderRegistry sets a custom [DecoderRegistry]. func WithDecoderRegistry(r DecoderRegistry) Option { return optionFunc(func(v *Viper) { if r == nil { return } v.decoderRegistry = r }) } // WithCodecRegistry sets a custom [EncoderRegistry] and [DecoderRegistry]. func WithCodecRegistry(r CodecRegistry) Option { return optionFunc(func(v *Viper) { if r == nil { return } v.encoderRegistry = r v.decoderRegistry = r }) } // DefaultCodecRegistry is a simple implementation of [CodecRegistry] that allows registering custom [Codec]s. type DefaultCodecRegistry struct { codecs map[string]Codec mu sync.RWMutex once sync.Once } // NewCodecRegistry returns a new [CodecRegistry], ready to accept custom [Codec]s. func NewCodecRegistry() *DefaultCodecRegistry { r := &DefaultCodecRegistry{} r.init() return r } func (r *DefaultCodecRegistry) init() { r.once.Do(func() { r.codecs = map[string]Codec{} }) } // RegisterCodec registers a custom [Codec]. // // Format is case-insensitive. func (r *DefaultCodecRegistry) RegisterCodec(format string, codec Codec) error { r.init() r.mu.Lock() defer r.mu.Unlock() r.codecs[strings.ToLower(format)] = codec return nil } // Encoder implements the [EncoderRegistry] interface. // // Format is case-insensitive. func (r *DefaultCodecRegistry) Encoder(format string) (Encoder, error) { encoder, ok := r.codec(format) if !ok { return nil, errors.New("encoder not found for this format") } return encoder, nil } // Decoder implements the [DecoderRegistry] interface. // // Format is case-insensitive. func (r *DefaultCodecRegistry) Decoder(format string) (Decoder, error) { decoder, ok := r.codec(format) if !ok { return nil, errors.New("decoder not found for this format") } return decoder, nil } func (r *DefaultCodecRegistry) codec(format string) (Codec, bool) { r.mu.Lock() defer r.mu.Unlock() format = strings.ToLower(format) if r.codecs != nil { codec, ok := r.codecs[format] if ok { return codec, true } } switch format { case "yaml", "yml": return yaml.Codec{}, true case "json": return json.Codec{}, true case "toml": return toml.Codec{}, true case "dotenv", "env": return &dotenv.Codec{}, true } return nil, false } ================================================ FILE: encoding_test.go ================================================ package viper import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type codec struct{} func (codec) Encode(_ map[string]any) ([]byte, error) { return nil, nil } func (codec) Decode(_ []byte, _ map[string]any) error { return nil } func TestDefaultCodecRegistry(t *testing.T) { t.Run("OK", func(t *testing.T) { registry := NewCodecRegistry() c := codec{} err := registry.RegisterCodec("myformat", c) require.NoError(t, err) encoder, err := registry.Encoder("myformat") require.NoError(t, err) assert.Equal(t, c, encoder) decoder, err := registry.Decoder("myformat") require.NoError(t, err) assert.Equal(t, c, decoder) }) t.Run("CodecNotFound", func(t *testing.T) { registry := NewCodecRegistry() _, err := registry.Encoder("myformat") require.Error(t, err) _, err = registry.Decoder("myformat") require.Error(t, err) }) t.Run("FormatIsCaseInsensitive", func(t *testing.T) { registry := NewCodecRegistry() c := codec{} err := registry.RegisterCodec("MYFORMAT", c) require.NoError(t, err) { encoder, err := registry.Encoder("myformat") require.NoError(t, err) assert.Equal(t, c, encoder) } { encoder, err := registry.Encoder("MYFORMAT") require.NoError(t, err) assert.Equal(t, c, encoder) } { decoder, err := registry.Decoder("myformat") require.NoError(t, err) assert.Equal(t, c, decoder) } { decoder, err := registry.Decoder("MYFORMAT") require.NoError(t, err) assert.Equal(t, c, decoder) } }) t.Run("OverrideDefault", func(t *testing.T) { registry := NewCodecRegistry() c := codec{} err := registry.RegisterCodec("yaml", c) require.NoError(t, err) encoder, err := registry.Encoder("yaml") require.NoError(t, err) assert.Equal(t, c, encoder) decoder, err := registry.Decoder("yaml") require.NoError(t, err) assert.Equal(t, c, decoder) }) } ================================================ FILE: errors.go ================================================ package viper import ( "fmt" ) // FileLookupError is returned when Viper cannot resolve a configuration file. // // This is meant to be a common interface for all file look-up errors, occurring either because a // file does not exist or because it cannot find any file matching finder criteria. type FileLookupError interface { error fileLookup() } // ConfigFileNotFoundError denotes failing to find a configuration file from a search. // // Deprecated: This is error wraps [FileNotFoundFromSearchError], which should be used instead. type ConfigFileNotFoundError struct { locations []string name string } // Error returns the formatted error. func (e ConfigFileNotFoundError) Error() string { return e.Unwrap().Error() } // Unwraps to FileNotFoundFromSearchError. func (e ConfigFileNotFoundError) Unwrap() error { return FileNotFoundFromSearchError(e) } // FileNotFoundFromSearchError denotes failing to find a configuration file from a search. // Wraps ConfigFileNotFoundError. type FileNotFoundFromSearchError struct { locations []string name string } func (e FileNotFoundFromSearchError) fileLookup() {} // Error returns the formatted error. func (e FileNotFoundFromSearchError) Error() string { message := fmt.Sprintf("File %q not found", e.name) if len(e.locations) > 0 { message += fmt.Sprintf(" in %v", e.locations) } return message } // FileNotFoundError denotes failing to find a specific configuration file. type FileNotFoundError struct { err error path string } func (e FileNotFoundError) fileLookup() {} // Error returns the formatted error. func (e FileNotFoundError) Error() string { return fmt.Sprintf("file not found: %s", e.path) } // ConfigFileAlreadyExistsError denotes failure to write new configuration file. type ConfigFileAlreadyExistsError string // Error returns the formatted error when configuration already exists. func (e ConfigFileAlreadyExistsError) Error() string { return fmt.Sprintf("Config File %q Already Exists", string(e)) } // ConfigMarshalError happens when failing to marshal the configuration. type ConfigMarshalError struct { err error } // Error returns the formatted configuration error. func (e ConfigMarshalError) Error() string { return fmt.Sprintf("While marshaling config: %s", e.err.Error()) } // UnsupportedConfigError denotes encountering an unsupported // configuration filetype. type UnsupportedConfigError string // Error returns the formatted configuration error. func (str UnsupportedConfigError) Error() string { return fmt.Sprintf("Unsupported Config Type %q", string(str)) } ================================================ FILE: experimental.go ================================================ package viper // ExperimentalBindStruct tells Viper to use the new bind struct feature. func ExperimentalBindStruct() Option { return optionFunc(func(v *Viper) { v.experimentalBindStruct = true }) } ================================================ FILE: file.go ================================================ package viper import ( "os" "path/filepath" "github.com/sagikazarmark/locafero" "github.com/spf13/afero" ) // ExperimentalFinder tells Viper to use the new Finder interface for finding configuration files. func ExperimentalFinder() Option { return optionFunc(func(v *Viper) { v.experimentalFinder = true }) } // Search for a config file. func (v *Viper) findConfigFile() (string, error) { finder := v.finder if finder == nil && v.experimentalFinder { var names []string if v.configType != "" { names = locafero.NameWithOptionalExtensions(v.configName, SupportedExts...) } else { names = locafero.NameWithExtensions(v.configName, SupportedExts...) } finder = locafero.Finder{ Paths: v.configPaths, Names: names, Type: locafero.FileTypeFile, } } if finder != nil { return v.findConfigFileWithFinder(finder) } return v.findConfigFileOld() } func (v *Viper) findConfigFileWithFinder(finder Finder) (string, error) { results, err := finder.Find(v.fs) if err != nil { return "", err } if len(results) == 0 { return "", ConfigFileNotFoundError{name: v.configName, locations: v.configPaths} } // We call clean on the final result to ensure that the path is in its canonical form. // This is mostly for consistent path handling and to make sure tests pass. return results[0], nil } // Search all configPaths for any config file. // Returns the first path that exists (and is a config file). func (v *Viper) findConfigFileOld() (string, error) { v.logger.Info("searching for config in paths", "paths", v.configPaths) for _, cp := range v.configPaths { file := v.searchInPath(cp) if file != "" { return file, nil } } return "", ConfigFileNotFoundError{name: v.configName, locations: v.configPaths} } func (v *Viper) searchInPath(in string) (filename string) { v.logger.Debug("searching for config in path", "path", in) for _, ext := range SupportedExts { v.logger.Debug("checking if file exists", "file", filepath.Join(in, v.configName+"."+ext)) if b, _ := exists(v.fs, filepath.Join(in, v.configName+"."+ext)); b { v.logger.Debug("found file", "file", filepath.Join(in, v.configName+"."+ext)) return filepath.Join(in, v.configName+"."+ext) } } if v.configType != "" { if b, _ := exists(v.fs, filepath.Join(in, v.configName)); b { return filepath.Join(in, v.configName) } } return "" } // exists checks if file exists. func exists(fs afero.Fs, path string) (bool, error) { stat, err := fs.Stat(path) if err == nil { return !stat.IsDir(), nil } if os.IsNotExist(err) { return false, nil } return false, err } ================================================ FILE: finder.go ================================================ package viper import ( "errors" "github.com/spf13/afero" ) // WithFinder sets a custom [Finder]. func WithFinder(f Finder) Option { return optionFunc(func(v *Viper) { if f == nil { return } v.finder = f }) } // Finder looks for files and directories in an [afero.Fs] filesystem. type Finder interface { Find(fsys afero.Fs) ([]string, error) } // Finders combines multiple finders into one. func Finders(finders ...Finder) Finder { return &combinedFinder{finders: finders} } // combinedFinder is a Finder that combines multiple finders. type combinedFinder struct { finders []Finder } // Find implements the [Finder] interface. func (c *combinedFinder) Find(fsys afero.Fs) ([]string, error) { var results []string var errs []error for _, finder := range c.finders { if finder == nil { continue } r, err := finder.Find(fsys) if err != nil { errs = append(errs, err) continue } results = append(results, r...) } return results, errors.Join(errs...) } ================================================ FILE: finder_example_test.go ================================================ package viper_test import ( "fmt" "github.com/sagikazarmark/locafero" "github.com/spf13/afero" "github.com/spf13/viper" ) func ExampleFinder() { fs := afero.NewMemMapFs() fs.Mkdir("/home/user", 0o777) f, _ := fs.Create("/home/user/myapp.yaml") f.WriteString("foo: bar") f.Close() // HCL will have a "lower" priority in the search order fs.Create("/home/user/myapp.hcl") finder := locafero.Finder{ Paths: []string{"/home/user"}, Names: locafero.NameWithExtensions("myapp", viper.SupportedExts...), Type: locafero.FileTypeFile, // This is important! } v := viper.NewWithOptions(viper.WithFinder(finder)) v.SetFs(fs) v.ReadInConfig() fmt.Println(v.GetString("foo")) // Output: // bar } func ExampleFinders() { fs := afero.NewMemMapFs() fs.Mkdir("/home/user", 0o777) f, _ := fs.Create("/home/user/myapp.yaml") f.WriteString("foo: bar") f.Close() fs.Mkdir("/etc/myapp", 0o777) fs.Create("/etc/myapp/config.yaml") // Combine multiple finders to search for files in multiple locations with different criteria finder := viper.Finders( locafero.Finder{ Paths: []string{"/home/user"}, Names: locafero.NameWithExtensions("myapp", viper.SupportedExts...), Type: locafero.FileTypeFile, // This is important! }, locafero.Finder{ Paths: []string{"/etc/myapp"}, Names: []string{"config.yaml"}, // Only accept YAML files in the system config directory Type: locafero.FileTypeFile, // This is important! }, ) v := viper.NewWithOptions(viper.WithFinder(finder)) v.SetFs(fs) v.ReadInConfig() fmt.Println(v.GetString("foo")) // Output: // bar } ================================================ FILE: finder_test.go ================================================ package viper import ( "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type finderStub struct { results []string } func (f finderStub) Find(_ afero.Fs) ([]string, error) { return f.results, nil } func TestFinders(t *testing.T) { finder := Finders( finderStub{ results: []string{ "/home/user/.viper.yaml", }, }, finderStub{ results: []string{ "/etc/viper/config.yaml", }, }, ) results, err := finder.Find(afero.NewMemMapFs()) require.NoError(t, err) expected := []string{ "/home/user/.viper.yaml", "/etc/viper/config.yaml", } assert.Equal(t, expected, results) } ================================================ FILE: flags.go ================================================ package viper import "github.com/spf13/pflag" // FlagValueSet is an interface that users can implement // to bind a set of flags to viper. type FlagValueSet interface { VisitAll(fn func(FlagValue)) } // FlagValue is an interface that users can implement // to bind different flags to viper. type FlagValue interface { HasChanged() bool Name() string ValueString() string ValueType() string } // pflagValueSet is a wrapper around *pflag.ValueSet // that implements FlagValueSet. type pflagValueSet struct { flags *pflag.FlagSet } // VisitAll iterates over all *pflag.Flag inside the *pflag.FlagSet. func (p pflagValueSet) VisitAll(fn func(flag FlagValue)) { p.flags.VisitAll(func(flag *pflag.Flag) { fn(pflagValue{flag}) }) } // pflagValue is a wrapper around *pflag.flag // that implements FlagValue. type pflagValue struct { flag *pflag.Flag } // HasChanged returns whether the flag has changes or not. func (p pflagValue) HasChanged() bool { return p.flag.Changed } // Name returns the name of the flag. func (p pflagValue) Name() string { return p.flag.Name } // ValueString returns the value of the flag as a string. func (p pflagValue) ValueString() string { return p.flag.Value.String() } // ValueType returns the type of the flag as a string. func (p pflagValue) ValueType() string { return p.flag.Value.Type() } ================================================ FILE: flags_test.go ================================================ package viper import ( "testing" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBindFlagValueSet(t *testing.T) { Reset() flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) testValues := map[string]*string{ "host": nil, "port": nil, "endpoint": nil, } mutatedTestValues := map[string]string{ "host": "localhost", "port": "6060", "endpoint": "/public", } for name := range testValues { testValues[name] = flagSet.String(name, "", "test") } flagValueSet := pflagValueSet{flagSet} err := BindFlagValues(flagValueSet) require.NoError(t, err, "error binding flag set") flagSet.VisitAll(func(flag *pflag.Flag) { flag.Value.Set(mutatedTestValues[flag.Name]) flag.Changed = true }) for name, expected := range mutatedTestValues { assert.Equal(t, expected, Get(name)) } } func TestBindFlagValue(t *testing.T) { testString := "testing" testValue := newStringValue(testString, &testString) flag := &pflag.Flag{ Name: "testflag", Value: testValue, Changed: false, } flagValue := pflagValue{flag} BindFlagValue("testvalue", flagValue) assert.Equal(t, testString, Get("testvalue")) flag.Value.Set("testing_mutate") flag.Changed = true // hack for pflag usage assert.Equal(t, "testing_mutate", Get("testvalue")) } ================================================ FILE: flake.nix ================================================ { description = "Viper"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; devenv.url = "github:cachix/devenv"; }; outputs = inputs@{ flake-parts, ... }: flake-parts.lib.mkFlake { inherit inputs; } { imports = [ inputs.devenv.flakeModule ]; systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; perSystem = { pkgs, ... }: { devenv.shells = { default = { languages = { go.enable = true; }; git-hooks.hooks = { nixpkgs-fmt.enable = true; yamllint.enable = true; }; packages = with pkgs; [ gnumake golangci-lint yamllint ]; scripts = { versions.exec = '' go version golangci-lint version ''; }; enterShell = '' versions ''; # https://github.com/cachix/devenv/issues/528#issuecomment-1556108767 containers = pkgs.lib.mkForce { }; }; }; }; }; } ================================================ FILE: go.mod ================================================ module github.com/spf13/viper go 1.23.0 require ( github.com/fsnotify/fsnotify v1.9.0 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/pelletier/go-toml/v2 v2.2.4 github.com/sagikazarmark/locafero v0.12.0 github.com/spf13/afero v1.15.0 github.com/spf13/cast v1.10.0 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/subosito/gotenv v1.6.0 go.yaml.in/yaml/v3 v3.0.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.28.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/encoding/dotenv/codec.go ================================================ package dotenv import ( "bytes" "fmt" "sort" "strings" "github.com/subosito/gotenv" ) const keyDelimiter = "_" // Codec implements the encoding.Encoder and encoding.Decoder interfaces for encoding data containing environment variables // (commonly called as dotenv format). type Codec struct{} // Encode encodes a map[string]any into a dotenv byte slice. func (Codec) Encode(v map[string]any) ([]byte, error) { flattened := map[string]any{} flattened = flattenAndMergeMap(flattened, v, "", keyDelimiter) keys := make([]string, 0, len(flattened)) for key := range flattened { keys = append(keys, key) } sort.Strings(keys) var buf bytes.Buffer for _, key := range keys { _, err := buf.WriteString(fmt.Sprintf("%v=%v\n", strings.ToUpper(key), flattened[key])) if err != nil { return nil, err } } return buf.Bytes(), nil } // Decode decodes a dotenv byte slice into a map[string]any. func (Codec) Decode(b []byte, v map[string]any) error { var buf bytes.Buffer _, err := buf.Write(b) if err != nil { return err } env, err := gotenv.StrictParse(&buf) if err != nil { return err } for key, value := range env { v[key] = value } return nil } ================================================ FILE: internal/encoding/dotenv/codec_test.go ================================================ package dotenv import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // original form of the data. const original = `# key-value pair KEY=value ` // encoded form of the data. const encoded = `KEY=value ` // data is Viper's internal representation. var data = map[string]any{ "KEY": "value", } func TestCodec_Encode(t *testing.T) { codec := Codec{} b, err := codec.Encode(data) require.NoError(t, err) assert.Equal(t, encoded, string(b)) } func TestCodec_Decode(t *testing.T) { t.Run("OK", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(original), v) require.NoError(t, err) assert.Equal(t, data, v) }) t.Run("InvalidData", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(`invalid data`), v) require.Error(t, err) t.Logf("decoding failed as expected: %s", err) }) } ================================================ FILE: internal/encoding/dotenv/map_utils.go ================================================ package dotenv import ( "strings" "github.com/spf13/cast" ) // flattenAndMergeMap recursively flattens the given map into a new map // Code is based on the function with the same name in the main package. // TODO: move it to a common place. func flattenAndMergeMap(shadow, m map[string]any, prefix, delimiter string) map[string]any { if shadow != nil && prefix != "" && shadow[prefix] != nil { // prefix is shadowed => nothing more to flatten return shadow } if shadow == nil { shadow = make(map[string]any) } var m2 map[string]any if prefix != "" { prefix += delimiter } for k, val := range m { fullKey := prefix + k switch val := val.(type) { case map[string]any: m2 = val case map[any]any: m2 = cast.ToStringMap(val) default: // immediate value shadow[strings.ToLower(fullKey)] = val continue } // recursively merge to shadow map shadow = flattenAndMergeMap(shadow, m2, fullKey, delimiter) } return shadow } ================================================ FILE: internal/encoding/json/codec.go ================================================ package json import ( "encoding/json" ) // Codec implements the encoding.Encoder and encoding.Decoder interfaces for JSON encoding. type Codec struct{} // Encode encodes a map[string]any into a JSON byte slice. func (Codec) Encode(v map[string]any) ([]byte, error) { // TODO: expose prefix and indent in the Codec as setting? return json.MarshalIndent(v, "", " ") } // Decode decodes a JSON byte slice into a map[string]any. func (Codec) Decode(b []byte, v map[string]any) error { return json.Unmarshal(b, &v) } ================================================ FILE: internal/encoding/json/codec_test.go ================================================ package json import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // encoded form of the data. const encoded = `{ "key": "value", "list": [ "item1", "item2", "item3" ], "map": { "key": "value" }, "nested_map": { "map": { "key": "value", "list": [ "item1", "item2", "item3" ] } } }` // data is Viper's internal representation. var data = map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, "map": map[string]any{ "key": "value", }, "nested_map": map[string]any{ "map": map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, }, }, } func TestCodec_Encode(t *testing.T) { codec := Codec{} b, err := codec.Encode(data) require.NoError(t, err) assert.JSONEq(t, encoded, string(b)) } func TestCodec_Decode(t *testing.T) { t.Run("OK", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(encoded), v) require.NoError(t, err) assert.Equal(t, data, v) }) t.Run("InvalidData", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(`invalid data`), v) require.Error(t, err) t.Logf("decoding failed as expected: %s", err) }) } ================================================ FILE: internal/encoding/toml/codec.go ================================================ package toml import ( "github.com/pelletier/go-toml/v2" ) // Codec implements the encoding.Encoder and encoding.Decoder interfaces for TOML encoding. type Codec struct{} // Encode encodes a map[string]any into a TOML byte slice. func (Codec) Encode(v map[string]any) ([]byte, error) { return toml.Marshal(v) } // Decode decodes a TOML byte slice into a map[string]any. func (Codec) Decode(b []byte, v map[string]any) error { return toml.Unmarshal(b, &v) } ================================================ FILE: internal/encoding/toml/codec_test.go ================================================ package toml import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // original form of the data. const original = `# key-value pair key = "value" list = ["item1", "item2", "item3"] [map] key = "value" # nested # map [nested_map] [nested_map.map] key = "value" list = [ "item1", "item2", "item3", ] ` // encoded form of the data. const encoded = `key = 'value' list = ['item1', 'item2', 'item3'] [map] key = 'value' [nested_map] [nested_map.map] key = 'value' list = ['item1', 'item2', 'item3'] ` // data is Viper's internal representation. var data = map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, "map": map[string]any{ "key": "value", }, "nested_map": map[string]any{ "map": map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, }, }, } func TestCodec_Encode(t *testing.T) { codec := Codec{} b, err := codec.Encode(data) require.NoError(t, err) assert.Equal(t, encoded, string(b)) } func TestCodec_Decode(t *testing.T) { t.Run("OK", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(original), v) require.NoError(t, err) assert.Equal(t, data, v) }) t.Run("InvalidData", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(`invalid data`), v) require.Error(t, err) t.Logf("decoding failed as expected: %s", err) }) } ================================================ FILE: internal/encoding/yaml/codec.go ================================================ package yaml import "go.yaml.in/yaml/v3" // Codec implements the encoding.Encoder and encoding.Decoder interfaces for YAML encoding. type Codec struct{} // Encode encodes a map[string]any into a YAML byte slice. func (Codec) Encode(v map[string]any) ([]byte, error) { return yaml.Marshal(v) } // Decode decodes a YAML byte slice into a map[string]any. func (Codec) Decode(b []byte, v map[string]any) error { return yaml.Unmarshal(b, &v) } ================================================ FILE: internal/encoding/yaml/codec_test.go ================================================ package yaml import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // original form of the data. const original = `# key-value pair key: value list: - item1 - item2 - item3 map: key: value # nested # map nested_map: map: key: value list: - item1 - item2 - item3 ` // encoded form of the data. const encoded = `key: value list: - item1 - item2 - item3 map: key: value nested_map: map: key: value list: - item1 - item2 - item3 ` // decoded form of the data. // // In case of YAML it's slightly different from Viper's internal representation // (e.g. map is decoded into a map with interface key). var decoded = map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, "map": map[string]any{ "key": "value", }, "nested_map": map[string]any{ "map": map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, }, }, } // data is Viper's internal representation. var data = map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, "map": map[string]any{ "key": "value", }, "nested_map": map[string]any{ "map": map[string]any{ "key": "value", "list": []any{ "item1", "item2", "item3", }, }, }, } func TestCodec_Encode(t *testing.T) { codec := Codec{} b, err := codec.Encode(data) require.NoError(t, err) assert.Equal(t, encoded, string(b)) } func TestCodec_Decode(t *testing.T) { t.Run("OK", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(original), v) require.NoError(t, err) assert.Equal(t, decoded, v) }) t.Run("InvalidData", func(t *testing.T) { codec := Codec{} v := map[string]any{} err := codec.Decode([]byte(`invalid data`), v) require.Error(t, err) t.Logf("decoding failed as expected: %s", err) }) } ================================================ FILE: internal/features/bind_struct.go ================================================ //go:build viper_bind_struct package features // BindStruct is a feature flag for enabling/disabling the config binding to structs. const BindStruct = true ================================================ FILE: internal/features/bind_struct_default.go ================================================ //go:build !viper_bind_struct package features // BindStruct is a feature flag for enabling/disabling the config binding to structs. const BindStruct = false ================================================ FILE: internal/features/finder.go ================================================ //go:build viper_finder package features // Finder is a feature flag for enabling/disabling the config finder. const Finder = true ================================================ FILE: internal/features/finder_default.go ================================================ //go:build !viper_finder package features // Finder is a feature flag for enabling/disabling the config finder. const Finder = false ================================================ FILE: internal/testutil/filepath.go ================================================ package testutil import ( "path/filepath" "testing" ) // AbsFilePath calls filepath.Abs on path. func AbsFilePath(t *testing.T, path string) string { t.Helper() s, err := filepath.Abs(path) if err != nil { t.Fatal(err) } return s } ================================================ FILE: logger.go ================================================ package viper import ( "context" "log/slog" ) // WithLogger sets a custom logger. func WithLogger(l *slog.Logger) Option { return optionFunc(func(v *Viper) { v.logger = l }) } type discardHandler struct{} func (n *discardHandler) Enabled(_ context.Context, _ slog.Level) bool { return false } func (n *discardHandler) Handle(_ context.Context, _ slog.Record) error { return nil } func (n *discardHandler) WithAttrs(_ []slog.Attr) slog.Handler { return n } func (n *discardHandler) WithGroup(_ string) slog.Handler { return n } ================================================ FILE: overrides_test.go ================================================ package viper import ( "strings" "testing" "github.com/spf13/cast" "github.com/stretchr/testify/assert" ) type layer int const ( defaultLayer layer = iota + 1 overrideLayer ) func TestNestedOverrides(t *testing.T) { assert := assert.New(t) var v *Viper // Case 0: value overridden by a value overrideDefault(assert, "tom", 10, "tom", 20) // "tom" is first given 10 as default value, then overridden by 20 override(assert, "tom", 10, "tom", 20) // "tom" is first given value 10, then overridden by 20 overrideDefault(assert, "tom.age", 10, "tom.age", 20) override(assert, "tom.age", 10, "tom.age", 20) overrideDefault(assert, "sawyer.tom.age", 10, "sawyer.tom.age", 20) override(assert, "sawyer.tom.age", 10, "sawyer.tom.age", 20) // Case 1: key:value overridden by a value v = overrideDefault(assert, "tom.age", 10, "tom", "boy") // "tom.age" is first given 10 as default value, then "tom" is overridden by "boy" assert.Nil(v.Get("tom.age")) // "tom.age" should not exist anymore v = override(assert, "tom.age", 10, "tom", "boy") assert.Nil(v.Get("tom.age")) // Case 2: value overridden by a key:value overrideDefault(assert, "tom", "boy", "tom.age", 10) // "tom" is first given "boy" as default value, then "tom" is overridden by map{"age":10} override(assert, "tom.age", 10, "tom", "boy") // Case 3: key:value overridden by a key:value v = overrideDefault(assert, "tom.size", 4, "tom.age", 10) assert.Equal(4, v.Get("tom.size")) // value should still be reachable v = override(assert, "tom.size", 4, "tom.age", 10) assert.Equal(4, v.Get("tom.size")) deepCheckValue(assert, v, overrideLayer, []string{"tom", "size"}, 4) // Case 4: key:value overridden by a map v = overrideDefault(assert, "tom.size", 4, "tom", map[string]any{"age": 10}) // "tom.size" is first given "4" as default value, then "tom" is overridden by map{"age":10} assert.Equal(4, v.Get("tom.size")) // "tom.size" should still be reachable assert.Equal(10, v.Get("tom.age")) // new value should be there deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, 10) // new value should be there v = override(assert, "tom.size", 4, "tom", map[string]any{"age": 10}) assert.Nil(v.Get("tom.size")) assert.Equal(10, v.Get("tom.age")) deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, 10) // Case 5: array overridden by a value overrideDefault(assert, "tom", []int{10, 20}, "tom", 30) override(assert, "tom", []int{10, 20}, "tom", 30) overrideDefault(assert, "tom.age", []int{10, 20}, "tom.age", 30) override(assert, "tom.age", []int{10, 20}, "tom.age", 30) // Case 6: array overridden by an array overrideDefault(assert, "tom", []int{10, 20}, "tom", []int{30, 40}) override(assert, "tom", []int{10, 20}, "tom", []int{30, 40}) overrideDefault(assert, "tom.age", []int{10, 20}, "tom.age", []int{30, 40}) v = override(assert, "tom.age", []int{10, 20}, "tom.age", []int{30, 40}) // explicit array merge: s, ok := v.Get("tom.age").([]int) if assert.True(ok, "tom[\"age\"] is not a slice") { v.Set("tom.age", append(s, []int{50, 60}...)) assert.Equal([]int{30, 40, 50, 60}, v.Get("tom.age")) deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, []int{30, 40, 50, 60}) } } func overrideDefault(assert *assert.Assertions, firstPath string, firstValue any, secondPath string, secondValue any) *Viper { return overrideFromLayer(defaultLayer, assert, firstPath, firstValue, secondPath, secondValue) } func override(assert *assert.Assertions, firstPath string, firstValue any, secondPath string, secondValue any) *Viper { return overrideFromLayer(overrideLayer, assert, firstPath, firstValue, secondPath, secondValue) } // overrideFromLayer performs the sequential override and low-level checks. // // First assignment is made on layer l for path firstPath with value firstValue, // the second one on the override layer (i.e., with the Set() function) // for path secondPath with value secondValue. // // firstPath and secondPath can include an arbitrary number of dots to indicate // a nested element. // // After each assignment, the value is checked, retrieved both by its full path // and by its key sequence (successive maps). func overrideFromLayer(l layer, assert *assert.Assertions, firstPath string, firstValue any, secondPath string, secondValue any) *Viper { v := New() firstKeys := strings.Split(firstPath, v.keyDelim) if assert == nil || len(firstKeys) == 0 || firstKeys[0] == "" { return v } // Set and check first value switch l { case defaultLayer: v.SetDefault(firstPath, firstValue) case overrideLayer: v.Set(firstPath, firstValue) default: return v } assert.Equal(firstValue, v.Get(firstPath)) deepCheckValue(assert, v, l, firstKeys, firstValue) // Override and check new value secondKeys := strings.Split(secondPath, v.keyDelim) if len(secondKeys) == 0 || secondKeys[0] == "" { return v } v.Set(secondPath, secondValue) assert.Equal(secondValue, v.Get(secondPath)) deepCheckValue(assert, v, overrideLayer, secondKeys, secondValue) return v } // deepCheckValue checks that all given keys correspond to a valid path in the // configuration map of the given layer, and that the final value equals the one given. func deepCheckValue(assert *assert.Assertions, v *Viper, l layer, keys []string, value any) { if assert == nil || v == nil || len(keys) == 0 || keys[0] == "" { return } // init var val any var ms string switch l { case defaultLayer: val = v.defaults ms = "v.defaults" case overrideLayer: val = v.override ms = "v.override" } // loop through map var m map[string]any for _, k := range keys { if val == nil { assert.Failf("%s is not a map[string]any", ms) return } // deep scan of the map to get the final value switch val := val.(type) { case map[any]any: m = cast.ToStringMap(val) case map[string]any: m = val default: assert.Failf("%s is not a map[string]any", ms) return } ms = ms + "[\"" + k + "\"]" val = m[k] } assert.Equal(value, val) } ================================================ FILE: remote/go.mod ================================================ module github.com/spf13/viper/remote go 1.23.8 replace github.com/spf13/viper => ../ require ( github.com/sagikazarmark/crypt v0.31.0 github.com/spf13/viper v1.21.0 ) require ( cloud.google.com/go v0.120.0 // indirect cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.8.0 // indirect cloud.google.com/go/firestore v1.18.0 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/consul/api v1.32.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nats-io/nats.go v1.45.0 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.etcd.io/etcd/api/v3 v3.6.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect go.etcd.io/etcd/client/v2 v2.305.22 // indirect go.etcd.io/etcd/client/v3 v3.6.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/api v0.248.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.7 // indirect ) ================================================ FILE: remote/go.sum ================================================ cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/nats.go v1.45.0 h1:/wGPbnYXDM0pLKFjZTX+2JOw9TQPoIgTFrUaH97giwA= github.com/nats-io/nats.go v1.45.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.31.0 h1:JJLrH7UojwA5KBkWuuk9x6UgHMzBaU2J2RHpEzUlpAc= github.com/sagikazarmark/crypt v0.31.0/go.mod h1:X8SJJi7WiZU/Rgdr//EtoELirhl3vah7L7/fcBsO5Hk= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= go.etcd.io/etcd/client/v2 v2.305.22 h1:FedDsGxor5iE9muhXm1CgE/TiSVOtgyB5+NYCHPzA2Q= go.etcd.io/etcd/client/v2 v2.305.22/go.mod h1:VP7+1hEKyfGPuRdDmtT8GjM2HcVCKVlGmxfv3NwmrII= go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y= google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: remote/remote.go ================================================ // Copyright © 2015 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // Package remote integrates the remote features of Viper. package remote import ( "bytes" "io" "os" "strings" crypt "github.com/sagikazarmark/crypt/config" "github.com/spf13/viper" ) type remoteConfigProvider struct{} func (rc remoteConfigProvider) Get(rp viper.RemoteProvider) (io.Reader, error) { cm, err := getConfigManager(rp) if err != nil { return nil, err } b, err := cm.Get(rp.Path()) if err != nil { return nil, err } return bytes.NewReader(b), nil } func (rc remoteConfigProvider) Watch(rp viper.RemoteProvider) (io.Reader, error) { cm, err := getConfigManager(rp) if err != nil { return nil, err } resp, err := cm.Get(rp.Path()) if err != nil { return nil, err } return bytes.NewReader(resp), nil } func (rc remoteConfigProvider) WatchChannel(rp viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) { cm, err := getConfigManager(rp) if err != nil { return nil, nil } quit := make(chan bool) quitwc := make(chan bool) viperResponsCh := make(chan *viper.RemoteResponse) cryptoResponseCh := cm.Watch(rp.Path(), quit) // need this function to convert the Channel response form crypt.Response to viper.Response go func(cr <-chan *crypt.Response, vr chan<- *viper.RemoteResponse, quitwc <-chan bool, quit chan<- bool) { for { select { case <-quitwc: quit <- true return case resp := <-cr: vr <- &viper.RemoteResponse{ Error: resp.Error, Value: resp.Value, } } } }(cryptoResponseCh, viperResponsCh, quitwc, quit) return viperResponsCh, quitwc } func getConfigManager(rp viper.RemoteProvider) (crypt.ConfigManager, error) { var cm crypt.ConfigManager var err error endpoints := strings.Split(rp.Endpoint(), ";") if rp.SecretKeyring() != "" { var kr *os.File kr, err = os.Open(rp.SecretKeyring()) if err != nil { return nil, err } defer kr.Close() switch rp.Provider() { case "etcd": cm, err = crypt.NewEtcdConfigManager(endpoints, kr) case "etcd3": cm, err = crypt.NewEtcdV3ConfigManager(endpoints, kr) case "firestore": cm, err = crypt.NewFirestoreConfigManager(endpoints, kr) case "nats": cm, err = crypt.NewNatsConfigManager(endpoints, kr) default: cm, err = crypt.NewConsulConfigManager(endpoints, kr) } } else { switch rp.Provider() { case "etcd": cm, err = crypt.NewStandardEtcdConfigManager(endpoints) case "etcd3": cm, err = crypt.NewStandardEtcdV3ConfigManager(endpoints) case "firestore": cm, err = crypt.NewStandardFirestoreConfigManager(endpoints) case "nats": cm, err = crypt.NewStandardNatsConfigManager(endpoints) default: cm, err = crypt.NewStandardConsulConfigManager(endpoints) } } if err != nil { return nil, err } return cm, nil } func init() { viper.RemoteConfig = &remoteConfigProvider{} } ================================================ FILE: remote.go ================================================ package viper import ( "bytes" "fmt" "io" "reflect" "slices" ) // SupportedRemoteProviders are universally supported remote providers. var SupportedRemoteProviders = []string{"etcd", "etcd3", "consul", "firestore", "nats"} func resetRemote() { SupportedRemoteProviders = []string{"etcd", "etcd3", "consul", "firestore", "nats"} } type remoteConfigFactory interface { Get(rp RemoteProvider) (io.Reader, error) Watch(rp RemoteProvider) (io.Reader, error) WatchChannel(rp RemoteProvider) (<-chan *RemoteResponse, chan bool) } // RemoteResponse represents a response from a remote configuration provider. type RemoteResponse struct { Value []byte Error error } // RemoteConfig is optional, see the remote package. var RemoteConfig remoteConfigFactory // UnsupportedRemoteProviderError denotes encountering an unsupported remote // provider. Currently only etcd and Consul are supported. type UnsupportedRemoteProviderError string // Error returns the formatted remote provider error. func (str UnsupportedRemoteProviderError) Error() string { return fmt.Sprintf("Unsupported Remote Provider Type %q", string(str)) } // RemoteConfigError denotes encountering an error while trying to // pull the configuration from the remote provider. type RemoteConfigError string // Error returns the formatted remote provider error. func (rce RemoteConfigError) Error() string { return fmt.Sprintf("Remote Configurations Error: %s", string(rce)) } type defaultRemoteProvider struct { provider string endpoint string path string secretKeyring string } func (rp defaultRemoteProvider) Provider() string { return rp.provider } func (rp defaultRemoteProvider) Endpoint() string { return rp.endpoint } func (rp defaultRemoteProvider) Path() string { return rp.path } func (rp defaultRemoteProvider) SecretKeyring() string { return rp.secretKeyring } // RemoteProvider stores the configuration necessary // to connect to a remote key/value store. // Optional secretKeyring to unencrypt encrypted values // can be provided. type RemoteProvider interface { Provider() string Endpoint() string Path() string SecretKeyring() string } // AddRemoteProvider adds a remote configuration source. // Remote Providers are searched in the order they are added. // provider is a string value: "etcd", "etcd3", "consul", "firestore" or "nats" are currently supported. // endpoint is the url. etcd requires http://ip:port, consul requires ip:port, nats requires nats://ip:port // path is the path in the k/v store to retrieve configuration // To retrieve a config file called myapp.json from /configs/myapp.json // you should set path to /configs and set config name (SetConfigName()) to // "myapp". func AddRemoteProvider(provider, endpoint, path string) error { return v.AddRemoteProvider(provider, endpoint, path) } // AddRemoteProvider adds a remote configuration source. // Remote Providers are searched in the order they are added. // provider is a string value: "etcd", "etcd3", "consul", "firestore" or "nats" are currently supported. // endpoint is the url. etcd requires http://ip:port, consul requires ip:port, nats requires nats://ip:port // path is the path in the k/v store to retrieve configuration // To retrieve a config file called myapp.json from /configs/myapp.json // you should set path to /configs and set config name (SetConfigName()) to // "myapp". func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error { if !slices.Contains(SupportedRemoteProviders, provider) { return UnsupportedRemoteProviderError(provider) } if provider != "" && endpoint != "" { v.logger.Info("adding remote provider", "provider", provider, "endpoint", endpoint) rp := &defaultRemoteProvider{ endpoint: endpoint, provider: provider, path: path, } if !v.providerPathExists(rp) { v.remoteProviders = append(v.remoteProviders, rp) } } return nil } // AddSecureRemoteProvider adds a remote configuration source. // Secure Remote Providers are searched in the order they are added. // provider is a string value: "etcd", "etcd3", "consul", "firestore" or "nats" are currently supported. // endpoint is the url. etcd requires http://ip:port consul requires ip:port // secretkeyring is the filepath to your openpgp secret keyring. e.g. /etc/secrets/myring.gpg // path is the path in the k/v store to retrieve configuration // To retrieve a config file called myapp.json from /configs/myapp.json // you should set path to /configs and set config name (SetConfigName()) to // "myapp". // Secure Remote Providers are implemented with github.com/sagikazarmark/crypt. func AddSecureRemoteProvider(provider, endpoint, path, secretkeyring string) error { return v.AddSecureRemoteProvider(provider, endpoint, path, secretkeyring) } // AddSecureRemoteProvider adds a remote configuration source. // Secure Remote Providers are searched in the order they are added. // provider is a string value: "etcd", "etcd3", "consul", "firestore" or "nats" are currently supported. // endpoint is the url. etcd requires http://ip:port consul requires ip:port // secretkeyring is the filepath to your openpgp secret keyring. e.g. /etc/secrets/myring.gpg // path is the path in the k/v store to retrieve configuration // To retrieve a config file called myapp.json from /configs/myapp.json // you should set path to /configs and set config name (SetConfigName()) to // "myapp". // Secure Remote Providers are implemented with github.com/sagikazarmark/crypt. func (v *Viper) AddSecureRemoteProvider(provider, endpoint, path, secretkeyring string) error { if !slices.Contains(SupportedRemoteProviders, provider) { return UnsupportedRemoteProviderError(provider) } if provider != "" && endpoint != "" { v.logger.Info("adding remote provider", "provider", provider, "endpoint", endpoint) rp := &defaultRemoteProvider{ endpoint: endpoint, provider: provider, path: path, secretKeyring: secretkeyring, } if !v.providerPathExists(rp) { v.remoteProviders = append(v.remoteProviders, rp) } } return nil } func (v *Viper) providerPathExists(p *defaultRemoteProvider) bool { for _, y := range v.remoteProviders { if reflect.DeepEqual(y, p) { return true } } return false } // ReadRemoteConfig attempts to get configuration from a remote source // and read it in the remote configuration registry. func ReadRemoteConfig() error { return v.ReadRemoteConfig() } // ReadRemoteConfig attempts to get configuration from a remote source // and read it in the remote configuration registry. func (v *Viper) ReadRemoteConfig() error { return v.getKeyValueConfig() } // WatchRemoteConfig updates configuration from available remote providers. func WatchRemoteConfig() error { return v.WatchRemoteConfig() } // WatchRemoteConfig updates configuration from available remote providers. func (v *Viper) WatchRemoteConfig() error { return v.watchKeyValueConfig() } // WatchRemoteConfigOnChannel updates configuration from available remote providers. func (v *Viper) WatchRemoteConfigOnChannel() error { return v.watchKeyValueConfigOnChannel() } // Retrieve the first found remote configuration. func (v *Viper) getKeyValueConfig() error { if RemoteConfig == nil { return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/spf13/viper/remote'") } if len(v.remoteProviders) == 0 { return RemoteConfigError("No Remote Providers") } for _, rp := range v.remoteProviders { val, err := v.getRemoteConfig(rp) if err != nil { v.logger.Error(fmt.Errorf("get remote config: %w", err).Error()) continue } v.kvstore = val return nil } return RemoteConfigError("No Files Found") } func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]any, error) { reader, err := RemoteConfig.Get(provider) if err != nil { return nil, err } err = v.unmarshalReader(reader, v.kvstore) return v.kvstore, err } // Retrieve the first found remote configuration. func (v *Viper) watchKeyValueConfigOnChannel() error { if len(v.remoteProviders) == 0 { return RemoteConfigError("No Remote Providers") } for _, rp := range v.remoteProviders { respc, _ := RemoteConfig.WatchChannel(rp) // Todo: Add quit channel go func(rc <-chan *RemoteResponse) { for { b := <-rc reader := bytes.NewReader(b.Value) err := v.unmarshalReader(reader, v.kvstore) if err != nil { v.logger.Error(fmt.Errorf("failed to unmarshal remote config: %w", err).Error()) } } }(respc) return nil } return RemoteConfigError("No Files Found") } // Retrieve the first found remote configuration. func (v *Viper) watchKeyValueConfig() error { if len(v.remoteProviders) == 0 { return RemoteConfigError("No Remote Providers") } for _, rp := range v.remoteProviders { val, err := v.watchRemoteConfig(rp) if err != nil { v.logger.Error(fmt.Errorf("watch remote config: %w", err).Error()) continue } v.kvstore = val return nil } return RemoteConfigError("No Files Found") } func (v *Viper) watchRemoteConfig(provider RemoteProvider) (map[string]any, error) { reader, err := RemoteConfig.Watch(provider) if err != nil { return nil, err } err = v.unmarshalReader(reader, v.kvstore) return v.kvstore, err } ================================================ FILE: util.go ================================================ // Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // Viper is a application configuration system. // It believes that applications can be configured a variety of ways // via flags, ENVIRONMENT variables, configuration files retrieved // from the file system, or a remote key/value store. package viper import ( "fmt" "log/slog" "os" "path/filepath" "runtime" "strings" "unicode" "github.com/spf13/cast" ) // ConfigParseError denotes failing to parse configuration file. type ConfigParseError struct { err error } // Error returns the formatted configuration error. func (pe ConfigParseError) Error() string { return fmt.Sprintf("While parsing config: %s", pe.err.Error()) } // Unwrap returns the wrapped error. func (pe ConfigParseError) Unwrap() error { return pe.err } // toCaseInsensitiveValue checks if the value is a map; // if so, create a copy and lower-case the keys recursively. func toCaseInsensitiveValue(value any) any { switch v := value.(type) { case map[any]any: value = copyAndInsensitiviseMap(cast.ToStringMap(v)) case map[string]any: value = copyAndInsensitiviseMap(v) } return value } // copyAndInsensitiviseMap behaves like insensitiviseMap, but creates a copy of // any map it makes case insensitive. func copyAndInsensitiviseMap(m map[string]any) map[string]any { nm := make(map[string]any) for key, val := range m { lkey := strings.ToLower(key) switch v := val.(type) { case map[any]any: nm[lkey] = copyAndInsensitiviseMap(cast.ToStringMap(v)) case map[string]any: nm[lkey] = copyAndInsensitiviseMap(v) default: nm[lkey] = v } } return nm } func insensitiviseVal(val any) any { switch v := val.(type) { case map[any]any: // nested map: cast and recursively insensitivise val = cast.ToStringMap(val) insensitiviseMap(val.(map[string]any)) case map[string]any: // nested map: recursively insensitivise insensitiviseMap(v) case []any: // nested array: recursively insensitivise insensitiveArray(v) } return val } func insensitiviseMap(m map[string]any) { for key, val := range m { val = insensitiviseVal(val) lower := strings.ToLower(key) if key != lower { // remove old key (not lower-cased) delete(m, key) } // update map m[lower] = val } } func insensitiveArray(a []any) { for i, val := range a { a[i] = insensitiviseVal(val) } } func absPathify(logger *slog.Logger, inPath string) string { logger.Info("trying to resolve absolute path", "path", inPath) if inPath == "$HOME" || strings.HasPrefix(inPath, "$HOME"+string(os.PathSeparator)) { inPath = userHomeDir() + inPath[5:] } inPath = os.ExpandEnv(inPath) if filepath.IsAbs(inPath) { return filepath.Clean(inPath) } p, err := filepath.Abs(inPath) if err == nil { return filepath.Clean(p) } logger.Error(fmt.Errorf("could not discover absolute path: %w", err).Error()) return "" } func userHomeDir() string { if runtime.GOOS == "windows" { home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") if home == "" { home = os.Getenv("USERPROFILE") } return home } return os.Getenv("HOME") } func safeMul(a, b uint) uint { c := a * b if a > 1 && b > 1 && c/b != a { return 0 } return c } // parseSizeInBytes converts strings like 1GB or 12 mb into an unsigned integer number of bytes. func parseSizeInBytes(sizeStr string) uint { sizeStr = strings.TrimSpace(sizeStr) lastChar := len(sizeStr) - 1 multiplier := uint(1) if lastChar > 0 { if sizeStr[lastChar] == 'b' || sizeStr[lastChar] == 'B' { if lastChar > 1 { switch unicode.ToLower(rune(sizeStr[lastChar-1])) { case 'k': multiplier = 1 << 10 sizeStr = strings.TrimSpace(sizeStr[:lastChar-1]) case 'm': multiplier = 1 << 20 sizeStr = strings.TrimSpace(sizeStr[:lastChar-1]) case 'g': multiplier = 1 << 30 sizeStr = strings.TrimSpace(sizeStr[:lastChar-1]) default: multiplier = 1 sizeStr = strings.TrimSpace(sizeStr[:lastChar]) } } } } size := max(cast.ToInt(sizeStr), 0) return safeMul(uint(size), multiplier) } // deepSearch scans deep maps, following the key indexes listed in the // sequence "path". // The last value is expected to be another map, and is returned. // // In case intermediate keys do not exist, or map to a non-map value, // a new map is created and inserted, and the search continues from there: // the initial map "m" may be modified! func deepSearch(m map[string]any, path []string) map[string]any { for _, k := range path { m2, ok := m[k] if !ok { // intermediate key does not exist // => create it and continue from there m3 := make(map[string]any) m[k] = m3 m = m3 continue } m3, ok := m2.(map[string]any) if !ok { // intermediate key is a value // => replace with a new map m3 = make(map[string]any) m[k] = m3 } // continue search from here m = m3 } return m } ================================================ FILE: util_test.go ================================================ // Copyright © 2016 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // Viper is a application configuration system. // It believes that applications can be configured a variety of ways // via flags, ENVIRONMENT variables, configuration files retrieved // from the file system, or a remote key/value store. package viper import ( "log/slog" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestCopyAndInsensitiviseMap(t *testing.T) { var ( given = map[string]any{ "Foo": 32, "Bar": map[any]any{ "ABc": "A", "cDE": "B", }, } expected = map[string]any{ "foo": 32, "bar": map[string]any{ "abc": "A", "cde": "B", }, } ) got := copyAndInsensitiviseMap(given) assert.Equal(t, expected, got) _, ok := given["foo"] assert.False(t, ok) _, ok = given["bar"] assert.False(t, ok) m := given["Bar"].(map[any]any) _, ok = m["ABc"] assert.True(t, ok) } func TestAbsPathify(t *testing.T) { skipWindows(t) home := userHomeDir() homer := filepath.Join(home, "homer") wd, _ := os.Getwd() t.Setenv("HOMER_ABSOLUTE_PATH", homer) t.Setenv("VAR_WITH_RELATIVE_PATH", "relative") tests := []struct { input string output string }{ {"", wd}, {"sub", filepath.Join(wd, "sub")}, {"./", wd}, {"./sub", filepath.Join(wd, "sub")}, {"$HOME", home}, {"$HOME/", home}, {"$HOME/sub", filepath.Join(home, "sub")}, {"$HOMER_ABSOLUTE_PATH", homer}, {"$HOMER_ABSOLUTE_PATH/", homer}, {"$HOMER_ABSOLUTE_PATH/sub", filepath.Join(homer, "sub")}, {"$VAR_WITH_RELATIVE_PATH", filepath.Join(wd, "relative")}, {"$VAR_WITH_RELATIVE_PATH/", filepath.Join(wd, "relative")}, {"$VAR_WITH_RELATIVE_PATH/sub", filepath.Join(wd, "relative", "sub")}, } for _, test := range tests { got := absPathify(slog.Default(), test.input) assert.Equal(t, test.output, got) } } ================================================ FILE: viper.go ================================================ // Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // Viper is an application configuration system. // It believes that applications can be configured a variety of ways // via flags, ENVIRONMENT variables, configuration files retrieved // from the file system, or a remote key/value store. // Each item takes precedence over the item below it: // overrides // flag // env // config // key/value store // default package viper import ( "bytes" "encoding/csv" "errors" "fmt" "io" fs "io/fs" "log/slog" "os" "path/filepath" "reflect" "slices" "strconv" "strings" "sync" "time" "github.com/fsnotify/fsnotify" "github.com/go-viper/mapstructure/v2" "github.com/spf13/afero" "github.com/spf13/cast" "github.com/spf13/pflag" "github.com/spf13/viper/internal/features" ) var v *Viper func init() { v = New() } // A DecoderConfigOption can be passed to viper.Unmarshal to configure // mapstructure.DecoderConfig options. type DecoderConfigOption func(*mapstructure.DecoderConfig) // DecodeHook returns a DecoderConfigOption which overrides the default // DecoderConfig.DecodeHook value, the default is: // // mapstructure.ComposeDecodeHookFunc( // mapstructure.StringToTimeDurationHookFunc(), // mapstructure.StringToSliceHookFunc(","), // ) func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption { return func(c *mapstructure.DecoderConfig) { c.DecodeHook = hook } } // Viper is a prioritized configuration registry. It // maintains a set of configuration sources, fetches // values to populate those, and provides them according // to the source's priority. // The priority of the sources is the following: // 1. overrides // 2. flags // 3. env. variables // 4. config file // 5. key/value store // 6. defaults // // For example, if values from the following sources were loaded: // // Defaults : { // "secret": "", // "user": "default", // "endpoint": "https://localhost" // } // Config : { // "user": "root" // "secret": "defaultsecret" // } // Env : { // "secret": "somesecretkey" // } // // The resulting config will have the following values: // // { // "secret": "somesecretkey", // "user": "root", // "endpoint": "https://localhost" // } // // Note: Vipers are not safe for concurrent Get() and Set() operations. type Viper struct { // Delimiter that separates a list of keys // used to access a nested value in one go keyDelim string // A set of paths to look for the config file in configPaths []string // The filesystem to read config from. fs afero.Fs finder Finder // A set of remote providers to search for the configuration remoteProviders []*defaultRemoteProvider // Name of file to look for inside the path configName string configFile string configType string configPermissions os.FileMode envPrefix string automaticEnvApplied bool envKeyReplacer StringReplacer allowEmptyEnv bool parents []string config map[string]any override map[string]any defaults map[string]any kvstore map[string]any pflags map[string]FlagValue env map[string][]string aliases map[string]string typeByDefValue bool onConfigChange func(fsnotify.Event) logger *slog.Logger encoderRegistry EncoderRegistry decoderRegistry DecoderRegistry decodeHook mapstructure.DecodeHookFunc experimentalFinder bool experimentalBindStruct bool } // New returns an initialized Viper instance. func New() *Viper { v := new(Viper) v.keyDelim = "." v.configName = "config" v.configPermissions = os.FileMode(0o644) v.fs = afero.NewOsFs() v.config = make(map[string]any) v.parents = []string{} v.override = make(map[string]any) v.defaults = make(map[string]any) v.kvstore = make(map[string]any) v.pflags = make(map[string]FlagValue) v.env = make(map[string][]string) v.aliases = make(map[string]string) v.typeByDefValue = false v.logger = slog.New(&discardHandler{}) codecRegistry := NewCodecRegistry() v.encoderRegistry = codecRegistry v.decoderRegistry = codecRegistry v.experimentalFinder = features.Finder v.experimentalBindStruct = features.BindStruct return v } // Option configures Viper using the functional options paradigm popularized by Rob Pike and Dave Cheney. // If you're unfamiliar with this style, // see https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html and // https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis. type Option interface { apply(v *Viper) } type optionFunc func(v *Viper) func (fn optionFunc) apply(v *Viper) { fn(v) } // KeyDelimiter sets the delimiter used for determining key parts. // By default it's value is ".". func KeyDelimiter(d string) Option { return optionFunc(func(v *Viper) { v.keyDelim = d }) } // StringReplacer applies a set of replacements to a string. type StringReplacer interface { // Replace returns a copy of s with all replacements performed. Replace(s string) string } // EnvKeyReplacer sets a replacer used for mapping environment variables to internal keys. func EnvKeyReplacer(r StringReplacer) Option { return optionFunc(func(v *Viper) { if r == nil { return } v.envKeyReplacer = r }) } // WithDecodeHook sets a default decode hook for mapstructure. func WithDecodeHook(h mapstructure.DecodeHookFunc) Option { return optionFunc(func(v *Viper) { if h == nil { return } v.decodeHook = h }) } // NewWithOptions creates a new Viper instance. func NewWithOptions(opts ...Option) *Viper { v := New() for _, opt := range opts { opt.apply(v) } return v } // SetOptions sets the options on the global Viper instance. // // Be careful when using this function: subsequent calls may override options you set. // It's always better to use a local Viper instance. func SetOptions(opts ...Option) { for _, opt := range opts { opt.apply(v) } } // Reset is intended for testing, will reset all to default settings. // In the public interface for the viper package so applications // can use it in their testing as well. func Reset() { v = New() SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"} resetRemote() } // SupportedExts are universally supported extensions. var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"} // OnConfigChange sets the event handler that is called when a config file changes. func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) } // OnConfigChange sets the event handler that is called when a config file changes. func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) { v.onConfigChange = run } // WatchConfig starts watching a config file for changes. func WatchConfig() { v.WatchConfig() } // WatchConfig starts watching a config file for changes. func (v *Viper) WatchConfig() { initWG := sync.WaitGroup{} initWG.Add(1) go func() { watcher, err := fsnotify.NewWatcher() if err != nil { v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err)) os.Exit(1) } defer watcher.Close() // we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way filename, err := v.getConfigFile() if err != nil { v.logger.Error(fmt.Sprintf("get config file: %s", err)) initWG.Done() return } configFile := filepath.Clean(filename) configDir, _ := filepath.Split(configFile) realConfigFile, _ := filepath.EvalSymlinks(filename) eventsWG := sync.WaitGroup{} eventsWG.Add(1) go func() { for { select { case event, ok := <-watcher.Events: if !ok { // 'Events' channel is closed eventsWG.Done() return } currentConfigFile, _ := filepath.EvalSymlinks(filename) // we only care about the config file with the following cases: // 1 - if the config file was modified or created // 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement) if (filepath.Clean(event.Name) == configFile && (event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) || (currentConfigFile != "" && currentConfigFile != realConfigFile) { realConfigFile = currentConfigFile err := v.ReadInConfig() if err != nil { v.logger.Error(fmt.Sprintf("read config file: %s", err)) } if v.onConfigChange != nil { v.onConfigChange(event) } } else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) { eventsWG.Done() return } case err, ok := <-watcher.Errors: if ok { // 'Errors' channel is not closed v.logger.Error(fmt.Sprintf("watcher error: %s", err)) } eventsWG.Done() return } } }() err = watcher.Add(configDir) if err != nil { v.logger.Error(fmt.Sprintf("failed to add watcher: %s", err)) initWG.Done() return } initWG.Done() // done initializing the watch in this go routine, so the parent routine can move on... eventsWG.Wait() // now, wait for event loop to end in this go-routine... }() initWG.Wait() // make sure that the go routine above fully ended before returning } // SetConfigFile explicitly defines the path, name and extension of the config file. // Viper will use this and not check any of the config paths. func SetConfigFile(in string) { v.SetConfigFile(in) } // SetConfigFile explicitly defines the path, name and extension of the config file. // Viper will use this and not check any of the config paths. func (v *Viper) SetConfigFile(in string) { if in != "" { v.configFile = in } } // SetEnvPrefix defines a prefix that ENVIRONMENT variables will use. // E.g. if your prefix is "spf", the env registry will look for env // variables that start with "SPF_". func SetEnvPrefix(in string) { v.SetEnvPrefix(in) } // SetEnvPrefix defines a prefix that ENVIRONMENT variables will use. // E.g. if your prefix is "spf", the env registry will look for env // variables that start with "SPF_". func (v *Viper) SetEnvPrefix(in string) { if in != "" { v.envPrefix = in } } // GetEnvPrefix returns the environment variable prefix. func GetEnvPrefix() string { return v.GetEnvPrefix() } // GetEnvPrefix returns the environment variable prefix. func (v *Viper) GetEnvPrefix() string { return v.envPrefix } func (v *Viper) mergeWithEnvPrefix(in string) string { if v.envPrefix != "" { return strings.ToUpper(v.envPrefix + "_" + in) } return strings.ToUpper(in) } // AllowEmptyEnv tells Viper to consider set, // but empty environment variables as valid values instead of falling back. // For backward compatibility reasons this is false by default. func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) } // AllowEmptyEnv tells Viper to consider set, // but empty environment variables as valid values instead of falling back. // For backward compatibility reasons this is false by default. func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) { v.allowEmptyEnv = allowEmptyEnv } // TODO: should getEnv logic be moved into find(). Can generalize the use of // rewriting keys many things, Ex: Get('someKey') -> some_key // (camel case to snake case for JSON keys perhaps) // getEnv is a wrapper around os.Getenv which replaces characters in the original // key. This allows env vars which have different keys than the config object // keys. func (v *Viper) getEnv(key string) (string, bool) { if v.envKeyReplacer != nil { key = v.envKeyReplacer.Replace(key) } val, ok := os.LookupEnv(key) return val, ok && (v.allowEmptyEnv || val != "") } // ConfigFileUsed returns the file used to populate the config registry. func ConfigFileUsed() string { return v.ConfigFileUsed() } // ConfigFileUsed returns the file used to populate the config registry. func (v *Viper) ConfigFileUsed() string { return v.configFile } // AddConfigPath adds a path for Viper to search for the config file in. // Can be called multiple times to define multiple search paths. func AddConfigPath(in string) { v.AddConfigPath(in) } // AddConfigPath adds a path for Viper to search for the config file in. // Can be called multiple times to define multiple search paths. func (v *Viper) AddConfigPath(in string) { if v.finder != nil { v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "AddConfigPath")) } if in != "" { absin := absPathify(v.logger, in) v.logger.Info("adding path to search paths", "path", absin) if !slices.Contains(v.configPaths, absin) { v.configPaths = append(v.configPaths, absin) } } } // searchMap recursively searches for a value for path in source map. // Returns nil if not found. // Note: This assumes that the path entries and map keys are lower cased. func (v *Viper) searchMap(source map[string]any, path []string) any { if len(path) == 0 { return source } next, ok := source[path[0]] if ok { // Fast path if len(path) == 1 { return next } // Nested case switch next := next.(type) { case map[any]any: return v.searchMap(cast.ToStringMap(next), path[1:]) case map[string]any: // Type assertion is safe here since it is only reached // if the type of `next` is the same as the type being asserted return v.searchMap(next, path[1:]) default: // got a value but nested key expected, return "nil" for not found return nil } } return nil } // searchIndexableWithPathPrefixes recursively searches for a value for path in source map/slice. // // While searchMap() considers each path element as a single map key or slice index, this // function searches for, and prioritizes, merged path elements. // e.g., if in the source, "foo" is defined with a sub-key "bar", and "foo.bar" // is also defined, this latter value is returned for path ["foo", "bar"]. // // This should be useful only at config level (other maps may not contain dots // in their keys). // // Note: This assumes that the path entries and map keys are lower cased. func (v *Viper) searchIndexableWithPathPrefixes(source any, path []string) any { if len(path) == 0 { return source } // search for path prefixes, starting from the longest one for i := len(path); i > 0; i-- { prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim)) var val any switch sourceIndexable := source.(type) { case []any: val = v.searchSliceWithPathPrefixes(sourceIndexable, prefixKey, i, path) case map[string]any: val = v.searchMapWithPathPrefixes(sourceIndexable, prefixKey, i, path) } if val != nil { return val } } // not found return nil } // searchSliceWithPathPrefixes searches for a value for path in sourceSlice // // This function is part of the searchIndexableWithPathPrefixes recurring search and // should not be called directly from functions other than searchIndexableWithPathPrefixes. func (v *Viper) searchSliceWithPathPrefixes( sourceSlice []any, prefixKey string, pathIndex int, path []string, ) any { // if the prefixKey is not a number or it is out of bounds of the slice index, err := strconv.Atoi(prefixKey) if err != nil || len(sourceSlice) <= index { return nil } next := sourceSlice[index] // Fast path if pathIndex == len(path) { return next } switch n := next.(type) { case map[any]any: return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:]) case map[string]any, []any: return v.searchIndexableWithPathPrefixes(n, path[pathIndex:]) default: // got a value but nested key expected, do nothing and look for next prefix } // not found return nil } // searchMapWithPathPrefixes searches for a value for path in sourceMap // // This function is part of the searchIndexableWithPathPrefixes recurring search and // should not be called directly from functions other than searchIndexableWithPathPrefixes. func (v *Viper) searchMapWithPathPrefixes( sourceMap map[string]any, prefixKey string, pathIndex int, path []string, ) any { next, ok := sourceMap[prefixKey] if !ok { return nil } // Fast path if pathIndex == len(path) { return next } // Nested case switch n := next.(type) { case map[any]any: return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:]) case map[string]any, []any: return v.searchIndexableWithPathPrefixes(n, path[pathIndex:]) default: // got a value but nested key expected, do nothing and look for next prefix } // not found return nil } // isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere // on its path in the map. // e.g., if "foo.bar" has a value in the given map, it “shadows” // // "foo.bar.baz" in a lower-priority map func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]any) string { var parentVal any for i := 1; i < len(path); i++ { parentVal = v.searchMap(m, path[0:i]) if parentVal == nil { // not found, no need to add more path elements return "" } switch parentVal.(type) { case map[any]any: continue case map[string]any: continue default: // parentVal is a regular value which shadows "path" return strings.Join(path[0:i], v.keyDelim) } } return "" } // isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere // in a sub-path of the map. // e.g., if "foo.bar" has a value in the given map, it “shadows” // // "foo.bar.baz" in a lower-priority map func (v *Viper) isPathShadowedInFlatMap(path []string, mi any) string { // unify input map var m map[string]interface{} switch miv := mi.(type) { case map[string]string: m = castMapStringToMapInterface(miv) case map[string]FlagValue: m = castMapFlagToMapInterface(miv) default: return "" } // scan paths var parentKey string for i := 1; i < len(path); i++ { parentKey = strings.Join(path[0:i], v.keyDelim) if _, ok := m[parentKey]; ok { return parentKey } } return "" } // isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere // in the environment, when automatic env is on. // e.g., if "foo.bar" has a value in the environment, it “shadows” // // "foo.bar.baz" in a lower-priority map func (v *Viper) isPathShadowedInAutoEnv(path []string) string { var parentKey string for i := 1; i < len(path); i++ { parentKey = strings.Join(path[0:i], v.keyDelim) if _, ok := v.getEnv(v.mergeWithEnvPrefix(parentKey)); ok { return parentKey } } return "" } // SetTypeByDefaultValue enables or disables the inference of a key value's // type when the Get function is used based upon a key's default value as // opposed to the value returned based on the normal fetch logic. // // For example, if a key has a default value of []string{} and the same key // is set via an environment variable to "a b c", a call to the Get function // would return a string slice for the key if the key's type is inferred by // the default value and the Get function would return: // // []string {"a", "b", "c"} // // Otherwise the Get function would return: // // "a b c" func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) } // SetTypeByDefaultValue enables or disables the inference of a key value's // type when the Get function is used based upon a key's default value as // opposed to the value returned based on the normal fetch logic. // // For example, if a key has a default value of []string{} and the same key // is set via an environment variable to "a b c", a call to the Get function // would return a string slice for the key if the key's type is inferred by // the default value and the Get function would return: // // []string {"a", "b", "c"} // // Otherwise the Get function would return: // // "a b c" func (v *Viper) SetTypeByDefaultValue(enable bool) { v.typeByDefValue = enable } // GetViper gets the global Viper instance. func GetViper() *Viper { return v } // Get can retrieve any value given the key to use. // Get is case-insensitive for a key. // Get has the behavior of returning the value associated with the first // place from where it is set. Viper will check in the following order: // override, flag, env, config file, key/value store, default // // Get returns an interface. For a specific value use one of the Get____ methods. func Get(key string) any { return v.Get(key) } // Get retrieves the value associated with the key. // Get is case-insensitive for a key. // Get has the behavior of returning the value associated with the first // place from where it is set. Viper will check in the following order: // override, flag, env, config file, key/value store, default // // Get returns an interface. For a specific value use one of the Get____ methods. func (v *Viper) Get(key string) any { lcaseKey := strings.ToLower(key) val := v.find(lcaseKey, true) if val == nil { return nil } if v.typeByDefValue { // TODO(bep) this branch isn't covered by a single test. valType := val path := strings.Split(lcaseKey, v.keyDelim) defVal := v.searchMap(v.defaults, path) if defVal != nil { valType = defVal } switch valType.(type) { case bool: return cast.ToBool(val) case string: return cast.ToString(val) case int32, int16, int8, int: return cast.ToInt(val) case uint: return cast.ToUint(val) case uint32: return cast.ToUint32(val) case uint64: return cast.ToUint64(val) case int64: return cast.ToInt64(val) case float64, float32: return cast.ToFloat64(val) case time.Time: return cast.ToTime(val) case time.Duration: return cast.ToDuration(val) case []string: return cast.ToStringSlice(val) case []int: return cast.ToIntSlice(val) case []time.Duration: return cast.ToDurationSlice(val) } } return val } // Sub returns new Viper instance representing a sub tree of this instance. // Sub is case-insensitive for a key. func Sub(key string) *Viper { return v.Sub(key) } // Sub returns a new Viper instance representing a sub tree of this instance. // Sub is case-insensitive for a key. func (v *Viper) Sub(key string) *Viper { subv := New() data := v.Get(key) if data == nil { return nil } if reflect.TypeOf(data).Kind() == reflect.Map { subv.parents = append([]string(nil), v.parents...) subv.parents = append(subv.parents, strings.ToLower(key)) subv.automaticEnvApplied = v.automaticEnvApplied subv.envPrefix = v.envPrefix subv.envKeyReplacer = v.envKeyReplacer subv.keyDelim = v.keyDelim subv.config = cast.ToStringMap(data) return subv } return nil } // GetString returns the value associated with the key as a string. func GetString(key string) string { return v.GetString(key) } // GetString returns the value associated with the key as a string. func (v *Viper) GetString(key string) string { return cast.ToString(v.Get(key)) } // GetBool returns the value associated with the key as a boolean. func GetBool(key string) bool { return v.GetBool(key) } // GetBool returns the value associated with the key as a boolean. func (v *Viper) GetBool(key string) bool { return cast.ToBool(v.Get(key)) } // GetInt returns the value associated with the key as an integer. func GetInt(key string) int { return v.GetInt(key) } // GetInt returns the value associated with the key as an integer. func (v *Viper) GetInt(key string) int { return cast.ToInt(v.Get(key)) } // GetInt32 returns the value associated with the key as an integer. func GetInt32(key string) int32 { return v.GetInt32(key) } // GetInt32 returns the value associated with the key as an integer. func (v *Viper) GetInt32(key string) int32 { return cast.ToInt32(v.Get(key)) } // GetInt64 returns the value associated with the key as an integer. func GetInt64(key string) int64 { return v.GetInt64(key) } // GetInt64 returns the value associated with the key as an integer. func (v *Viper) GetInt64(key string) int64 { return cast.ToInt64(v.Get(key)) } // GetUint8 returns the value associated with the key as an unsigned integer. func GetUint8(key string) uint8 { return v.GetUint8(key) } // GetUint8 returns the value associated with the key as an unsigned integer. func (v *Viper) GetUint8(key string) uint8 { return cast.ToUint8(v.Get(key)) } // GetUint returns the value associated with the key as an unsigned integer. func GetUint(key string) uint { return v.GetUint(key) } // GetUint returns the value associated with the key as an unsigned integer. func (v *Viper) GetUint(key string) uint { return cast.ToUint(v.Get(key)) } // GetUint16 returns the value associated with the key as an unsigned integer. func GetUint16(key string) uint16 { return v.GetUint16(key) } // GetUint16 returns the value associated with the key as an unsigned integer. func (v *Viper) GetUint16(key string) uint16 { return cast.ToUint16(v.Get(key)) } // GetUint32 returns the value associated with the key as an unsigned integer. func GetUint32(key string) uint32 { return v.GetUint32(key) } // GetUint32 returns the value associated with the key as an unsigned integer. func (v *Viper) GetUint32(key string) uint32 { return cast.ToUint32(v.Get(key)) } // GetUint64 returns the value associated with the key as an unsigned integer. func GetUint64(key string) uint64 { return v.GetUint64(key) } // GetUint64 returns the value associated with the key as an unsigned integer. func (v *Viper) GetUint64(key string) uint64 { return cast.ToUint64(v.Get(key)) } // GetFloat64 returns the value associated with the key as a float64. func GetFloat64(key string) float64 { return v.GetFloat64(key) } // GetFloat64 returns the value associated with the key as a float64. func (v *Viper) GetFloat64(key string) float64 { return cast.ToFloat64(v.Get(key)) } // GetTime returns the value associated with the key as time. func GetTime(key string) time.Time { return v.GetTime(key) } // GetTime returns the value associated with the key as time. func (v *Viper) GetTime(key string) time.Time { return cast.ToTime(v.Get(key)) } // GetDuration returns the value associated with the key as a duration. func GetDuration(key string) time.Duration { return v.GetDuration(key) } // GetDuration returns the value associated with the key as a duration. func (v *Viper) GetDuration(key string) time.Duration { return cast.ToDuration(v.Get(key)) } // GetIntSlice returns the value associated with the key as a slice of int values. func GetIntSlice(key string) []int { return v.GetIntSlice(key) } // GetIntSlice returns the value associated with the key as a slice of int values. func (v *Viper) GetIntSlice(key string) []int { return cast.ToIntSlice(v.Get(key)) } // GetStringSlice returns the value associated with the key as a slice of strings. func GetStringSlice(key string) []string { return v.GetStringSlice(key) } // GetStringSlice returns the value associated with the key as a slice of strings. func (v *Viper) GetStringSlice(key string) []string { return cast.ToStringSlice(v.Get(key)) } // GetStringMap returns the value associated with the key as a map of interfaces. func GetStringMap(key string) map[string]any { return v.GetStringMap(key) } // GetStringMap returns the value associated with the key as a map of interfaces. func (v *Viper) GetStringMap(key string) map[string]any { return cast.ToStringMap(v.Get(key)) } // GetStringMapString returns the value associated with the key as a map of strings. func GetStringMapString(key string) map[string]string { return v.GetStringMapString(key) } // GetStringMapString returns the value associated with the key as a map of strings. func (v *Viper) GetStringMapString(key string) map[string]string { return cast.ToStringMapString(v.Get(key)) } // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. func GetStringMapStringSlice(key string) map[string][]string { return v.GetStringMapStringSlice(key) } // GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. func (v *Viper) GetStringMapStringSlice(key string) map[string][]string { return cast.ToStringMapStringSlice(v.Get(key)) } // GetSizeInBytes returns the size of the value associated with the given key // in bytes. func GetSizeInBytes(key string) uint { return v.GetSizeInBytes(key) } // GetSizeInBytes returns the size of the value associated with the given key // in bytes. func (v *Viper) GetSizeInBytes(key string) uint { sizeStr := cast.ToString(v.Get(key)) return parseSizeInBytes(sizeStr) } // UnmarshalKey takes a single key and unmarshals it into a Struct. func UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error { return v.UnmarshalKey(key, rawVal, opts...) } // UnmarshalKey takes a single key and unmarshals it into a Struct. func (v *Viper) UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error { return decode(v.Get(key), v.defaultDecoderConfig(rawVal, opts...)) } // Unmarshal unmarshals the config into a Struct. Make sure that the tags // on the fields of the structure are properly set. func Unmarshal(rawVal any, opts ...DecoderConfigOption) error { return v.Unmarshal(rawVal, opts...) } // Unmarshal unmarshals the config into a Struct. Make sure that the tags // on the fields of the structure are properly set. func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error { keys := v.AllKeys() if v.experimentalBindStruct { // TODO: make this optional? structKeys, err := v.decodeStructKeys(rawVal, opts...) if err != nil { return err } keys = append(keys, structKeys...) } // TODO: struct keys should be enough? return decode(v.getSettings(keys), v.defaultDecoderConfig(rawVal, opts...)) } func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]string, error) { var structKeyMap map[string]any err := decode(input, v.defaultDecoderConfig(&structKeyMap, opts...)) if err != nil { return nil, err } flattenedStructKeyMap := v.flattenAndMergeMap(map[string]bool{}, structKeyMap, "") r := make([]string, 0, len(flattenedStructKeyMap)) for v := range flattenedStructKeyMap { r = append(r, v) } return r, nil } // defaultDecoderConfig returns default mapstructure.DecoderConfig with support // of time.Duration values & string slices. func (v *Viper) defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig { decodeHook := v.decodeHook if decodeHook == nil { decodeHook = mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), // mapstructure.StringToSliceHookFunc(","), stringToWeakSliceHookFunc(","), ) } c := &mapstructure.DecoderConfig{ Metadata: nil, WeaklyTypedInput: true, DecodeHook: decodeHook, } for _, opt := range opts { opt(c) } // Do not allow overwriting the output c.Result = output return c } // As of mapstructure v2.0.0 StringToSliceHookFunc checks if the return type is a string slice. // This function removes that check. // TODO: implement a function that checks if the value can be converted to the return type and use it instead. func stringToWeakSliceHookFunc(sep string) mapstructure.DecodeHookFunc { return func( f reflect.Type, t reflect.Type, data interface{}, ) (interface{}, error) { if f.Kind() != reflect.String || t.Kind() != reflect.Slice { return data, nil } raw := data.(string) if raw == "" { return []string{}, nil } return strings.Split(raw, sep), nil } } // decode is a wrapper around mapstructure.Decode that mimics the WeakDecode functionality. func decode(input any, config *mapstructure.DecoderConfig) error { decoder, err := mapstructure.NewDecoder(config) if err != nil { return err } return decoder.Decode(input) } // UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent // in the destination struct. func UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error { return v.UnmarshalExact(rawVal, opts...) } // UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent // in the destination struct. func (v *Viper) UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error { config := v.defaultDecoderConfig(rawVal, opts...) config.ErrorUnused = true keys := v.AllKeys() if v.experimentalBindStruct { // TODO: make this optional? structKeys, err := v.decodeStructKeys(rawVal, opts...) if err != nil { return err } keys = append(keys, structKeys...) } // TODO: struct keys should be enough? return decode(v.getSettings(keys), config) } // BindPFlags binds a full flag set to the configuration, using each flag's long // name as the config key. func BindPFlags(flags *pflag.FlagSet) error { return v.BindPFlags(flags) } // BindPFlags binds a full flag set to the configuration, using each flag's long // name as the config key. func (v *Viper) BindPFlags(flags *pflag.FlagSet) error { return v.BindFlagValues(pflagValueSet{flags}) } // BindPFlag binds a specific key to a pflag (as used by cobra). // Example (where serverCmd is a Cobra instance): // // serverCmd.Flags().Int("port", 1138, "Port to run Application server on") // Viper.BindPFlag("port", serverCmd.Flags().Lookup("port")) func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) } // BindPFlag binds a specific key to a pflag (as used by cobra). // Example (where serverCmd is a Cobra instance): // // serverCmd.Flags().Int("port", 1138, "Port to run Application server on") // Viper.BindPFlag("port", serverCmd.Flags().Lookup("port")) func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error { if flag == nil { return fmt.Errorf("flag for %q is nil", key) } return v.BindFlagValue(key, pflagValue{flag}) } // BindFlagValues binds a full FlagValue set to the configuration, using each flag's long // name as the config key. func BindFlagValues(flags FlagValueSet) error { return v.BindFlagValues(flags) } // BindFlagValues binds a full FlagValue set to the configuration, using each flag's long // name as the config key. func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) { flags.VisitAll(func(flag FlagValue) { if err = v.BindFlagValue(flag.Name(), flag); err != nil { return } }) return nil } // BindFlagValue binds a specific key to a FlagValue. func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) } // BindFlagValue binds a specific key to a FlagValue. func (v *Viper) BindFlagValue(key string, flag FlagValue) error { if flag == nil { return fmt.Errorf("flag for %q is nil", key) } v.pflags[strings.ToLower(key)] = flag return nil } // BindEnv binds a Viper key to a ENV variable. // ENV variables are case sensitive. // If only a key is provided, it will use the env key matching the key, uppercased. // If more arguments are provided, they will represent the env variable names that // should bind to this key and will be taken in the specified order. // EnvPrefix will be used when set when env name is not provided. func BindEnv(input ...string) error { return v.BindEnv(input...) } // BindEnv binds a Viper key to a ENV variable. // ENV variables are case sensitive. // If only a key is provided, it will use the env key matching the key, uppercased. // If more arguments are provided, they will represent the env variable names that // should bind to this key and will be taken in the specified order. // EnvPrefix will be used when set when env name is not provided. func (v *Viper) BindEnv(input ...string) error { if len(input) == 0 { return fmt.Errorf("missing key to bind to") } key := strings.ToLower(input[0]) if len(input) == 1 { v.env[key] = append(v.env[key], v.mergeWithEnvPrefix(key)) } else { v.env[key] = append(v.env[key], input[1:]...) } return nil } // MustBindEnv wraps BindEnv in a panic. // If there is an error binding an environment variable, MustBindEnv will // panic. func MustBindEnv(input ...string) { v.MustBindEnv(input...) } // MustBindEnv wraps BindEnv in a panic. // If there is an error binding an environment variable, MustBindEnv will // panic. func (v *Viper) MustBindEnv(input ...string) { if err := v.BindEnv(input...); err != nil { panic(fmt.Sprintf("error while binding environment variable: %v", err)) } } // Given a key, find the value. // // Viper will check to see if an alias exists first. // Viper will then check in the following order: // flag, env, config file, key/value store. // Lastly, if no value was found and flagDefault is true, and if the key // corresponds to a flag, the flag's default value is returned. // // Note: this assumes a lower-cased key given. func (v *Viper) find(lcaseKey string, flagDefault bool) any { var ( val any exists bool path = strings.Split(lcaseKey, v.keyDelim) nested = len(path) > 1 ) // compute the path through the nested maps to the nested value if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" { return nil } // if the requested key is an alias, then return the proper key lcaseKey = v.realKey(lcaseKey) path = strings.Split(lcaseKey, v.keyDelim) nested = len(path) > 1 // Set() override first val = v.searchMap(v.override, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.override) != "" { return nil } // PFlag override next flag, exists := v.pflags[lcaseKey] if exists && flag.HasChanged() { switch flag.ValueType() { case "int", "int8", "int16", "int32", "int64": return cast.ToInt(flag.ValueString()) case "bool": return cast.ToBool(flag.ValueString()) case "stringSlice", "stringArray": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return res case "boolSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToBoolSlice(res) case "intSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToIntSlice(res) case "uintSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToUintSlice(res) case "float64Slice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToFloat64Slice(res) case "durationSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") slice := strings.Split(s, ",") return cast.ToDurationSlice(slice) case "stringToString": return stringToStringConv(flag.ValueString()) case "stringToInt": return stringToIntConv(flag.ValueString()) default: return flag.ValueString() } } if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" { return nil } // Env override next if v.automaticEnvApplied { envKey := strings.Join(append(v.parents, lcaseKey), ".") // even if it hasn't been registered, if automaticEnv is used, // check any Get request if val, ok := v.getEnv(v.mergeWithEnvPrefix(envKey)); ok { return val } if nested && v.isPathShadowedInAutoEnv(path) != "" { return nil } } envkeys, exists := v.env[lcaseKey] if exists { for _, envkey := range envkeys { if val, ok := v.getEnv(envkey); ok { return val } } } if nested && v.isPathShadowedInFlatMap(path, v.env) != "" { return nil } // Config file next val = v.searchIndexableWithPathPrefixes(v.config, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.config) != "" { return nil } // K/V store next val = v.searchMap(v.kvstore, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" { return nil } // Default next val = v.searchMap(v.defaults, path) if val != nil { return val } if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" { return nil } if flagDefault { // last chance: if no value is found and a flag does exist for the key, // get the flag's default value even if the flag's value has not been set. if flag, exists := v.pflags[lcaseKey]; exists { switch flag.ValueType() { case "int", "int8", "int16", "int32", "int64": return cast.ToInt(flag.ValueString()) case "bool": return cast.ToBool(flag.ValueString()) case "stringSlice", "stringArray": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return res case "boolSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToBoolSlice(res) case "intSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToIntSlice(res) case "uintSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToUintSlice(res) case "float64Slice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") res, _ := readAsCSV(s) return cast.ToFloat64Slice(res) case "stringToString": return stringToStringConv(flag.ValueString()) case "stringToInt": return stringToIntConv(flag.ValueString()) case "durationSlice": s := strings.TrimPrefix(flag.ValueString(), "[") s = strings.TrimSuffix(s, "]") slice := strings.Split(s, ",") return cast.ToDurationSlice(slice) default: return flag.ValueString() } } // last item, no need to check shadowing } return nil } func readAsCSV(val string) ([]string, error) { if val == "" { return []string{}, nil } stringReader := strings.NewReader(val) csvReader := csv.NewReader(stringReader) return csvReader.Read() } // mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/master/string_to_string.go#L79 // alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap. func stringToStringConv(val string) any { val = strings.Trim(val, "[]") // An empty string would cause an empty map if val == "" { return map[string]any{} } r := csv.NewReader(strings.NewReader(val)) ss, err := r.Read() if err != nil { return nil } out := make(map[string]any, len(ss)) for _, pair := range ss { k, vv, found := strings.Cut(pair, "=") if !found { return nil } out[k] = vv } return out } // mostly copied from pflag's implementation of this operation here https://github.com/spf13/pflag/blob/d5e0c0615acee7028e1e2740a11102313be88de1/string_to_int.go#L68 // alterations are: errors are swallowed, map[string]any is returned in order to enable cast.ToStringMap. func stringToIntConv(val string) any { val = strings.Trim(val, "[]") // An empty string would cause an empty map if val == "" { return map[string]any{} } ss := strings.Split(val, ",") out := make(map[string]any, len(ss)) for _, pair := range ss { k, vv, found := strings.Cut(pair, "=") if !found { return nil } var err error out[k], err = strconv.Atoi(vv) if err != nil { return nil } } return out } // IsSet checks to see if the key has been set in any of the data locations. // IsSet is case-insensitive for a key. func IsSet(key string) bool { return v.IsSet(key) } // IsSet checks to see if the key has been set in any of the data locations. // IsSet is case-insensitive for a key. func (v *Viper) IsSet(key string) bool { lcaseKey := strings.ToLower(key) val := v.find(lcaseKey, false) return val != nil } // AutomaticEnv makes Viper check if environment variables match any of the existing keys // (config, default or flags). If matching env vars are found, they are loaded into Viper. func AutomaticEnv() { v.AutomaticEnv() } // AutomaticEnv makes Viper check if environment variables match any of the existing keys // (config, default or flags). If matching env vars are found, they are loaded into Viper. func (v *Viper) AutomaticEnv() { v.automaticEnvApplied = true } // SetEnvKeyReplacer sets the strings.Replacer on the viper object // Useful for mapping an environmental variable to a key that does // not match it. func SetEnvKeyReplacer(r *strings.Replacer) { v.SetEnvKeyReplacer(r) } // SetEnvKeyReplacer sets the strings.Replacer on the viper object // Useful for mapping an environmental variable to a key that does // not match it. func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) { v.envKeyReplacer = r } // RegisterAlias creates an alias that provides another accessor for the same key. // This enables one to change a name without breaking the application. func RegisterAlias(alias, key string) { v.RegisterAlias(alias, key) } // RegisterAlias creates an alias that provides another accessor for the same key. // This enables one to change a name without breaking the application. func (v *Viper) RegisterAlias(alias, key string) { v.registerAlias(alias, strings.ToLower(key)) } func (v *Viper) registerAlias(alias, key string) { alias = strings.ToLower(alias) if alias != key && alias != v.realKey(key) { _, exists := v.aliases[alias] if !exists { // if we alias something that exists in one of the maps to another // name, we'll never be able to get that value using the original // name, so move the config value to the new realkey. if val, ok := v.config[alias]; ok { delete(v.config, alias) v.config[key] = val } if val, ok := v.kvstore[alias]; ok { delete(v.kvstore, alias) v.kvstore[key] = val } if val, ok := v.defaults[alias]; ok { delete(v.defaults, alias) v.defaults[key] = val } if val, ok := v.override[alias]; ok { delete(v.override, alias) v.override[key] = val } v.aliases[alias] = key } } else { v.logger.Warn("creating circular reference alias", "alias", alias, "key", key, "real_key", v.realKey(key)) } } func (v *Viper) realKey(key string) string { newkey, exists := v.aliases[key] if exists { v.logger.Debug("key is an alias", "alias", key, "to", newkey) return v.realKey(newkey) } return key } // InConfig checks to see if the given key (or an alias) is in the config file. func InConfig(key string) bool { return v.InConfig(key) } // InConfig checks to see if the given key (or an alias) is in the config file. func (v *Viper) InConfig(key string) bool { lcaseKey := strings.ToLower(key) // if the requested key is an alias, then return the proper key lcaseKey = v.realKey(lcaseKey) path := strings.Split(lcaseKey, v.keyDelim) return v.searchIndexableWithPathPrefixes(v.config, path) != nil } // SetDefault sets the default value for this key. // SetDefault is case-insensitive for a key. // Default only used when no value is provided by the user via flag, config or ENV. func SetDefault(key string, value any) { v.SetDefault(key, value) } // SetDefault sets the default value for this key. // SetDefault is case-insensitive for a key. // Default only used when no value is provided by the user via flag, config or ENV. func (v *Viper) SetDefault(key string, value any) { // If alias passed in, then set the proper default key = v.realKey(strings.ToLower(key)) value = toCaseInsensitiveValue(value) path := strings.Split(key, v.keyDelim) lastKey := strings.ToLower(path[len(path)-1]) deepestMap := deepSearch(v.defaults, path[0:len(path)-1]) // set innermost value deepestMap[lastKey] = value } // Set sets the value for the key in the override register. // Set is case-insensitive for a key. // Will be used instead of values obtained via // flags, config file, ENV, default, or key/value store. func Set(key string, value any) { v.Set(key, value) } // Set sets the value for the key in the override register. // Set is case-insensitive for a key. // Will be used instead of values obtained via // flags, config file, ENV, default, or key/value store. func (v *Viper) Set(key string, value any) { // If alias passed in, then set the proper override key = v.realKey(strings.ToLower(key)) value = toCaseInsensitiveValue(value) path := strings.Split(key, v.keyDelim) lastKey := strings.ToLower(path[len(path)-1]) deepestMap := deepSearch(v.override, path[0:len(path)-1]) // set innermost value deepestMap[lastKey] = value } // ReadInConfig will discover and load the configuration file from disk // and key/value stores, searching in one of the defined paths. func ReadInConfig() error { return v.ReadInConfig() } // ReadInConfig will discover and load the configuration file from disk // and key/value stores, searching in one of the defined paths. func (v *Viper) ReadInConfig() error { v.logger.Info("attempting to read in config file") filename, err := v.getConfigFile() if err != nil { return err } if !slices.Contains(SupportedExts, v.getConfigType()) { return UnsupportedConfigError(v.getConfigType()) } v.logger.Debug("reading file", "file", filename) file, err := afero.ReadFile(v.fs, filename) if errors.Is(err, fs.ErrNotExist) { // The specified config file is missing return FileNotFoundError{err: err, path: filename} } else if err != nil { // We hit some other error from the filesystem that isn't a missing file return err } config := make(map[string]any) err = v.unmarshalReader(bytes.NewReader(file), config) if err != nil { return err } v.config = config return nil } // MergeInConfig merges a new configuration with an existing config. func MergeInConfig() error { return v.MergeInConfig() } // MergeInConfig merges a new configuration with an existing config. func (v *Viper) MergeInConfig() error { v.logger.Info("attempting to merge in config file") filename, err := v.getConfigFile() if err != nil { return err } if !slices.Contains(SupportedExts, v.getConfigType()) { return UnsupportedConfigError(v.getConfigType()) } file, err := afero.ReadFile(v.fs, filename) if err != nil { return err } return v.MergeConfig(bytes.NewReader(file)) } // ReadConfig will read a configuration file, setting existing keys to nil if the // key does not exist in the file. func ReadConfig(in io.Reader) error { return v.ReadConfig(in) } // ReadConfig reads a configuration file, setting existing keys to nil if the // key does not exist in the file. func (v *Viper) ReadConfig(in io.Reader) error { config := make(map[string]any) err := v.unmarshalReader(in, config) if err != nil { return err } v.config = config return nil } // MergeConfig merges a new configuration with an existing config. func MergeConfig(in io.Reader) error { return v.MergeConfig(in) } // MergeConfig merges a new configuration with an existing config. func (v *Viper) MergeConfig(in io.Reader) error { config := make(map[string]any) if err := v.unmarshalReader(in, config); err != nil { return err } return v.MergeConfigMap(config) } // MergeConfigMap merges the configuration from the map given with an existing config. // Note that the map given may be modified. func MergeConfigMap(cfg map[string]any) error { return v.MergeConfigMap(cfg) } // MergeConfigMap merges the configuration from the map given with an existing config. // Note that the map given may be modified. func (v *Viper) MergeConfigMap(cfg map[string]any) error { if v.config == nil { v.config = make(map[string]any) } insensitiviseMap(cfg) mergeMaps(cfg, v.config, nil) return nil } // WriteConfig writes the current configuration to a file. func WriteConfig() error { return v.WriteConfig() } // WriteConfig writes the current configuration to a file. func (v *Viper) WriteConfig() error { filename, err := v.getConfigFile() if err != nil { return err } return v.writeConfig(filename, true) } // SafeWriteConfig writes current configuration to file only if the file does not exist. func SafeWriteConfig() error { return v.SafeWriteConfig() } // SafeWriteConfig writes current configuration to file only if the file does not exist. func (v *Viper) SafeWriteConfig() error { if len(v.configPaths) < 1 { return errors.New("missing configuration for 'configPath'") } return v.SafeWriteConfigAs(filepath.Join(v.configPaths[0], v.configName+"."+v.configType)) } // WriteConfigAs writes current configuration to a given filename. func WriteConfigAs(filename string) error { return v.WriteConfigAs(filename) } // WriteConfigAs writes current configuration to a given filename. func (v *Viper) WriteConfigAs(filename string) error { return v.writeConfig(filename, true) } // WriteConfigTo writes current configuration to an [io.Writer]. func WriteConfigTo(w io.Writer) error { return v.WriteConfigTo(w) } // WriteConfigTo writes current configuration to an [io.Writer]. func (v *Viper) WriteConfigTo(w io.Writer) error { format := strings.ToLower(v.getConfigType()) if !slices.Contains(SupportedExts, format) { return UnsupportedConfigError(format) } return v.marshalWriter(w, format) } // SafeWriteConfigAs writes current configuration to a given filename if it does not exist. func SafeWriteConfigAs(filename string) error { return v.SafeWriteConfigAs(filename) } // SafeWriteConfigAs writes current configuration to a given filename if it does not exist. func (v *Viper) SafeWriteConfigAs(filename string) error { alreadyExists, err := afero.Exists(v.fs, filename) if alreadyExists && err == nil { return ConfigFileAlreadyExistsError(filename) } return v.writeConfig(filename, false) } func (v *Viper) writeConfig(filename string, force bool) error { v.logger.Info("attempting to write configuration to file") var configType string ext := filepath.Ext(filename) if ext != "" && ext != filepath.Base(filename) { configType = ext[1:] } else { configType = v.configType } if configType == "" { return fmt.Errorf("config type could not be determined for %s", filename) } if !slices.Contains(SupportedExts, configType) { return UnsupportedConfigError(configType) } if v.config == nil { v.config = make(map[string]any) } flags := os.O_CREATE | os.O_TRUNC | os.O_WRONLY if !force { flags |= os.O_EXCL } f, err := v.fs.OpenFile(filename, flags, v.configPermissions) if err != nil { return err } defer f.Close() if err := v.marshalWriter(f, configType); err != nil { return err } return f.Sync() } func (v *Viper) unmarshalReader(in io.Reader, c map[string]any) error { format := strings.ToLower(v.getConfigType()) if format == "" { return errors.New("cannot decode configuration: unable to determine config type") } buf := new(bytes.Buffer) _, err := buf.ReadFrom(in) if err != nil { return fmt.Errorf("failed to read configuration from input: %w", err) } // TODO: remove this once SupportedExts is deprecated/removed if !slices.Contains(SupportedExts, format) { return UnsupportedConfigError(format) } // TODO: return [UnsupportedConfigError] if the registry does not contain the format // TODO: consider deprecating this error type decoder, err := v.decoderRegistry.Decoder(format) if err != nil { return ConfigParseError{err} } err = decoder.Decode(buf.Bytes(), c) if err != nil { return ConfigParseError{err} } insensitiviseMap(c) return nil } // Marshal a map into Writer. func (v *Viper) marshalWriter(w io.Writer, configType string) error { c := v.AllSettings() encoder, err := v.encoderRegistry.Encoder(configType) if err != nil { return ConfigMarshalError{err} } b, err := encoder.Encode(c) if err != nil { return ConfigMarshalError{err} } _, err = w.Write(b) if err != nil { return ConfigMarshalError{err} } return nil } func keyExists(k string, m map[string]any) string { lk := strings.ToLower(k) for mk := range m { lmk := strings.ToLower(mk) if lmk == lk { return mk } } return "" } func castToMapStringInterface( src map[any]any, ) map[string]any { tgt := map[string]any{} for k, v := range src { tgt[fmt.Sprintf("%v", k)] = v } return tgt } func castMapStringSliceToMapInterface(src map[string][]string) map[string]any { tgt := map[string]any{} for k, v := range src { tgt[k] = v } return tgt } func castMapStringToMapInterface(src map[string]string) map[string]any { tgt := map[string]any{} for k, v := range src { tgt[k] = v } return tgt } func castMapFlagToMapInterface(src map[string]FlagValue) map[string]any { tgt := map[string]any{} for k, v := range src { tgt[k] = v } return tgt } // mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's // insistence on parsing nested structures as `map[any]any` // instead of using a `string` as the key for nest structures beyond one level // deep. Both map types are supported as there is a go-yaml fork that uses // `map[string]any` instead. func mergeMaps(src, tgt map[string]any, itgt map[any]any) { for sk, sv := range src { tk := keyExists(sk, tgt) if tk == "" { v.logger.Debug("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv) tgt[sk] = sv if itgt != nil { itgt[sk] = sv } continue } tv, ok := tgt[tk] if !ok { v.logger.Debug("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv) tgt[sk] = sv if itgt != nil { itgt[sk] = sv } continue } svType := reflect.TypeOf(sv) tvType := reflect.TypeOf(tv) v.logger.Debug( "processing", "key", sk, "st", svType, "tt", tvType, "sv", sv, "tv", tv, ) switch ttv := tv.(type) { case map[any]any: v.logger.Debug("merging maps (must convert)") tsv, ok := sv.(map[any]any) if !ok { v.logger.Error( "Could not cast sv to map[any]any", "key", sk, "st", svType, "tt", tvType, "sv", sv, "tv", tv, ) continue } ssv := castToMapStringInterface(tsv) stv := castToMapStringInterface(ttv) mergeMaps(ssv, stv, ttv) case map[string]any: v.logger.Debug("merging maps") tsv, ok := sv.(map[string]any) if !ok { v.logger.Error( "Could not cast sv to map[string]any", "key", sk, "st", svType, "tt", tvType, "sv", sv, "tv", tv, ) continue } mergeMaps(tsv, ttv, nil) default: v.logger.Debug("setting value") tgt[tk] = sv if itgt != nil { itgt[tk] = sv } } } } // AllKeys returns all keys holding a value, regardless of where they are set. // Nested keys are returned with a v.keyDelim separator. func AllKeys() []string { return v.AllKeys() } // AllKeys returns all keys holding a value, regardless of where they are set. // Nested keys are returned with a v.keyDelim separator. func (v *Viper) AllKeys() []string { m := map[string]bool{} // add all paths, by order of descending priority to ensure correct shadowing m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "") m = v.flattenAndMergeMap(m, v.override, "") m = v.mergeFlatMap(m, castMapFlagToMapInterface(v.pflags)) m = v.mergeFlatMap(m, castMapStringSliceToMapInterface(v.env)) m = v.flattenAndMergeMap(m, v.config, "") m = v.flattenAndMergeMap(m, v.kvstore, "") m = v.flattenAndMergeMap(m, v.defaults, "") // convert set of paths to list a := make([]string, 0, len(m)) for x := range m { a = append(a, x) } return a } // flattenAndMergeMap recursively flattens the given map into a map[string]bool // of key paths (used as a set, easier to manipulate than a []string): // - each path is merged into a single key string, delimited with v.keyDelim // - if a path is shadowed by an earlier value in the initial shadow map, // it is skipped. // // The resulting set of paths is merged to the given shadow set at the same time. func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]any, prefix string) map[string]bool { if shadow != nil && prefix != "" && shadow[prefix] { // prefix is shadowed => nothing more to flatten return shadow } if shadow == nil { shadow = make(map[string]bool) } var m2 map[string]any if prefix != "" { prefix += v.keyDelim } for k, val := range m { fullKey := prefix + k switch val := val.(type) { case map[string]any: m2 = val case map[any]any: m2 = cast.ToStringMap(val) default: // immediate value shadow[strings.ToLower(fullKey)] = true continue } // recursively merge to shadow map shadow = v.flattenAndMergeMap(shadow, m2, fullKey) } return shadow } // mergeFlatMap merges the given maps, excluding values of the second map // shadowed by values from the first map. func (v *Viper) mergeFlatMap(shadow map[string]bool, m map[string]any) map[string]bool { // scan keys outer: for k := range m { path := strings.Split(k, v.keyDelim) // scan intermediate paths var parentKey string for i := 1; i < len(path); i++ { parentKey = strings.Join(path[0:i], v.keyDelim) if shadow[parentKey] { // path is shadowed, continue continue outer } } // add key shadow[strings.ToLower(k)] = true } return shadow } // AllSettings merges all settings and returns them as a map[string]any. func AllSettings() map[string]any { return v.AllSettings() } // AllSettings merges all settings and returns them as a map[string]any. func (v *Viper) AllSettings() map[string]any { return v.getSettings(v.AllKeys()) } func (v *Viper) getSettings(keys []string) map[string]any { m := map[string]any{} // start from the list of keys, and construct the map one value at a time for _, k := range keys { value := v.Get(k) if value == nil { // should not happen, since AllKeys() returns only keys holding a value, // check just in case anything changes continue } path := strings.Split(k, v.keyDelim) lastKey := strings.ToLower(path[len(path)-1]) deepestMap := deepSearch(m, path[0:len(path)-1]) // set innermost value deepestMap[lastKey] = value } return m } // SetFs sets the filesystem to use to read configuration. func SetFs(fs afero.Fs) { v.SetFs(fs) } // SetFs sets the filesystem to use to read configuration. func (v *Viper) SetFs(fs afero.Fs) { v.fs = fs } // SetConfigName sets name for the config file. // Does not include extension. func SetConfigName(in string) { v.SetConfigName(in) } // SetConfigName sets the name for the config file. // Does not include extension. func (v *Viper) SetConfigName(in string) { if v.finder != nil { v.logger.Warn("ineffective call to function: custom finder takes precedence", slog.String("function", "SetConfigName")) } if in != "" { v.configName = in v.configFile = "" } } // SetConfigType sets the type of the configuration returned by the // remote source, e.g. "json". func SetConfigType(in string) { v.SetConfigType(in) } // SetConfigType sets the type of the configuration returned by the // remote source, e.g. "json". func (v *Viper) SetConfigType(in string) { if in != "" { v.configType = in } } // SetConfigPermissions sets the permissions for the config file. func SetConfigPermissions(perm os.FileMode) { v.SetConfigPermissions(perm) } // SetConfigPermissions sets the permissions for the config file. func (v *Viper) SetConfigPermissions(perm os.FileMode) { v.configPermissions = perm.Perm() } func (v *Viper) getConfigType() string { if v.configType != "" { return v.configType } cf, err := v.getConfigFile() if err != nil { return "" } ext := filepath.Ext(cf) if len(ext) > 1 { return ext[1:] } return "" } func (v *Viper) getConfigFile() (string, error) { if v.configFile == "" { cf, err := v.findConfigFile() if err != nil { return "", err } v.configFile = cf } return v.configFile, nil } // Debug prints all configuration registries for debugging // purposes. func Debug() { v.Debug() } // DebugTo writes all configuration registries to the given [io.Writer]. func DebugTo(w io.Writer) { v.DebugTo(w) } // Debug prints all configuration registries for debugging purposes. func (v *Viper) Debug() { v.DebugTo(os.Stdout) } // DebugTo writes all configuration registries to the given [io.Writer]. func (v *Viper) DebugTo(w io.Writer) { fmt.Fprintf(w, "Aliases:\n%#v\n", v.aliases) fmt.Fprintf(w, "Override:\n%#v\n", v.override) fmt.Fprintf(w, "PFlags:\n%#v\n", v.pflags) fmt.Fprintf(w, "Env:\n%#v\n", v.env) fmt.Fprintf(w, "Key/Value Store:\n%#v\n", v.kvstore) fmt.Fprintf(w, "Config:\n%#v\n", v.config) fmt.Fprintf(w, "Defaults:\n%#v\n", v.defaults) } ================================================ FILE: viper_test.go ================================================ // Copyright © 2014 Steve Francia . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package viper import ( "bytes" "encoding/json" "io" "os" "os/exec" "path" "path/filepath" "reflect" "runtime" "strings" "sync" "testing" "time" "github.com/fsnotify/fsnotify" "github.com/go-viper/mapstructure/v2" "github.com/sagikazarmark/locafero" "github.com/spf13/afero" "github.com/spf13/cast" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/spf13/viper/internal/testutil" ) // var yamlExample = []byte(`Hacker: true // name: steve // hobbies: // - skateboarding // - snowboarding // - go // clothing: // jacket: leather // trousers: denim // pants: // size: large // age: 35 // eyes : brown // beard: true // `) var yamlExampleWithExtras = []byte(`Existing: true Bogus: true `) type testUnmarshalExtra struct { Existing bool } var tomlExample = []byte(` title = "TOML Example" [owner] organization = "MongoDB" Bio = "MongoDB Chief Developer Advocate & Hacker at Large" dob = 1979-05-27T07:32:00Z # First class dates? Why not?`) var dotenvExample = []byte(` TITLE_DOTENV="DotEnv Example" TYPE_DOTENV=donut NAME_DOTENV=Cake`) var jsonExample = []byte(`{ "id": "0001", "type": "donut", "name": "Cake", "ppu": 0.55, "batters": { "batter": [ { "type": "Regular" }, { "type": "Chocolate" }, { "type": "Blueberry" }, { "type": "Devil's Food" } ] } }`) var remoteExample = []byte(`{ "id":"0002", "type":"cronut", "newkey":"remote" }`) func initConfigs(v *Viper) { var r io.Reader v.SetConfigType("yaml") r = bytes.NewReader(yamlExample) v.unmarshalReader(r, v.config) v.SetConfigType("json") r = bytes.NewReader(jsonExample) v.unmarshalReader(r, v.config) v.SetConfigType("toml") r = bytes.NewReader(tomlExample) v.unmarshalReader(r, v.config) v.SetConfigType("env") r = bytes.NewReader(dotenvExample) v.unmarshalReader(r, v.config) v.SetConfigType("json") remote := bytes.NewReader(remoteExample) v.unmarshalReader(remote, v.kvstore) } func initConfig(typ, config string, v *Viper) { v.SetConfigType(typ) r := strings.NewReader(config) if err := v.unmarshalReader(r, v.config); err != nil { panic(err) } } // initDirs makes directories for testing. func initDirs(t *testing.T) (string, string) { var ( testDirs = []string{`a a`, `b`, `C_`} config = `improbable` ) if runtime.GOOS != "windows" { testDirs = append(testDirs, `d\d`) } root := t.TempDir() for _, dir := range testDirs { innerDir := filepath.Join(root, dir) err := os.Mkdir(innerDir, 0o750) require.NoError(t, err) err = os.WriteFile( filepath.Join(innerDir, config+".toml"), []byte(`key = "value is `+dir+`"`+"\n"), 0o640) require.NoError(t, err) } return root, config } // stubs for PFlag Values. type stringValue string func newStringValue(val string, p *string) *stringValue { *p = val return (*stringValue)(p) } func (s *stringValue) Set(val string) error { *s = stringValue(val) return nil } func (s *stringValue) Type() string { return "string" } func (s *stringValue) String() string { return string(*s) } func TestGetConfigFile(t *testing.T) { t.Run("config file set", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) v := New() v.SetFs(fs) v.AddConfigPath("/etc/viper") v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename) assert.NoError(t, err) }) t.Run("find file", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) v := New() v.SetFs(fs) v.AddConfigPath("/etc/viper") filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename) assert.NoError(t, err) }) t.Run("find files only", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/config"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/config/config.yaml")) require.NoError(t, err) v := New() v.SetFs(fs) v.AddConfigPath("/etc") v.AddConfigPath("/etc/config") filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/etc/config/config.yaml"), filename) assert.NoError(t, err) }) t.Run("precedence", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/home/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/home/viper/config.zml")) require.NoError(t, err) err = fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.bml")) require.NoError(t, err) err = fs.Mkdir(testutil.AbsFilePath(t, "/var/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/var/viper/config.yaml")) require.NoError(t, err) v := New() v.SetFs(fs) v.AddConfigPath("/home/viper") v.AddConfigPath("/etc/viper") v.AddConfigPath("/var/viper") filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/var/viper/config.yaml"), filename) assert.NoError(t, err) }) t.Run("without extension", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext")) require.NoError(t, err) v := New() v.SetFs(fs) v.AddConfigPath("/etc/viper") v.SetConfigName(".dotfilenoext") v.SetConfigType("yaml") filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"), filename) assert.NoError(t, err) }) t.Run("without extension and config type", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext")) require.NoError(t, err) v := New() v.SetFs(fs) v.AddConfigPath("/etc/viper") v.SetConfigName(".dotfilenoext") _, err = v.getConfigFile() // unless config type is set, files without extension // are not considered assert.Error(t, err) }) t.Run("experimental finder", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) v := NewWithOptions(ExperimentalFinder()) v.SetFs(fs) v.AddConfigPath("/etc/viper") filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), testutil.AbsFilePath(t, filename)) assert.NoError(t, err) }) t.Run("finder", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) _, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) finder := locafero.Finder{ Paths: []string{testutil.AbsFilePath(t, "/etc/viper")}, Names: locafero.NameWithExtensions("config", SupportedExts...), Type: locafero.FileTypeFile, } v := NewWithOptions(WithFinder(finder)) v.SetFs(fs) // These should be ineffective v.AddConfigPath("/etc/something_else") v.SetConfigName("not-config") filename, err := v.getConfigFile() assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), testutil.AbsFilePath(t, filename)) assert.NoError(t, err) }) } func TestReadInConfig(t *testing.T) { t.Run("config file set", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) _, err = file.WriteString(`key: value`) require.NoError(t, err) file.Close() v := New() v.SetFs(fs) v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) err = v.ReadInConfig() require.NoError(t, err) assert.Equal(t, "value", v.Get("key")) }) t.Run("find file", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) _, err = file.WriteString(`key: value`) require.NoError(t, err) file.Close() v := New() v.SetFs(fs) v.AddConfigPath("/etc/viper") err = v.ReadInConfig() require.NoError(t, err) assert.Equal(t, "value", v.Get("key")) }) t.Run("find file with experimental finder", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) _, err = file.WriteString(`key: value`) require.NoError(t, err) file.Close() v := NewWithOptions(ExperimentalFinder()) v.SetFs(fs) v.AddConfigPath("/etc/viper") err = v.ReadInConfig() require.NoError(t, err) assert.Equal(t, "value", v.Get("key")) }) t.Run("find file using a finder", func(t *testing.T) { fs := afero.NewMemMapFs() err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777) require.NoError(t, err) file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml")) require.NoError(t, err) _, err = file.WriteString(`key: value`) require.NoError(t, err) file.Close() finder := locafero.Finder{ Paths: []string{testutil.AbsFilePath(t, "/etc/viper")}, Names: locafero.NameWithExtensions("config", SupportedExts...), Type: locafero.FileTypeFile, } v := NewWithOptions(WithFinder(finder)) v.SetFs(fs) // These should be ineffective v.AddConfigPath("/etc/something_else") v.SetConfigName("not-config") err = v.ReadInConfig() require.NoError(t, err) assert.Equal(t, "value", v.Get("key")) }) } func TestDefault(t *testing.T) { v := New() v.SetDefault("age", 45) assert.Equal(t, 45, v.Get("age")) v.SetDefault("clothing.jacket", "slacks") assert.Equal(t, "slacks", v.Get("clothing.jacket")) v.SetConfigType("yaml") err := v.ReadConfig(bytes.NewBuffer(yamlExample)) require.NoError(t, err) assert.Equal(t, "leather", v.Get("clothing.jacket")) } func TestUnmarshaling(t *testing.T) { v := New() v.SetConfigType("yaml") r := bytes.NewReader(yamlExample) v.unmarshalReader(r, v.config) assert.True(t, v.InConfig("name")) assert.True(t, v.InConfig("clothing.jacket")) assert.False(t, v.InConfig("state")) assert.False(t, v.InConfig("clothing.hat")) assert.Equal(t, "steve", v.Get("name")) assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies")) assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing")) assert.Equal(t, 35, v.Get("age")) } func TestUnmarshalExact(t *testing.T) { v := New() target := &testUnmarshalExtra{} v.SetConfigType("yaml") r := bytes.NewReader(yamlExampleWithExtras) v.ReadConfig(r) err := v.UnmarshalExact(target) assert.Error(t, err, "UnmarshalExact should error when populating a struct from a conf that contains unused fields") } func TestOverrides(t *testing.T) { v := New() v.Set("age", 40) assert.Equal(t, 40, v.Get("age")) } func TestDefaultPost(t *testing.T) { v := New() assert.NotEqual(t, "NYC", v.Get("state")) v.SetDefault("state", "NYC") assert.Equal(t, "NYC", v.Get("state")) } func TestAliases(t *testing.T) { v := New() v.Set("age", 40) v.RegisterAlias("years", "age") assert.Equal(t, 40, v.Get("years")) v.Set("years", 45) assert.Equal(t, 45, v.Get("age")) } func TestAliasInConfigFile(t *testing.T) { v := New() v.SetConfigType("yaml") // Read the YAML data into Viper configuration require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)), "Error reading YAML data") v.RegisterAlias("beard", "hasbeard") assert.Equal(t, true, v.Get("hasbeard")) v.Set("hasbeard", false) assert.Equal(t, false, v.Get("beard")) } func TestYML(t *testing.T) { v := New() v.SetConfigType("yaml") // Read the YAML data into Viper configuration require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)), "Error reading YAML data") assert.Equal(t, "steve", v.Get("name")) } func TestJSON(t *testing.T) { v := New() v.SetConfigType("json") // Read the JSON data into Viper configuration require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading JSON data") assert.Equal(t, "0001", v.Get("id")) } func TestTOML(t *testing.T) { v := New() v.SetConfigType("toml") // Read the TOML data into Viper configuration require.NoError(t, v.ReadConfig(bytes.NewBuffer(tomlExample)), "Error reading toml data") assert.Equal(t, "TOML Example", v.Get("title")) } func TestDotEnv(t *testing.T) { v := New() v.SetConfigType("env") // Read the dotenv data into Viper configuration require.NoError(t, v.ReadConfig(bytes.NewBuffer(dotenvExample)), "Error reading env data") assert.Equal(t, "DotEnv Example", v.Get("title_dotenv")) } func TestRemotePrecedence(t *testing.T) { v := New() v.SetConfigType("json") // Read the remote data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data") assert.Equal(t, "0001", v.Get("id")) // update the kvstore with the remoteExample which should overite the key in v.config remote := bytes.NewReader(remoteExample) require.NoError(t, v.unmarshalReader(remote, v.kvstore), "Error reading json data in to kvstore") assert.Equal(t, "0001", v.Get("id")) assert.NotEqual(t, "cronut", v.Get("type")) assert.Equal(t, "remote", v.Get("newkey")) v.Set("newkey", "newvalue") assert.NotEqual(t, "remote", v.Get("newkey")) assert.Equal(t, "newvalue", v.Get("newkey")) } func TestEnv(t *testing.T) { v := New() v.SetConfigType("json") // Read the JSON data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data") v.BindEnv("id") v.BindEnv("f", "FOOD", "OLD_FOOD") t.Setenv("ID", "13") t.Setenv("FOOD", "apple") t.Setenv("OLD_FOOD", "banana") t.Setenv("NAME", "crunk") assert.Equal(t, "13", v.Get("id")) assert.Equal(t, "apple", v.Get("f")) assert.Equal(t, "Cake", v.Get("name")) v.AutomaticEnv() assert.Equal(t, "crunk", v.Get("name")) } func TestMultipleEnv(t *testing.T) { v := New() v.SetConfigType("json") // Read the JSON data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data") v.BindEnv("f", "FOOD", "OLD_FOOD") t.Setenv("OLD_FOOD", "banana") assert.Equal(t, "banana", v.Get("f")) } func TestEmptyEnv(t *testing.T) { v := New() v.SetConfigType("json") // Read the JSON data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data") v.BindEnv("type") // Empty environment variable v.BindEnv("name") // Bound, but not set environment variable t.Setenv("TYPE", "") assert.Equal(t, "donut", v.Get("type")) assert.Equal(t, "Cake", v.Get("name")) } func TestEmptyEnv_Allowed(t *testing.T) { v := New() v.SetConfigType("json") // Read the JSON data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data") v.AllowEmptyEnv(true) v.BindEnv("type") // Empty environment variable v.BindEnv("name") // Bound, but not set environment variable t.Setenv("TYPE", "") assert.Equal(t, "", v.Get("type")) assert.Equal(t, "Cake", v.Get("name")) } func TestEnvPrefix(t *testing.T) { v := New() v.SetConfigType("json") // Read the JSON data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading json data") v.SetEnvPrefix("foo") // will be uppercased automatically v.BindEnv("id") v.BindEnv("f", "FOOD") // not using prefix t.Setenv("FOO_ID", "13") t.Setenv("FOOD", "apple") t.Setenv("FOO_NAME", "crunk") assert.Equal(t, "13", v.Get("id")) assert.Equal(t, "apple", v.Get("f")) assert.Equal(t, "Cake", v.Get("name")) v.AutomaticEnv() assert.Equal(t, "crunk", v.Get("name")) } func TestAutoEnv(t *testing.T) { v := New() v.AutomaticEnv() t.Setenv("FOO_BAR", "13") assert.Equal(t, "13", v.Get("foo_bar")) } func TestAutoEnvWithPrefix(t *testing.T) { v := New() v.AutomaticEnv() v.SetEnvPrefix("Baz") t.Setenv("BAZ_BAR", "13") assert.Equal(t, "13", v.Get("bar")) } func TestSetEnvKeyReplacer(t *testing.T) { v := New() v.AutomaticEnv() t.Setenv("REFRESH_INTERVAL", "30s") replacer := strings.NewReplacer("-", "_") v.SetEnvKeyReplacer(replacer) assert.Equal(t, "30s", v.Get("refresh-interval")) } func TestEnvKeyReplacer(t *testing.T) { v := NewWithOptions(EnvKeyReplacer(strings.NewReplacer("-", "_"))) v.AutomaticEnv() t.Setenv("REFRESH_INTERVAL", "30s") assert.Equal(t, "30s", v.Get("refresh-interval")) } func TestEnvSubConfig(t *testing.T) { v := New() v.SetConfigType("yaml") // Read the YAML data into Viper configuration v.config require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)), "Error reading json data") v.AutomaticEnv() v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) t.Setenv("CLOTHING_PANTS_SIZE", "small") subv := v.Sub("clothing").Sub("pants") assert.Equal(t, "small", subv.Get("size")) // again with EnvPrefix v.SetEnvPrefix("foo") // will be uppercased automatically subWithPrefix := v.Sub("clothing").Sub("pants") t.Setenv("FOO_CLOTHING_PANTS_SIZE", "large") assert.Equal(t, "large", subWithPrefix.Get("size")) } func TestAllKeys(t *testing.T) { v := New() initConfigs(v) ks := []string{ "title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "title_dotenv", "type_dotenv", "name_dotenv", } dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") all := map[string]any{ "owner": map[string]any{ "organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob, }, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[string]any{ "trousers": "denim", "jacket": "leather", "pants": map[string]any{"size": "large"}, }, "id": "0001", "batters": map[string]any{ "batter": []any{ map[string]any{"type": "Regular"}, map[string]any{"type": "Chocolate"}, map[string]any{"type": "Blueberry"}, map[string]any{"type": "Devil's Food"}, }, }, "hacker": true, "beard": true, "hobbies": []any{ "skateboarding", "snowboarding", "go", }, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "title_dotenv": "DotEnv Example", "type_dotenv": "donut", "name_dotenv": "Cake", } assert.ElementsMatch(t, ks, v.AllKeys()) assert.Equal(t, all, v.AllSettings()) } func TestAllKeysWithEnv(t *testing.T) { v := New() // bind and define environment variables (including a nested one) v.BindEnv("id") v.BindEnv("foo.bar") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) t.Setenv("ID", "13") t.Setenv("FOO_BAR", "baz") assert.ElementsMatch(t, []string{"id", "foo.bar"}, v.AllKeys()) } func TestAliasesOfAliases(t *testing.T) { v := New() v.Set("Title", "Checking Case") v.RegisterAlias("Foo", "Bar") v.RegisterAlias("Bar", "Title") assert.Equal(t, "Checking Case", v.Get("FOO")) } func TestRecursiveAliases(t *testing.T) { v := New() v.Set("baz", "bat") v.RegisterAlias("Baz", "Roo") v.RegisterAlias("Roo", "baz") assert.Equal(t, "bat", v.Get("Baz")) } func TestUnmarshal(t *testing.T) { v := New() v.SetDefault("port", 1313) v.Set("name", "Steve") v.Set("duration", "1s1ms") v.Set("modes", []int{1, 2, 3}) type config struct { Port int Name string Duration time.Duration Modes []int } var C config require.NoError(t, v.Unmarshal(&C), "unable to decode into struct") assert.Equal( t, &config{ Name: "Steve", Port: 1313, Duration: time.Second + time.Millisecond, Modes: []int{1, 2, 3}, }, &C, ) v.Set("port", 1234) require.NoError(t, v.Unmarshal(&C), "unable to decode into struct") assert.Equal( t, &config{ Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond, Modes: []int{1, 2, 3}, }, &C, ) } func TestUnmarshalWithDefaultDecodeHook(t *testing.T) { opt := mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), // Custom Decode Hook Function func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) { if rf != reflect.String || rt != reflect.Map { return data, nil } m := map[string]string{} raw := data.(string) if raw == "" { return m, nil } err := json.Unmarshal([]byte(raw), &m) return m, err }, ) v := NewWithOptions(WithDecodeHook(opt)) v.Set("credentials", "{\"foo\":\"bar\"}") type config struct { Credentials map[string]string } var C config require.NoError(t, v.Unmarshal(&C), "unable to decode into struct") assert.Equal(t, &config{ Credentials: map[string]string{"foo": "bar"}, }, &C) } func TestUnmarshalWithDecoderOptions(t *testing.T) { v := New() v.Set("credentials", "{\"foo\":\"bar\"}") opt := DecodeHook(mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), // Custom Decode Hook Function func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) { if rf != reflect.String || rt != reflect.Map { return data, nil } m := map[string]string{} raw := data.(string) if raw == "" { return m, nil } err := json.Unmarshal([]byte(raw), &m) return m, err }, )) type config struct { Credentials map[string]string } var C config require.NoError(t, v.Unmarshal(&C, opt), "unable to decode into struct") assert.Equal(t, &config{ Credentials: map[string]string{"foo": "bar"}, }, &C) } func TestUnmarshalWithAutomaticEnv(t *testing.T) { t.Setenv("PORT", "1313") t.Setenv("NAME", "Steve") t.Setenv("DURATION", "1s1ms") t.Setenv("MODES", "1,2,3") t.Setenv("SECRET", "42") t.Setenv("FILESYSTEM_SIZE", "4096") type AuthConfig struct { Secret string `mapstructure:"secret"` } type StorageConfig struct { Size int `mapstructure:"size"` } type Configuration struct { Port int `mapstructure:"port"` Name string `mapstructure:"name"` Duration time.Duration `mapstructure:"duration"` // Infer name from struct Modes []int // Squash nested struct (omit prefix) Authentication AuthConfig `mapstructure:",squash"` // Different key Storage StorageConfig `mapstructure:"filesystem"` // Omitted field Flag bool `mapstructure:"flag"` } v := NewWithOptions(ExperimentalBindStruct()) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() t.Run("OK", func(t *testing.T) { var config Configuration if err := v.Unmarshal(&config); err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal( t, Configuration{ Name: "Steve", Port: 1313, Duration: time.Second + time.Millisecond, Modes: []int{1, 2, 3}, Authentication: AuthConfig{ Secret: "42", }, Storage: StorageConfig{ Size: 4096, }, }, config, ) }) t.Run("Precedence", func(t *testing.T) { var config Configuration v.Set("port", 1234) if err := v.Unmarshal(&config); err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal( t, Configuration{ Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond, Modes: []int{1, 2, 3}, Authentication: AuthConfig{ Secret: "42", }, Storage: StorageConfig{ Size: 4096, }, }, config, ) }) t.Run("Unset", func(t *testing.T) { var config Configuration err := v.Unmarshal(&config, func(config *mapstructure.DecoderConfig) { config.ErrorUnset = true }) assert.Error(t, err, "expected viper.Unmarshal to return error due to unset field 'FLAG'") }) t.Run("Exact", func(t *testing.T) { var config Configuration v.Set("port", 1234) if err := v.UnmarshalExact(&config); err != nil { t.Fatalf("unable to decode into struct, %v", err) } assert.Equal( t, Configuration{ Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond, Modes: []int{1, 2, 3}, Authentication: AuthConfig{ Secret: "42", }, Storage: StorageConfig{ Size: 4096, }, }, config, ) }) } func TestBindPFlags(t *testing.T) { v := New() // create independent Viper object flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) testValues := map[string]*string{ "host": nil, "port": nil, "endpoint": nil, } mutatedTestValues := map[string]string{ "host": "localhost", "port": "6060", "endpoint": "/public", } for name := range testValues { testValues[name] = flagSet.String(name, "", "test") } err := v.BindPFlags(flagSet) require.NoError(t, err, "error binding flag set") flagSet.VisitAll(func(flag *pflag.Flag) { flag.Value.Set(mutatedTestValues[flag.Name]) flag.Changed = true }) for name, expected := range mutatedTestValues { assert.Equal(t, expected, v.Get(name)) } } func TestBindPFlagsStringSlice(t *testing.T) { tests := []struct { Expected []string Value string }{ {[]string{}, ""}, {[]string{"jeden"}, "jeden"}, {[]string{"dwa", "trzy"}, "dwa,trzy"}, {[]string{"cztery", "piec , szesc"}, "cztery,\"piec , szesc\""}, } v := New() // create independent Viper object defaultVal := []string{"default"} v.SetDefault("stringslice", defaultVal) for _, testValue := range tests { flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.StringSlice("stringslice", testValue.Expected, "test") for _, changed := range []bool{true, false} { flagSet.VisitAll(func(f *pflag.Flag) { f.Value.Set(testValue.Value) f.Changed = changed }) err := v.BindPFlags(flagSet) require.NoError(t, err, "error binding flag set") type TestStr struct { StringSlice []string } val := &TestStr{} err = v.Unmarshal(val) require.NoError(t, err, "cannot unmarshal") if changed { assert.Equal(t, testValue.Expected, val.StringSlice) assert.Equal(t, testValue.Expected, v.Get("stringslice")) } else { assert.Equal(t, defaultVal, val.StringSlice) } } } } func TestBindPFlagsStringArray(t *testing.T) { tests := []struct { Expected []string Value string }{ {[]string{}, ""}, {[]string{"jeden"}, "jeden"}, {[]string{"dwa,trzy"}, "dwa,trzy"}, {[]string{"cztery,\"piec , szesc\""}, "cztery,\"piec , szesc\""}, } v := New() // create independent Viper object defaultVal := []string{"default"} v.SetDefault("stringarray", defaultVal) for _, testValue := range tests { flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.StringArray("stringarray", testValue.Expected, "test") for _, changed := range []bool{true, false} { flagSet.VisitAll(func(f *pflag.Flag) { f.Value.Set(testValue.Value) f.Changed = changed }) err := v.BindPFlags(flagSet) require.NoError(t, err, "error binding flag set") type TestStr struct { StringArray []string } val := &TestStr{} err = v.Unmarshal(val) require.NoError(t, err, "cannot unmarshal") if changed { assert.Equal(t, testValue.Expected, val.StringArray) assert.Equal(t, testValue.Expected, v.Get("stringarray")) } else { assert.Equal(t, defaultVal, val.StringArray) } } } } func TestBindPFlagsSlices(t *testing.T) { set := pflag.NewFlagSet("test", pflag.ContinueOnError) set.IntSlice("intslice", []int{}, "") set.BoolSlice("boolslice", []bool{}, "") set.Float64Slice("float64slice", []float64{}, "") set.UintSlice("uintslice", []uint{}, "") v := New() v.BindPFlags(set) set.Set("intslice", "1,2") assert.Equal(t, []int{1, 2}, v.Get("intslice")) set.Set("boolslice", "true,false") assert.Equal(t, []bool{true, false}, v.Get("boolslice")) set.Set("float64slice", "1.1,2.2") assert.Equal(t, []float64{1.1, 2.2}, v.Get("float64slice")) set.Set("uintslice", "1,2") assert.Equal(t, []uint{1, 2}, v.Get("uintslice")) } func TestSliceFlagsReturnCorrectType(t *testing.T) { flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.IntSlice("int", []int{1, 2}, "") flagSet.StringSlice("str", []string{"3", "4"}, "") flagSet.DurationSlice("duration", []time.Duration{5 * time.Second}, "") v := New() v.BindPFlags(flagSet) all := v.AllSettings() assert.IsType(t, []int{}, all["int"]) assert.IsType(t, []string{}, all["str"]) assert.IsType(t, []time.Duration{}, all["duration"]) } func TestBindPFlagsIntSlice(t *testing.T) { tests := []struct { Expected []int Value string }{ {[]int{}, ""}, {[]int{1}, "1"}, {[]int{2, 3}, "2,3"}, } v := New() // create independent Viper object defaultVal := []int{0} v.SetDefault("intslice", defaultVal) for _, testValue := range tests { flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.IntSlice("intslice", testValue.Expected, "test") for _, changed := range []bool{true, false} { flagSet.VisitAll(func(f *pflag.Flag) { f.Value.Set(testValue.Value) f.Changed = changed }) err := v.BindPFlags(flagSet) require.NoError(t, err, "error binding flag set") type TestInt struct { IntSlice []int } val := &TestInt{} err = v.Unmarshal(val) require.NoError(t, err, "cannot unmarshal") if changed { assert.Equal(t, testValue.Expected, val.IntSlice) assert.Equal(t, testValue.Expected, v.Get("intslice")) } else { assert.Equal(t, defaultVal, val.IntSlice) } } } } func TestBindPFlag(t *testing.T) { v := New() testString := "testing" testValue := newStringValue(testString, &testString) flag := &pflag.Flag{ Name: "testflag", Value: testValue, Changed: false, } v.BindPFlag("testvalue", flag) assert.Equal(t, testString, v.Get("testvalue")) flag.Value.Set("testing_mutate") flag.Changed = true // hack for pflag usage assert.Equal(t, "testing_mutate", v.Get("testvalue")) } func TestBindPFlagDetectNilFlag(t *testing.T) { v := New() result := v.BindPFlag("testvalue", nil) assert.Error(t, result) } func TestBindPFlagStringToString(t *testing.T) { tests := []struct { Expected map[string]string Value string }{ {map[string]string{}, ""}, {map[string]string{"yo": "hi"}, "yo=hi"}, {map[string]string{"yo": "hi", "oh": "hi=there"}, "yo=hi,oh=hi=there"}, {map[string]string{"yo": ""}, "yo="}, {map[string]string{"yo": "", "oh": "hi=there"}, "yo=,oh=hi=there"}, } v := New() // create independent Viper object defaultVal := map[string]string{} v.SetDefault("stringtostring", defaultVal) for _, testValue := range tests { flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.StringToString("stringtostring", testValue.Expected, "test") for _, changed := range []bool{true, false} { flagSet.VisitAll(func(f *pflag.Flag) { f.Value.Set(testValue.Value) f.Changed = changed }) err := v.BindPFlags(flagSet) require.NoError(t, err, "error binding flag set") type TestMap struct { StringToString map[string]string } val := &TestMap{} err = v.Unmarshal(val) require.NoError(t, err, "cannot unmarshal") if changed { assert.Equal(t, testValue.Expected, val.StringToString) } else { assert.Equal(t, defaultVal, val.StringToString) } } } } func TestBindPFlagStringToInt(t *testing.T) { tests := []struct { Expected map[string]int Value string }{ {map[string]int{"yo": 1, "oh": 21}, "yo=1,oh=21"}, {map[string]int{"yo": 100000000, "oh": 0}, "yo=100000000,oh=0"}, {map[string]int{}, "yo=2,oh=21.0"}, {map[string]int{}, "yo=,oh=20.99"}, {map[string]int{}, "yo=,oh="}, } v := New() // create independent Viper object defaultVal := map[string]int{} v.SetDefault("stringtoint", defaultVal) for _, testValue := range tests { flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) flagSet.StringToInt("stringtoint", testValue.Expected, "test") for _, changed := range []bool{true, false} { flagSet.VisitAll(func(f *pflag.Flag) { f.Value.Set(testValue.Value) f.Changed = changed }) err := v.BindPFlags(flagSet) require.NoError(t, err, "error binding flag set") type TestMap struct { StringToInt map[string]int } val := &TestMap{} err = v.Unmarshal(val) require.NoError(t, err, "cannot unmarshal") if changed { assert.Equal(t, testValue.Expected, val.StringToInt) } else { assert.Equal(t, defaultVal, val.StringToInt) } } } } func TestBoundCaseSensitivity(t *testing.T) { v := New() initConfigs(v) assert.Equal(t, "brown", v.Get("eyes")) v.BindEnv("eYEs", "TURTLE_EYES") t.Setenv("TURTLE_EYES", "blue") assert.Equal(t, "blue", v.Get("eyes")) testString := "green" testValue := newStringValue(testString, &testString) flag := &pflag.Flag{ Name: "eyeballs", Value: testValue, Changed: true, } v.BindPFlag("eYEs", flag) assert.Equal(t, "green", v.Get("eyes")) } func TestSizeInBytes(t *testing.T) { input := map[string]uint{ "": 0, "b": 0, "12 bytes": 0, "200000000000gb": 0, "12 b": 12, "43 MB": 43 * (1 << 20), "10mb": 10 * (1 << 20), "1gb": 1 << 30, } for str, expected := range input { assert.Equal(t, expected, parseSizeInBytes(str), str) } } func TestFindsNestedKeys(t *testing.T) { v := New() initConfigs(v) dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") v.Set("super", map[string]any{ "deep": map[string]any{ "nested": "value", }, }) expected := map[string]any{ "super": map[string]any{ "deep": map[string]any{ "nested": "value", }, }, "super.deep": map[string]any{ "nested": "value", }, "super.deep.nested": "value", "owner.organization": "MongoDB", "batters.batter": []any{ map[string]any{ "type": "Regular", }, map[string]any{ "type": "Chocolate", }, map[string]any{ "type": "Blueberry", }, map[string]any{ "type": "Devil's Food", }, }, "hobbies": []any{ "skateboarding", "snowboarding", "go", }, "TITLE_DOTENV": "DotEnv Example", "TYPE_DOTENV": "donut", "NAME_DOTENV": "Cake", "title": "TOML Example", "newkey": "remote", "batters": map[string]any{ "batter": []any{ map[string]any{ "type": "Regular", }, map[string]any{ "type": "Chocolate", }, map[string]any{ "type": "Blueberry", }, map[string]any{ "type": "Devil's Food", }, }, }, "eyes": "brown", "age": 35, "owner": map[string]any{ "organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob, }, "owner.bio": "MongoDB Chief Developer Advocate & Hacker at Large", "type": "donut", "id": "0001", "name": "Cake", "hacker": true, "ppu": 0.55, "clothing": map[string]any{ "jacket": "leather", "trousers": "denim", "pants": map[string]any{ "size": "large", }, }, "clothing.jacket": "leather", "clothing.pants.size": "large", "clothing.trousers": "denim", "owner.dob": dob, "beard": true, } for key, expectedValue := range expected { assert.Equal(t, expectedValue, v.Get(key)) } } func TestReadConfig(t *testing.T) { t.Run("ok", func(t *testing.T) { v := New() v.SetConfigType("yaml") err := v.ReadConfig(bytes.NewBuffer(yamlExample)) require.NoError(t, err) t.Log(v.AllKeys()) assert.True(t, v.InConfig("name")) assert.True(t, v.InConfig("clothing.jacket")) assert.False(t, v.InConfig("state")) assert.False(t, v.InConfig("clothing.hat")) assert.Equal(t, "steve", v.Get("name")) assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies")) assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing")) assert.Equal(t, 35, v.Get("age")) }) t.Run("missing config type", func(t *testing.T) { v := New() err := v.ReadConfig(bytes.NewBuffer(yamlExample)) require.Error(t, err) }) } func TestReadConfigWithSetConfigFile(t *testing.T) { v := New() v.SetConfigFile("config.yaml") // Dummy value to infer config type from file extension err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc)) require.NoError(t, err) assert.Equal(t, 45000, v.GetInt("hello.pop")) } func TestWrongFileNotFound(t *testing.T) { _, config := initDirs(t) v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) v.SetConfigFile(`whatareyoutalkingabout.yaml`) err := v.ReadInConfig() var fileLookupError FileLookupError // It matches all error types and the shared error interface. assert.ErrorAs(t, err, &FileNotFoundError{}) assert.ErrorAs(t, err, &fileLookupError) // Even though config did not load and the error might have // been ignored by the client, the default still loads assert.Equal(t, `default`, v.GetString(`key`)) } func TestIsSet(t *testing.T) { v := New() v.SetConfigType("yaml") /* config and defaults */ v.ReadConfig(bytes.NewBuffer(yamlExample)) v.SetDefault("clothing.shoes", "sneakers") assert.True(t, v.IsSet("clothing")) assert.True(t, v.IsSet("clothing.jacket")) assert.False(t, v.IsSet("clothing.jackets")) assert.True(t, v.IsSet("clothing.shoes")) /* state change */ assert.False(t, v.IsSet("helloworld")) v.Set("helloworld", "fubar") assert.True(t, v.IsSet("helloworld")) /* env */ v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.BindEnv("eyes") v.BindEnv("foo") v.BindEnv("clothing.hat") v.BindEnv("clothing.hats") t.Setenv("FOO", "bar") t.Setenv("CLOTHING_HAT", "bowler") assert.True(t, v.IsSet("eyes")) // in the config file assert.True(t, v.IsSet("foo")) // in the environment assert.True(t, v.IsSet("clothing.hat")) // in the environment assert.False(t, v.IsSet("clothing.hats")) // not defined /* flags */ flagset := pflag.NewFlagSet("testisset", pflag.ContinueOnError) flagset.Bool("foobaz", false, "foobaz") flagset.Bool("barbaz", false, "barbaz") foobaz, barbaz := flagset.Lookup("foobaz"), flagset.Lookup("barbaz") v.BindPFlag("foobaz", foobaz) v.BindPFlag("barbaz", barbaz) barbaz.Value.Set("true") barbaz.Changed = true // hack for pflag usage assert.False(t, v.IsSet("foobaz")) assert.True(t, v.IsSet("barbaz")) } func TestDirsSearch(t *testing.T) { root, config := initDirs(t) v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) entries, err := os.ReadDir(root) require.NoError(t, err) for _, e := range entries { if e.IsDir() { v.AddConfigPath(filepath.Join(root, e.Name())) } } err = v.ReadInConfig() require.NoError(t, err) assert.Equal(t, `value is `+filepath.Base(v.configPaths[0]), v.GetString(`key`)) } func TestWrongDirsSearchNotFound(t *testing.T) { _, config := initDirs(t) v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) v.AddConfigPath(`whattayoutalkingbout`) v.AddConfigPath(`thispathaintthere`) err := v.ReadInConfig() var fileLookupError FileLookupError // It matches all error types and the shared error interface. assert.ErrorAs(t, err, &ConfigFileNotFoundError{}) assert.ErrorAs(t, err, &FileNotFoundFromSearchError{}) assert.ErrorAs(t, err, &fileLookupError) // Even though config did not load and the error might have // been ignored by the client, the default still loads assert.Equal(t, `default`, v.GetString(`key`)) } func TestWrongDirsSearchNotFoundForMerge(t *testing.T) { _, config := initDirs(t) v := New() v.SetConfigName(config) v.SetDefault(`key`, `default`) v.AddConfigPath(`whattayoutalkingbout`) v.AddConfigPath(`thispathaintthere`) err := v.MergeInConfig() var fileLookupError FileLookupError // It matches both types of errors. assert.ErrorAs(t, err, &ConfigFileNotFoundError{}) assert.ErrorAs(t, err, &FileNotFoundFromSearchError{}) assert.ErrorAs(t, err, &fileLookupError) // Even though config did not load and the error might have // been ignored by the client, the default still loads assert.Equal(t, `default`, v.GetString(`key`)) } var yamlInvalid = []byte(`hash: map - foo - bar `) func TestUnwrapParseErrors(t *testing.T) { v := New() v.SetConfigType("yaml") assert.ErrorAs(t, v.ReadConfig(bytes.NewBuffer(yamlInvalid)), &ConfigParseError{}) } func TestSub(t *testing.T) { v := New() v.SetConfigType("yaml") v.ReadConfig(bytes.NewBuffer(yamlExample)) subv := v.Sub("clothing") assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("pants.size")) subv = v.Sub("clothing.pants") assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("size")) subv = v.Sub("clothing.pants.size") assert.Equal(t, (*Viper)(nil), subv) subv = v.Sub("missing.key") assert.Equal(t, (*Viper)(nil), subv) subv = v.Sub("clothing") assert.Equal(t, []string{"clothing"}, subv.parents) subv = v.Sub("clothing").Sub("pants") assert.Equal(t, []string{"clothing", "pants"}, subv.parents) } func TestSubWithKeyDelimiter(t *testing.T) { v := NewWithOptions(KeyDelimiter("::")) v.SetConfigType("yaml") r := strings.NewReader(string(yamlExampleWithDot)) err := v.unmarshalReader(r, v.config) require.NoError(t, err) subv := v.Sub("emails") assert.Equal(t, "01/02/03", subv.Get("steve@hacker.com::created")) } var jsonWriteExpected = []byte(`{ "batters": { "batter": [ { "type": "Regular" }, { "type": "Chocolate" }, { "type": "Blueberry" }, { "type": "Devil's Food" } ] }, "id": "0001", "name": "Cake", "ppu": 0.55, "type": "donut" }`) // var yamlWriteExpected = []byte(`age: 35 // beard: true // clothing: // jacket: leather // pants: // size: large // trousers: denim // eyes: brown // hacker: true // hobbies: // - skateboarding // - snowboarding // - go // name: steve // `) func TestWriteConfig(t *testing.T) { fs := afero.NewMemMapFs() testCases := map[string]struct { configName string inConfigType string outConfigType string fileName string input []byte expectedContent []byte }{ "json with file extension": { configName: "c", inConfigType: "json", outConfigType: "json", fileName: "c.json", input: jsonExample, expectedContent: jsonWriteExpected, }, "json without file extension": { configName: "c", inConfigType: "json", outConfigType: "json", fileName: "c", input: jsonExample, expectedContent: jsonWriteExpected, }, "json with file extension and mismatch type": { configName: "c", inConfigType: "json", outConfigType: "hcl", fileName: "c.json", input: jsonExample, expectedContent: jsonWriteExpected, }, "yaml with file extension": { configName: "c", inConfigType: "yaml", outConfigType: "yaml", fileName: "c.yaml", input: yamlExample, expectedContent: yamlWriteExpected, }, "yaml without file extension": { configName: "c", inConfigType: "yaml", outConfigType: "yaml", fileName: "c", input: yamlExample, expectedContent: yamlWriteExpected, }, "yaml with file extension and mismatch type": { configName: "c", inConfigType: "yaml", outConfigType: "json", fileName: "c.yaml", input: yamlExample, expectedContent: yamlWriteExpected, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { v := New() v.SetFs(fs) v.SetConfigName(tc.fileName) v.SetConfigType(tc.inConfigType) err := v.ReadConfig(bytes.NewBuffer(tc.input)) require.NoError(t, err) v.SetConfigType(tc.outConfigType) err = v.WriteConfigAs(tc.fileName) require.NoError(t, err) read, err := afero.ReadFile(fs, tc.fileName) require.NoError(t, err) assert.Equal(t, tc.expectedContent, read) }) } } func TestWriteConfigTOML(t *testing.T) { fs := afero.NewMemMapFs() testCases := map[string]struct { configName string configType string fileName string input []byte }{ "with file extension": { configName: "c", configType: "toml", fileName: "c.toml", input: tomlExample, }, "without file extension": { configName: "c", configType: "toml", fileName: "c", input: tomlExample, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { v := New() v.SetFs(fs) v.SetConfigName(tc.configName) v.SetConfigType(tc.configType) err := v.ReadConfig(bytes.NewBuffer(tc.input)) require.NoError(t, err) err = v.WriteConfigAs(tc.fileName) require.NoError(t, err) // The TOML String method does not order the contents. // Therefore, we must read the generated file and compare the data. v2 := New() v2.SetFs(fs) v2.SetConfigName(tc.configName) v2.SetConfigType(tc.configType) v2.SetConfigFile(tc.fileName) err = v2.ReadInConfig() require.NoError(t, err) assert.Equal(t, v.GetString("title"), v2.GetString("title")) assert.Equal(t, v.GetString("owner.bio"), v2.GetString("owner.bio")) assert.Equal(t, v.GetString("owner.dob"), v2.GetString("owner.dob")) assert.Equal(t, v.GetString("owner.organization"), v2.GetString("owner.organization")) }) } } func TestWriteConfigDotEnv(t *testing.T) { fs := afero.NewMemMapFs() testCases := map[string]struct { configName string configType string fileName string input []byte }{ "with file extension": { configName: "c", configType: "env", fileName: "c.env", input: dotenvExample, }, "without file extension": { configName: "c", configType: "env", fileName: "c", input: dotenvExample, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { v := New() v.SetFs(fs) v.SetConfigName(tc.configName) v.SetConfigType(tc.configType) err := v.ReadConfig(bytes.NewBuffer(tc.input)) require.NoError(t, err) err = v.WriteConfigAs(tc.fileName) require.NoError(t, err) // The TOML String method does not order the contents. // Therefore, we must read the generated file and compare the data. v2 := New() v2.SetFs(fs) v2.SetConfigName(tc.configName) v2.SetConfigType(tc.configType) v2.SetConfigFile(tc.fileName) err = v2.ReadInConfig() require.NoError(t, err) assert.Equal(t, v.GetString("title_dotenv"), v2.GetString("title_dotenv")) assert.Equal(t, v.GetString("type_dotenv"), v2.GetString("type_dotenv")) assert.Equal(t, v.GetString("kind_dotenv"), v2.GetString("kind_dotenv")) }) } } func TestSafeWriteConfig(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.AddConfigPath("/test") v.SetConfigName("c") v.SetConfigType("yaml") require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample))) require.NoError(t, v.SafeWriteConfig()) read, err := afero.ReadFile(fs, testutil.AbsFilePath(t, "/test/c.yaml")) require.NoError(t, err) assert.YAMLEq(t, string(yamlWriteExpected), string(read)) } func TestSafeWriteConfigWithMissingConfigPath(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.SetConfigName("c") v.SetConfigType("yaml") require.EqualError(t, v.SafeWriteConfig(), "missing configuration for 'configPath'") } func TestSafeWriteConfigWithExistingFile(t *testing.T) { v := New() fs := afero.NewMemMapFs() fs.Create(testutil.AbsFilePath(t, "/test/c.yaml")) v.SetFs(fs) v.AddConfigPath("/test") v.SetConfigName("c") v.SetConfigType("yaml") err := v.SafeWriteConfig() require.Error(t, err) _, ok := err.(ConfigFileAlreadyExistsError) assert.True(t, ok, "Expected ConfigFileAlreadyExistsError") } func TestSafeWriteAsConfig(t *testing.T) { v := New() fs := afero.NewMemMapFs() v.SetFs(fs) v.SetConfigType("yaml") err := v.ReadConfig(bytes.NewBuffer(yamlExample)) require.NoError(t, err) require.NoError(t, v.SafeWriteConfigAs("/test/c.yaml")) _, err = afero.ReadFile(fs, "/test/c.yaml") require.NoError(t, err) } func TestSafeWriteConfigAsWithExistingFile(t *testing.T) { v := New() fs := afero.NewMemMapFs() fs.Create("/test/c.yaml") v.SetFs(fs) err := v.SafeWriteConfigAs("/test/c.yaml") require.Error(t, err) _, ok := err.(ConfigFileAlreadyExistsError) assert.True(t, ok, "Expected ConfigFileAlreadyExistsError") } func TestWriteHiddenFile(t *testing.T) { v := New() fs := afero.NewMemMapFs() fs.Create(testutil.AbsFilePath(t, "/test/.config")) v.SetFs(fs) v.SetConfigName(".config") v.SetConfigType("yaml") v.AddConfigPath("/test") err := v.ReadInConfig() require.NoError(t, err) err = v.WriteConfig() require.NoError(t, err) } var yamlMergeExampleTgt = []byte(` hello: pop: 37890 largenum: 765432101234567 num2pow63: 9223372036854775808 universe: null world: - us - uk - fr - de `) var yamlMergeExampleSrc = []byte(` hello: pop: 45000 largenum: 7654321001234567 universe: - mw - ad ints: - 1 - 2 fu: bar `) var jsonMergeExampleTgt = []byte(` { "hello": { "foo": null, "pop": 123456 } } `) var jsonMergeExampleSrc = []byte(` { "hello": { "foo": "foo str", "pop": "pop str" } } `) func TestMergeConfig(t *testing.T) { v := New() v.SetConfigType("yml") err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)) require.NoError(t, err) assert.Equal(t, 37890, v.GetInt("hello.pop")) assert.Equal(t, int32(37890), v.GetInt32("hello.pop")) assert.Equal(t, int64(765432101234567), v.GetInt64("hello.largenum")) assert.Equal(t, uint8(2), v.GetUint8("hello.pop")) assert.Equal(t, uint(37890), v.GetUint("hello.pop")) assert.Equal(t, uint16(37890), v.GetUint16("hello.pop")) assert.Equal(t, uint32(37890), v.GetUint32("hello.pop")) assert.Equal(t, uint64(9223372036854775808), v.GetUint64("hello.num2pow63")) assert.Len(t, v.GetStringSlice("hello.world"), 4) assert.Empty(t, v.GetString("fu")) err = v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc)) require.NoError(t, err) assert.Equal(t, 45000, v.GetInt("hello.pop")) assert.Equal(t, int32(45000), v.GetInt32("hello.pop")) assert.Equal(t, int64(7654321001234567), v.GetInt64("hello.largenum")) assert.Len(t, v.GetStringSlice("hello.world"), 4) assert.Len(t, v.GetStringSlice("hello.universe"), 2) assert.Len(t, v.GetIntSlice("hello.ints"), 2) assert.Equal(t, "bar", v.GetString("fu")) } func TestMergeConfigWithSetConfigFile(t *testing.T) { v := New() v.SetConfigFile("config.yaml") // Dummy value to infer config type from file extension err := v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc)) require.NoError(t, err) assert.Equal(t, 45000, v.GetInt("hello.pop")) } func TestMergeConfigOverrideType(t *testing.T) { v := New() v.SetConfigType("json") err := v.ReadConfig(bytes.NewBuffer(jsonMergeExampleTgt)) require.NoError(t, err) err = v.MergeConfig(bytes.NewBuffer(jsonMergeExampleSrc)) require.NoError(t, err) assert.Equal(t, "pop str", v.GetString("hello.pop")) assert.Equal(t, "foo str", v.GetString("hello.foo")) } func TestMergeConfigNoMerge(t *testing.T) { v := New() v.SetConfigType("yml") err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)) require.NoError(t, err) assert.Equal(t, 37890, v.GetInt("hello.pop")) assert.Len(t, v.GetStringSlice("hello.world"), 4) assert.Empty(t, v.GetString("fu")) err = v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc)) require.NoError(t, err) assert.Equal(t, 45000, v.GetInt("hello.pop")) assert.Empty(t, v.GetStringSlice("hello.world")) assert.Len(t, v.GetStringSlice("hello.universe"), 2) assert.Len(t, v.GetIntSlice("hello.ints"), 2) assert.Equal(t, "bar", v.GetString("fu")) } func TestMergeConfigMap(t *testing.T) { v := New() v.SetConfigType("yml") err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)) require.NoError(t, err) assertFn := func(i int) { large := v.GetInt64("hello.largenum") pop := v.GetInt("hello.pop") assert.Equal(t, int64(765432101234567), large) assert.Equal(t, i, pop) } assertFn(37890) update := map[string]any{ "Hello": map[string]any{ "Pop": 1234, }, "World": map[any]any{ "Rock": 345, }, } err = v.MergeConfigMap(update) require.NoError(t, err) assert.Equal(t, 345, v.GetInt("world.rock")) assertFn(1234) } func TestUnmarshalingWithAliases(t *testing.T) { v := New() v.SetDefault("ID", 1) v.Set("name", "Steve") v.Set("lastname", "Owen") v.RegisterAlias("UserID", "ID") v.RegisterAlias("Firstname", "name") v.RegisterAlias("Surname", "lastname") type config struct { ID int FirstName string Surname string } var C config err := v.Unmarshal(&C) require.NoError(t, err, "unable to decode into struct") assert.Equal(t, &config{ID: 1, FirstName: "Steve", Surname: "Owen"}, &C) } func TestSetConfigNameClearsFileCache(t *testing.T) { v := New() v.SetConfigFile("/tmp/config.yaml") v.SetConfigName("default") f, err := v.getConfigFile() require.Error(t, err, "config file cache should have been cleared") assert.Empty(t, f) } func TestShadowedNestedValue(t *testing.T) { v := New() config := `name: steve clothing: jacket: leather trousers: denim pants: size: large ` initConfig("yaml", config, v) assert.Equal(t, "steve", v.GetString("name")) polyester := "polyester" v.SetDefault("clothing.shirt", polyester) v.SetDefault("clothing.jacket.price", 100) assert.Equal(t, "leather", v.GetString("clothing.jacket")) assert.Nil(t, v.Get("clothing.jacket.price")) assert.Equal(t, polyester, v.GetString("clothing.shirt")) clothingSettings := v.AllSettings()["clothing"].(map[string]any) assert.Equal(t, "leather", clothingSettings["jacket"]) assert.Equal(t, polyester, clothingSettings["shirt"]) } func TestDotParameter(t *testing.T) { v := New() v.SetConfigType("json") // Read the YAML data into Viper configuration require.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonExample)), "Error reading YAML data") // should take precedence over batters defined in jsonExample r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`)) v.unmarshalReader(r, v.config) actual := v.Get("batters.batter") expected := []any{map[string]any{"type": "Small"}} assert.Equal(t, expected, actual) } func TestCaseInsensitive(t *testing.T) { for _, config := range []struct { typ string content string }{ {"yaml", ` aBcD: 1 eF: gH: 2 iJk: 3 Lm: nO: 4 P: Q: 5 R: 6 `}, {"json", `{ "aBcD": 1, "eF": { "iJk": 3, "Lm": { "P": { "Q": 5, "R": 6 }, "nO": 4 }, "gH": 2 } }`}, {"toml", `aBcD = 1 [eF] gH = 2 iJk = 3 [eF.Lm] nO = 4 [eF.Lm.P] Q = 5 R = 6 `}, } { doTestCaseInsensitive(t, config.typ, config.content) } } func TestCaseInsensitiveSet(t *testing.T) { v := New() m1 := map[string]any{ "Foo": 32, "Bar": map[any]any{ "ABc": "A", "cDE": "B", }, } m2 := map[string]any{ "Foo": 52, "Bar": map[any]any{ "bCd": "A", "eFG": "B", }, } v.Set("Given1", m1) v.Set("Number1", 42) v.SetDefault("Given2", m2) v.SetDefault("Number2", 52) // Verify SetDefault assert.Equal(t, 52, v.Get("number2")) assert.Equal(t, 52, v.Get("given2.foo")) assert.Equal(t, "A", v.Get("given2.bar.bcd")) _, ok := m2["Foo"] assert.True(t, ok) // Verify Set assert.Equal(t, 42, v.Get("number1")) assert.Equal(t, 32, v.Get("given1.foo")) assert.Equal(t, "A", v.Get("given1.bar.abc")) _, ok = m1["Foo"] assert.True(t, ok) } func TestParseNested(t *testing.T) { v := New() type duration struct { Delay time.Duration } type item struct { Name string Delay time.Duration Nested duration } config := `[[parent]] delay="100ms" [parent.nested] delay="200ms" ` initConfig("toml", config, v) var items []item err := v.UnmarshalKey("parent", &items) require.NoError(t, err, "unable to decode into struct") assert.Len(t, items, 1) assert.Equal(t, 100*time.Millisecond, items[0].Delay) assert.Equal(t, 200*time.Millisecond, items[0].Nested.Delay) } func doTestCaseInsensitive(t *testing.T, typ, config string) { v := New() initConfig(typ, config, v) v.Set("RfD", true) assert.Equal(t, true, v.Get("rfd")) assert.Equal(t, true, v.Get("rFD")) assert.Equal(t, 1, cast.ToInt(v.Get("abcd"))) assert.Equal(t, 1, cast.ToInt(v.Get("Abcd"))) assert.Equal(t, 2, cast.ToInt(v.Get("ef.gh"))) assert.Equal(t, 3, cast.ToInt(v.Get("ef.ijk"))) assert.Equal(t, 4, cast.ToInt(v.Get("ef.lm.no"))) assert.Equal(t, 5, cast.ToInt(v.Get("ef.lm.p.q"))) } func newViperWithConfigFile(t *testing.T) (*Viper, string) { watchDir := t.TempDir() configFile := path.Join(watchDir, "config.yaml") err := os.WriteFile(configFile, []byte("foo: bar\n"), 0o640) require.NoError(t, err) v := New() v.SetConfigFile(configFile) err = v.ReadInConfig() require.NoError(t, err) require.Equal(t, "bar", v.Get("foo")) return v, configFile } func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string) { watchDir := t.TempDir() dataDir1 := path.Join(watchDir, "data1") err := os.Mkdir(dataDir1, 0o777) require.NoError(t, err) realConfigFile := path.Join(dataDir1, "config.yaml") t.Logf("Real config file location: %s\n", realConfigFile) err = os.WriteFile(realConfigFile, []byte("foo: bar\n"), 0o640) require.NoError(t, err) // now, symlink the tm `data1` dir to `data` in the baseDir os.Symlink(dataDir1, path.Join(watchDir, "data")) // and link the `/datadir1/config.yaml` to `/config.yaml` configFile := path.Join(watchDir, "config.yaml") os.Symlink(path.Join(watchDir, "data", "config.yaml"), configFile) t.Logf("Config file location: %s\n", path.Join(watchDir, "config.yaml")) // init Viper v := New() v.SetConfigFile(configFile) err = v.ReadInConfig() require.NoError(t, err) require.Equal(t, "bar", v.Get("foo")) return v, watchDir, configFile } func TestWatchFile(t *testing.T) { if runtime.GOOS == "linux" { // TODO(bep) FIX ME t.Skip("Skip test on Linux ...") } t.Run("file content changed", func(t *testing.T) { // given a `config.yaml` file being watched v, configFile := newViperWithConfigFile(t) _, err := os.Stat(configFile) require.NoError(t, err) t.Logf("test config file: %s\n", configFile) wg := sync.WaitGroup{} wg.Add(1) var wgDoneOnce sync.Once // OnConfigChange is called twice on Windows v.OnConfigChange(func(_ fsnotify.Event) { t.Logf("config file changed") wgDoneOnce.Do(func() { wg.Done() }) }) v.WatchConfig() // when overwriting the file and waiting for the custom change notification handler to be triggered err = os.WriteFile(configFile, []byte("foo: baz\n"), 0o640) wg.Wait() // then the config value should have changed require.NoError(t, err) assert.Equal(t, "baz", v.Get("foo")) }) t.Run("link to real file changed (à la Kubernetes)", func(t *testing.T) { // skip if not executed on Linux if runtime.GOOS != "linux" { t.Skipf("Skipping test as symlink replacements don't work on non-linux environment...") } v, watchDir, _ := newViperWithSymlinkedConfigFile(t) wg := sync.WaitGroup{} v.WatchConfig() v.OnConfigChange(func(_ fsnotify.Event) { t.Logf("config file changed") wg.Done() }) wg.Add(1) // when link to another `config.yaml` file dataDir2 := path.Join(watchDir, "data2") err := os.Mkdir(dataDir2, 0o777) require.NoError(t, err) configFile2 := path.Join(dataDir2, "config.yaml") err = os.WriteFile(configFile2, []byte("foo: baz\n"), 0o640) require.NoError(t, err) // change the symlink using the `ln -sfn` command err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run() require.NoError(t, err) wg.Wait() // then require.NoError(t, err) assert.Equal(t, "baz", v.Get("foo")) }) } func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) { flags := pflag.NewFlagSet("test", pflag.ContinueOnError) flags.String("foo.bar", "cobra_flag", "") v := New() assert.NoError(t, v.BindPFlags(flags)) config := &struct { Foo struct { Bar string } }{} assert.NoError(t, v.Unmarshal(config)) assert.Equal(t, "cobra_flag", config.Foo.Bar) } // var yamlExampleWithDot = []byte(`Hacker: true // name: steve // hobbies: // - skateboarding // - snowboarding // - go // clothing: // jacket: leather // trousers: denim // pants: // size: large // age: 35 // eyes : brown // beard: true // emails: // steve@hacker.com: // created: 01/02/03 // active: true // `) func TestKeyDelimiter(t *testing.T) { v := NewWithOptions(KeyDelimiter("::")) v.SetConfigType("yaml") r := strings.NewReader(string(yamlExampleWithDot)) err := v.unmarshalReader(r, v.config) require.NoError(t, err) values := map[string]any{ "image": map[string]any{ "repository": "someImage", "tag": "1.0.0", }, "ingress": map[string]any{ "annotations": map[string]any{ "traefik.frontend.rule.type": "PathPrefix", "traefik.ingress.kubernetes.io/ssl-redirect": "true", }, }, } v.SetDefault("charts::values", values) assert.Equal(t, "leather", v.GetString("clothing::jacket")) assert.Equal(t, "01/02/03", v.GetString("emails::steve@hacker.com::created")) type config struct { Charts struct { Values map[string]any } } expected := config{ Charts: struct { Values map[string]any }{ Values: values, }, } var actual config require.NoError(t, v.Unmarshal(&actual)) assert.Equal(t, expected, actual) } var yamlDeepNestedSlices = []byte(`TV: - title: "The Expanse" title_i18n: USA: "The Expanse" Japan: "エクスパンス -巨獣めざめる-" seasons: - first_released: "December 14, 2015" episodes: - title: "Dulcinea" air_date: "December 14, 2015" - title: "The Big Empty" air_date: "December 15, 2015" - title: "Remember the Cant" air_date: "December 22, 2015" - first_released: "February 1, 2017" episodes: - title: "Safe" air_date: "February 1, 2017" - title: "Doors & Corners" air_date: "February 1, 2017" - title: "Static" air_date: "February 8, 2017" episodes: - ["Dulcinea", "The Big Empty", "Remember the Cant"] - ["Safe", "Doors & Corners", "Static"] `) func TestSliceIndexAccess(t *testing.T) { v := New() v.SetConfigType("yaml") r := strings.NewReader(string(yamlDeepNestedSlices)) err := v.unmarshalReader(r, v.config) require.NoError(t, err) assert.Equal(t, "The Expanse", v.GetString("tv.0.title")) assert.Equal(t, "February 1, 2017", v.GetString("tv.0.seasons.1.first_released")) assert.Equal(t, "Static", v.GetString("tv.0.seasons.1.episodes.2.title")) assert.Equal(t, "December 15, 2015", v.GetString("tv.0.seasons.0.episodes.1.air_date")) // Test nested keys with capital letters assert.Equal(t, "The Expanse", v.GetString("tv.0.title_i18n.USA")) assert.Equal(t, "エクスパンス -巨獣めざめる-", v.GetString("tv.0.title_i18n.Japan")) // Test for index out of bounds assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released")) // Accessing multidimensional arrays assert.Equal(t, "Static", v.GetString("tv.0.episodes.1.2")) } func TestIsPathShadowedInFlatMap(t *testing.T) { v := New() stringMap := map[string]string{ "foo": "value", } flagMap := map[string]FlagValue{ "foo": pflagValue{}, } path1 := []string{"foo", "bar"} expected1 := "foo" // "foo.bar" should shadowed by "foo" assert.Equal(t, expected1, v.isPathShadowedInFlatMap(path1, stringMap)) assert.Equal(t, expected1, v.isPathShadowedInFlatMap(path1, flagMap)) path2 := []string{"bar", "foo"} expected2 := "" // "bar.foo" should not shadowed by "foo" assert.Equal(t, expected2, v.isPathShadowedInFlatMap(path2, stringMap)) assert.Equal(t, expected2, v.isPathShadowedInFlatMap(path2, flagMap)) } func TestFlagShadow(t *testing.T) { v := New() v.SetDefault("foo.bar1.bar2", "default") flags := pflag.NewFlagSet("test", pflag.ContinueOnError) flags.String("foo.bar1", "shadowed", "") flags.VisitAll(func(flag *pflag.Flag) { flag.Changed = true }) v.BindPFlags(flags) assert.Equal(t, "shadowed", v.GetString("foo.bar1")) // the default "foo.bar1.bar2" value should shadowed by flag "foo.bar1" value // and should return an empty string assert.Equal(t, "", v.GetString("foo.bar1.bar2")) } func BenchmarkGetBool(b *testing.B) { key := "BenchmarkGetBool" v = New() v.Set(key, true) for i := 0; i < b.N; i++ { if !v.GetBool(key) { b.Fatal("GetBool returned false") } } } func BenchmarkGet(b *testing.B) { key := "BenchmarkGet" v = New() v.Set(key, true) for i := 0; i < b.N; i++ { if !v.Get(key).(bool) { b.Fatal("Get returned false") } } } // BenchmarkGetBoolFromMap is the "perfect result" for the above. func BenchmarkGetBoolFromMap(b *testing.B) { m := make(map[string]bool) key := "BenchmarkGetBool" m[key] = true for i := 0; i < b.N; i++ { if !m[key] { b.Fatal("Map value was false") } } } // Skip some tests on Windows that kept failing when Windows was added to the CI as a target. func skipWindows(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Skip test on Windows") } } ================================================ FILE: viper_yaml_test.go ================================================ package viper var yamlExample = []byte(`Hacker: true name: steve hobbies: - skateboarding - snowboarding - go clothing: jacket: leather trousers: denim pants: size: large age: 35 eyes : brown beard: true `) var yamlWriteExpected = []byte(`age: 35 beard: true clothing: jacket: leather pants: size: large trousers: denim eyes: brown hacker: true hobbies: - skateboarding - snowboarding - go name: steve `) var yamlExampleWithDot = []byte(`Hacker: true name: steve hobbies: - skateboarding - snowboarding - go clothing: jacket: leather trousers: denim pants: size: large age: 35 eyes : brown beard: true emails: steve@hacker.com: created: 01/02/03 active: true `)