mirror of
https://github.com/spf13/viper.git
synced 2026-09-20 03:18:21 +00:00
Compare commits
+1
-1
@@ -11,5 +11,5 @@ trim_trailing_whitespace = true
|
|||||||
[*.go]
|
[*.go]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
[{Makefile, *.mk}]
|
[{Makefile,*.mk}]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|||||||
@@ -19,13 +19,22 @@ body:
|
|||||||
required: true
|
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.
|
- 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
|
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
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Viper Version
|
||||||
description: What version of Viper are you using?
|
description: What version of Viper are you using?
|
||||||
placeholder: 1.8.1
|
placeholder: 1.8.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Go Version
|
||||||
|
description: What version of Go are you using?
|
||||||
|
placeholder: "1.16"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Config Source
|
label: Config Source
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: 🚀 Feature request
|
name: 🎉 Feature request
|
||||||
description: Suggest an idea for Viper
|
description: Suggest an idea for Viper
|
||||||
labels: [kind/enhancement]
|
labels: [kind/enhancement]
|
||||||
body:
|
body:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
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:
|
||||||
|
- "*"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: PR Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, labeled, unlabeled, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-label:
|
||||||
|
name: Release note label
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check minimum labels
|
||||||
|
uses: mheap/github-action-required-labels@v4
|
||||||
|
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"
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: js
|
||||||
|
goarch: wasm
|
||||||
|
- goos: aix
|
||||||
|
goarch: ppc64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- 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.17', '1.18', '1.19', '1.20']
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=readonly
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -race -v ./...
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
|
||||||
|
- name: Test (without race detector)
|
||||||
|
run: go test -v ./...
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=readonly
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.52.2
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest]
|
|
||||||
go: ['1.14', '1.15', '1.16']
|
|
||||||
env:
|
|
||||||
VERBOSE: 1
|
|
||||||
GOFLAGS: -mod=readonly
|
|
||||||
GOPROXY: https://proxy.golang.org
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: make lint
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: make test
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '22 16 * * 3'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ jobs:
|
|||||||
comment:
|
comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v4
|
- uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
github.issues.createComment({
|
github.rest.issues.createComment({
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
+2
-2
@@ -6,11 +6,11 @@ jobs:
|
|||||||
comment:
|
comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v4
|
- uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
github.issues.createComment({
|
github.rest.issues.createComment({
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
name: WASM
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GOFLAGS: -mod=readonly
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: '1.16'
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Ensure Viper compiles for WASM
|
|
||||||
run: GOOS=js GOARCH=wasm go build .
|
|
||||||
@@ -3,7 +3,10 @@ run:
|
|||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gci:
|
gci:
|
||||||
local-prefixes: github.com/spf13/viper
|
sections:
|
||||||
|
- standard
|
||||||
|
- default
|
||||||
|
- prefix(github.com/spf13/viper)
|
||||||
golint:
|
golint:
|
||||||
min-confidence: 0
|
min-confidence: 0
|
||||||
goimports:
|
goimports:
|
||||||
@@ -13,14 +16,12 @@ linters:
|
|||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
- exportloopref
|
||||||
- gci
|
- gci
|
||||||
- goconst
|
|
||||||
- gofmt
|
- gofmt
|
||||||
- gofumpt
|
- gofumpt
|
||||||
- goimports
|
- goimports
|
||||||
@@ -41,14 +42,12 @@ linters:
|
|||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- tparallel
|
- tparallel
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
|
|
||||||
@@ -62,6 +61,7 @@ linters:
|
|||||||
# - gochecknoglobals
|
# - gochecknoglobals
|
||||||
# - gochecknoinits
|
# - gochecknoinits
|
||||||
# - gocognit
|
# - gocognit
|
||||||
|
# - goconst
|
||||||
# - gocritic
|
# - gocritic
|
||||||
# - gocyclo
|
# - gocyclo
|
||||||
# - godot
|
# - godot
|
||||||
@@ -80,6 +80,11 @@ linters:
|
|||||||
# - goheader
|
# - goheader
|
||||||
# - gomodguard
|
# - gomodguard
|
||||||
|
|
||||||
|
# deprecated
|
||||||
|
# - deadcode
|
||||||
|
# - structcheck
|
||||||
|
# - varcheck
|
||||||
|
|
||||||
# don't enable:
|
# don't enable:
|
||||||
# - asciicheck
|
# - asciicheck
|
||||||
# - funlen
|
# - funlen
|
||||||
@@ -15,8 +15,8 @@ TEST_FORMAT = short-verbose
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
# Dependency versions
|
# Dependency versions
|
||||||
GOTESTSUM_VERSION = 1.6.4
|
GOTESTSUM_VERSION = 1.9.0
|
||||||
GOLANGCI_VERSION = 1.40.1
|
GOLANGCI_VERSION = 1.52.2
|
||||||
|
|
||||||
# Add the ability to override some variables
|
# Add the ability to override some variables
|
||||||
# Use with care
|
# Use with care
|
||||||
@@ -48,7 +48,7 @@ bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION}
|
|||||||
@ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint
|
@ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint
|
||||||
bin/golangci-lint-${GOLANGCI_VERSION}:
|
bin/golangci-lint-${GOLANGCI_VERSION}:
|
||||||
@mkdir -p bin
|
@mkdir -p bin
|
||||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION}
|
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION}
|
||||||
@mv bin/golangci-lint "$@"
|
@mv bin/golangci-lint "$@"
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
[](https://github.com/avelino/awesome-go#configuration)
|
[](https://github.com/avelino/awesome-go#configuration)
|
||||||
[](https://repl.it/@sagikazarmark/Viper-example#main.go)
|
[](https://repl.it/@sagikazarmark/Viper-example#main.go)
|
||||||
|
|
||||||
[](https://github.com/spf13/viper/actions?query=workflow%3ACI)
|
[](https://github.com/spf13/viper/actions?query=workflow%3ACI)
|
||||||
[](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[](https://goreportcard.com/report/github.com/spf13/viper)
|
[](https://goreportcard.com/report/github.com/spf13/viper)
|
||||||

|

|
||||||
[](https://pkg.go.dev/mod/github.com/spf13/viper)
|
[](https://pkg.go.dev/mod/github.com/spf13/viper)
|
||||||
|
|
||||||
**Go configuration with fangs!**
|
**Go configuration with fangs!**
|
||||||
@@ -27,6 +27,9 @@ Many Go projects are built using Viper including:
|
|||||||
* [doctl](https://github.com/digitalocean/doctl)
|
* [doctl](https://github.com/digitalocean/doctl)
|
||||||
* [Clairctl](https://github.com/jgsqware/clairctl)
|
* [Clairctl](https://github.com/jgsqware/clairctl)
|
||||||
* [Mercure](https://mercure.rocks)
|
* [Mercure](https://mercure.rocks)
|
||||||
|
* [Meshery](https://github.com/meshery/meshery)
|
||||||
|
* [Bearer](https://github.com/bearer/bearer)
|
||||||
|
* [Coder](https://github.com/coder/coder)
|
||||||
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -40,8 +43,8 @@ go get github.com/spf13/viper
|
|||||||
|
|
||||||
## What is Viper?
|
## What is Viper?
|
||||||
|
|
||||||
Viper is a complete configuration solution for Go applications including 12-Factor apps. It is designed
|
Viper is a complete configuration solution for Go applications including [12-Factor apps](https://12factor.net/#the_twelve_factors).
|
||||||
to work within an application, and can handle all types of configuration needs
|
It is designed to work within an application, and can handle all types of configuration needs
|
||||||
and formats. It supports:
|
and formats. It supports:
|
||||||
|
|
||||||
* setting defaults
|
* setting defaults
|
||||||
@@ -119,7 +122,7 @@ viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search
|
|||||||
viper.AddConfigPath(".") // optionally look for config in the working directory
|
viper.AddConfigPath(".") // optionally look for config in the working directory
|
||||||
err := viper.ReadInConfig() // Find and read the config file
|
err := viper.ReadInConfig() // Find and read the config file
|
||||||
if err != nil { // Handle errors reading the config file
|
if err != nil { // Handle errors reading the config file
|
||||||
panic(fmt.Errorf("Fatal error config file: %w \n", err))
|
panic(fmt.Errorf("fatal error config file: %w", err))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -127,11 +130,11 @@ You can handle the specific case where no config file is found like this:
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
// Config file not found; ignore error if desired
|
// Config file not found; ignore error if desired
|
||||||
} else {
|
} else {
|
||||||
// Config file was found but another error was produced
|
// Config file was found but another error was produced
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config file found and successfully parsed
|
// Config file found and successfully parsed
|
||||||
@@ -175,10 +178,10 @@ Optionally you can provide a function for Viper to run each time a change occurs
|
|||||||
**Make sure you add all of the configPaths prior to calling `WatchConfig()`**
|
**Make sure you add all of the configPaths prior to calling `WatchConfig()`**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
viper.WatchConfig()
|
|
||||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
fmt.Println("Config file changed:", e.Name)
|
fmt.Println("Config file changed:", e.Name)
|
||||||
})
|
})
|
||||||
|
viper.WatchConfig()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reading Config from io.Reader
|
### Reading Config from io.Reader
|
||||||
@@ -354,7 +357,7 @@ func main() {
|
|||||||
|
|
||||||
i := viper.GetInt("flagname") // retrieve value from viper
|
i := viper.GetInt("flagname") // retrieve value from viper
|
||||||
|
|
||||||
...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -447,6 +450,13 @@ viper.SetConfigType("json") // because there is no file extension in a stream of
|
|||||||
err := viper.ReadRemoteConfig()
|
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
|
#### Consul
|
||||||
You need to set a key to Consul key/value storage with JSON value containing your desired config.
|
You need to set a key to Consul key/value storage with JSON value containing your desired config.
|
||||||
For example, create a Consul key/value store key `MY_CONSUL_KEY` with value:
|
For example, create a Consul key/value store key `MY_CONSUL_KEY` with value:
|
||||||
@@ -503,18 +513,18 @@ runtime_viper.Unmarshal(&runtime_conf)
|
|||||||
// open a goroutine to watch remote changes forever
|
// open a goroutine to watch remote changes forever
|
||||||
go func(){
|
go func(){
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Second * 5) // delay after each request
|
time.Sleep(time.Second * 5) // delay after each request
|
||||||
|
|
||||||
// currently, only tested with etcd support
|
// currently, only tested with etcd support
|
||||||
err := runtime_viper.WatchRemoteConfig()
|
err := runtime_viper.WatchRemoteConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to read remote config: %v", err)
|
log.Errorf("unable to read remote config: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal new config into our runtime config struct. you can also use channel
|
// unmarshal new config into our runtime config struct. you can also use channel
|
||||||
// to implement a signal to notify the system of the changes
|
// to implement a signal to notify the system of the changes
|
||||||
runtime_viper.Unmarshal(&runtime_conf)
|
runtime_viper.Unmarshal(&runtime_conf)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
```
|
```
|
||||||
@@ -546,7 +556,7 @@ Example:
|
|||||||
```go
|
```go
|
||||||
viper.GetString("logfile") // case-insensitive Setting & Getting
|
viper.GetString("logfile") // case-insensitive Setting & Getting
|
||||||
if viper.GetBool("verbose") {
|
if viper.GetBool("verbose") {
|
||||||
fmt.Println("verbose enabled")
|
fmt.Println("verbose enabled")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
### Accessing nested keys
|
### Accessing nested keys
|
||||||
@@ -594,7 +604,7 @@ configuration level.
|
|||||||
|
|
||||||
Viper can access array indices by using numbers in the path. For example:
|
Viper can access array indices by using numbers in the path. For example:
|
||||||
|
|
||||||
```json
|
```jsonc
|
||||||
{
|
{
|
||||||
"host": {
|
"host": {
|
||||||
"address": "localhost",
|
"address": "localhost",
|
||||||
@@ -622,7 +632,7 @@ GetInt("host.ports.1") // returns 6029
|
|||||||
Lastly, if there exists a key that matches the delimited key path, its value
|
Lastly, if there exists a key that matches the delimited key path, its value
|
||||||
will be returned instead. E.g.
|
will be returned instead. E.g.
|
||||||
|
|
||||||
```json
|
```jsonc
|
||||||
{
|
{
|
||||||
"datastore.metric.host": "0.0.0.0",
|
"datastore.metric.host": "0.0.0.0",
|
||||||
"host": {
|
"host": {
|
||||||
@@ -669,7 +679,7 @@ So instead of doing that let's pass a Viper instance to the constructor that rep
|
|||||||
```go
|
```go
|
||||||
cache1Config := viper.Sub("cache.cache1")
|
cache1Config := viper.Sub("cache.cache1")
|
||||||
if cache1Config == nil { // Sub returns nil if the key cannot be found
|
if cache1Config == nil { // Sub returns nil if the key cannot be found
|
||||||
panic("cache configuration not found")
|
panic("cache configuration not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
cache1 := NewCache(cache1Config)
|
cache1 := NewCache(cache1Config)
|
||||||
@@ -681,10 +691,10 @@ Internally, the `NewCache` function can address `max-items` and `item-size` keys
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
func NewCache(v *Viper) *Cache {
|
func NewCache(v *Viper) *Cache {
|
||||||
return &Cache{
|
return &Cache{
|
||||||
MaxItems: v.GetInt("max-items"),
|
MaxItems: v.GetInt("max-items"),
|
||||||
ItemSize: v.GetInt("item-size"),
|
ItemSize: v.GetInt("item-size"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -726,18 +736,18 @@ you have to change the delimiter:
|
|||||||
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||||
|
|
||||||
v.SetDefault("chart::values", map[string]interface{}{
|
v.SetDefault("chart::values", map[string]interface{}{
|
||||||
"ingress": map[string]interface{}{
|
"ingress": map[string]interface{}{
|
||||||
"annotations": map[string]interface{}{
|
"annotations": map[string]interface{}{
|
||||||
"traefik.frontend.rule.type": "PathPrefix",
|
"traefik.frontend.rule.type": "PathPrefix",
|
||||||
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
|
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
Chart struct{
|
Chart struct{
|
||||||
Values map[string]interface{}
|
Values map[string]interface{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var C config
|
var C config
|
||||||
@@ -794,17 +804,17 @@ You can use your favorite format's marshaller with the config returned by `AllSe
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
// ...
|
// ...
|
||||||
)
|
)
|
||||||
|
|
||||||
func yamlStringSettings() string {
|
func yamlStringSettings() string {
|
||||||
c := viper.AllSettings()
|
c := viper.AllSettings()
|
||||||
bs, err := yaml.Marshal(c)
|
bs, err := yaml.Marshal(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to marshal config to YAML: %v", err)
|
log.Fatalf("unable to marshal config to YAML: %v", err)
|
||||||
}
|
}
|
||||||
return string(bs)
|
return string(bs)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,12 @@ The solution is easy: switch to using Go Modules.
|
|||||||
Please refer to the [wiki](https://github.com/golang/go/wiki/Modules) on how to do that.
|
Please refer to the [wiki](https://github.com/golang/go/wiki/Modules) on how to do that.
|
||||||
|
|
||||||
**tl;dr* `export GO111MODULE=on`
|
**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)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build viper_logger
|
||||||
|
// +build viper_logger
|
||||||
|
|
||||||
|
package viper
|
||||||
|
|
||||||
|
// WithLogger sets a custom logger.
|
||||||
|
func WithLogger(l Logger) Option {
|
||||||
|
return optionFunc(func(v *Viper) {
|
||||||
|
v.logger = l
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
//go:build go1.16 && finder
|
||||||
|
// +build go1.16,finder
|
||||||
|
|
||||||
|
package viper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
type finder struct {
|
||||||
|
paths []string
|
||||||
|
fileNames []string
|
||||||
|
extensions []string
|
||||||
|
|
||||||
|
withoutExtension bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f finder) Find(fsys fs.FS) (string, error) {
|
||||||
|
for _, searchPath := range f.paths {
|
||||||
|
for _, fileName := range f.fileNames {
|
||||||
|
for _, extension := range f.extensions {
|
||||||
|
filePath := path.Join(searchPath, fileName+"."+extension)
|
||||||
|
|
||||||
|
ok, err := fileExists(fsys, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.withoutExtension {
|
||||||
|
filePath := path.Join(searchPath, fileName)
|
||||||
|
|
||||||
|
ok, err := fileExists(fsys, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(fsys fs.FS, filePath string) (bool, error) {
|
||||||
|
fileInfo, err := fs.Stat(fsys, filePath)
|
||||||
|
if err == nil {
|
||||||
|
return !fileInfo.IsDir(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
//go:build go1.16 && finder
|
||||||
|
// +build go1.16,finder
|
||||||
|
|
||||||
|
package viper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFinder(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fsys := fstest.MapFS{
|
||||||
|
"home/user/.config": &fstest.MapFile{},
|
||||||
|
"home/user/config.json": &fstest.MapFile{},
|
||||||
|
"home/user/config.yaml": &fstest.MapFile{},
|
||||||
|
"home/user/data.json": &fstest.MapFile{},
|
||||||
|
"etc/config/.config": &fstest.MapFile{},
|
||||||
|
"etc/config/a_random_file.txt": &fstest.MapFile{},
|
||||||
|
"etc/config/config.json": &fstest.MapFile{},
|
||||||
|
"etc/config/config.yaml": &fstest.MapFile{},
|
||||||
|
"etc/config/config.xml": &fstest.MapFile{},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
fsys func() fs.FS
|
||||||
|
finder finder
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "find file",
|
||||||
|
fsys: func() fs.FS { return fsys },
|
||||||
|
finder: finder{
|
||||||
|
paths: []string{"etc/config"},
|
||||||
|
fileNames: []string{"config"},
|
||||||
|
extensions: []string{"json"},
|
||||||
|
},
|
||||||
|
result: "etc/config/config.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not found",
|
||||||
|
fsys: func() fs.FS { return fsys },
|
||||||
|
finder: finder{
|
||||||
|
paths: []string{"var/config"},
|
||||||
|
fileNames: []string{"config"},
|
||||||
|
extensions: []string{"json"},
|
||||||
|
},
|
||||||
|
result: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty search params",
|
||||||
|
fsys: func() fs.FS { return fsys },
|
||||||
|
finder: finder{},
|
||||||
|
result: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "precedence",
|
||||||
|
fsys: func() fs.FS { return fsys },
|
||||||
|
finder: finder{
|
||||||
|
paths: []string{"var/config", "home/user", "etc/config"},
|
||||||
|
fileNames: []string{"aconfig", "config"},
|
||||||
|
extensions: []string{"zml", "xml", "json"},
|
||||||
|
},
|
||||||
|
result: "home/user/config.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without extension",
|
||||||
|
fsys: func() fs.FS { return fsys },
|
||||||
|
finder: finder{
|
||||||
|
paths: []string{"var/config", "home/user", "etc/config"},
|
||||||
|
fileNames: []string{".config"},
|
||||||
|
extensions: []string{"zml", "xml", "json"},
|
||||||
|
|
||||||
|
withoutExtension: true,
|
||||||
|
},
|
||||||
|
result: "home/user/.config",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fsys := testCase.fsys()
|
||||||
|
|
||||||
|
result, err := testCase.finder.Find(fsys)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.result, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,76 @@
|
|||||||
module github.com/spf13/viper
|
module github.com/spf13/viper
|
||||||
|
|
||||||
go 1.12
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bketelsen/crypt v0.0.4
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/fsnotify/fsnotify v1.4.9
|
|
||||||
github.com/hashicorp/hcl v1.0.0
|
github.com/hashicorp/hcl v1.0.0
|
||||||
github.com/magiconair/properties v1.8.5
|
github.com/magiconair/properties v1.8.7
|
||||||
github.com/mitchellh/mapstructure v1.4.1
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/pelletier/go-toml v1.9.3
|
github.com/pelletier/go-toml/v2 v2.0.8
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
github.com/sagikazarmark/crypt v0.10.0
|
||||||
github.com/spf13/afero v1.6.0
|
github.com/spf13/afero v1.9.5
|
||||||
github.com/spf13/cast v1.3.1
|
github.com/spf13/cast v1.5.1
|
||||||
github.com/spf13/jwalterweatherman v1.1.0
|
github.com/spf13/jwalterweatherman v1.1.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.8.3
|
||||||
github.com/subosito/gotenv v1.2.0
|
github.com/subosito/gotenv v1.4.2
|
||||||
gopkg.in/ini.v1 v1.62.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go v0.110.0 // indirect
|
||||||
|
cloud.google.com/go/compute v1.19.0 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
cloud.google.com/go/firestore v1.9.0 // indirect
|
||||||
|
cloud.google.com/go/longrunning v0.4.1 // indirect
|
||||||
|
github.com/armon/go-metrics v0.4.0 // indirect
|
||||||
|
github.com/coreos/go-semver v0.3.0 // indirect
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.3 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
|
||||||
|
github.com/hashicorp/consul/api v1.20.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-hclog v1.2.0 // indirect
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.3.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/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.9 // indirect
|
||||||
|
go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect
|
||||||
|
go.etcd.io/etcd/client/v2 v2.305.7 // indirect
|
||||||
|
go.etcd.io/etcd/client/v3 v3.5.9 // indirect
|
||||||
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
|
go.uber.org/zap v1.21.0 // indirect
|
||||||
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
|
golang.org/x/net v0.10.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.7.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.8.0 // indirect
|
||||||
|
golang.org/x/text v0.9.0 // indirect
|
||||||
|
golang.org/x/time v0.1.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
google.golang.org/api v0.122.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
|
google.golang.org/grpc v1.55.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decoder decodes the contents of b into a v representation.
|
// Decoder decodes the contents of b into v.
|
||||||
// It's primarily used for decoding contents of a file into a map[string]interface{}.
|
// It's primarily used for decoding contents of a file into a map[string]interface{}.
|
||||||
type Decoder interface {
|
type Decoder interface {
|
||||||
Decode(b []byte, v interface{}) error
|
Decode(b []byte, v map[string]interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -48,7 +48,7 @@ func (e *DecoderRegistry) RegisterDecoder(format string, enc Decoder) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decode calls the underlying Decoder based on the format.
|
// Decode calls the underlying Decoder based on the format.
|
||||||
func (e *DecoderRegistry) Decode(format string, b []byte, v interface{}) error {
|
func (e *DecoderRegistry) Decode(format string, b []byte, v map[string]interface{}) error {
|
||||||
e.mu.RLock()
|
e.mu.RLock()
|
||||||
decoder, ok := e.decoders[format]
|
decoder, ok := e.decoders[format]
|
||||||
e.mu.RUnlock()
|
e.mu.RUnlock()
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package encoding
|
package encoding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type decoder struct {
|
type decoder struct {
|
||||||
v interface{}
|
v map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d decoder) Decode(_ []byte, v interface{}) error {
|
func (d decoder) Decode(_ []byte, v map[string]interface{}) error {
|
||||||
rv := v.(*string)
|
for key, value := range d.v {
|
||||||
*rv = d.v.(string)
|
v[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -44,7 +46,9 @@ func TestDecoderRegistry_Decode(t *testing.T) {
|
|||||||
t.Run("OK", func(t *testing.T) {
|
t.Run("OK", func(t *testing.T) {
|
||||||
registry := NewDecoderRegistry()
|
registry := NewDecoderRegistry()
|
||||||
decoder := decoder{
|
decoder := decoder{
|
||||||
v: "decoded value",
|
v: map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := registry.RegisterDecoder("myformat", decoder)
|
err := registry.RegisterDecoder("myformat", decoder)
|
||||||
@@ -52,24 +56,24 @@ func TestDecoderRegistry_Decode(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var v string
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
err = registry.Decode("myformat", []byte("some value"), &v)
|
err = registry.Decode("myformat", []byte("key: value"), v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v != "decoded value" {
|
if !reflect.DeepEqual(decoder.v, v) {
|
||||||
t.Fatalf("expected 'decoded value', got: %#v", v)
|
t.Fatalf("decoded value does not match the expected one\nactual: %+v\nexpected: %+v", v, decoder.v)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("DecoderNotFound", func(t *testing.T) {
|
t.Run("DecoderNotFound", func(t *testing.T) {
|
||||||
registry := NewDecoderRegistry()
|
registry := NewDecoderRegistry()
|
||||||
|
|
||||||
var v string
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
err := registry.Decode("myformat", []byte("some value"), &v)
|
err := registry.Decode("myformat", nil, v)
|
||||||
if err != ErrDecoderNotFound {
|
if err != ErrDecoderNotFound {
|
||||||
t.Fatalf("expected ErrDecoderNotFound, got: %v", err)
|
t.Fatalf("expected ErrDecoderNotFound, got: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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{}
|
||||||
|
|
||||||
|
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
|
flattened := map[string]interface{}{}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Codec) Decode(b []byte, v map[string]interface{}) 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package dotenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// original form of the data
|
||||||
|
const original = `# key-value pair
|
||||||
|
KEY=value
|
||||||
|
`
|
||||||
|
|
||||||
|
// encoded form of the data
|
||||||
|
const encoded = `KEY=value
|
||||||
|
`
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"KEY": "value",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(data, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 tha main package.
|
||||||
|
// TODO: move it to a common place
|
||||||
|
func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} {
|
||||||
|
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
||||||
|
// prefix is shadowed => nothing more to flatten
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
if shadow == nil {
|
||||||
|
shadow = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var m2 map[string]interface{}
|
||||||
|
if prefix != "" {
|
||||||
|
prefix += delimiter
|
||||||
|
}
|
||||||
|
for k, val := range m {
|
||||||
|
fullKey := prefix + k
|
||||||
|
switch val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
m2 = val.(map[string]interface{})
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
// Encoder encodes the contents of v into a byte representation.
|
// Encoder encodes the contents of v into a byte representation.
|
||||||
// It's primarily used for encoding a map[string]interface{} into a file format.
|
// It's primarily used for encoding a map[string]interface{} into a file format.
|
||||||
type Encoder interface {
|
type Encoder interface {
|
||||||
Encode(v interface{}) ([]byte, error)
|
Encode(v map[string]interface{}) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -47,7 +47,7 @@ func (e *EncoderRegistry) RegisterEncoder(format string, enc Encoder) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EncoderRegistry) Encode(format string, v interface{}) ([]byte, error) {
|
func (e *EncoderRegistry) Encode(format string, v map[string]interface{}) ([]byte, error) {
|
||||||
e.mu.RLock()
|
e.mu.RLock()
|
||||||
encoder, ok := e.encoders[format]
|
encoder, ok := e.encoders[format]
|
||||||
e.mu.RUnlock()
|
e.mu.RUnlock()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type encoder struct {
|
|||||||
b []byte
|
b []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e encoder) Encode(_ interface{}) ([]byte, error) {
|
func (e encoder) Encode(_ map[string]interface{}) ([]byte, error) {
|
||||||
return e.b, nil
|
return e.b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func TestEncoderRegistry_Decode(t *testing.T) {
|
|||||||
t.Run("OK", func(t *testing.T) {
|
t.Run("OK", func(t *testing.T) {
|
||||||
registry := NewEncoderRegistry()
|
registry := NewEncoderRegistry()
|
||||||
encoder := encoder{
|
encoder := encoder{
|
||||||
b: []byte("encoded value"),
|
b: []byte("key: value"),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := registry.RegisterEncoder("myformat", encoder)
|
err := registry.RegisterEncoder("myformat", encoder)
|
||||||
@@ -49,20 +49,20 @@ func TestEncoderRegistry_Decode(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := registry.Encode("myformat", "some value")
|
b, err := registry.Encode("myformat", map[string]interface{}{"key": "value"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(b) != "encoded value" {
|
if string(b) != "key: value" {
|
||||||
t.Fatalf("expected 'encoded value', got: %#v", string(b))
|
t.Fatalf("expected 'key: value', got: %#v", string(b))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("EncoderNotFound", func(t *testing.T) {
|
t.Run("EncoderNotFound", func(t *testing.T) {
|
||||||
registry := NewEncoderRegistry()
|
registry := NewEncoderRegistry()
|
||||||
|
|
||||||
_, err := registry.Encode("myformat", "some value")
|
_, err := registry.Encode("myformat", map[string]interface{}{"key": "value"})
|
||||||
if err != ErrEncoderNotFound {
|
if err != ErrEncoderNotFound {
|
||||||
t.Fatalf("expected ErrEncoderNotFound, got: %v", err)
|
t.Fatalf("expected ErrEncoderNotFound, got: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
// TODO: add printer config to the codec?
|
// TODO: add printer config to the codec?
|
||||||
type Codec struct{}
|
type Codec struct{}
|
||||||
|
|
||||||
func (Codec) Encode(v interface{}) ([]byte, error) {
|
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -35,6 +35,6 @@ func (Codec) Encode(v interface{}) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v interface{}) error {
|
func (Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
return hcl.Unmarshal(b, v)
|
return hcl.Unmarshal(b, &v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package hcl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// original form of the data
|
||||||
|
const original = `# key-value pair
|
||||||
|
"key" = "value"
|
||||||
|
|
||||||
|
// list
|
||||||
|
"list" = ["item1", "item2", "item3"]
|
||||||
|
|
||||||
|
/* map */
|
||||||
|
"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 HCL it's slightly different from Viper's internal representation
|
||||||
|
// (eg. map is decoded into a list of maps)
|
||||||
|
var decoded = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
"map": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested_map": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"map": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"nested_map": map[string]interface{}{
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(decoded, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, decoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package ini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadOptions contains all customized options used for load data source(s).
|
||||||
|
// This type is added here for convenience: this way consumers can import a single package called "ini".
|
||||||
|
type LoadOptions = ini.LoadOptions
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for INI encoding.
|
||||||
|
type Codec struct {
|
||||||
|
KeyDelimiter string
|
||||||
|
LoadOptions LoadOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
|
cfg := ini.Empty()
|
||||||
|
ini.PrettyFormat = false
|
||||||
|
|
||||||
|
flattened := map[string]interface{}{}
|
||||||
|
|
||||||
|
flattened = flattenAndMergeMap(flattened, v, "", c.keyDelimiter())
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(flattened))
|
||||||
|
|
||||||
|
for key := range flattened {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
sectionName, keyName := "", key
|
||||||
|
|
||||||
|
lastSep := strings.LastIndex(key, ".")
|
||||||
|
if lastSep != -1 {
|
||||||
|
sectionName = key[:(lastSep)]
|
||||||
|
keyName = key[(lastSep + 1):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is this a good idea?
|
||||||
|
if sectionName == "default" {
|
||||||
|
sectionName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Section(sectionName).Key(keyName).SetValue(cast.ToString(flattened[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
_, err := cfg.WriteTo(&buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
|
cfg := ini.Empty(c.LoadOptions)
|
||||||
|
|
||||||
|
err := cfg.Append(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sections := cfg.Sections()
|
||||||
|
|
||||||
|
for i := 0; i < len(sections); i++ {
|
||||||
|
section := sections[i]
|
||||||
|
keys := section.Keys()
|
||||||
|
|
||||||
|
for j := 0; j < len(keys); j++ {
|
||||||
|
key := keys[j]
|
||||||
|
value := cfg.Section(section.Name()).Key(key.Name()).String()
|
||||||
|
|
||||||
|
deepestMap := deepSearch(v, strings.Split(section.Name(), c.keyDelimiter()))
|
||||||
|
|
||||||
|
// set innermost value
|
||||||
|
deepestMap[key.Name()] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) keyDelimiter() string {
|
||||||
|
if c.KeyDelimiter == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.KeyDelimiter
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package ini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// original form of the data
|
||||||
|
const original = `; key-value pair
|
||||||
|
key=value ; key-value pair
|
||||||
|
|
||||||
|
# map
|
||||||
|
[map] # map
|
||||||
|
key=%(key)s
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// encoded form of the data
|
||||||
|
const encoded = `key=value
|
||||||
|
|
||||||
|
[map]
|
||||||
|
key=value
|
||||||
|
`
|
||||||
|
|
||||||
|
// decoded form of the data
|
||||||
|
//
|
||||||
|
// in case of INI it's slightly different from Viper's internal representation
|
||||||
|
// (eg. top level keys land in a section called default)
|
||||||
|
var decoded = map[string]interface{}{
|
||||||
|
"DEFAULT": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"default": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(decoded, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, decoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package ini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// THIS CODE IS COPIED HERE: IT SHOULD NOT BE MODIFIED
|
||||||
|
// AT SOME POINT IT WILL BE MOVED TO A COMMON PLACE
|
||||||
|
// 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]interface{}, path []string) map[string]interface{} {
|
||||||
|
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]interface{})
|
||||||
|
m[k] = m3
|
||||||
|
m = m3
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m3, ok := m2.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// intermediate key is a value
|
||||||
|
// => replace with a new map
|
||||||
|
m3 = make(map[string]interface{})
|
||||||
|
m[k] = m3
|
||||||
|
}
|
||||||
|
// continue search from here
|
||||||
|
m = m3
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// flattenAndMergeMap recursively flattens the given map into a new map
|
||||||
|
// Code is based on the function with the same name in tha main package.
|
||||||
|
// TODO: move it to a common place
|
||||||
|
func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} {
|
||||||
|
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
||||||
|
// prefix is shadowed => nothing more to flatten
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
if shadow == nil {
|
||||||
|
shadow = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var m2 map[string]interface{}
|
||||||
|
if prefix != "" {
|
||||||
|
prefix += delimiter
|
||||||
|
}
|
||||||
|
for k, val := range m {
|
||||||
|
fullKey := prefix + k
|
||||||
|
switch val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
m2 = val.(map[string]interface{})
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package javaproperties
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/magiconair/properties"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for Java properties encoding.
|
||||||
|
type Codec struct {
|
||||||
|
KeyDelimiter string
|
||||||
|
|
||||||
|
// Store read properties on the object so that we can write back in order with comments.
|
||||||
|
// This will only be used if the configuration read is a properties file.
|
||||||
|
// TODO: drop this feature in v2
|
||||||
|
// TODO: make use of the global properties object optional
|
||||||
|
Properties *properties.Properties
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
|
if c.Properties == nil {
|
||||||
|
c.Properties = properties.NewProperties()
|
||||||
|
}
|
||||||
|
|
||||||
|
flattened := map[string]interface{}{}
|
||||||
|
|
||||||
|
flattened = flattenAndMergeMap(flattened, v, "", c.keyDelimiter())
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(flattened))
|
||||||
|
|
||||||
|
for key := range flattened {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
_, _, err := c.Properties.Set(key, cast.ToString(flattened[key]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
_, err := c.Properties.WriteComment(&buf, "#", properties.UTF8)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
|
var err error
|
||||||
|
c.Properties, err = properties.Load(b, properties.UTF8)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range c.Properties.Keys() {
|
||||||
|
// ignore existence check: we know it's there
|
||||||
|
value, _ := c.Properties.Get(key)
|
||||||
|
|
||||||
|
// recursively build nested maps
|
||||||
|
path := strings.Split(key, c.keyDelimiter())
|
||||||
|
lastKey := strings.ToLower(path[len(path)-1])
|
||||||
|
deepestMap := deepSearch(v, path[0:len(path)-1])
|
||||||
|
|
||||||
|
// set innermost value
|
||||||
|
deepestMap[lastKey] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Codec) keyDelimiter() string {
|
||||||
|
if c.KeyDelimiter == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.KeyDelimiter
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package javaproperties
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// original form of the data
|
||||||
|
const original = `#key-value pair
|
||||||
|
key = value
|
||||||
|
map.key = value
|
||||||
|
`
|
||||||
|
|
||||||
|
// encoded form of the data
|
||||||
|
const encoded = `key = value
|
||||||
|
map.key = value
|
||||||
|
`
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(data, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
t.Skip("TODO: needs invalid data example")
|
||||||
|
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
codec.Decode([]byte(``), v)
|
||||||
|
|
||||||
|
if len(v) > 0 {
|
||||||
|
t.Fatalf("expected map to be empty when data is invalid\nactual: %#v", v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_DecodeEncode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if original != string(b) {
|
||||||
|
t.Fatalf("encoded value does not match the original\nactual: %#v\nexpected: %#v", string(b), original)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package javaproperties
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// THIS CODE IS COPIED HERE: IT SHOULD NOT BE MODIFIED
|
||||||
|
// AT SOME POINT IT WILL BE MOVED TO A COMMON PLACE
|
||||||
|
// 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]interface{}, path []string) map[string]interface{} {
|
||||||
|
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]interface{})
|
||||||
|
m[k] = m3
|
||||||
|
m = m3
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m3, ok := m2.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
// intermediate key is a value
|
||||||
|
// => replace with a new map
|
||||||
|
m3 = make(map[string]interface{})
|
||||||
|
m[k] = m3
|
||||||
|
}
|
||||||
|
// continue search from here
|
||||||
|
m = m3
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// flattenAndMergeMap recursively flattens the given map into a new map
|
||||||
|
// Code is based on the function with the same name in tha main package.
|
||||||
|
// TODO: move it to a common place
|
||||||
|
func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} {
|
||||||
|
if shadow != nil && prefix != "" && shadow[prefix] != nil {
|
||||||
|
// prefix is shadowed => nothing more to flatten
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
if shadow == nil {
|
||||||
|
shadow = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var m2 map[string]interface{}
|
||||||
|
if prefix != "" {
|
||||||
|
prefix += delimiter
|
||||||
|
}
|
||||||
|
for k, val := range m {
|
||||||
|
fullKey := prefix + k
|
||||||
|
switch val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
m2 = val.(map[string]interface{})
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for JSON encoding.
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for JSON encoding.
|
||||||
type Codec struct{}
|
type Codec struct{}
|
||||||
|
|
||||||
func (Codec) Encode(v interface{}) ([]byte, error) {
|
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
// TODO: expose prefix and indent in the Codec as setting?
|
// TODO: expose prefix and indent in the Codec as setting?
|
||||||
return json.MarshalIndent(v, "", " ")
|
return json.MarshalIndent(v, "", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v interface{}) error {
|
func (Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
return json.Unmarshal(b, v)
|
return json.Unmarshal(b, &v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"nested_map": map[string]interface{}{
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(encoded), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(data, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,45 +1,16 @@
|
|||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pelletier/go-toml"
|
"github.com/pelletier/go-toml/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for TOML encoding.
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for TOML encoding.
|
||||||
type Codec struct{}
|
type Codec struct{}
|
||||||
|
|
||||||
func (Codec) Encode(v interface{}) ([]byte, error) {
|
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
if m, ok := v.(map[string]interface{}); ok {
|
|
||||||
t, err := toml.TreeFromMap(m)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := t.ToTomlString()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return toml.Marshal(v)
|
return toml.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v interface{}) error {
|
func (Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
tree, err := toml.LoadBytes(b)
|
return toml.Unmarshal(b, &v)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m, ok := v.(*map[string]interface{}); ok {
|
|
||||||
vmap := *m
|
|
||||||
tmap := tree.ToMap()
|
|
||||||
for k, v := range tmap {
|
|
||||||
vmap[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return tree.Unmarshal(v)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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']
|
||||||
|
`
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"nested_map": map[string]interface{}{
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(data, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package yaml
|
package yaml
|
||||||
|
|
||||||
import "gopkg.in/yaml.v2"
|
import "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for YAML encoding.
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for YAML encoding.
|
||||||
type Codec struct{}
|
type Codec struct{}
|
||||||
|
|
||||||
func (Codec) Encode(v interface{}) ([]byte, error) {
|
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
|
||||||
return yaml.Marshal(v)
|
return yaml.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Codec) Decode(b []byte, v interface{}) error {
|
func (Codec) Decode(b []byte, v map[string]interface{}) error {
|
||||||
return yaml.Unmarshal(b, v)
|
return yaml.Unmarshal(b, &v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// (eg. map is decoded into a map with interface key)
|
||||||
|
var decoded = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"nested_map": map[string]interface{}{
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viper's internal representation
|
||||||
|
var data = map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"nested_map": map[string]interface{}{
|
||||||
|
"map": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
"list": []interface{}{
|
||||||
|
"item1",
|
||||||
|
"item2",
|
||||||
|
"item3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Encode(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
b, err := codec.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoded != string(b) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", string(b), encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(original), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(decoded, v) {
|
||||||
|
t.Fatalf("decoded value does not match the expected one\nactual: %#v\nexpected: %#v", v, decoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidData", func(t *testing.T) {
|
||||||
|
codec := Codec{}
|
||||||
|
|
||||||
|
v := map[string]interface{}{}
|
||||||
|
|
||||||
|
err := codec.Decode([]byte(`invalid data`), v)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decoding to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("decoding failed as expected: %s", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !go1.17
|
||||||
|
// +build !go1.17
|
||||||
|
|
||||||
package testutil
|
package testutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -18,11 +21,6 @@ func Setenv(t *testing.T, name, val string) {
|
|||||||
setenv(t, name, val, true)
|
setenv(t, name, val, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsetenv unsets an environment variable for the duration of a test.
|
|
||||||
func Unsetenv(t *testing.T, name string) {
|
|
||||||
setenv(t, name, "", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setenv sets or unsets an environment variable to a temporary value for the
|
// setenv sets or unsets an environment variable to a temporary value for the
|
||||||
// duration of the test
|
// duration of the test
|
||||||
func setenv(t *testing.T, name, val string, valOK bool) {
|
func setenv(t *testing.T, name, val string, valOK bool) {
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build go1.17
|
||||||
|
// +build go1.17
|
||||||
|
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setenv sets an environment variable to a temporary value for the
|
||||||
|
// duration of the test.
|
||||||
|
//
|
||||||
|
// This shim can be removed once support for Go <1.17 is dropped.
|
||||||
|
func Setenv(t *testing.T, name, val string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
t.Setenv(name, val)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package viper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is a unified interface for various logging use cases and practices, including:
|
||||||
|
// - leveled logging
|
||||||
|
// - structured logging
|
||||||
|
type Logger interface {
|
||||||
|
// Trace logs a Trace event.
|
||||||
|
//
|
||||||
|
// Even more fine-grained information than Debug events.
|
||||||
|
// Loggers not supporting this level should fall back to Debug.
|
||||||
|
Trace(msg string, keyvals ...interface{})
|
||||||
|
|
||||||
|
// Debug logs a Debug event.
|
||||||
|
//
|
||||||
|
// A verbose series of information events.
|
||||||
|
// They are useful when debugging the system.
|
||||||
|
Debug(msg string, keyvals ...interface{})
|
||||||
|
|
||||||
|
// Info logs an Info event.
|
||||||
|
//
|
||||||
|
// General information about what's happening inside the system.
|
||||||
|
Info(msg string, keyvals ...interface{})
|
||||||
|
|
||||||
|
// Warn logs a Warn(ing) event.
|
||||||
|
//
|
||||||
|
// Non-critical events that should be looked at.
|
||||||
|
Warn(msg string, keyvals ...interface{})
|
||||||
|
|
||||||
|
// Error logs an Error event.
|
||||||
|
//
|
||||||
|
// Critical events that require immediate attention.
|
||||||
|
// Loggers commonly provide Fatal and Panic levels above Error level,
|
||||||
|
// but exiting and panicing is out of scope for a logging library.
|
||||||
|
Error(msg string, keyvals ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type jwwLogger struct{}
|
||||||
|
|
||||||
|
func (jwwLogger) Trace(msg string, keyvals ...interface{}) {
|
||||||
|
jww.TRACE.Printf(jwwLogMessage(msg, keyvals...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwwLogger) Debug(msg string, keyvals ...interface{}) {
|
||||||
|
jww.DEBUG.Printf(jwwLogMessage(msg, keyvals...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwwLogger) Info(msg string, keyvals ...interface{}) {
|
||||||
|
jww.INFO.Printf(jwwLogMessage(msg, keyvals...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwwLogger) Warn(msg string, keyvals ...interface{}) {
|
||||||
|
jww.WARN.Printf(jwwLogMessage(msg, keyvals...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jwwLogger) Error(msg string, keyvals ...interface{}) {
|
||||||
|
jww.ERROR.Printf(jwwLogMessage(msg, keyvals...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func jwwLogMessage(msg string, keyvals ...interface{}) string {
|
||||||
|
out := msg
|
||||||
|
|
||||||
|
if len(keyvals) > 0 && len(keyvals)%2 == 1 {
|
||||||
|
keyvals = append(keyvals, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i <= len(keyvals)-2; i += 2 {
|
||||||
|
out = fmt.Sprintf("%s %v=%v", out, keyvals[i], keyvals[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
+13
-7
@@ -10,8 +10,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
crypt "github.com/bketelsen/crypt/config"
|
crypt "github.com/sagikazarmark/crypt/config"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -75,6 +76,7 @@ func getConfigManager(rp viper.RemoteProvider) (crypt.ConfigManager, error) {
|
|||||||
var cm crypt.ConfigManager
|
var cm crypt.ConfigManager
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
endpoints := strings.Split(rp.Endpoint(), ";")
|
||||||
if rp.SecretKeyring() != "" {
|
if rp.SecretKeyring() != "" {
|
||||||
var kr *os.File
|
var kr *os.File
|
||||||
kr, err = os.Open(rp.SecretKeyring())
|
kr, err = os.Open(rp.SecretKeyring())
|
||||||
@@ -84,20 +86,24 @@ func getConfigManager(rp viper.RemoteProvider) (crypt.ConfigManager, error) {
|
|||||||
defer kr.Close()
|
defer kr.Close()
|
||||||
switch rp.Provider() {
|
switch rp.Provider() {
|
||||||
case "etcd":
|
case "etcd":
|
||||||
cm, err = crypt.NewEtcdConfigManager([]string{rp.Endpoint()}, kr)
|
cm, err = crypt.NewEtcdConfigManager(endpoints, kr)
|
||||||
|
case "etcd3":
|
||||||
|
cm, err = crypt.NewEtcdV3ConfigManager(endpoints, kr)
|
||||||
case "firestore":
|
case "firestore":
|
||||||
cm, err = crypt.NewFirestoreConfigManager([]string{rp.Endpoint()}, kr)
|
cm, err = crypt.NewFirestoreConfigManager(endpoints, kr)
|
||||||
default:
|
default:
|
||||||
cm, err = crypt.NewConsulConfigManager([]string{rp.Endpoint()}, kr)
|
cm, err = crypt.NewConsulConfigManager(endpoints, kr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch rp.Provider() {
|
switch rp.Provider() {
|
||||||
case "etcd":
|
case "etcd":
|
||||||
cm, err = crypt.NewStandardEtcdConfigManager([]string{rp.Endpoint()})
|
cm, err = crypt.NewStandardEtcdConfigManager(endpoints)
|
||||||
|
case "etcd3":
|
||||||
|
cm, err = crypt.NewStandardEtcdV3ConfigManager(endpoints)
|
||||||
case "firestore":
|
case "firestore":
|
||||||
cm, err = crypt.NewStandardFirestoreConfigManager([]string{rp.Endpoint()})
|
cm, err = crypt.NewStandardFirestoreConfigManager(endpoints)
|
||||||
default:
|
default:
|
||||||
cm, err = crypt.NewStandardConsulConfigManager([]string{rp.Endpoint()})
|
cm, err = crypt.NewStandardConsulConfigManager(endpoints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigParseError denotes failing to parse configuration file.
|
// ConfigParseError denotes failing to parse configuration file.
|
||||||
@@ -33,6 +31,11 @@ func (pe ConfigParseError) Error() string {
|
|||||||
return fmt.Sprintf("While parsing config: %s", pe.err.Error())
|
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;
|
// toCaseInsensitiveValue checks if the value is a map;
|
||||||
// if so, create a copy and lower-case the keys recursively.
|
// if so, create a copy and lower-case the keys recursively.
|
||||||
func toCaseInsensitiveValue(value interface{}) interface{} {
|
func toCaseInsensitiveValue(value interface{}) interface{} {
|
||||||
@@ -66,18 +69,25 @@ func copyAndInsensitiviseMap(m map[string]interface{}) map[string]interface{} {
|
|||||||
return nm
|
return nm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insensitiviseVal(val interface{}) interface{} {
|
||||||
|
switch val.(type) {
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
// nested map: cast and recursively insensitivise
|
||||||
|
val = cast.ToStringMap(val)
|
||||||
|
insensitiviseMap(val.(map[string]interface{}))
|
||||||
|
case map[string]interface{}:
|
||||||
|
// nested map: recursively insensitivise
|
||||||
|
insensitiviseMap(val.(map[string]interface{}))
|
||||||
|
case []interface{}:
|
||||||
|
// nested array: recursively insensitivise
|
||||||
|
insensitiveArray(val.([]interface{}))
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
func insensitiviseMap(m map[string]interface{}) {
|
func insensitiviseMap(m map[string]interface{}) {
|
||||||
for key, val := range m {
|
for key, val := range m {
|
||||||
switch val.(type) {
|
val = insensitiviseVal(val)
|
||||||
case map[interface{}]interface{}:
|
|
||||||
// nested map: cast and recursively insensitivise
|
|
||||||
val = cast.ToStringMap(val)
|
|
||||||
insensitiviseMap(val.(map[string]interface{}))
|
|
||||||
case map[string]interface{}:
|
|
||||||
// nested map: recursively insensitivise
|
|
||||||
insensitiviseMap(val.(map[string]interface{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
lower := strings.ToLower(key)
|
lower := strings.ToLower(key)
|
||||||
if key != lower {
|
if key != lower {
|
||||||
// remove old key (not lower-cased)
|
// remove old key (not lower-cased)
|
||||||
@@ -88,26 +98,20 @@ func insensitiviseMap(m map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func absPathify(inPath string) string {
|
func insensitiveArray(a []interface{}) {
|
||||||
jww.INFO.Println("Trying to resolve absolute path to", inPath)
|
for i, val := range a {
|
||||||
|
a[i] = insensitiviseVal(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func absPathify(logger Logger, inPath string) string {
|
||||||
|
logger.Info("trying to resolve absolute path", "path", inPath)
|
||||||
|
|
||||||
if inPath == "$HOME" || strings.HasPrefix(inPath, "$HOME"+string(os.PathSeparator)) {
|
if inPath == "$HOME" || strings.HasPrefix(inPath, "$HOME"+string(os.PathSeparator)) {
|
||||||
inPath = userHomeDir() + inPath[5:]
|
inPath = userHomeDir() + inPath[5:]
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(inPath, "$") {
|
inPath = os.ExpandEnv(inPath)
|
||||||
end := strings.Index(inPath, string(os.PathSeparator))
|
|
||||||
|
|
||||||
var value, suffix string
|
|
||||||
if end == -1 {
|
|
||||||
value = os.Getenv(inPath[1:])
|
|
||||||
} else {
|
|
||||||
value = os.Getenv(inPath[1:end])
|
|
||||||
suffix = inPath[end:]
|
|
||||||
}
|
|
||||||
|
|
||||||
inPath = value + suffix
|
|
||||||
}
|
|
||||||
|
|
||||||
if filepath.IsAbs(inPath) {
|
if filepath.IsAbs(inPath) {
|
||||||
return filepath.Clean(inPath)
|
return filepath.Clean(inPath)
|
||||||
@@ -118,21 +122,9 @@ func absPathify(inPath string) string {
|
|||||||
return filepath.Clean(p)
|
return filepath.Clean(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
jww.ERROR.Println("Couldn't discover absolute path")
|
logger.Error(fmt.Errorf("could not discover absolute path: %w", err).Error())
|
||||||
jww.ERROR.Println(err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file Exists
|
return ""
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stringInSlice(a string, list []string) bool {
|
func stringInSlice(a string, list []string) bool {
|
||||||
|
|||||||
+3
-1
@@ -58,6 +58,8 @@ func TestCopyAndInsensitiviseMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAbsPathify(t *testing.T) {
|
func TestAbsPathify(t *testing.T) {
|
||||||
|
skipWindows(t)
|
||||||
|
|
||||||
home := userHomeDir()
|
home := userHomeDir()
|
||||||
homer := filepath.Join(home, "homer")
|
homer := filepath.Join(home, "homer")
|
||||||
wd, _ := os.Getwd()
|
wd, _ := os.Getwd()
|
||||||
@@ -85,7 +87,7 @@ func TestAbsPathify(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
got := absPathify(test.input)
|
got := absPathify(jwwLogger{}, test.input)
|
||||||
if got != test.output {
|
if got != test.output {
|
||||||
t.Errorf("Got %v\nexpected\n%q", got, test.output)
|
t.Errorf("Got %v\nexpected\n%q", got, test.output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -35,17 +34,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/magiconair/properties"
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/subosito/gotenv"
|
|
||||||
"gopkg.in/ini.v1"
|
|
||||||
|
|
||||||
"github.com/spf13/viper/internal/encoding"
|
"github.com/spf13/viper/internal/encoding"
|
||||||
|
"github.com/spf13/viper/internal/encoding/dotenv"
|
||||||
"github.com/spf13/viper/internal/encoding/hcl"
|
"github.com/spf13/viper/internal/encoding/hcl"
|
||||||
|
"github.com/spf13/viper/internal/encoding/ini"
|
||||||
|
"github.com/spf13/viper/internal/encoding/javaproperties"
|
||||||
"github.com/spf13/viper/internal/encoding/json"
|
"github.com/spf13/viper/internal/encoding/json"
|
||||||
"github.com/spf13/viper/internal/encoding/toml"
|
"github.com/spf13/viper/internal/encoding/toml"
|
||||||
"github.com/spf13/viper/internal/encoding/yaml"
|
"github.com/spf13/viper/internal/encoding/yaml"
|
||||||
@@ -68,44 +66,8 @@ type RemoteResponse struct {
|
|||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
encoderRegistry = encoding.NewEncoderRegistry()
|
|
||||||
decoderRegistry = encoding.NewDecoderRegistry()
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
v = New()
|
v = New()
|
||||||
|
|
||||||
{
|
|
||||||
codec := yaml.Codec{}
|
|
||||||
|
|
||||||
encoderRegistry.RegisterEncoder("yaml", codec)
|
|
||||||
decoderRegistry.RegisterDecoder("yaml", codec)
|
|
||||||
|
|
||||||
encoderRegistry.RegisterEncoder("yml", codec)
|
|
||||||
decoderRegistry.RegisterDecoder("yml", codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
codec := json.Codec{}
|
|
||||||
|
|
||||||
encoderRegistry.RegisterEncoder("json", codec)
|
|
||||||
decoderRegistry.RegisterDecoder("json", codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
codec := toml.Codec{}
|
|
||||||
|
|
||||||
encoderRegistry.RegisterEncoder("toml", codec)
|
|
||||||
decoderRegistry.RegisterDecoder("toml", codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
codec := hcl.Codec{}
|
|
||||||
|
|
||||||
encoderRegistry.RegisterEncoder("hcl", codec)
|
|
||||||
decoderRegistry.RegisterDecoder("hcl", codec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type remoteConfigFactory interface {
|
type remoteConfigFactory interface {
|
||||||
@@ -169,10 +131,10 @@ type DecoderConfigOption func(*mapstructure.DecoderConfig)
|
|||||||
// DecodeHook returns a DecoderConfigOption which overrides the default
|
// DecodeHook returns a DecoderConfigOption which overrides the default
|
||||||
// DecoderConfig.DecodeHook value, the default is:
|
// DecoderConfig.DecodeHook value, the default is:
|
||||||
//
|
//
|
||||||
// mapstructure.ComposeDecodeHookFunc(
|
// mapstructure.ComposeDecodeHookFunc(
|
||||||
// mapstructure.StringToTimeDurationHookFunc(),
|
// mapstructure.StringToTimeDurationHookFunc(),
|
||||||
// mapstructure.StringToSliceHookFunc(","),
|
// mapstructure.StringToSliceHookFunc(","),
|
||||||
// )
|
// )
|
||||||
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
|
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
|
||||||
return func(c *mapstructure.DecoderConfig) {
|
return func(c *mapstructure.DecoderConfig) {
|
||||||
c.DecodeHook = hook
|
c.DecodeHook = hook
|
||||||
@@ -193,18 +155,18 @@ func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
|
|||||||
//
|
//
|
||||||
// For example, if values from the following sources were loaded:
|
// For example, if values from the following sources were loaded:
|
||||||
//
|
//
|
||||||
// Defaults : {
|
// Defaults : {
|
||||||
// "secret": "",
|
// "secret": "",
|
||||||
// "user": "default",
|
// "user": "default",
|
||||||
// "endpoint": "https://localhost"
|
// "endpoint": "https://localhost"
|
||||||
// }
|
// }
|
||||||
// Config : {
|
// Config : {
|
||||||
// "user": "root"
|
// "user": "root"
|
||||||
// "secret": "defaultsecret"
|
// "secret": "defaultsecret"
|
||||||
// }
|
// }
|
||||||
// Env : {
|
// Env : {
|
||||||
// "secret": "somesecretkey"
|
// "secret": "somesecretkey"
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// The resulting config will have the following values:
|
// The resulting config will have the following values:
|
||||||
//
|
//
|
||||||
@@ -243,6 +205,7 @@ type Viper struct {
|
|||||||
envKeyReplacer StringReplacer
|
envKeyReplacer StringReplacer
|
||||||
allowEmptyEnv bool
|
allowEmptyEnv bool
|
||||||
|
|
||||||
|
parents []string
|
||||||
config map[string]interface{}
|
config map[string]interface{}
|
||||||
override map[string]interface{}
|
override map[string]interface{}
|
||||||
defaults map[string]interface{}
|
defaults map[string]interface{}
|
||||||
@@ -252,11 +215,13 @@ type Viper struct {
|
|||||||
aliases map[string]string
|
aliases map[string]string
|
||||||
typeByDefValue bool
|
typeByDefValue bool
|
||||||
|
|
||||||
// Store read properties on the object so that we can write back in order with comments.
|
|
||||||
// This will only be used if the configuration read is a properties file.
|
|
||||||
properties *properties.Properties
|
|
||||||
|
|
||||||
onConfigChange func(fsnotify.Event)
|
onConfigChange func(fsnotify.Event)
|
||||||
|
|
||||||
|
logger Logger
|
||||||
|
|
||||||
|
// TODO: should probably be protected with a mutex
|
||||||
|
encoderRegistry *encoding.EncoderRegistry
|
||||||
|
decoderRegistry *encoding.DecoderRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns an initialized Viper instance.
|
// New returns an initialized Viper instance.
|
||||||
@@ -264,9 +229,10 @@ func New() *Viper {
|
|||||||
v := new(Viper)
|
v := new(Viper)
|
||||||
v.keyDelim = "."
|
v.keyDelim = "."
|
||||||
v.configName = "config"
|
v.configName = "config"
|
||||||
v.configPermissions = os.FileMode(0644)
|
v.configPermissions = os.FileMode(0o644)
|
||||||
v.fs = afero.NewOsFs()
|
v.fs = afero.NewOsFs()
|
||||||
v.config = make(map[string]interface{})
|
v.config = make(map[string]interface{})
|
||||||
|
v.parents = []string{}
|
||||||
v.override = make(map[string]interface{})
|
v.override = make(map[string]interface{})
|
||||||
v.defaults = make(map[string]interface{})
|
v.defaults = make(map[string]interface{})
|
||||||
v.kvstore = make(map[string]interface{})
|
v.kvstore = make(map[string]interface{})
|
||||||
@@ -274,6 +240,9 @@ func New() *Viper {
|
|||||||
v.env = make(map[string][]string)
|
v.env = make(map[string][]string)
|
||||||
v.aliases = make(map[string]string)
|
v.aliases = make(map[string]string)
|
||||||
v.typeByDefValue = false
|
v.typeByDefValue = false
|
||||||
|
v.logger = jwwLogger{}
|
||||||
|
|
||||||
|
v.resetEncoding()
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
@@ -321,6 +290,8 @@ func NewWithOptions(opts ...Option) *Viper {
|
|||||||
opt.apply(v)
|
opt.apply(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.resetEncoding()
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,8 +300,86 @@ func NewWithOptions(opts ...Option) *Viper {
|
|||||||
// can use it in their testing as well.
|
// can use it in their testing as well.
|
||||||
func Reset() {
|
func Reset() {
|
||||||
v = New()
|
v = New()
|
||||||
SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"}
|
SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}
|
||||||
SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}
|
SupportedRemoteProviders = []string{"etcd", "etcd3", "consul", "firestore"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make this lazy initialization instead
|
||||||
|
func (v *Viper) resetEncoding() {
|
||||||
|
encoderRegistry := encoding.NewEncoderRegistry()
|
||||||
|
decoderRegistry := encoding.NewDecoderRegistry()
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := yaml.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("yaml", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("yaml", codec)
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("yml", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("yml", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := json.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("json", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("json", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := toml.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("toml", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("toml", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := hcl.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("hcl", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("hcl", codec)
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("tfvars", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("tfvars", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := ini.Codec{
|
||||||
|
KeyDelimiter: v.keyDelim,
|
||||||
|
LoadOptions: v.iniLoadOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("ini", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("ini", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := &javaproperties.Codec{
|
||||||
|
KeyDelimiter: v.keyDelim,
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("properties", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("properties", codec)
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("props", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("props", codec)
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("prop", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("prop", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := &dotenv.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("dotenv", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("dotenv", codec)
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("env", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("env", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.encoderRegistry = encoderRegistry
|
||||||
|
v.decoderRegistry = decoderRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
type defaultRemoteProvider struct {
|
type defaultRemoteProvider struct {
|
||||||
@@ -368,31 +417,37 @@ type RemoteProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SupportedExts are universally supported extensions.
|
// SupportedExts are universally supported extensions.
|
||||||
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"}
|
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}
|
||||||
|
|
||||||
// SupportedRemoteProviders are universally supported remote providers.
|
// SupportedRemoteProviders are universally supported remote providers.
|
||||||
var SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}
|
var SupportedRemoteProviders = []string{"etcd", "etcd3", "consul", "firestore"}
|
||||||
|
|
||||||
|
// OnConfigChange sets the event handler that is called when a config file changes.
|
||||||
func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) }
|
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)) {
|
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
|
||||||
v.onConfigChange = run
|
v.onConfigChange = run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WatchConfig starts watching a config file for changes.
|
||||||
func WatchConfig() { v.WatchConfig() }
|
func WatchConfig() { v.WatchConfig() }
|
||||||
|
|
||||||
|
// WatchConfig starts watching a config file for changes.
|
||||||
func (v *Viper) WatchConfig() {
|
func (v *Viper) WatchConfig() {
|
||||||
initWG := sync.WaitGroup{}
|
initWG := sync.WaitGroup{}
|
||||||
initWG.Add(1)
|
initWG.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
watcher, err := newWatcher()
|
watcher, err := newWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
defer watcher.Close()
|
||||||
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
|
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
|
||||||
filename, err := v.getConfigFile()
|
filename, err := v.getConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
v.logger.Error(fmt.Sprintf("get config file: %s", err))
|
||||||
initWG.Done()
|
initWG.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -415,27 +470,25 @@ func (v *Viper) WatchConfig() {
|
|||||||
// we only care about the config file with the following cases:
|
// we only care about the config file with the following cases:
|
||||||
// 1 - if the config file was modified or created
|
// 1 - if the config file was modified or created
|
||||||
// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
|
// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
|
||||||
const writeOrCreateMask = fsnotify.Write | fsnotify.Create
|
|
||||||
if (filepath.Clean(event.Name) == configFile &&
|
if (filepath.Clean(event.Name) == configFile &&
|
||||||
event.Op&writeOrCreateMask != 0) ||
|
(event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) ||
|
||||||
(currentConfigFile != "" && currentConfigFile != realConfigFile) {
|
(currentConfigFile != "" && currentConfigFile != realConfigFile) {
|
||||||
realConfigFile = currentConfigFile
|
realConfigFile = currentConfigFile
|
||||||
err := v.ReadInConfig()
|
err := v.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error reading config file: %v\n", err)
|
v.logger.Error(fmt.Sprintf("read config file: %s", err))
|
||||||
}
|
}
|
||||||
if v.onConfigChange != nil {
|
if v.onConfigChange != nil {
|
||||||
v.onConfigChange(event)
|
v.onConfigChange(event)
|
||||||
}
|
}
|
||||||
} else if filepath.Clean(event.Name) == configFile &&
|
} else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) {
|
||||||
event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
|
|
||||||
eventsWG.Done()
|
eventsWG.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case err, ok := <-watcher.Errors:
|
case err, ok := <-watcher.Errors:
|
||||||
if ok { // 'Errors' channel is not closed
|
if ok { // 'Errors' channel is not closed
|
||||||
log.Printf("watcher error: %v\n", err)
|
v.logger.Error(fmt.Sprintf("watcher error: %s", err))
|
||||||
}
|
}
|
||||||
eventsWG.Done()
|
eventsWG.Done()
|
||||||
return
|
return
|
||||||
@@ -514,8 +567,9 @@ func AddConfigPath(in string) { v.AddConfigPath(in) }
|
|||||||
|
|
||||||
func (v *Viper) AddConfigPath(in string) {
|
func (v *Viper) AddConfigPath(in string) {
|
||||||
if in != "" {
|
if in != "" {
|
||||||
absin := absPathify(in)
|
absin := absPathify(v.logger, in)
|
||||||
jww.INFO.Println("adding", absin, "to paths to search")
|
|
||||||
|
v.logger.Info("adding path to search paths", "path", absin)
|
||||||
if !stringInSlice(absin, v.configPaths) {
|
if !stringInSlice(absin, v.configPaths) {
|
||||||
v.configPaths = append(v.configPaths, absin)
|
v.configPaths = append(v.configPaths, absin)
|
||||||
}
|
}
|
||||||
@@ -524,7 +578,7 @@ func (v *Viper) AddConfigPath(in string) {
|
|||||||
|
|
||||||
// AddRemoteProvider adds a remote configuration source.
|
// AddRemoteProvider adds a remote configuration source.
|
||||||
// Remote Providers are searched in the order they are added.
|
// Remote Providers are searched in the order they are added.
|
||||||
// provider is a string value: "etcd", "consul" or "firestore" are currently supported.
|
// provider is a string value: "etcd", "etcd3", "consul" or "firestore" are currently supported.
|
||||||
// endpoint is the url. etcd requires http://ip:port consul requires ip:port
|
// endpoint is the url. etcd requires http://ip:port consul requires ip:port
|
||||||
// path is the path in the k/v store to retrieve configuration
|
// path is the path in the k/v store to retrieve configuration
|
||||||
// To retrieve a config file called myapp.json from /configs/myapp.json
|
// To retrieve a config file called myapp.json from /configs/myapp.json
|
||||||
@@ -539,7 +593,8 @@ func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error {
|
|||||||
return UnsupportedRemoteProviderError(provider)
|
return UnsupportedRemoteProviderError(provider)
|
||||||
}
|
}
|
||||||
if provider != "" && endpoint != "" {
|
if provider != "" && endpoint != "" {
|
||||||
jww.INFO.Printf("adding %s:%s to remote provider list", provider, endpoint)
|
v.logger.Info("adding remote provider", "provider", provider, "endpoint", endpoint)
|
||||||
|
|
||||||
rp := &defaultRemoteProvider{
|
rp := &defaultRemoteProvider{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
@@ -554,7 +609,7 @@ func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error {
|
|||||||
|
|
||||||
// AddSecureRemoteProvider adds a remote configuration source.
|
// AddSecureRemoteProvider adds a remote configuration source.
|
||||||
// Secure Remote Providers are searched in the order they are added.
|
// Secure Remote Providers are searched in the order they are added.
|
||||||
// provider is a string value: "etcd", "consul" or "firestore" are currently supported.
|
// provider is a string value: "etcd", "etcd3", "consul" or "firestore" are currently supported.
|
||||||
// endpoint is the url. etcd requires http://ip:port consul requires ip:port
|
// 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
|
// 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
|
// path is the path in the k/v store to retrieve configuration
|
||||||
@@ -571,7 +626,8 @@ func (v *Viper) AddSecureRemoteProvider(provider, endpoint, path, secretkeyring
|
|||||||
return UnsupportedRemoteProviderError(provider)
|
return UnsupportedRemoteProviderError(provider)
|
||||||
}
|
}
|
||||||
if provider != "" && endpoint != "" {
|
if provider != "" && endpoint != "" {
|
||||||
jww.INFO.Printf("adding %s:%s to remote provider list", provider, endpoint)
|
v.logger.Info("adding remote provider", "provider", provider, "endpoint", endpoint)
|
||||||
|
|
||||||
rp := &defaultRemoteProvider{
|
rp := &defaultRemoteProvider{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
provider: provider,
|
provider: provider,
|
||||||
@@ -734,7 +790,8 @@ func (v *Viper) searchMapWithPathPrefixes(
|
|||||||
// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
|
// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
|
||||||
// on its path in the map.
|
// on its path in the map.
|
||||||
// e.g., if "foo.bar" has a value in the given map, it “shadows”
|
// e.g., if "foo.bar" has a value in the given map, it “shadows”
|
||||||
// "foo.bar.baz" in a lower-priority map
|
//
|
||||||
|
// "foo.bar.baz" in a lower-priority map
|
||||||
func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{}) string {
|
func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{}) string {
|
||||||
var parentVal interface{}
|
var parentVal interface{}
|
||||||
for i := 1; i < len(path); i++ {
|
for i := 1; i < len(path); i++ {
|
||||||
@@ -759,7 +816,8 @@ func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{})
|
|||||||
// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
|
// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
|
||||||
// in a sub-path of the map.
|
// in a sub-path of the map.
|
||||||
// e.g., if "foo.bar" has a value in the given map, it “shadows”
|
// e.g., if "foo.bar" has a value in the given map, it “shadows”
|
||||||
// "foo.bar.baz" in a lower-priority map
|
//
|
||||||
|
// "foo.bar.baz" in a lower-priority map
|
||||||
func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
|
func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
|
||||||
// unify input map
|
// unify input map
|
||||||
var m map[string]interface{}
|
var m map[string]interface{}
|
||||||
@@ -784,7 +842,8 @@ func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
|
|||||||
// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
|
// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
|
||||||
// in the environment, when automatic env is on.
|
// in the environment, when automatic env is on.
|
||||||
// e.g., if "foo.bar" has a value in the environment, it “shadows”
|
// e.g., if "foo.bar" has a value in the environment, it “shadows”
|
||||||
// "foo.bar.baz" in a lower-priority map
|
//
|
||||||
|
// "foo.bar.baz" in a lower-priority map
|
||||||
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
|
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
|
||||||
var parentKey string
|
var parentKey string
|
||||||
for i := 1; i < len(path); i++ {
|
for i := 1; i < len(path); i++ {
|
||||||
@@ -805,11 +864,11 @@ func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
|
|||||||
// would return a string slice for the key if the key's type is inferred by
|
// 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:
|
// the default value and the Get function would return:
|
||||||
//
|
//
|
||||||
// []string {"a", "b", "c"}
|
// []string {"a", "b", "c"}
|
||||||
//
|
//
|
||||||
// Otherwise the Get function would return:
|
// Otherwise the Get function would return:
|
||||||
//
|
//
|
||||||
// "a b c"
|
// "a b c"
|
||||||
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }
|
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }
|
||||||
|
|
||||||
func (v *Viper) SetTypeByDefaultValue(enable bool) {
|
func (v *Viper) SetTypeByDefaultValue(enable bool) {
|
||||||
@@ -871,6 +930,8 @@ func (v *Viper) Get(key string) interface{} {
|
|||||||
return cast.ToStringSlice(val)
|
return cast.ToStringSlice(val)
|
||||||
case []int:
|
case []int:
|
||||||
return cast.ToIntSlice(val)
|
return cast.ToIntSlice(val)
|
||||||
|
case []time.Duration:
|
||||||
|
return cast.ToDurationSlice(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,6 +950,10 @@ func (v *Viper) Sub(key string) *Viper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reflect.TypeOf(data).Kind() == reflect.Map {
|
if reflect.TypeOf(data).Kind() == reflect.Map {
|
||||||
|
subv.parents = append(v.parents, strings.ToLower(key))
|
||||||
|
subv.automaticEnvApplied = v.automaticEnvApplied
|
||||||
|
subv.envPrefix = v.envPrefix
|
||||||
|
subv.envKeyReplacer = v.envKeyReplacer
|
||||||
subv.config = cast.ToStringMap(data)
|
subv.config = cast.ToStringMap(data)
|
||||||
return subv
|
return subv
|
||||||
}
|
}
|
||||||
@@ -937,6 +1002,13 @@ func (v *Viper) GetUint(key string) uint {
|
|||||||
return cast.ToUint(v.Get(key))
|
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) }
|
||||||
|
|
||||||
|
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.
|
// GetUint32 returns the value associated with the key as an unsigned integer.
|
||||||
func GetUint32(key string) uint32 { return v.GetUint32(key) }
|
func GetUint32(key string) uint32 { return v.GetUint32(key) }
|
||||||
|
|
||||||
@@ -1035,7 +1107,7 @@ func (v *Viper) Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error
|
|||||||
return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...))
|
return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot
|
// defaultDecoderConfig returns default mapstructure.DecoderConfig with support
|
||||||
// of time.Duration values & string slices
|
// of time.Duration values & string slices
|
||||||
func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
|
func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
|
||||||
c := &mapstructure.DecoderConfig{
|
c := &mapstructure.DecoderConfig{
|
||||||
@@ -1086,9 +1158,8 @@ func (v *Viper) BindPFlags(flags *pflag.FlagSet) error {
|
|||||||
// BindPFlag binds a specific key to a pflag (as used by cobra).
|
// BindPFlag binds a specific key to a pflag (as used by cobra).
|
||||||
// Example (where serverCmd is a Cobra instance):
|
// Example (where serverCmd is a Cobra instance):
|
||||||
//
|
//
|
||||||
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
|
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
|
||||||
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
|
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
|
||||||
//
|
|
||||||
func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) }
|
func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) }
|
||||||
|
|
||||||
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error {
|
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error {
|
||||||
@@ -1146,6 +1217,17 @@ func (v *Viper) BindEnv(input ...string) error {
|
|||||||
return nil
|
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...) }
|
||||||
|
|
||||||
|
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.
|
// Given a key, find the value.
|
||||||
//
|
//
|
||||||
// Viper will check to see if an alias exists first.
|
// Viper will check to see if an alias exists first.
|
||||||
@@ -1200,8 +1282,15 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
|
|||||||
s = strings.TrimSuffix(s, "]")
|
s = strings.TrimSuffix(s, "]")
|
||||||
res, _ := readAsCSV(s)
|
res, _ := readAsCSV(s)
|
||||||
return cast.ToIntSlice(res)
|
return cast.ToIntSlice(res)
|
||||||
|
case "durationSlice":
|
||||||
|
s := strings.TrimPrefix(flag.ValueString(), "[")
|
||||||
|
s = strings.TrimSuffix(s, "]")
|
||||||
|
slice := strings.Split(s, ",")
|
||||||
|
return cast.ToDurationSlice(slice)
|
||||||
case "stringToString":
|
case "stringToString":
|
||||||
return stringToStringConv(flag.ValueString())
|
return stringToStringConv(flag.ValueString())
|
||||||
|
case "stringToInt":
|
||||||
|
return stringToIntConv(flag.ValueString())
|
||||||
default:
|
default:
|
||||||
return flag.ValueString()
|
return flag.ValueString()
|
||||||
}
|
}
|
||||||
@@ -1212,9 +1301,10 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
|
|||||||
|
|
||||||
// Env override next
|
// Env override next
|
||||||
if v.automaticEnvApplied {
|
if v.automaticEnvApplied {
|
||||||
|
envKey := strings.Join(append(v.parents, lcaseKey), ".")
|
||||||
// even if it hasn't been registered, if automaticEnv is used,
|
// even if it hasn't been registered, if automaticEnv is used,
|
||||||
// check any Get request
|
// check any Get request
|
||||||
if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
|
if val, ok := v.getEnv(v.mergeWithEnvPrefix(envKey)); ok {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
if nested && v.isPathShadowedInAutoEnv(path) != "" {
|
if nested && v.isPathShadowedInAutoEnv(path) != "" {
|
||||||
@@ -1281,6 +1371,13 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
|
|||||||
return cast.ToIntSlice(res)
|
return cast.ToIntSlice(res)
|
||||||
case "stringToString":
|
case "stringToString":
|
||||||
return stringToStringConv(flag.ValueString())
|
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:
|
default:
|
||||||
return flag.ValueString()
|
return flag.ValueString()
|
||||||
}
|
}
|
||||||
@@ -1324,6 +1421,30 @@ func stringToStringConv(val string) interface{} {
|
|||||||
return out
|
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]interface{} is returned in order to enable cast.ToStringMap
|
||||||
|
func stringToIntConv(val string) interface{} {
|
||||||
|
val = strings.Trim(val, "[]")
|
||||||
|
// An empty string would cause an empty map
|
||||||
|
if len(val) == 0 {
|
||||||
|
return map[string]interface{}{}
|
||||||
|
}
|
||||||
|
ss := strings.Split(val, ",")
|
||||||
|
out := make(map[string]interface{}, len(ss))
|
||||||
|
for _, pair := range ss {
|
||||||
|
kv := strings.SplitN(pair, "=", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
out[kv[0]], err = strconv.Atoi(kv[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// IsSet checks to see if the key has been set in any of the data locations.
|
// IsSet checks to see if the key has been set in any of the data locations.
|
||||||
// IsSet is case-insensitive for a key.
|
// IsSet is case-insensitive for a key.
|
||||||
func IsSet(key string) bool { return v.IsSet(key) }
|
func IsSet(key string) bool { return v.IsSet(key) }
|
||||||
@@ -1387,14 +1508,15 @@ func (v *Viper) registerAlias(alias string, key string) {
|
|||||||
v.aliases[alias] = key
|
v.aliases[alias] = key
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
jww.WARN.Println("Creating circular reference alias", alias, key, v.realKey(key))
|
v.logger.Warn("creating circular reference alias", "alias", alias, "key", key, "real_key", v.realKey(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Viper) realKey(key string) string {
|
func (v *Viper) realKey(key string) string {
|
||||||
newkey, exists := v.aliases[key]
|
newkey, exists := v.aliases[key]
|
||||||
if exists {
|
if exists {
|
||||||
jww.DEBUG.Println("Alias", key, "to", newkey)
|
v.logger.Debug("key is an alias", "alias", key, "to", newkey)
|
||||||
|
|
||||||
return v.realKey(newkey)
|
return v.realKey(newkey)
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
@@ -1404,11 +1526,13 @@ func (v *Viper) realKey(key string) string {
|
|||||||
func InConfig(key string) bool { return v.InConfig(key) }
|
func InConfig(key string) bool { return v.InConfig(key) }
|
||||||
|
|
||||||
func (v *Viper) InConfig(key string) bool {
|
func (v *Viper) InConfig(key string) bool {
|
||||||
// if the requested key is an alias, then return the proper key
|
lcaseKey := strings.ToLower(key)
|
||||||
key = v.realKey(key)
|
|
||||||
|
|
||||||
_, exists := v.config[key]
|
// if the requested key is an alias, then return the proper key
|
||||||
return exists
|
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 sets the default value for this key.
|
||||||
@@ -1453,7 +1577,7 @@ func (v *Viper) Set(key string, value interface{}) {
|
|||||||
func ReadInConfig() error { return v.ReadInConfig() }
|
func ReadInConfig() error { return v.ReadInConfig() }
|
||||||
|
|
||||||
func (v *Viper) ReadInConfig() error {
|
func (v *Viper) ReadInConfig() error {
|
||||||
jww.INFO.Println("Attempting to read in config file")
|
v.logger.Info("attempting to read in config file")
|
||||||
filename, err := v.getConfigFile()
|
filename, err := v.getConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1463,7 +1587,7 @@ func (v *Viper) ReadInConfig() error {
|
|||||||
return UnsupportedConfigError(v.getConfigType())
|
return UnsupportedConfigError(v.getConfigType())
|
||||||
}
|
}
|
||||||
|
|
||||||
jww.DEBUG.Println("Reading file: ", filename)
|
v.logger.Debug("reading file", "file", filename)
|
||||||
file, err := afero.ReadFile(v.fs, filename)
|
file, err := afero.ReadFile(v.fs, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1484,7 +1608,7 @@ func (v *Viper) ReadInConfig() error {
|
|||||||
func MergeInConfig() error { return v.MergeInConfig() }
|
func MergeInConfig() error { return v.MergeInConfig() }
|
||||||
|
|
||||||
func (v *Viper) MergeInConfig() error {
|
func (v *Viper) MergeInConfig() error {
|
||||||
jww.INFO.Println("Attempting to merge in config file")
|
v.logger.Info("attempting to merge in config file")
|
||||||
filename, err := v.getConfigFile()
|
filename, err := v.getConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1575,11 +1699,12 @@ func (v *Viper) SafeWriteConfigAs(filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Viper) writeConfig(filename string, force bool) error {
|
func (v *Viper) writeConfig(filename string, force bool) error {
|
||||||
jww.INFO.Println("Attempting to write configuration to file.")
|
v.logger.Info("attempting to write configuration to file")
|
||||||
|
|
||||||
var configType string
|
var configType string
|
||||||
|
|
||||||
ext := filepath.Ext(filename)
|
ext := filepath.Ext(filename)
|
||||||
if ext != "" {
|
if ext != "" && ext != filepath.Base(filename) {
|
||||||
configType = ext[1:]
|
configType = ext[1:]
|
||||||
} else {
|
} else {
|
||||||
configType = v.configType
|
configType = v.configType
|
||||||
@@ -1622,53 +1747,11 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
|
|||||||
buf.ReadFrom(in)
|
buf.ReadFrom(in)
|
||||||
|
|
||||||
switch format := strings.ToLower(v.getConfigType()); format {
|
switch format := strings.ToLower(v.getConfigType()); format {
|
||||||
case "yaml", "yml", "json", "toml", "hcl":
|
case "yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "properties", "props", "prop", "dotenv", "env":
|
||||||
err := decoderRegistry.Decode(format, buf.Bytes(), &c)
|
err := v.decoderRegistry.Decode(format, buf.Bytes(), c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConfigParseError{err}
|
return ConfigParseError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "dotenv", "env":
|
|
||||||
env, err := gotenv.StrictParse(buf)
|
|
||||||
if err != nil {
|
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
for k, v := range env {
|
|
||||||
c[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
case "properties", "props", "prop":
|
|
||||||
v.properties = properties.NewProperties()
|
|
||||||
var err error
|
|
||||||
if v.properties, err = properties.Load(buf.Bytes(), properties.UTF8); err != nil {
|
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
for _, key := range v.properties.Keys() {
|
|
||||||
value, _ := v.properties.Get(key)
|
|
||||||
// recursively build nested maps
|
|
||||||
path := strings.Split(key, ".")
|
|
||||||
lastKey := strings.ToLower(path[len(path)-1])
|
|
||||||
deepestMap := deepSearch(c, path[0:len(path)-1])
|
|
||||||
// set innermost value
|
|
||||||
deepestMap[lastKey] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ini":
|
|
||||||
cfg := ini.Empty(v.iniLoadOptions)
|
|
||||||
err := cfg.Append(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
sections := cfg.Sections()
|
|
||||||
for i := 0; i < len(sections); i++ {
|
|
||||||
section := sections[i]
|
|
||||||
keys := section.Keys()
|
|
||||||
for j := 0; j < len(keys); j++ {
|
|
||||||
key := keys[j]
|
|
||||||
value := cfg.Section(section.Name()).Key(key.Name()).String()
|
|
||||||
c[section.Name()+"."+key.Name()] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insensitiviseMap(c)
|
insensitiviseMap(c)
|
||||||
@@ -1679,8 +1762,8 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
|
|||||||
func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
||||||
c := v.AllSettings()
|
c := v.AllSettings()
|
||||||
switch configType {
|
switch configType {
|
||||||
case "yaml", "yml", "json", "toml", "hcl":
|
case "yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "prop", "props", "properties", "dotenv", "env":
|
||||||
b, err := encoderRegistry.Encode(configType, c)
|
b, err := v.encoderRegistry.Encode(configType, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConfigMarshalError{err}
|
return ConfigMarshalError{err}
|
||||||
}
|
}
|
||||||
@@ -1689,50 +1772,6 @@ func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ConfigMarshalError{err}
|
return ConfigMarshalError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "prop", "props", "properties":
|
|
||||||
if v.properties == nil {
|
|
||||||
v.properties = properties.NewProperties()
|
|
||||||
}
|
|
||||||
p := v.properties
|
|
||||||
for _, key := range v.AllKeys() {
|
|
||||||
_, _, err := p.Set(key, v.GetString(key))
|
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err := p.WriteComment(f, "#", properties.UTF8)
|
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "dotenv", "env":
|
|
||||||
lines := []string{}
|
|
||||||
for _, key := range v.AllKeys() {
|
|
||||||
envName := strings.ToUpper(strings.Replace(key, ".", "_", -1))
|
|
||||||
val := v.Get(key)
|
|
||||||
lines = append(lines, fmt.Sprintf("%v=%v", envName, val))
|
|
||||||
}
|
|
||||||
s := strings.Join(lines, "\n")
|
|
||||||
if _, err := f.WriteString(s); err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ini":
|
|
||||||
keys := v.AllKeys()
|
|
||||||
cfg := ini.Empty()
|
|
||||||
ini.PrettyFormat = false
|
|
||||||
for i := 0; i < len(keys); i++ {
|
|
||||||
key := keys[i]
|
|
||||||
lastSep := strings.LastIndex(key, ".")
|
|
||||||
sectionName := key[:(lastSep)]
|
|
||||||
keyName := key[(lastSep + 1):]
|
|
||||||
if sectionName == "default" {
|
|
||||||
sectionName = ""
|
|
||||||
}
|
|
||||||
cfg.Section(sectionName).Key(keyName).SetValue(v.GetString(key))
|
|
||||||
}
|
|
||||||
cfg.WriteTo(f)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1749,7 +1788,8 @@ func keyExists(k string, m map[string]interface{}) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func castToMapStringInterface(
|
func castToMapStringInterface(
|
||||||
src map[interface{}]interface{}) map[string]interface{} {
|
src map[interface{}]interface{},
|
||||||
|
) map[string]interface{} {
|
||||||
tgt := map[string]interface{}{}
|
tgt := map[string]interface{}{}
|
||||||
for k, v := range src {
|
for k, v := range src {
|
||||||
tgt[fmt.Sprintf("%v", k)] = v
|
tgt[fmt.Sprintf("%v", k)] = v
|
||||||
@@ -1787,11 +1827,12 @@ func castMapFlagToMapInterface(src map[string]FlagValue) map[string]interface{}
|
|||||||
// deep. Both map types are supported as there is a go-yaml fork that uses
|
// deep. Both map types are supported as there is a go-yaml fork that uses
|
||||||
// `map[string]interface{}` instead.
|
// `map[string]interface{}` instead.
|
||||||
func mergeMaps(
|
func mergeMaps(
|
||||||
src, tgt map[string]interface{}, itgt map[interface{}]interface{}) {
|
src, tgt map[string]interface{}, itgt map[interface{}]interface{},
|
||||||
|
) {
|
||||||
for sk, sv := range src {
|
for sk, sv := range src {
|
||||||
tk := keyExists(sk, tgt)
|
tk := keyExists(sk, tgt)
|
||||||
if tk == "" {
|
if tk == "" {
|
||||||
jww.TRACE.Printf("tk=\"\", tgt[%s]=%v", sk, sv)
|
v.logger.Trace("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
|
||||||
tgt[sk] = sv
|
tgt[sk] = sv
|
||||||
if itgt != nil {
|
if itgt != nil {
|
||||||
itgt[sk] = sv
|
itgt[sk] = sv
|
||||||
@@ -1801,7 +1842,7 @@ func mergeMaps(
|
|||||||
|
|
||||||
tv, ok := tgt[tk]
|
tv, ok := tgt[tk]
|
||||||
if !ok {
|
if !ok {
|
||||||
jww.TRACE.Printf("tgt[%s] != ok, tgt[%s]=%v", tk, sk, sv)
|
v.logger.Trace("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
|
||||||
tgt[sk] = sv
|
tgt[sk] = sv
|
||||||
if itgt != nil {
|
if itgt != nil {
|
||||||
itgt[sk] = sv
|
itgt[sk] = sv
|
||||||
@@ -1811,28 +1852,52 @@ func mergeMaps(
|
|||||||
|
|
||||||
svType := reflect.TypeOf(sv)
|
svType := reflect.TypeOf(sv)
|
||||||
tvType := reflect.TypeOf(tv)
|
tvType := reflect.TypeOf(tv)
|
||||||
if tvType != nil && svType != tvType { // Allow for the target to be nil
|
|
||||||
jww.ERROR.Printf(
|
|
||||||
"svType != tvType; key=%s, st=%v, tt=%v, sv=%v, tv=%v",
|
|
||||||
sk, svType, tvType, sv, tv)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
jww.TRACE.Printf("processing key=%s, st=%v, tt=%v, sv=%v, tv=%v",
|
v.logger.Trace(
|
||||||
sk, svType, tvType, sv, tv)
|
"processing",
|
||||||
|
"key", sk,
|
||||||
|
"st", svType,
|
||||||
|
"tt", tvType,
|
||||||
|
"sv", sv,
|
||||||
|
"tv", tv,
|
||||||
|
)
|
||||||
|
|
||||||
switch ttv := tv.(type) {
|
switch ttv := tv.(type) {
|
||||||
case map[interface{}]interface{}:
|
case map[interface{}]interface{}:
|
||||||
jww.TRACE.Printf("merging maps (must convert)")
|
v.logger.Trace("merging maps (must convert)")
|
||||||
tsv := sv.(map[interface{}]interface{})
|
tsv, ok := sv.(map[interface{}]interface{})
|
||||||
|
if !ok {
|
||||||
|
v.logger.Error(
|
||||||
|
"Could not cast sv to map[interface{}]interface{}",
|
||||||
|
"key", sk,
|
||||||
|
"st", svType,
|
||||||
|
"tt", tvType,
|
||||||
|
"sv", sv,
|
||||||
|
"tv", tv,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
ssv := castToMapStringInterface(tsv)
|
ssv := castToMapStringInterface(tsv)
|
||||||
stv := castToMapStringInterface(ttv)
|
stv := castToMapStringInterface(ttv)
|
||||||
mergeMaps(ssv, stv, ttv)
|
mergeMaps(ssv, stv, ttv)
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
jww.TRACE.Printf("merging maps")
|
v.logger.Trace("merging maps")
|
||||||
mergeMaps(sv.(map[string]interface{}), ttv, nil)
|
tsv, ok := sv.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
v.logger.Error(
|
||||||
|
"Could not cast sv to map[string]interface{}",
|
||||||
|
"key", sk,
|
||||||
|
"st", svType,
|
||||||
|
"tt", tvType,
|
||||||
|
"sv", sv,
|
||||||
|
"tv", tv,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mergeMaps(tsv, ttv, nil)
|
||||||
default:
|
default:
|
||||||
jww.TRACE.Printf("setting value")
|
v.logger.Trace("setting value")
|
||||||
tgt[tk] = sv
|
tgt[tk] = sv
|
||||||
if itgt != nil {
|
if itgt != nil {
|
||||||
itgt[tk] = sv
|
itgt[tk] = sv
|
||||||
@@ -1864,10 +1929,14 @@ func (v *Viper) getKeyValueConfig() error {
|
|||||||
return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/spf13/viper/remote'")
|
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 {
|
for _, rp := range v.remoteProviders {
|
||||||
val, err := v.getRemoteConfig(rp)
|
val, err := v.getRemoteConfig(rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jww.ERROR.Printf("get remote config: %s", err)
|
v.logger.Error(fmt.Errorf("get remote config: %w", err).Error())
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1890,6 +1959,10 @@ func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]interface{}
|
|||||||
|
|
||||||
// Retrieve the first found remote configuration.
|
// Retrieve the first found remote configuration.
|
||||||
func (v *Viper) watchKeyValueConfigOnChannel() error {
|
func (v *Viper) watchKeyValueConfigOnChannel() error {
|
||||||
|
if len(v.remoteProviders) == 0 {
|
||||||
|
return RemoteConfigError("No Remote Providers")
|
||||||
|
}
|
||||||
|
|
||||||
for _, rp := range v.remoteProviders {
|
for _, rp := range v.remoteProviders {
|
||||||
respc, _ := RemoteConfig.WatchChannel(rp)
|
respc, _ := RemoteConfig.WatchChannel(rp)
|
||||||
// Todo: Add quit channel
|
// Todo: Add quit channel
|
||||||
@@ -1907,9 +1980,15 @@ func (v *Viper) watchKeyValueConfigOnChannel() error {
|
|||||||
|
|
||||||
// Retrieve the first found remote configuration.
|
// Retrieve the first found remote configuration.
|
||||||
func (v *Viper) watchKeyValueConfig() error {
|
func (v *Viper) watchKeyValueConfig() error {
|
||||||
|
if len(v.remoteProviders) == 0 {
|
||||||
|
return RemoteConfigError("No Remote Providers")
|
||||||
|
}
|
||||||
|
|
||||||
for _, rp := range v.remoteProviders {
|
for _, rp := range v.remoteProviders {
|
||||||
val, err := v.watchRemoteConfig(rp)
|
val, err := v.watchRemoteConfig(rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
v.logger.Error(fmt.Errorf("watch remote config: %w", err).Error())
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
v.kvstore = val
|
v.kvstore = val
|
||||||
@@ -1952,9 +2031,10 @@ func (v *Viper) AllKeys() []string {
|
|||||||
|
|
||||||
// flattenAndMergeMap recursively flattens the given map into a map[string]bool
|
// flattenAndMergeMap recursively flattens the given map into a map[string]bool
|
||||||
// of key paths (used as a set, easier to manipulate than a []string):
|
// 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
|
// - 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,
|
// - if a path is shadowed by an earlier value in the initial shadow map,
|
||||||
// it is skipped.
|
// it is skipped.
|
||||||
|
//
|
||||||
// The resulting set of paths is merged to the given shadow set at the same time.
|
// 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]interface{}, prefix string) map[string]bool {
|
func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interface{}, prefix string) map[string]bool {
|
||||||
if shadow != nil && prefix != "" && shadow[prefix] {
|
if shadow != nil && prefix != "" && shadow[prefix] {
|
||||||
@@ -2103,49 +2183,19 @@ func (v *Viper) getConfigFile() (string, error) {
|
|||||||
return v.configFile, nil
|
return v.configFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Viper) searchInPath(in string) (filename string) {
|
|
||||||
jww.DEBUG.Println("Searching for config in ", in)
|
|
||||||
for _, ext := range SupportedExts {
|
|
||||||
jww.DEBUG.Println("Checking for", filepath.Join(in, v.configName+"."+ext))
|
|
||||||
if b, _ := exists(v.fs, filepath.Join(in, v.configName+"."+ext)); b {
|
|
||||||
jww.DEBUG.Println("Found: ", 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 ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search all configPaths for any config file.
|
|
||||||
// Returns the first path that exists (and is a config file).
|
|
||||||
func (v *Viper) findConfigFile() (string, error) {
|
|
||||||
jww.INFO.Println("Searching for config in ", v.configPaths)
|
|
||||||
|
|
||||||
for _, cp := range v.configPaths {
|
|
||||||
file := v.searchInPath(cp)
|
|
||||||
if file != "" {
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug prints all configuration registries for debugging
|
// Debug prints all configuration registries for debugging
|
||||||
// purposes.
|
// purposes.
|
||||||
func Debug() { v.Debug() }
|
func Debug() { v.Debug() }
|
||||||
|
func DebugTo(w io.Writer) { v.DebugTo(w) }
|
||||||
|
|
||||||
func (v *Viper) Debug() {
|
func (v *Viper) Debug() { v.DebugTo(os.Stdout) }
|
||||||
fmt.Printf("Aliases:\n%#v\n", v.aliases)
|
|
||||||
fmt.Printf("Override:\n%#v\n", v.override)
|
func (v *Viper) DebugTo(w io.Writer) {
|
||||||
fmt.Printf("PFlags:\n%#v\n", v.pflags)
|
fmt.Fprintf(w, "Aliases:\n%#v\n", v.aliases)
|
||||||
fmt.Printf("Env:\n%#v\n", v.env)
|
fmt.Fprintf(w, "Override:\n%#v\n", v.override)
|
||||||
fmt.Printf("Key/Value Store:\n%#v\n", v.kvstore)
|
fmt.Fprintf(w, "PFlags:\n%#v\n", v.pflags)
|
||||||
fmt.Printf("Config:\n%#v\n", v.config)
|
fmt.Fprintf(w, "Env:\n%#v\n", v.env)
|
||||||
fmt.Printf("Defaults:\n%#v\n", v.defaults)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//go:build !go1.16 || !finder
|
||||||
|
// +build !go1.16 !finder
|
||||||
|
|
||||||
|
package viper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Search all configPaths for any config file.
|
||||||
|
// Returns the first path that exists (and is a config file).
|
||||||
|
func (v *Viper) findConfigFile() (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{v.configName, fmt.Sprintf("%s", 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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build go1.16 && finder
|
||||||
|
// +build go1.16,finder
|
||||||
|
|
||||||
|
package viper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Search all configPaths for any config file.
|
||||||
|
// Returns the first path that exists (and is a config file).
|
||||||
|
func (v *Viper) findConfigFile() (string, error) {
|
||||||
|
finder := finder{
|
||||||
|
paths: v.configPaths,
|
||||||
|
fileNames: []string{v.configName},
|
||||||
|
extensions: SupportedExts,
|
||||||
|
withoutExtension: v.configType != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := finder.Find(afero.NewIOFS(v.fs))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file == "" {
|
||||||
|
return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
+432
-115
@@ -8,6 +8,7 @@ package viper
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
@@ -33,21 +34,21 @@ import (
|
|||||||
"github.com/spf13/viper/internal/testutil"
|
"github.com/spf13/viper/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var yamlExample = []byte(`Hacker: true
|
// var yamlExample = []byte(`Hacker: true
|
||||||
name: steve
|
// name: steve
|
||||||
hobbies:
|
// hobbies:
|
||||||
- skateboarding
|
// - skateboarding
|
||||||
- snowboarding
|
// - snowboarding
|
||||||
- go
|
// - go
|
||||||
clothing:
|
// clothing:
|
||||||
jacket: leather
|
// jacket: leather
|
||||||
trousers: denim
|
// trousers: denim
|
||||||
pants:
|
// pants:
|
||||||
size: large
|
// size: large
|
||||||
age: 35
|
// age: 35
|
||||||
eyes : brown
|
// eyes : brown
|
||||||
beard: true
|
// beard: true
|
||||||
`)
|
// `)
|
||||||
|
|
||||||
var yamlExampleWithExtras = []byte(`Existing: true
|
var yamlExampleWithExtras = []byte(`Existing: true
|
||||||
Bogus: true
|
Bogus: true
|
||||||
@@ -263,13 +264,13 @@ func initDirs(t *testing.T) (string, string, func()) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
for _, dir := range testDirs {
|
for _, dir := range testDirs {
|
||||||
err = os.Mkdir(dir, 0750)
|
err = os.Mkdir(dir, 0o750)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
err = ioutil.WriteFile(
|
err = ioutil.WriteFile(
|
||||||
path.Join(dir, config+".toml"),
|
path.Join(dir, config+".toml"),
|
||||||
[]byte("key = \"value is "+dir+"\"\n"),
|
[]byte("key = \"value is "+dir+"\"\n"),
|
||||||
0640)
|
0o640)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,65 +302,192 @@ func (s *stringValue) String() string {
|
|||||||
return string(*s)
|
return string(*s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasics(t *testing.T) {
|
func TestGetConfigFile(t *testing.T) {
|
||||||
SetConfigFile("/tmp/config.yaml")
|
t.Run("config file set", func(t *testing.T) {
|
||||||
filename, err := v.getConfigFile()
|
fs := afero.NewMemMapFs()
|
||||||
assert.Equal(t, "/tmp/config.yaml", filename)
|
|
||||||
assert.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.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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchInPath_WithoutConfigTypeSet(t *testing.T) {
|
func TestReadInConfig(t *testing.T) {
|
||||||
filename := ".dotfilenoext"
|
t.Run("config file set", func(t *testing.T) {
|
||||||
path := "/tmp"
|
fs := afero.NewMemMapFs()
|
||||||
file := filepath.Join(path, filename)
|
|
||||||
SetConfigName(filename)
|
|
||||||
AddConfigPath(path)
|
|
||||||
_, createErr := v.fs.Create(file)
|
|
||||||
defer func() {
|
|
||||||
_ = v.fs.Remove(file)
|
|
||||||
}()
|
|
||||||
assert.NoError(t, createErr)
|
|
||||||
_, err := v.getConfigFile()
|
|
||||||
// unless config type is set, files without extension
|
|
||||||
// are not considered
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSearchInPath(t *testing.T) {
|
err := fs.Mkdir("/etc/viper", 0o777)
|
||||||
filename := ".dotfilenoext"
|
require.NoError(t, err)
|
||||||
path := "/tmp"
|
|
||||||
file := filepath.Join(path, filename)
|
|
||||||
SetConfigName(filename)
|
|
||||||
SetConfigType("yaml")
|
|
||||||
AddConfigPath(path)
|
|
||||||
_, createErr := v.fs.Create(file)
|
|
||||||
defer func() {
|
|
||||||
_ = v.fs.Remove(file)
|
|
||||||
}()
|
|
||||||
assert.NoError(t, createErr)
|
|
||||||
filename, err := v.getConfigFile()
|
|
||||||
assert.Equal(t, file, filename)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSearchInPath_FilesOnly(t *testing.T) {
|
file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
||||||
fs := afero.NewMemMapFs()
|
require.NoError(t, err)
|
||||||
|
|
||||||
err := fs.Mkdir("/tmp/config", 0777)
|
_, err = file.Write([]byte(`key: value`))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = fs.Create("/tmp/config/config.yaml")
|
file.Close()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
v := New()
|
v := New()
|
||||||
|
|
||||||
v.SetFs(fs)
|
v.SetFs(fs)
|
||||||
v.AddConfigPath("/tmp")
|
v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
|
||||||
v.AddConfigPath("/tmp/config")
|
|
||||||
|
|
||||||
filename, err := v.getConfigFile()
|
err = v.ReadInConfig()
|
||||||
assert.Equal(t, "/tmp/config/config.yaml", filename)
|
require.NoError(t, err)
|
||||||
assert.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.Write([]byte(`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"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefault(t *testing.T) {
|
func TestDefault(t *testing.T) {
|
||||||
@@ -382,7 +510,9 @@ func TestUnmarshaling(t *testing.T) {
|
|||||||
|
|
||||||
unmarshalReader(r, v.config)
|
unmarshalReader(r, v.config)
|
||||||
assert.True(t, InConfig("name"))
|
assert.True(t, InConfig("name"))
|
||||||
|
assert.True(t, InConfig("clothing.jacket"))
|
||||||
assert.False(t, InConfig("state"))
|
assert.False(t, InConfig("state"))
|
||||||
|
assert.False(t, InConfig("clothing.hat"))
|
||||||
assert.Equal(t, "steve", Get("name"))
|
assert.Equal(t, "steve", Get("name"))
|
||||||
assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, Get("hobbies"))
|
assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, Get("hobbies"))
|
||||||
assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{"size": "large"}}, Get("clothing"))
|
assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{"size": "large"}}, Get("clothing"))
|
||||||
@@ -604,6 +734,24 @@ func TestEnvKeyReplacer(t *testing.T) {
|
|||||||
assert.Equal(t, "30s", v.Get("refresh-interval"))
|
assert.Equal(t, "30s", v.Get("refresh-interval"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnvSubConfig(t *testing.T) {
|
||||||
|
initYAML()
|
||||||
|
|
||||||
|
v.AutomaticEnv()
|
||||||
|
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|
||||||
|
testutil.Setenv(t, "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")
|
||||||
|
testutil.Setenv(t, "FOO_CLOTHING_PANTS_SIZE", "large")
|
||||||
|
assert.Equal(t, "large", subWithPrefix.Get("size"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestAllKeys(t *testing.T) {
|
func TestAllKeys(t *testing.T) {
|
||||||
initConfigs()
|
initConfigs()
|
||||||
|
|
||||||
@@ -745,8 +893,10 @@ func TestAliasesOfAliases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRecursiveAliases(t *testing.T) {
|
func TestRecursiveAliases(t *testing.T) {
|
||||||
|
Set("baz", "bat")
|
||||||
RegisterAlias("Baz", "Roo")
|
RegisterAlias("Baz", "Roo")
|
||||||
RegisterAlias("Roo", "baz")
|
RegisterAlias("Roo", "baz")
|
||||||
|
assert.Equal(t, "bat", Get("Baz"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshal(t *testing.T) {
|
func TestUnmarshal(t *testing.T) {
|
||||||
@@ -869,7 +1019,7 @@ func TestBindPFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: dupl
|
//nolint:dupl
|
||||||
func TestBindPFlagsStringSlice(t *testing.T) {
|
func TestBindPFlagsStringSlice(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Expected []string
|
Expected []string
|
||||||
@@ -917,7 +1067,7 @@ func TestBindPFlagsStringSlice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: dupl
|
//nolint:dupl
|
||||||
func TestBindPFlagsStringArray(t *testing.T) {
|
func TestBindPFlagsStringArray(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Expected []string
|
Expected []string
|
||||||
@@ -965,7 +1115,29 @@ func TestBindPFlagsStringArray(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: dupl
|
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()
|
||||||
|
|
||||||
|
if _, ok := all["int"].([]int); !ok {
|
||||||
|
t.Errorf("unexpected type %T expected []int", all["int"])
|
||||||
|
}
|
||||||
|
if _, ok := all["str"].([]string); !ok {
|
||||||
|
t.Errorf("unexpected type %T expected []string", all["str"])
|
||||||
|
}
|
||||||
|
if _, ok := all["duration"].([]time.Duration); !ok {
|
||||||
|
t.Errorf("unexpected type %T expected []time.Duration", all["duration"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:dupl
|
||||||
func TestBindPFlagsIntSlice(t *testing.T) {
|
func TestBindPFlagsIntSlice(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Expected []int
|
Expected []int
|
||||||
@@ -1084,6 +1256,53 @@ func TestBindPFlagStringToString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error binding flag set, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestMap struct {
|
||||||
|
StringToInt map[string]int
|
||||||
|
}
|
||||||
|
val := &TestMap{}
|
||||||
|
if err := v.Unmarshal(val); err != nil {
|
||||||
|
t.Fatalf("%+#v cannot unmarshal: %s", testValue.Value, err)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
assert.Equal(t, testValue.Expected, val.StringToInt)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, defaultVal, val.StringToInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBoundCaseSensitivity(t *testing.T) {
|
func TestBoundCaseSensitivity(t *testing.T) {
|
||||||
assert.Equal(t, "brown", Get("eyes"))
|
assert.Equal(t, "brown", Get("eyes"))
|
||||||
|
|
||||||
@@ -1239,7 +1458,9 @@ func TestReadBufConfig(t *testing.T) {
|
|||||||
t.Log(v.AllKeys())
|
t.Log(v.AllKeys())
|
||||||
|
|
||||||
assert.True(t, v.InConfig("name"))
|
assert.True(t, v.InConfig("name"))
|
||||||
|
assert.True(t, v.InConfig("clothing.jacket"))
|
||||||
assert.False(t, v.InConfig("state"))
|
assert.False(t, v.InConfig("state"))
|
||||||
|
assert.False(t, v.InConfig("clothing.hat"))
|
||||||
assert.Equal(t, "steve", v.Get("name"))
|
assert.Equal(t, "steve", v.Get("name"))
|
||||||
assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
|
assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
|
||||||
assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{"size": "large"}}, v.Get("clothing"))
|
assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[string]interface{}{"size": "large"}}, v.Get("clothing"))
|
||||||
@@ -1353,6 +1574,18 @@ func TestWrongDirsSearchNotFoundForMerge(t *testing.T) {
|
|||||||
assert.Equal(t, `default`, v.GetString(`key`))
|
assert.Equal(t, `default`, v.GetString(`key`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var yamlInvalid = []byte(`hash: map
|
||||||
|
- foo
|
||||||
|
- bar
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestUnwrapParseErrors(t *testing.T) {
|
||||||
|
SetConfigType("yaml")
|
||||||
|
if !errors.As(ReadConfig(bytes.NewBuffer(yamlInvalid)), &ConfigParseError{}) {
|
||||||
|
t.Fatalf("not a ConfigParseError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSub(t *testing.T) {
|
func TestSub(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
v.SetConfigType("yaml")
|
v.SetConfigType("yaml")
|
||||||
@@ -1369,6 +1602,14 @@ func TestSub(t *testing.T) {
|
|||||||
|
|
||||||
subv = v.Sub("missing.key")
|
subv = v.Sub("missing.key")
|
||||||
assert.Equal(t, (*Viper)(nil), subv)
|
assert.Equal(t, (*Viper)(nil), subv)
|
||||||
|
|
||||||
|
subv = v.Sub("clothing")
|
||||||
|
assert.Equal(t, subv.parents[0], "clothing")
|
||||||
|
|
||||||
|
subv = v.Sub("clothing").Sub("pants")
|
||||||
|
assert.Equal(t, len(subv.parents), 2)
|
||||||
|
assert.Equal(t, subv.parents[0], "clothing")
|
||||||
|
assert.Equal(t, subv.parents[1], "pants")
|
||||||
}
|
}
|
||||||
|
|
||||||
var hclWriteExpected = []byte(`"foos" = {
|
var hclWriteExpected = []byte(`"foos" = {
|
||||||
@@ -1427,21 +1668,21 @@ p_ppu = 0.55
|
|||||||
p_batters.batter.type = Regular
|
p_batters.batter.type = Regular
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var yamlWriteExpected = []byte(`age: 35
|
// var yamlWriteExpected = []byte(`age: 35
|
||||||
beard: true
|
// beard: true
|
||||||
clothing:
|
// clothing:
|
||||||
jacket: leather
|
// jacket: leather
|
||||||
pants:
|
// pants:
|
||||||
size: large
|
// size: large
|
||||||
trousers: denim
|
// trousers: denim
|
||||||
eyes: brown
|
// eyes: brown
|
||||||
hacker: true
|
// hacker: true
|
||||||
hobbies:
|
// hobbies:
|
||||||
- skateboarding
|
// - skateboarding
|
||||||
- snowboarding
|
// - snowboarding
|
||||||
- go
|
// - go
|
||||||
name: steve
|
// name: steve
|
||||||
`)
|
// `)
|
||||||
|
|
||||||
func TestWriteConfig(t *testing.T) {
|
func TestWriteConfig(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
@@ -1685,7 +1926,7 @@ func TestSafeWriteConfig(t *testing.T) {
|
|||||||
v.SetConfigType("yaml")
|
v.SetConfigType("yaml")
|
||||||
require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)))
|
require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)))
|
||||||
require.NoError(t, v.SafeWriteConfig())
|
require.NoError(t, v.SafeWriteConfig())
|
||||||
read, err := afero.ReadFile(fs, "/test/c.yaml")
|
read, err := afero.ReadFile(fs, testutil.AbsFilePath(t, "/test/c.yaml"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, yamlWriteExpected, read)
|
assert.Equal(t, yamlWriteExpected, read)
|
||||||
}
|
}
|
||||||
@@ -1702,7 +1943,7 @@ func TestSafeWriteConfigWithMissingConfigPath(t *testing.T) {
|
|||||||
func TestSafeWriteConfigWithExistingFile(t *testing.T) {
|
func TestSafeWriteConfigWithExistingFile(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
fs.Create("/test/c.yaml")
|
fs.Create(testutil.AbsFilePath(t, "/test/c.yaml"))
|
||||||
v.SetFs(fs)
|
v.SetFs(fs)
|
||||||
v.AddConfigPath("/test")
|
v.AddConfigPath("/test")
|
||||||
v.SetConfigName("c")
|
v.SetConfigName("c")
|
||||||
@@ -1738,6 +1979,23 @@ func TestSafeWriteConfigAsWithExistingFile(t *testing.T) {
|
|||||||
assert.True(t, ok, "Expected 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(`
|
var yamlMergeExampleTgt = []byte(`
|
||||||
hello:
|
hello:
|
||||||
pop: 37890
|
pop: 37890
|
||||||
@@ -1764,6 +2022,24 @@ hello:
|
|||||||
fu: bar
|
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) {
|
func TestMergeConfig(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
v.SetConfigType("yml")
|
v.SetConfigType("yml")
|
||||||
@@ -1787,6 +2063,10 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
t.Fatalf("uint pop != 37890, = %d", pop)
|
t.Fatalf("uint pop != 37890, = %d", pop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pop := v.GetUint16("hello.pop"); pop != uint16(37890) {
|
||||||
|
t.Fatalf("uint pop != 37890, = %d", pop)
|
||||||
|
}
|
||||||
|
|
||||||
if pop := v.GetUint32("hello.pop"); pop != 37890 {
|
if pop := v.GetUint32("hello.pop"); pop != 37890 {
|
||||||
t.Fatalf("uint32 pop != 37890, = %d", pop)
|
t.Fatalf("uint32 pop != 37890, = %d", pop)
|
||||||
}
|
}
|
||||||
@@ -1836,6 +2116,26 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeConfigOverrideType(t *testing.T) {
|
||||||
|
v := New()
|
||||||
|
v.SetConfigType("json")
|
||||||
|
if err := v.ReadConfig(bytes.NewBuffer(jsonMergeExampleTgt)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.MergeConfig(bytes.NewBuffer(jsonMergeExampleSrc)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pop := v.GetString("hello.pop"); pop != "pop str" {
|
||||||
|
t.Fatalf("pop != \"pop str\", = %s", pop)
|
||||||
|
}
|
||||||
|
|
||||||
|
if foo := v.GetString("hello.foo"); foo != "foo str" {
|
||||||
|
t.Fatalf("foo != \"foo str\", = %s", foo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMergeConfigNoMerge(t *testing.T) {
|
func TestMergeConfigNoMerge(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
v.SetConfigType("yml")
|
v.SetConfigType("yml")
|
||||||
@@ -2142,7 +2442,7 @@ func newViperWithConfigFile(t *testing.T) (*Viper, string, func()) {
|
|||||||
watchDir, err := ioutil.TempDir("", "")
|
watchDir, err := ioutil.TempDir("", "")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
configFile := path.Join(watchDir, "config.yaml")
|
configFile := path.Join(watchDir, "config.yaml")
|
||||||
err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0640)
|
err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0o640)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
os.RemoveAll(watchDir)
|
os.RemoveAll(watchDir)
|
||||||
@@ -2159,11 +2459,11 @@ func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string, func
|
|||||||
watchDir, err := ioutil.TempDir("", "")
|
watchDir, err := ioutil.TempDir("", "")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
dataDir1 := path.Join(watchDir, "data1")
|
dataDir1 := path.Join(watchDir, "data1")
|
||||||
err = os.Mkdir(dataDir1, 0777)
|
err = os.Mkdir(dataDir1, 0o777)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
realConfigFile := path.Join(dataDir1, "config.yaml")
|
realConfigFile := path.Join(dataDir1, "config.yaml")
|
||||||
t.Logf("Real config file location: %s\n", realConfigFile)
|
t.Logf("Real config file location: %s\n", realConfigFile)
|
||||||
err = ioutil.WriteFile(realConfigFile, []byte("foo: bar\n"), 0640)
|
err = ioutil.WriteFile(realConfigFile, []byte("foo: bar\n"), 0o640)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
os.RemoveAll(watchDir)
|
os.RemoveAll(watchDir)
|
||||||
@@ -2198,13 +2498,16 @@ func TestWatchFile(t *testing.T) {
|
|||||||
t.Logf("test config file: %s\n", configFile)
|
t.Logf("test config file: %s\n", configFile)
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
var wgDoneOnce sync.Once // OnConfigChange is called twice on Windows
|
||||||
v.OnConfigChange(func(in fsnotify.Event) {
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
t.Logf("config file changed")
|
t.Logf("config file changed")
|
||||||
wg.Done()
|
wgDoneOnce.Do(func() {
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
v.WatchConfig()
|
v.WatchConfig()
|
||||||
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
||||||
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640)
|
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0o640)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
// then the config value should have changed
|
// then the config value should have changed
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@@ -2227,10 +2530,10 @@ func TestWatchFile(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
// when link to another `config.yaml` file
|
// when link to another `config.yaml` file
|
||||||
dataDir2 := path.Join(watchDir, "data2")
|
dataDir2 := path.Join(watchDir, "data2")
|
||||||
err := os.Mkdir(dataDir2, 0777)
|
err := os.Mkdir(dataDir2, 0o777)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
configFile2 := path.Join(dataDir2, "config.yaml")
|
configFile2 := path.Join(dataDir2, "config.yaml")
|
||||||
err = ioutil.WriteFile(configFile2, []byte("foo: baz\n"), 0640)
|
err = ioutil.WriteFile(configFile2, []byte("foo: baz\n"), 0o640)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
// change the symlink using the `ln -sfn` command
|
// change the symlink using the `ln -sfn` command
|
||||||
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
|
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
|
||||||
@@ -2259,25 +2562,25 @@ func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
|
|||||||
assert.Equal(t, "cobra_flag", config.Foo.Bar)
|
assert.Equal(t, "cobra_flag", config.Foo.Bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
var yamlExampleWithDot = []byte(`Hacker: true
|
// var yamlExampleWithDot = []byte(`Hacker: true
|
||||||
name: steve
|
// name: steve
|
||||||
hobbies:
|
// hobbies:
|
||||||
- skateboarding
|
// - skateboarding
|
||||||
- snowboarding
|
// - snowboarding
|
||||||
- go
|
// - go
|
||||||
clothing:
|
// clothing:
|
||||||
jacket: leather
|
// jacket: leather
|
||||||
trousers: denim
|
// trousers: denim
|
||||||
pants:
|
// pants:
|
||||||
size: large
|
// size: large
|
||||||
age: 35
|
// age: 35
|
||||||
eyes : brown
|
// eyes : brown
|
||||||
beard: true
|
// beard: true
|
||||||
emails:
|
// emails:
|
||||||
steve@hacker.com:
|
// steve@hacker.com:
|
||||||
created: 01/02/03
|
// created: 01/02/03
|
||||||
active: true
|
// active: true
|
||||||
`)
|
// `)
|
||||||
|
|
||||||
func TestKeyDelimiter(t *testing.T) {
|
func TestKeyDelimiter(t *testing.T) {
|
||||||
v := NewWithOptions(KeyDelimiter("::"))
|
v := NewWithOptions(KeyDelimiter("::"))
|
||||||
@@ -2327,7 +2630,10 @@ func TestKeyDelimiter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var yamlDeepNestedSlices = []byte(`TV:
|
var yamlDeepNestedSlices = []byte(`TV:
|
||||||
- title: "The expanse"
|
- title: "The Expanse"
|
||||||
|
title_i18n:
|
||||||
|
USA: "The Expanse"
|
||||||
|
Japan: "エクスパンス -巨獣めざめる-"
|
||||||
seasons:
|
seasons:
|
||||||
- first_released: "December 14, 2015"
|
- first_released: "December 14, 2015"
|
||||||
episodes:
|
episodes:
|
||||||
@@ -2357,11 +2663,15 @@ func TestSliceIndexAccess(t *testing.T) {
|
|||||||
err := v.unmarshalReader(r, v.config)
|
err := v.unmarshalReader(r, v.config)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "The expanse", v.GetString("tv.0.title"))
|
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, "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, "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"))
|
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
|
// Test for index out of bounds
|
||||||
assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released"))
|
assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released"))
|
||||||
|
|
||||||
@@ -2405,3 +2715,10 @@ func BenchmarkGetBoolFromMap(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
`)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// +build !js
|
//go:build darwin || dragonfly || freebsd || openbsd || linux || netbsd || solaris || windows
|
||||||
|
// +build darwin dragonfly freebsd openbsd linux netbsd solaris windows
|
||||||
|
|
||||||
package viper
|
package viper
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
// +build js,wasm
|
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
|
||||||
|
// +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
|
||||||
|
|
||||||
package viper
|
package viper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func newWatcher() (*watcher, error) {
|
||||||
|
return &watcher{}, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
type watcher struct {
|
type watcher struct {
|
||||||
Events chan fsnotify.Event
|
Events chan fsnotify.Event
|
||||||
Errors chan error
|
Errors chan error
|
||||||
@@ -24,7 +30,3 @@ func (*watcher) Add(name string) error {
|
|||||||
func (*watcher) Remove(name string) error {
|
func (*watcher) Remove(name string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWatcher() (*watcher, error) {
|
|
||||||
return &watcher{}, errors.New("fsnotify is not supported on WASM")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user