Compare commits

..
4 Commits
Author SHA1 Message Date
Mark Sagi-Kazar 99e9188c7c docs: propose deprecating the global Viper instance
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-09-23 10:07:37 +02:00
Mark Sagi-Kazar c943b3ef27 docs: propose dropping file writing support
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-09-22 23:10:51 +02:00
Mark Sagi-Kazar a0f1caa4ed docs: document a decision about Go version support
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-09-16 09:04:54 +02:00
Mark Sagi-Kazar d2e3a7e5c2 docs: document some architectural decisions
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-07-20 23:54:12 +02:00
65 changed files with 926 additions and 4095 deletions
+1
View File
@@ -0,0 +1 @@
docs/adr
+1 -1
View File
@@ -11,5 +11,5 @@ trim_trailing_whitespace = true
[*.go]
indent_style = tab
[{Makefile,*.mk}]
[{Makefile, *.mk}]
indent_style = tab
+1 -10
View File
@@ -19,22 +19,13 @@ body:
required: true
- label: I am not looking for support or already pursued the available [support channels](https://github.com/spf13/viper/issues/new/choose) without success.
required: true
- label: I have checked the [troubleshooting guide](https://github.com/spf13/viper/blob/master/TROUBLESHOOTING.md) for my problem, without success.
required: true
- type: input
attributes:
label: Viper Version
label: Version
description: What version of Viper are you using?
placeholder: 1.8.1
validations:
required: true
- type: input
attributes:
label: Go Version
description: What version of Go are you using?
placeholder: "1.16"
validations:
required: true
- type: dropdown
attributes:
label: Config Source
+1 -1
View File
@@ -1,4 +1,4 @@
name: 🎉 Feature request
name: 🚀 Feature request
description: Suggest an idea for Viper
labels: [kind/enhancement]
body:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 21 KiB

-30
View File
@@ -1,30 +0,0 @@
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:
- "*"
-18
View File
@@ -1,18 +0,0 @@
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"
-85
View File
@@ -1,85 +0,0 @@
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
+35
View File
@@ -0,0 +1,35 @@
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
-72
View File
@@ -1,72 +0,0 @@
# 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:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v4
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
@@ -6,11 +6,11 @@ jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v4
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
+26
View File
@@ -0,0 +1,26 @@
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 .
+5 -10
View File
@@ -3,10 +3,7 @@ run:
linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/spf13/viper)
local-prefixes: github.com/spf13/viper
golint:
min-confidence: 0
goimports:
@@ -16,12 +13,14 @@ linters:
disable-all: true
enable:
- bodyclose
- deadcode
- dogsled
- dupl
- durationcheck
- exhaustive
- exportloopref
- gci
- goconst
- gofmt
- gofumpt
- goimports
@@ -42,12 +41,14 @@ linters:
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- stylecheck
- tparallel
- typecheck
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- whitespace
@@ -61,7 +62,6 @@ linters:
# - gochecknoglobals
# - gochecknoinits
# - gocognit
# - goconst
# - gocritic
# - gocyclo
# - godot
@@ -80,11 +80,6 @@ linters:
# - goheader
# - gomodguard
# deprecated
# - deadcode
# - structcheck
# - varcheck
# don't enable:
# - asciicheck
# - funlen
+3 -3
View File
@@ -15,8 +15,8 @@ TEST_FORMAT = short-verbose
endif
# Dependency versions
GOTESTSUM_VERSION = 1.9.0
GOLANGCI_VERSION = 1.52.2
GOTESTSUM_VERSION = 1.6.4
GOLANGCI_VERSION = 1.40.1
# Add the ability to override some variables
# Use with care
@@ -48,7 +48,7 @@ bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION}
@ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint
bin/golangci-lint-${GOLANGCI_VERSION}:
@mkdir -p bin
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION}
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION}
@mv bin/golangci-lint "$@"
.PHONY: lint
+46 -56
View File
@@ -8,10 +8,10 @@
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#configuration)
[![run on repl.it](https://repl.it/badge/github/sagikazarmark/Viper-example)](https://repl.it/@sagikazarmark/Viper-example#main.go)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spf13/viper/ci.yaml?branch=master&style=flat-square)](https://github.com/spf13/viper/actions?query=workflow%3ACI)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/spf13/viper/CI?style=flat-square)](https://github.com/spf13/viper/actions?query=workflow%3ACI)
[![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Go Report Card](https://goreportcard.com/badge/github.com/spf13/viper?style=flat-square)](https://goreportcard.com/report/github.com/spf13/viper)
![Go Version](https://img.shields.io/badge/go%20version-%3E=1.16-61CFDD.svg?style=flat-square)
![Go Version](https://img.shields.io/badge/go%20version-%3E=1.14-61CFDD.svg?style=flat-square)
[![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/spf13/viper)](https://pkg.go.dev/mod/github.com/spf13/viper)
**Go configuration with fangs!**
@@ -27,9 +27,6 @@ Many Go projects are built using Viper including:
* [doctl](https://github.com/digitalocean/doctl)
* [Clairctl](https://github.com/jgsqware/clairctl)
* [Mercure](https://mercure.rocks)
* [Meshery](https://github.com/meshery/meshery)
* [Bearer](https://github.com/bearer/bearer)
* [Coder](https://github.com/coder/coder)
## Install
@@ -43,8 +40,8 @@ go get github.com/spf13/viper
## What is Viper?
Viper is a complete configuration solution for Go applications including [12-Factor apps](https://12factor.net/#the_twelve_factors).
It is designed to work within an application, and can handle all types of configuration needs
Viper is a complete configuration solution for Go applications including 12-Factor apps. It is designed
to work within an application, and can handle all types of configuration needs
and formats. It supports:
* setting defaults
@@ -122,7 +119,7 @@ viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search
viper.AddConfigPath(".") // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("fatal error config file: %w", err))
panic(fmt.Errorf("Fatal error config file: %w \n", err))
}
```
@@ -130,11 +127,11 @@ You can handle the specific case where no config file is found like this:
```go
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
} else {
// Config file was found but another error was produced
}
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
} else {
// Config file was found but another error was produced
}
}
// Config file found and successfully parsed
@@ -178,10 +175,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()`**
```go
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()
```
### Reading Config from io.Reader
@@ -357,7 +354,7 @@ func main() {
i := viper.GetInt("flagname") // retrieve value from viper
// ...
...
}
```
@@ -450,13 +447,6 @@ viper.SetConfigType("json") // because there is no file extension in a stream of
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
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:
@@ -513,18 +503,18 @@ runtime_viper.Unmarshal(&runtime_conf)
// open a goroutine to watch remote changes forever
go func(){
for {
time.Sleep(time.Second * 5) // delay after each request
time.Sleep(time.Second * 5) // delay after each request
// currently, only tested with etcd support
err := runtime_viper.WatchRemoteConfig()
if err != nil {
log.Errorf("unable to read remote config: %v", err)
continue
}
// currently, only tested with etcd support
err := runtime_viper.WatchRemoteConfig()
if err != nil {
log.Errorf("unable to read remote config: %v", err)
continue
}
// unmarshal new config into our runtime config struct. you can also use channel
// to implement a signal to notify the system of the changes
runtime_viper.Unmarshal(&runtime_conf)
// unmarshal new config into our runtime config struct. you can also use channel
// to implement a signal to notify the system of the changes
runtime_viper.Unmarshal(&runtime_conf)
}
}()
```
@@ -556,7 +546,7 @@ Example:
```go
viper.GetString("logfile") // case-insensitive Setting & Getting
if viper.GetBool("verbose") {
fmt.Println("verbose enabled")
fmt.Println("verbose enabled")
}
```
### Accessing nested keys
@@ -604,7 +594,7 @@ configuration level.
Viper can access array indices by using numbers in the path. For example:
```jsonc
```json
{
"host": {
"address": "localhost",
@@ -632,7 +622,7 @@ GetInt("host.ports.1") // returns 6029
Lastly, if there exists a key that matches the delimited key path, its value
will be returned instead. E.g.
```jsonc
```json
{
"datastore.metric.host": "0.0.0.0",
"host": {
@@ -679,7 +669,7 @@ So instead of doing that let's pass a Viper instance to the constructor that rep
```go
cache1Config := viper.Sub("cache.cache1")
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)
@@ -691,10 +681,10 @@ Internally, the `NewCache` function can address `max-items` and `item-size` keys
```go
func NewCache(v *Viper) *Cache {
return &Cache{
MaxItems: v.GetInt("max-items"),
ItemSize: v.GetInt("item-size"),
}
return &Cache{
MaxItems: v.GetInt("max-items"),
ItemSize: v.GetInt("item-size"),
}
}
```
@@ -736,18 +726,18 @@ you have to change the delimiter:
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
v.SetDefault("chart::values", map[string]interface{}{
"ingress": map[string]interface{}{
"annotations": map[string]interface{}{
"traefik.frontend.rule.type": "PathPrefix",
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
},
},
"ingress": map[string]interface{}{
"annotations": map[string]interface{}{
"traefik.frontend.rule.type": "PathPrefix",
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
},
},
})
type config struct {
Chart struct{
Values map[string]interface{}
}
Values map[string]interface{}
}
}
var C config
@@ -804,17 +794,17 @@ You can use your favorite format's marshaller with the config returned by `AllSe
```go
import (
yaml "gopkg.in/yaml.v2"
// ...
yaml "gopkg.in/yaml.v2"
// ...
)
func yamlStringSettings() string {
c := viper.AllSettings()
bs, err := yaml.Marshal(c)
if err != nil {
log.Fatalf("unable to marshal config to YAML: %v", err)
}
return string(bs)
c := viper.AllSettings()
bs, err := yaml.Marshal(c)
if err != nil {
log.Fatalf("unable to marshal config to YAML: %v", err)
}
return string(bs)
}
```
-9
View File
@@ -21,12 +21,3 @@ 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.
**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,19 @@
# 1. Record architecture decisions
Date: 2021-07-20
## Status
Proposed
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
@@ -0,0 +1,32 @@
# 2. Prefer making backward compatible changes
Date: 2021-07-20
## Status
Proposed
Referenced by [3. Extract components with heavy dependencies from the core](0003-extract-components-with-heavy-dependencies-from-the-core.md)
Referenced by [4. Use separate GitHub organization for new packages](0004-use-separate-github-organization-for-new-packages.md)
Referenced by [7. Drop writing support](0007-drop-writing-support.md)
## Context
Architecturally speaking Viper became a giant over the years: it hides a lot of complexity behind a simple interface.
That simple interface, however, is what makes Viper extremely popular.
## Decision
In order to keep the library useful to people, we should prefer making backward compatible changes to Viper, even between major releases.
This is not a hard rule forbiding breaking changes though: when it makes sense, breaking changes are allowed,
but keeping things backward compatible is a priority.
## Consequences
Although major versions allow breaking changes, a major release is no reason to break things that already work for a lot of people,
even if it might not be the best possible solution.
Instead of breaking things, introducing new interfaces should be the default way of fixing architectural problems,
leaving old interfaces intact.
@@ -0,0 +1,26 @@
# 3. Extract components with heavy dependencies from the core
Date: 2021-07-20
## Status
Proposed
References [2. Prefer making backward compatible changes](0002-prefer-making-backward-compatible-changes.md)
Referenced by [4. Use separate GitHub organization for new packages](0004-use-separate-github-organization-for-new-packages.md)
## Context
Viper (v1) currently imports a bunch of external dependencies (for encoding/decoding, remote stores, etc)
that make the library itself quite a heavy dependency.
## Decision
Move components with external dependencies out of the core to separate packages.
## Consequences
Viper 1 will have to continue importing all of these packages to maintain backwards compatibility.
Viper 2 (and future versions) on the other hand can break backwards compatibility and require users to import the required packages.
@@ -0,0 +1,24 @@
# 4. Use separate GitHub organization for new packages
Date: 2021-07-20
## Status
Proposed
References [2. Prefer making backward compatible changes](0002-prefer-making-backward-compatible-changes.md)
References [3. Extract components with heavy dependencies from the core](0003-extract-components-with-heavy-dependencies-from-the-core.md)
## Context
The core Viper package is under a personal GitHub account which makes collaborative development a bit difficult.
## Decision
Create new Go modules in the [go-viper](https://github.com/go-viper) organization.
Keep the core library under [Steve's personal account](https://github.com/spf13/viper) for backward compatibility purposes.
## Consequences
It'll be easier to create new modules and to add new functionality to Viper without having to add new dependencies to the core library.
@@ -0,0 +1,30 @@
# 5. Deprecate setters in favor of functional options during initialization
Date: 2021-07-20
## Status
Proposed
Referenced by [8. Deprecate the global Viper instance](0008-deprecate-the-global-viper-instance.md)
## Context
The Viper struct currently acts as a facade for reading, writing and watching configuration for changes.
Some of the configuration parameters can be changed runtime using setters which often lead to issues
with concurrent activities.
## Decision
Deprecate setters in favor of using functional options for configuring Viper when it's initialized.
Drop setters in Viper 2.
## Consequences
Since Viper's interface is usually invoked from a lot of places,
moving configuration to the place where it is initialized makes using Viper safer
(ie. someone can't just randomly call `Set` when they are only supposed to call `Get*`).
This change will also clarify what roles Viper can be used in and
makes the separation of internal components easier based on these roles.
+20
View File
@@ -0,0 +1,20 @@
# 6. Go version support
Date: 2021-09-16
## Status
Proposed
## Context
From time to time new features are released in the Go language.
Relying on those features means dropping support for older Go versions.
## Decision
Follow the [Go release policy](https://golang.org/doc/devel/release#policy) and support the last two major versions of Go.
## Consequences
Support for older Go versions will happen every 6 months according to the Go release cycle.
+22
View File
@@ -0,0 +1,22 @@
# 7. Drop writing support
Date: 2021-09-22
## Status
Proposed
References [2. Prefer making backward compatible changes](0002-prefer-making-backward-compatible-changes.md)
## Context
The number one source of issues for Viper comes from the fact that it supports both reading and writing.
It causes concurrency issues and has lots of inconsistencies.
## Decision
Drop file writing support from Viper in v2.
## Consequences
This is going to be a major breaking change in the library, but it will make maintenance significantly easier.
@@ -0,0 +1,23 @@
# 8. Deprecate the global Viper instance
Date: 2021-09-23
## Status
Proposed
References [5. Deprecate setters in favor of functional options during initialization](0005-deprecate-setters-in-favor-of-functional-options-during-initialization.md)
## Context
With the deprecation of setters in favor of functional options, it becomes almost impossible to get away with instantiating Viper.
In addition to that, people should be discouraged from accessing a global Viper instance.
## Decision
Deprecate the global Viper instance and the global access functions.
## Consequences
People will still be able to create a global instance of their own,
but instantiating a custom Viper instance will become the primary solution for using Viper.
-11
View File
@@ -1,11 +0,0 @@
//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
})
}
-65
View File
@@ -1,65 +0,0 @@
//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
View File
@@ -1,100 +0,0 @@
//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)
})
}
}
+13 -68
View File
@@ -1,76 +1,21 @@
module github.com/spf13/viper
go 1.17
go 1.12
require (
github.com/fsnotify/fsnotify v1.6.0
github.com/bketelsen/crypt v0.0.4
github.com/fsnotify/fsnotify v1.4.9
github.com/hashicorp/hcl v1.0.0
github.com/magiconair/properties v1.8.7
github.com/mitchellh/mapstructure v1.5.0
github.com/pelletier/go-toml/v2 v2.0.8
github.com/sagikazarmark/crypt v0.10.0
github.com/spf13/afero v1.9.5
github.com/spf13/cast v1.5.1
github.com/magiconair/properties v1.8.5
github.com/mitchellh/mapstructure v1.4.1
github.com/pelletier/go-toml v1.9.3
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/spf13/afero v1.6.0
github.com/spf13/cast v1.3.1
github.com/spf13/jwalterweatherman v1.1.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.3
github.com/subosito/gotenv v1.4.2
gopkg.in/ini.v1 v1.67.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
github.com/stretchr/testify v1.7.0
github.com/subosito/gotenv v1.2.0
gopkg.in/ini.v1 v1.62.0
gopkg.in/yaml.v2 v2.4.0
)
+100 -1286
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -4,10 +4,10 @@ import (
"sync"
)
// Decoder decodes the contents of b into v.
// Decoder decodes the contents of b into a v representation.
// It's primarily used for decoding contents of a file into a map[string]interface{}.
type Decoder interface {
Decode(b []byte, v map[string]interface{}) error
Decode(b []byte, v interface{}) error
}
const (
@@ -48,7 +48,7 @@ func (e *DecoderRegistry) RegisterDecoder(format string, enc Decoder) error {
}
// Decode calls the underlying Decoder based on the format.
func (e *DecoderRegistry) Decode(format string, b []byte, v map[string]interface{}) error {
func (e *DecoderRegistry) Decode(format string, b []byte, v interface{}) error {
e.mu.RLock()
decoder, ok := e.decoders[format]
e.mu.RUnlock()
+11 -15
View File
@@ -1,18 +1,16 @@
package encoding
import (
"reflect"
"testing"
)
type decoder struct {
v map[string]interface{}
v interface{}
}
func (d decoder) Decode(_ []byte, v map[string]interface{}) error {
for key, value := range d.v {
v[key] = value
}
func (d decoder) Decode(_ []byte, v interface{}) error {
rv := v.(*string)
*rv = d.v.(string)
return nil
}
@@ -46,9 +44,7 @@ func TestDecoderRegistry_Decode(t *testing.T) {
t.Run("OK", func(t *testing.T) {
registry := NewDecoderRegistry()
decoder := decoder{
v: map[string]interface{}{
"key": "value",
},
v: "decoded value",
}
err := registry.RegisterDecoder("myformat", decoder)
@@ -56,24 +52,24 @@ func TestDecoderRegistry_Decode(t *testing.T) {
t.Fatal(err)
}
v := map[string]interface{}{}
var v string
err = registry.Decode("myformat", []byte("key: value"), v)
err = registry.Decode("myformat", []byte("some value"), &v)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(decoder.v, v) {
t.Fatalf("decoded value does not match the expected one\nactual: %+v\nexpected: %+v", v, decoder.v)
if v != "decoded value" {
t.Fatalf("expected 'decoded value', got: %#v", v)
}
})
t.Run("DecoderNotFound", func(t *testing.T) {
registry := NewDecoderRegistry()
v := map[string]interface{}{}
var v string
err := registry.Decode("myformat", nil, v)
err := registry.Decode("myformat", []byte("some value"), &v)
if err != ErrDecoderNotFound {
t.Fatalf("expected ErrDecoderNotFound, got: %v", err)
}
-61
View File
@@ -1,61 +0,0 @@
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
}
-63
View File
@@ -1,63 +0,0 @@
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)
})
}
-41
View File
@@ -1,41 +0,0 @@
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
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
// Encoder encodes the contents of v into a byte representation.
// It's primarily used for encoding a map[string]interface{} into a file format.
type Encoder interface {
Encode(v map[string]interface{}) ([]byte, error)
Encode(v interface{}) ([]byte, error)
}
const (
@@ -47,7 +47,7 @@ func (e *EncoderRegistry) RegisterEncoder(format string, enc Encoder) error {
return nil
}
func (e *EncoderRegistry) Encode(format string, v map[string]interface{}) ([]byte, error) {
func (e *EncoderRegistry) Encode(format string, v interface{}) ([]byte, error) {
e.mu.RLock()
encoder, ok := e.encoders[format]
e.mu.RUnlock()
+6 -6
View File
@@ -8,7 +8,7 @@ type encoder struct {
b []byte
}
func (e encoder) Encode(_ map[string]interface{}) ([]byte, error) {
func (e encoder) Encode(_ interface{}) ([]byte, error) {
return e.b, nil
}
@@ -41,7 +41,7 @@ func TestEncoderRegistry_Decode(t *testing.T) {
t.Run("OK", func(t *testing.T) {
registry := NewEncoderRegistry()
encoder := encoder{
b: []byte("key: value"),
b: []byte("encoded value"),
}
err := registry.RegisterEncoder("myformat", encoder)
@@ -49,20 +49,20 @@ func TestEncoderRegistry_Decode(t *testing.T) {
t.Fatal(err)
}
b, err := registry.Encode("myformat", map[string]interface{}{"key": "value"})
b, err := registry.Encode("myformat", "some value")
if err != nil {
t.Fatal(err)
}
if string(b) != "key: value" {
t.Fatalf("expected 'key: value', got: %#v", string(b))
if string(b) != "encoded value" {
t.Fatalf("expected 'encoded value', got: %#v", string(b))
}
})
t.Run("EncoderNotFound", func(t *testing.T) {
registry := NewEncoderRegistry()
_, err := registry.Encode("myformat", map[string]interface{}{"key": "value"})
_, err := registry.Encode("myformat", "some value")
if err != ErrEncoderNotFound {
t.Fatalf("expected ErrEncoderNotFound, got: %v", err)
}
+3 -3
View File
@@ -12,7 +12,7 @@ import (
// TODO: add printer config to the codec?
type Codec struct{}
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
func (Codec) Encode(v interface{}) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
@@ -35,6 +35,6 @@ func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
return buf.Bytes(), nil
}
func (Codec) Decode(b []byte, v map[string]interface{}) error {
return hcl.Unmarshal(b, &v)
func (Codec) Decode(b []byte, v interface{}) error {
return hcl.Unmarshal(b, v)
}
-140
View File
@@ -1,140 +0,0 @@
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)
})
}
-99
View File
@@ -1,99 +0,0 @@
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
}
-111
View File
@@ -1,111 +0,0 @@
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)
})
}
-74
View File
@@ -1,74 +0,0 @@
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
}
-86
View File
@@ -1,86 +0,0 @@
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
}
@@ -1,89 +0,0 @@
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)
}
}
@@ -1,74 +0,0 @@
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
}
+3 -3
View File
@@ -7,11 +7,11 @@ import (
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for JSON encoding.
type Codec struct{}
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
func (Codec) Encode(v interface{}) ([]byte, error) {
// TODO: expose prefix and indent in the Codec as setting?
return json.MarshalIndent(v, "", " ")
}
func (Codec) Decode(b []byte, v map[string]interface{}) error {
return json.Unmarshal(b, &v)
func (Codec) Decode(b []byte, v interface{}) error {
return json.Unmarshal(b, v)
}
-95
View File
@@ -1,95 +0,0 @@
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)
})
}
+33 -4
View File
@@ -1,16 +1,45 @@
package toml
import (
"github.com/pelletier/go-toml/v2"
"github.com/pelletier/go-toml"
)
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for TOML encoding.
type Codec struct{}
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
func (Codec) Encode(v 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)
}
func (Codec) Decode(b []byte, v map[string]interface{}) error {
return toml.Unmarshal(b, &v)
func (Codec) Decode(b []byte, v interface{}) error {
tree, err := toml.LoadBytes(b)
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)
}
-105
View File
@@ -1,105 +0,0 @@
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)
})
}
+4 -4
View File
@@ -1,14 +1,14 @@
package yaml
import "gopkg.in/yaml.v3"
import "gopkg.in/yaml.v2"
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for YAML encoding.
type Codec struct{}
func (Codec) Encode(v map[string]interface{}) ([]byte, error) {
func (Codec) Encode(v interface{}) ([]byte, error) {
return yaml.Marshal(v)
}
func (Codec) Decode(b []byte, v map[string]interface{}) error {
return yaml.Unmarshal(b, &v)
func (Codec) Decode(b []byte, v interface{}) error {
return yaml.Unmarshal(b, v)
}
-136
View File
@@ -1,136 +0,0 @@
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,6 +1,3 @@
//go:build !go1.17
// +build !go1.17
package testutil
import (
@@ -21,6 +18,11 @@ func Setenv(t *testing.T, name, val string) {
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
// duration of the test
func setenv(t *testing.T, name, val string, valOK bool) {
-18
View File
@@ -1,18 +0,0 @@
//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)
}
-18
View File
@@ -1,18 +0,0 @@
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
}
-77
View File
@@ -1,77 +0,0 @@
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
}
+7 -13
View File
@@ -10,9 +10,8 @@ import (
"bytes"
"io"
"os"
"strings"
crypt "github.com/sagikazarmark/crypt/config"
crypt "github.com/bketelsen/crypt/config"
"github.com/spf13/viper"
)
@@ -76,7 +75,6 @@ func getConfigManager(rp viper.RemoteProvider) (crypt.ConfigManager, error) {
var cm crypt.ConfigManager
var err error
endpoints := strings.Split(rp.Endpoint(), ";")
if rp.SecretKeyring() != "" {
var kr *os.File
kr, err = os.Open(rp.SecretKeyring())
@@ -86,24 +84,20 @@ func getConfigManager(rp viper.RemoteProvider) (crypt.ConfigManager, error) {
defer kr.Close()
switch rp.Provider() {
case "etcd":
cm, err = crypt.NewEtcdConfigManager(endpoints, kr)
case "etcd3":
cm, err = crypt.NewEtcdV3ConfigManager(endpoints, kr)
cm, err = crypt.NewEtcdConfigManager([]string{rp.Endpoint()}, kr)
case "firestore":
cm, err = crypt.NewFirestoreConfigManager(endpoints, kr)
cm, err = crypt.NewFirestoreConfigManager([]string{rp.Endpoint()}, kr)
default:
cm, err = crypt.NewConsulConfigManager(endpoints, kr)
cm, err = crypt.NewConsulConfigManager([]string{rp.Endpoint()}, kr)
}
} else {
switch rp.Provider() {
case "etcd":
cm, err = crypt.NewStandardEtcdConfigManager(endpoints)
case "etcd3":
cm, err = crypt.NewStandardEtcdV3ConfigManager(endpoints)
cm, err = crypt.NewStandardEtcdConfigManager([]string{rp.Endpoint()})
case "firestore":
cm, err = crypt.NewStandardFirestoreConfigManager(endpoints)
cm, err = crypt.NewStandardFirestoreConfigManager([]string{rp.Endpoint()})
default:
cm, err = crypt.NewStandardConsulConfigManager(endpoints)
cm, err = crypt.NewStandardConsulConfigManager([]string{rp.Endpoint()})
}
}
if err != nil {
+41 -33
View File
@@ -18,7 +18,9 @@ import (
"strings"
"unicode"
"github.com/spf13/afero"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
)
// ConfigParseError denotes failing to parse configuration file.
@@ -31,11 +33,6 @@ func (pe ConfigParseError) Error() string {
return fmt.Sprintf("While parsing config: %s", pe.err.Error())
}
// Unwrap returns the wrapped error.
func (pe ConfigParseError) Unwrap() error {
return pe.err
}
// toCaseInsensitiveValue checks if the value is a map;
// if so, create a copy and lower-case the keys recursively.
func toCaseInsensitiveValue(value interface{}) interface{} {
@@ -69,25 +66,18 @@ func copyAndInsensitiviseMap(m map[string]interface{}) map[string]interface{} {
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{}) {
for key, val := range m {
val = insensitiviseVal(val)
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{}))
}
lower := strings.ToLower(key)
if key != lower {
// remove old key (not lower-cased)
@@ -98,20 +88,26 @@ func insensitiviseMap(m map[string]interface{}) {
}
}
func insensitiveArray(a []interface{}) {
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)
func absPathify(inPath string) string {
jww.INFO.Println("Trying to resolve absolute path to", inPath)
if inPath == "$HOME" || strings.HasPrefix(inPath, "$HOME"+string(os.PathSeparator)) {
inPath = userHomeDir() + inPath[5:]
}
inPath = os.ExpandEnv(inPath)
if strings.HasPrefix(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) {
return filepath.Clean(inPath)
@@ -122,11 +118,23 @@ func absPathify(logger Logger, inPath string) string {
return filepath.Clean(p)
}
logger.Error(fmt.Errorf("could not discover absolute path: %w", err).Error())
jww.ERROR.Println("Couldn't discover absolute path")
jww.ERROR.Println(err)
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
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
+1 -3
View File
@@ -58,8 +58,6 @@ func TestCopyAndInsensitiviseMap(t *testing.T) {
}
func TestAbsPathify(t *testing.T) {
skipWindows(t)
home := userHomeDir()
homer := filepath.Join(home, "homer")
wd, _ := os.Getwd()
@@ -87,7 +85,7 @@ func TestAbsPathify(t *testing.T) {
}
for _, test := range tests {
got := absPathify(jwwLogger{}, test.input)
got := absPathify(test.input)
if got != test.output {
t.Errorf("Got %v\nexpected\n%q", got, test.output)
}
+253 -303
View File
@@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
@@ -34,16 +35,17 @@ import (
"time"
"github.com/fsnotify/fsnotify"
"github.com/magiconair/properties"
"github.com/mitchellh/mapstructure"
"github.com/spf13/afero"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/pflag"
"github.com/subosito/gotenv"
"gopkg.in/ini.v1"
"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/ini"
"github.com/spf13/viper/internal/encoding/javaproperties"
"github.com/spf13/viper/internal/encoding/json"
"github.com/spf13/viper/internal/encoding/toml"
"github.com/spf13/viper/internal/encoding/yaml"
@@ -66,8 +68,44 @@ type RemoteResponse struct {
Error error
}
var (
encoderRegistry = encoding.NewEncoderRegistry()
decoderRegistry = encoding.NewDecoderRegistry()
)
func init() {
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 {
@@ -131,10 +169,10 @@ type DecoderConfigOption func(*mapstructure.DecoderConfig)
// DecodeHook returns a DecoderConfigOption which overrides the default
// DecoderConfig.DecodeHook value, the default is:
//
// mapstructure.ComposeDecodeHookFunc(
// mapstructure.StringToTimeDurationHookFunc(),
// mapstructure.StringToSliceHookFunc(","),
// )
// mapstructure.ComposeDecodeHookFunc(
// mapstructure.StringToTimeDurationHookFunc(),
// mapstructure.StringToSliceHookFunc(","),
// )
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
return func(c *mapstructure.DecoderConfig) {
c.DecodeHook = hook
@@ -155,18 +193,18 @@ func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
//
// For example, if values from the following sources were loaded:
//
// Defaults : {
// "secret": "",
// "user": "default",
// "endpoint": "https://localhost"
// }
// Config : {
// "user": "root"
// "secret": "defaultsecret"
// }
// Env : {
// "secret": "somesecretkey"
// }
// Defaults : {
// "secret": "",
// "user": "default",
// "endpoint": "https://localhost"
// }
// Config : {
// "user": "root"
// "secret": "defaultsecret"
// }
// Env : {
// "secret": "somesecretkey"
// }
//
// The resulting config will have the following values:
//
@@ -205,7 +243,6 @@ type Viper struct {
envKeyReplacer StringReplacer
allowEmptyEnv bool
parents []string
config map[string]interface{}
override map[string]interface{}
defaults map[string]interface{}
@@ -215,13 +252,11 @@ type Viper struct {
aliases map[string]string
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)
logger Logger
// TODO: should probably be protected with a mutex
encoderRegistry *encoding.EncoderRegistry
decoderRegistry *encoding.DecoderRegistry
}
// New returns an initialized Viper instance.
@@ -229,10 +264,9 @@ func New() *Viper {
v := new(Viper)
v.keyDelim = "."
v.configName = "config"
v.configPermissions = os.FileMode(0o644)
v.configPermissions = os.FileMode(0644)
v.fs = afero.NewOsFs()
v.config = make(map[string]interface{})
v.parents = []string{}
v.override = make(map[string]interface{})
v.defaults = make(map[string]interface{})
v.kvstore = make(map[string]interface{})
@@ -240,9 +274,6 @@ func New() *Viper {
v.env = make(map[string][]string)
v.aliases = make(map[string]string)
v.typeByDefValue = false
v.logger = jwwLogger{}
v.resetEncoding()
return v
}
@@ -290,8 +321,6 @@ func NewWithOptions(opts ...Option) *Viper {
opt.apply(v)
}
v.resetEncoding()
return v
}
@@ -300,86 +329,8 @@ func NewWithOptions(opts ...Option) *Viper {
// can use it in their testing as well.
func Reset() {
v = New()
SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}
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
SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"}
SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}
}
type defaultRemoteProvider struct {
@@ -417,37 +368,31 @@ type RemoteProvider interface {
}
// SupportedExts are universally supported extensions.
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"}
// SupportedRemoteProviders are universally supported remote providers.
var SupportedRemoteProviders = []string{"etcd", "etcd3", "consul", "firestore"}
var SupportedRemoteProviders = []string{"etcd", "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) }
// OnConfigChange sets the event handler that is called when a config file changes.
func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
v.onConfigChange = run
}
// WatchConfig starts watching a config file for changes.
func WatchConfig() { v.WatchConfig() }
// WatchConfig starts watching a config file for changes.
func (v *Viper) WatchConfig() {
initWG := sync.WaitGroup{}
initWG.Add(1)
go func() {
watcher, err := newWatcher()
if err != nil {
v.logger.Error(fmt.Sprintf("failed to create watcher: %s", err))
os.Exit(1)
log.Fatal(err)
}
defer watcher.Close()
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
filename, err := v.getConfigFile()
if err != nil {
v.logger.Error(fmt.Sprintf("get config file: %s", err))
log.Printf("error: %v\n", err)
initWG.Done()
return
}
@@ -470,25 +415,27 @@ func (v *Viper) WatchConfig() {
// we only care about the config file with the following cases:
// 1 - if the config file was modified or created
// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
const writeOrCreateMask = fsnotify.Write | fsnotify.Create
if (filepath.Clean(event.Name) == configFile &&
(event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) ||
event.Op&writeOrCreateMask != 0) ||
(currentConfigFile != "" && currentConfigFile != realConfigFile) {
realConfigFile = currentConfigFile
err := v.ReadInConfig()
if err != nil {
v.logger.Error(fmt.Sprintf("read config file: %s", err))
log.Printf("error reading config file: %v\n", err)
}
if v.onConfigChange != nil {
v.onConfigChange(event)
}
} else if filepath.Clean(event.Name) == configFile && event.Has(fsnotify.Remove) {
} else if filepath.Clean(event.Name) == configFile &&
event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
eventsWG.Done()
return
}
case err, ok := <-watcher.Errors:
if ok { // 'Errors' channel is not closed
v.logger.Error(fmt.Sprintf("watcher error: %s", err))
log.Printf("watcher error: %v\n", err)
}
eventsWG.Done()
return
@@ -567,9 +514,8 @@ func AddConfigPath(in string) { v.AddConfigPath(in) }
func (v *Viper) AddConfigPath(in string) {
if in != "" {
absin := absPathify(v.logger, in)
v.logger.Info("adding path to search paths", "path", absin)
absin := absPathify(in)
jww.INFO.Println("adding", absin, "to paths to search")
if !stringInSlice(absin, v.configPaths) {
v.configPaths = append(v.configPaths, absin)
}
@@ -578,7 +524,7 @@ func (v *Viper) AddConfigPath(in string) {
// AddRemoteProvider adds a remote configuration source.
// Remote Providers are searched in the order they are added.
// provider is a string value: "etcd", "etcd3", "consul" or "firestore" are currently supported.
// provider is a string value: "etcd", "consul" or "firestore" are currently supported.
// 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
// To retrieve a config file called myapp.json from /configs/myapp.json
@@ -593,8 +539,7 @@ func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error {
return UnsupportedRemoteProviderError(provider)
}
if provider != "" && endpoint != "" {
v.logger.Info("adding remote provider", "provider", provider, "endpoint", endpoint)
jww.INFO.Printf("adding %s:%s to remote provider list", provider, endpoint)
rp := &defaultRemoteProvider{
endpoint: endpoint,
provider: provider,
@@ -609,7 +554,7 @@ func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error {
// AddSecureRemoteProvider adds a remote configuration source.
// Secure Remote Providers are searched in the order they are added.
// provider is a string value: "etcd", "etcd3", "consul" or "firestore" are currently supported.
// provider is a string value: "etcd", "consul" or "firestore" are currently supported.
// endpoint is the url. etcd requires http://ip:port consul requires ip:port
// secretkeyring is the filepath to your openpgp secret keyring. e.g. /etc/secrets/myring.gpg
// path is the path in the k/v store to retrieve configuration
@@ -626,8 +571,7 @@ func (v *Viper) AddSecureRemoteProvider(provider, endpoint, path, secretkeyring
return UnsupportedRemoteProviderError(provider)
}
if provider != "" && endpoint != "" {
v.logger.Info("adding remote provider", "provider", provider, "endpoint", endpoint)
jww.INFO.Printf("adding %s:%s to remote provider list", provider, endpoint)
rp := &defaultRemoteProvider{
endpoint: endpoint,
provider: provider,
@@ -790,8 +734,7 @@ func (v *Viper) searchMapWithPathPrefixes(
// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
// on its path in the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
// "foo.bar.baz" in a lower-priority map
// "foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{}) string {
var parentVal interface{}
for i := 1; i < len(path); i++ {
@@ -816,8 +759,7 @@ func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{})
// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
// in a sub-path of the map.
// e.g., if "foo.bar" has a value in the given map, it “shadows”
//
// "foo.bar.baz" in a lower-priority map
// "foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
// unify input map
var m map[string]interface{}
@@ -842,8 +784,7 @@ func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
// in the environment, when automatic env is on.
// e.g., if "foo.bar" has a value in the environment, it “shadows”
//
// "foo.bar.baz" in a lower-priority map
// "foo.bar.baz" in a lower-priority map
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
var parentKey string
for i := 1; i < len(path); i++ {
@@ -864,11 +805,11 @@ func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
// would return a string slice for the key if the key's type is inferred by
// the default value and the Get function would return:
//
// []string {"a", "b", "c"}
// []string {"a", "b", "c"}
//
// Otherwise the Get function would return:
//
// "a b c"
// "a b c"
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }
func (v *Viper) SetTypeByDefaultValue(enable bool) {
@@ -930,8 +871,6 @@ func (v *Viper) Get(key string) interface{} {
return cast.ToStringSlice(val)
case []int:
return cast.ToIntSlice(val)
case []time.Duration:
return cast.ToDurationSlice(val)
}
}
@@ -950,10 +889,6 @@ func (v *Viper) Sub(key string) *Viper {
}
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)
return subv
}
@@ -1002,13 +937,6 @@ func (v *Viper) GetUint(key string) uint {
return cast.ToUint(v.Get(key))
}
// GetUint16 returns the value associated with the key as an unsigned integer.
func GetUint16(key string) uint16 { return v.GetUint16(key) }
func (v *Viper) GetUint16(key string) uint16 {
return cast.ToUint16(v.Get(key))
}
// GetUint32 returns the value associated with the key as an unsigned integer.
func GetUint32(key string) uint32 { return v.GetUint32(key) }
@@ -1107,7 +1035,7 @@ func (v *Viper) Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error
return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...))
}
// defaultDecoderConfig returns default mapstructure.DecoderConfig with support
// defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot
// of time.Duration values & string slices
func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
c := &mapstructure.DecoderConfig{
@@ -1158,8 +1086,9 @@ func (v *Viper) BindPFlags(flags *pflag.FlagSet) error {
// BindPFlag binds a specific key to a pflag (as used by cobra).
// Example (where serverCmd is a Cobra instance):
//
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
// serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
// Viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
//
func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) }
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error {
@@ -1217,17 +1146,6 @@ func (v *Viper) BindEnv(input ...string) error {
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.
//
// Viper will check to see if an alias exists first.
@@ -1282,15 +1200,8 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
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":
return stringToStringConv(flag.ValueString())
case "stringToInt":
return stringToIntConv(flag.ValueString())
default:
return flag.ValueString()
}
@@ -1301,10 +1212,9 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
// Env override next
if v.automaticEnvApplied {
envKey := strings.Join(append(v.parents, lcaseKey), ".")
// even if it hasn't been registered, if automaticEnv is used,
// check any Get request
if val, ok := v.getEnv(v.mergeWithEnvPrefix(envKey)); ok {
if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
return val
}
if nested && v.isPathShadowedInAutoEnv(path) != "" {
@@ -1371,13 +1281,6 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
case "stringToInt":
return stringToIntConv(flag.ValueString())
case "durationSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
slice := strings.Split(s, ",")
return cast.ToDurationSlice(slice)
default:
return flag.ValueString()
}
@@ -1421,30 +1324,6 @@ func stringToStringConv(val string) interface{} {
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 is case-insensitive for a key.
func IsSet(key string) bool { return v.IsSet(key) }
@@ -1508,15 +1387,14 @@ func (v *Viper) registerAlias(alias string, key string) {
v.aliases[alias] = key
}
} else {
v.logger.Warn("creating circular reference alias", "alias", alias, "key", key, "real_key", v.realKey(key))
jww.WARN.Println("Creating circular reference alias", alias, key, v.realKey(key))
}
}
func (v *Viper) realKey(key string) string {
newkey, exists := v.aliases[key]
if exists {
v.logger.Debug("key is an alias", "alias", key, "to", newkey)
jww.DEBUG.Println("Alias", key, "to", newkey)
return v.realKey(newkey)
}
return key
@@ -1526,13 +1404,11 @@ func (v *Viper) realKey(key string) string {
func InConfig(key string) bool { return v.InConfig(key) }
func (v *Viper) InConfig(key string) bool {
lcaseKey := strings.ToLower(key)
// if the requested key is an alias, then return the proper key
lcaseKey = v.realKey(lcaseKey)
path := strings.Split(lcaseKey, v.keyDelim)
key = v.realKey(key)
return v.searchIndexableWithPathPrefixes(v.config, path) != nil
_, exists := v.config[key]
return exists
}
// SetDefault sets the default value for this key.
@@ -1577,7 +1453,7 @@ func (v *Viper) Set(key string, value interface{}) {
func ReadInConfig() error { return v.ReadInConfig() }
func (v *Viper) ReadInConfig() error {
v.logger.Info("attempting to read in config file")
jww.INFO.Println("Attempting to read in config file")
filename, err := v.getConfigFile()
if err != nil {
return err
@@ -1587,7 +1463,7 @@ func (v *Viper) ReadInConfig() error {
return UnsupportedConfigError(v.getConfigType())
}
v.logger.Debug("reading file", "file", filename)
jww.DEBUG.Println("Reading file: ", filename)
file, err := afero.ReadFile(v.fs, filename)
if err != nil {
return err
@@ -1608,7 +1484,7 @@ func (v *Viper) ReadInConfig() error {
func MergeInConfig() error { return v.MergeInConfig() }
func (v *Viper) MergeInConfig() error {
v.logger.Info("attempting to merge in config file")
jww.INFO.Println("Attempting to merge in config file")
filename, err := v.getConfigFile()
if err != nil {
return err
@@ -1699,12 +1575,11 @@ func (v *Viper) SafeWriteConfigAs(filename string) error {
}
func (v *Viper) writeConfig(filename string, force bool) error {
v.logger.Info("attempting to write configuration to file")
jww.INFO.Println("Attempting to write configuration to file.")
var configType string
ext := filepath.Ext(filename)
if ext != "" && ext != filepath.Base(filename) {
if ext != "" {
configType = ext[1:]
} else {
configType = v.configType
@@ -1747,11 +1622,53 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
buf.ReadFrom(in)
switch format := strings.ToLower(v.getConfigType()); format {
case "yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "properties", "props", "prop", "dotenv", "env":
err := v.decoderRegistry.Decode(format, buf.Bytes(), c)
case "yaml", "yml", "json", "toml", "hcl":
err := decoderRegistry.Decode(format, buf.Bytes(), &c)
if err != nil {
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)
@@ -1762,8 +1679,8 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
func (v *Viper) marshalWriter(f afero.File, configType string) error {
c := v.AllSettings()
switch configType {
case "yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "prop", "props", "properties", "dotenv", "env":
b, err := v.encoderRegistry.Encode(configType, c)
case "yaml", "yml", "json", "toml", "hcl":
b, err := encoderRegistry.Encode(configType, c)
if err != nil {
return ConfigMarshalError{err}
}
@@ -1772,6 +1689,50 @@ func (v *Viper) marshalWriter(f afero.File, configType string) error {
if err != nil {
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
}
@@ -1788,8 +1749,7 @@ func keyExists(k string, m map[string]interface{}) string {
}
func castToMapStringInterface(
src map[interface{}]interface{},
) map[string]interface{} {
src map[interface{}]interface{}) map[string]interface{} {
tgt := map[string]interface{}{}
for k, v := range src {
tgt[fmt.Sprintf("%v", k)] = v
@@ -1827,12 +1787,11 @@ func castMapFlagToMapInterface(src map[string]FlagValue) map[string]interface{}
// deep. Both map types are supported as there is a go-yaml fork that uses
// `map[string]interface{}` instead.
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 {
tk := keyExists(sk, tgt)
if tk == "" {
v.logger.Trace("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
jww.TRACE.Printf("tk=\"\", tgt[%s]=%v", sk, sv)
tgt[sk] = sv
if itgt != nil {
itgt[sk] = sv
@@ -1842,7 +1801,7 @@ func mergeMaps(
tv, ok := tgt[tk]
if !ok {
v.logger.Trace("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
jww.TRACE.Printf("tgt[%s] != ok, tgt[%s]=%v", tk, sk, sv)
tgt[sk] = sv
if itgt != nil {
itgt[sk] = sv
@@ -1852,52 +1811,28 @@ func mergeMaps(
svType := reflect.TypeOf(sv)
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
}
v.logger.Trace(
"processing",
"key", sk,
"st", svType,
"tt", tvType,
"sv", sv,
"tv", tv,
)
jww.TRACE.Printf("processing key=%s, st=%v, tt=%v, sv=%v, tv=%v",
sk, svType, tvType, sv, tv)
switch ttv := tv.(type) {
case map[interface{}]interface{}:
v.logger.Trace("merging maps (must convert)")
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
}
jww.TRACE.Printf("merging maps (must convert)")
tsv := sv.(map[interface{}]interface{})
ssv := castToMapStringInterface(tsv)
stv := castToMapStringInterface(ttv)
mergeMaps(ssv, stv, ttv)
case map[string]interface{}:
v.logger.Trace("merging maps")
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)
jww.TRACE.Printf("merging maps")
mergeMaps(sv.(map[string]interface{}), ttv, nil)
default:
v.logger.Trace("setting value")
jww.TRACE.Printf("setting value")
tgt[tk] = sv
if itgt != nil {
itgt[tk] = sv
@@ -1929,14 +1864,10 @@ 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'")
}
if len(v.remoteProviders) == 0 {
return RemoteConfigError("No Remote Providers")
}
for _, rp := range v.remoteProviders {
val, err := v.getRemoteConfig(rp)
if err != nil {
v.logger.Error(fmt.Errorf("get remote config: %w", err).Error())
jww.ERROR.Printf("get remote config: %s", err)
continue
}
@@ -1959,10 +1890,6 @@ func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]interface{}
// Retrieve the first found remote configuration.
func (v *Viper) watchKeyValueConfigOnChannel() error {
if len(v.remoteProviders) == 0 {
return RemoteConfigError("No Remote Providers")
}
for _, rp := range v.remoteProviders {
respc, _ := RemoteConfig.WatchChannel(rp)
// Todo: Add quit channel
@@ -1980,15 +1907,9 @@ func (v *Viper) watchKeyValueConfigOnChannel() error {
// Retrieve the first found remote configuration.
func (v *Viper) watchKeyValueConfig() error {
if len(v.remoteProviders) == 0 {
return RemoteConfigError("No Remote Providers")
}
for _, rp := range v.remoteProviders {
val, err := v.watchRemoteConfig(rp)
if err != nil {
v.logger.Error(fmt.Errorf("watch remote config: %w", err).Error())
continue
}
v.kvstore = val
@@ -2031,10 +1952,9 @@ func (v *Viper) AllKeys() []string {
// flattenAndMergeMap recursively flattens the given map into a map[string]bool
// of key paths (used as a set, easier to manipulate than a []string):
// - each path is merged into a single key string, delimited with v.keyDelim
// - if a path is shadowed by an earlier value in the initial shadow map,
// it is skipped.
//
// - each path is merged into a single key string, delimited with v.keyDelim
// - if a path is shadowed by an earlier value in the initial shadow map,
// it is skipped.
// The resulting set of paths is merged to the given shadow set at the same time.
func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interface{}, prefix string) map[string]bool {
if shadow != nil && prefix != "" && shadow[prefix] {
@@ -2183,19 +2103,49 @@ func (v *Viper) getConfigFile() (string, error) {
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
// purposes.
func Debug() { v.Debug() }
func DebugTo(w io.Writer) { v.DebugTo(w) }
func Debug() { v.Debug() }
func (v *Viper) Debug() { v.DebugTo(os.Stdout) }
func (v *Viper) DebugTo(w io.Writer) {
fmt.Fprintf(w, "Aliases:\n%#v\n", v.aliases)
fmt.Fprintf(w, "Override:\n%#v\n", v.override)
fmt.Fprintf(w, "PFlags:\n%#v\n", v.pflags)
fmt.Fprintf(w, "Env:\n%#v\n", v.env)
fmt.Fprintf(w, "Key/Value Store:\n%#v\n", v.kvstore)
fmt.Fprintf(w, "Config:\n%#v\n", v.config)
fmt.Fprintf(w, "Defaults:\n%#v\n", v.defaults)
func (v *Viper) Debug() {
fmt.Printf("Aliases:\n%#v\n", v.aliases)
fmt.Printf("Override:\n%#v\n", v.override)
fmt.Printf("PFlags:\n%#v\n", v.pflags)
fmt.Printf("Env:\n%#v\n", v.env)
fmt.Printf("Key/Value Store:\n%#v\n", v.kvstore)
fmt.Printf("Config:\n%#v\n", v.config)
fmt.Printf("Defaults:\n%#v\n", v.defaults)
}
-57
View File
@@ -1,57 +0,0 @@
//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
}
-32
View File
@@ -1,32 +0,0 @@
//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
}
+115 -432
View File
@@ -8,7 +8,6 @@ package viper
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"os"
@@ -34,21 +33,21 @@ import (
"github.com/spf13/viper/internal/testutil"
)
// var yamlExample = []byte(`Hacker: true
// name: steve
// hobbies:
// - skateboarding
// - snowboarding
// - go
// clothing:
// jacket: leather
// trousers: denim
// pants:
// size: large
// age: 35
// eyes : brown
// beard: true
// `)
var yamlExample = []byte(`Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
pants:
size: large
age: 35
eyes : brown
beard: true
`)
var yamlExampleWithExtras = []byte(`Existing: true
Bogus: true
@@ -264,13 +263,13 @@ func initDirs(t *testing.T) (string, string, func()) {
require.Nil(t, err)
for _, dir := range testDirs {
err = os.Mkdir(dir, 0o750)
err = os.Mkdir(dir, 0750)
assert.Nil(t, err)
err = ioutil.WriteFile(
path.Join(dir, config+".toml"),
[]byte("key = \"value is "+dir+"\"\n"),
0o640)
0640)
assert.Nil(t, err)
}
@@ -302,192 +301,65 @@ func (s *stringValue) String() string {
return string(*s)
}
func TestGetConfigFile(t *testing.T) {
t.Run("config file set", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
require.NoError(t, err)
v := New()
v.SetFs(fs)
v.AddConfigPath("/etc/viper")
v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
filename, err := v.getConfigFile()
assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename)
assert.NoError(t, err)
})
t.Run("find file", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
require.NoError(t, err)
v := New()
v.SetFs(fs)
v.AddConfigPath("/etc/viper")
filename, err := v.getConfigFile()
assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), filename)
assert.NoError(t, err)
})
t.Run("find files only", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/config"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/config/config.yaml"))
require.NoError(t, err)
v := New()
v.SetFs(fs)
v.AddConfigPath("/etc")
v.AddConfigPath("/etc/config")
filename, err := v.getConfigFile()
assert.Equal(t, testutil.AbsFilePath(t, "/etc/config/config.yaml"), filename)
assert.NoError(t, err)
})
t.Run("precedence", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/home/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/home/viper/config.zml"))
require.NoError(t, err)
err = fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.bml"))
require.NoError(t, err)
err = fs.Mkdir(testutil.AbsFilePath(t, "/var/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/var/viper/config.yaml"))
require.NoError(t, err)
v := New()
v.SetFs(fs)
v.AddConfigPath("/home/viper")
v.AddConfigPath("/etc/viper")
v.AddConfigPath("/var/viper")
filename, err := v.getConfigFile()
assert.Equal(t, testutil.AbsFilePath(t, "/var/viper/config.yaml"), filename)
assert.NoError(t, err)
})
t.Run("without extension", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"))
require.NoError(t, err)
v := New()
v.SetFs(fs)
v.AddConfigPath("/etc/viper")
v.SetConfigName(".dotfilenoext")
v.SetConfigType("yaml")
filename, err := v.getConfigFile()
assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"), filename)
assert.NoError(t, err)
})
t.Run("without extension and config type", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)
_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/.dotfilenoext"))
require.NoError(t, err)
v := New()
v.SetFs(fs)
v.AddConfigPath("/etc/viper")
v.SetConfigName(".dotfilenoext")
_, err = v.getConfigFile()
// unless config type is set, files without extension
// are not considered
assert.Error(t, err)
})
func TestBasics(t *testing.T) {
SetConfigFile("/tmp/config.yaml")
filename, err := v.getConfigFile()
assert.Equal(t, "/tmp/config.yaml", filename)
assert.NoError(t, err)
}
func TestReadInConfig(t *testing.T) {
t.Run("config file set", func(t *testing.T) {
fs := afero.NewMemMapFs()
func TestSearchInPath_WithoutConfigTypeSet(t *testing.T) {
filename := ".dotfilenoext"
path := "/tmp"
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)
}
err := fs.Mkdir("/etc/viper", 0o777)
require.NoError(t, err)
func TestSearchInPath(t *testing.T) {
filename := ".dotfilenoext"
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)
}
file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
require.NoError(t, err)
func TestSearchInPath_FilesOnly(t *testing.T) {
fs := afero.NewMemMapFs()
_, err = file.Write([]byte(`key: value`))
require.NoError(t, err)
err := fs.Mkdir("/tmp/config", 0777)
require.NoError(t, err)
file.Close()
_, err = fs.Create("/tmp/config/config.yaml")
require.NoError(t, err)
v := New()
v := New()
v.SetFs(fs)
v.SetConfigFile(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
v.SetFs(fs)
v.AddConfigPath("/tmp")
v.AddConfigPath("/tmp/config")
err = v.ReadInConfig()
require.NoError(t, err)
assert.Equal(t, "value", v.Get("key"))
})
t.Run("find file", func(t *testing.T) {
fs := afero.NewMemMapFs()
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)
file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
require.NoError(t, err)
_, err = file.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"))
})
filename, err := v.getConfigFile()
assert.Equal(t, "/tmp/config/config.yaml", filename)
assert.NoError(t, err)
}
func TestDefault(t *testing.T) {
@@ -510,9 +382,7 @@ func TestUnmarshaling(t *testing.T) {
unmarshalReader(r, v.config)
assert.True(t, InConfig("name"))
assert.True(t, InConfig("clothing.jacket"))
assert.False(t, InConfig("state"))
assert.False(t, InConfig("clothing.hat"))
assert.Equal(t, "steve", Get("name"))
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"))
@@ -734,24 +604,6 @@ func TestEnvKeyReplacer(t *testing.T) {
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) {
initConfigs()
@@ -893,10 +745,8 @@ func TestAliasesOfAliases(t *testing.T) {
}
func TestRecursiveAliases(t *testing.T) {
Set("baz", "bat")
RegisterAlias("Baz", "Roo")
RegisterAlias("Roo", "baz")
assert.Equal(t, "bat", Get("Baz"))
}
func TestUnmarshal(t *testing.T) {
@@ -1019,7 +869,7 @@ func TestBindPFlags(t *testing.T) {
}
}
//nolint:dupl
// nolint: dupl
func TestBindPFlagsStringSlice(t *testing.T) {
tests := []struct {
Expected []string
@@ -1067,7 +917,7 @@ func TestBindPFlagsStringSlice(t *testing.T) {
}
}
//nolint:dupl
// nolint: dupl
func TestBindPFlagsStringArray(t *testing.T) {
tests := []struct {
Expected []string
@@ -1115,29 +965,7 @@ func TestBindPFlagsStringArray(t *testing.T) {
}
}
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
// nolint: dupl
func TestBindPFlagsIntSlice(t *testing.T) {
tests := []struct {
Expected []int
@@ -1256,53 +1084,6 @@ 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) {
assert.Equal(t, "brown", Get("eyes"))
@@ -1458,9 +1239,7 @@ func TestReadBufConfig(t *testing.T) {
t.Log(v.AllKeys())
assert.True(t, v.InConfig("name"))
assert.True(t, v.InConfig("clothing.jacket"))
assert.False(t, v.InConfig("state"))
assert.False(t, v.InConfig("clothing.hat"))
assert.Equal(t, "steve", v.Get("name"))
assert.Equal(t, []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"))
@@ -1574,18 +1353,6 @@ func TestWrongDirsSearchNotFoundForMerge(t *testing.T) {
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) {
v := New()
v.SetConfigType("yaml")
@@ -1602,14 +1369,6 @@ func TestSub(t *testing.T) {
subv = v.Sub("missing.key")
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" = {
@@ -1668,21 +1427,21 @@ p_ppu = 0.55
p_batters.batter.type = Regular
`)
// 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 yamlWriteExpected = []byte(`age: 35
beard: true
clothing:
jacket: leather
pants:
size: large
trousers: denim
eyes: brown
hacker: true
hobbies:
- skateboarding
- snowboarding
- go
name: steve
`)
func TestWriteConfig(t *testing.T) {
fs := afero.NewMemMapFs()
@@ -1926,7 +1685,7 @@ func TestSafeWriteConfig(t *testing.T) {
v.SetConfigType("yaml")
require.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlExample)))
require.NoError(t, v.SafeWriteConfig())
read, err := afero.ReadFile(fs, testutil.AbsFilePath(t, "/test/c.yaml"))
read, err := afero.ReadFile(fs, "/test/c.yaml")
require.NoError(t, err)
assert.Equal(t, yamlWriteExpected, read)
}
@@ -1943,7 +1702,7 @@ func TestSafeWriteConfigWithMissingConfigPath(t *testing.T) {
func TestSafeWriteConfigWithExistingFile(t *testing.T) {
v := New()
fs := afero.NewMemMapFs()
fs.Create(testutil.AbsFilePath(t, "/test/c.yaml"))
fs.Create("/test/c.yaml")
v.SetFs(fs)
v.AddConfigPath("/test")
v.SetConfigName("c")
@@ -1979,23 +1738,6 @@ func TestSafeWriteConfigAsWithExistingFile(t *testing.T) {
assert.True(t, ok, "Expected ConfigFileAlreadyExistsError")
}
func TestWriteHiddenFile(t *testing.T) {
v := New()
fs := afero.NewMemMapFs()
fs.Create(testutil.AbsFilePath(t, "/test/.config"))
v.SetFs(fs)
v.SetConfigName(".config")
v.SetConfigType("yaml")
v.AddConfigPath("/test")
err := v.ReadInConfig()
require.NoError(t, err)
err = v.WriteConfig()
require.NoError(t, err)
}
var yamlMergeExampleTgt = []byte(`
hello:
pop: 37890
@@ -2022,24 +1764,6 @@ hello:
fu: bar
`)
var jsonMergeExampleTgt = []byte(`
{
"hello": {
"foo": null,
"pop": 123456
}
}
`)
var jsonMergeExampleSrc = []byte(`
{
"hello": {
"foo": "foo str",
"pop": "pop str"
}
}
`)
func TestMergeConfig(t *testing.T) {
v := New()
v.SetConfigType("yml")
@@ -2063,10 +1787,6 @@ func TestMergeConfig(t *testing.T) {
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 {
t.Fatalf("uint32 pop != 37890, = %d", pop)
}
@@ -2116,26 +1836,6 @@ 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) {
v := New()
v.SetConfigType("yml")
@@ -2442,7 +2142,7 @@ func newViperWithConfigFile(t *testing.T) (*Viper, string, func()) {
watchDir, err := ioutil.TempDir("", "")
require.Nil(t, err)
configFile := path.Join(watchDir, "config.yaml")
err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0o640)
err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0640)
require.Nil(t, err)
cleanup := func() {
os.RemoveAll(watchDir)
@@ -2459,11 +2159,11 @@ func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string, func
watchDir, err := ioutil.TempDir("", "")
require.Nil(t, err)
dataDir1 := path.Join(watchDir, "data1")
err = os.Mkdir(dataDir1, 0o777)
err = os.Mkdir(dataDir1, 0777)
require.Nil(t, err)
realConfigFile := path.Join(dataDir1, "config.yaml")
t.Logf("Real config file location: %s\n", realConfigFile)
err = ioutil.WriteFile(realConfigFile, []byte("foo: bar\n"), 0o640)
err = ioutil.WriteFile(realConfigFile, []byte("foo: bar\n"), 0640)
require.Nil(t, err)
cleanup := func() {
os.RemoveAll(watchDir)
@@ -2498,16 +2198,13 @@ func TestWatchFile(t *testing.T) {
t.Logf("test config file: %s\n", configFile)
wg := sync.WaitGroup{}
wg.Add(1)
var wgDoneOnce sync.Once // OnConfigChange is called twice on Windows
v.OnConfigChange(func(in fsnotify.Event) {
t.Logf("config file changed")
wgDoneOnce.Do(func() {
wg.Done()
})
wg.Done()
})
v.WatchConfig()
// when overwriting the file and waiting for the custom change notification handler to be triggered
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0o640)
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640)
wg.Wait()
// then the config value should have changed
require.Nil(t, err)
@@ -2530,10 +2227,10 @@ func TestWatchFile(t *testing.T) {
wg.Add(1)
// when link to another `config.yaml` file
dataDir2 := path.Join(watchDir, "data2")
err := os.Mkdir(dataDir2, 0o777)
err := os.Mkdir(dataDir2, 0777)
require.Nil(t, err)
configFile2 := path.Join(dataDir2, "config.yaml")
err = ioutil.WriteFile(configFile2, []byte("foo: baz\n"), 0o640)
err = ioutil.WriteFile(configFile2, []byte("foo: baz\n"), 0640)
require.Nil(t, err)
// change the symlink using the `ln -sfn` command
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
@@ -2562,25 +2259,25 @@ func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
assert.Equal(t, "cobra_flag", config.Foo.Bar)
}
// var yamlExampleWithDot = []byte(`Hacker: true
// name: steve
// hobbies:
// - skateboarding
// - snowboarding
// - go
// clothing:
// jacket: leather
// trousers: denim
// pants:
// size: large
// age: 35
// eyes : brown
// beard: true
// emails:
// steve@hacker.com:
// created: 01/02/03
// active: true
// `)
var yamlExampleWithDot = []byte(`Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
pants:
size: large
age: 35
eyes : brown
beard: true
emails:
steve@hacker.com:
created: 01/02/03
active: true
`)
func TestKeyDelimiter(t *testing.T) {
v := NewWithOptions(KeyDelimiter("::"))
@@ -2630,10 +2327,7 @@ func TestKeyDelimiter(t *testing.T) {
}
var yamlDeepNestedSlices = []byte(`TV:
- title: "The Expanse"
title_i18n:
USA: "The Expanse"
Japan: "エクスパンス -巨獣めざめる-"
- title: "The expanse"
seasons:
- first_released: "December 14, 2015"
episodes:
@@ -2663,15 +2357,11 @@ func TestSliceIndexAccess(t *testing.T) {
err := v.unmarshalReader(r, v.config)
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, "Static", v.GetString("tv.0.seasons.1.episodes.2.title"))
assert.Equal(t, "December 15, 2015", v.GetString("tv.0.seasons.0.episodes.1.air_date"))
// Test nested keys with capital letters
assert.Equal(t, "The Expanse", v.GetString("tv.0.title_i18n.USA"))
assert.Equal(t, "エクスパンス -巨獣めざめる-", v.GetString("tv.0.title_i18n.Japan"))
// Test for index out of bounds
assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released"))
@@ -2715,10 +2405,3 @@ 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")
}
}
-53
View File
@@ -1,53 +0,0 @@
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 -2
View File
@@ -1,5 +1,4 @@
//go:build darwin || dragonfly || freebsd || openbsd || linux || netbsd || solaris || windows
// +build darwin dragonfly freebsd openbsd linux netbsd solaris windows
// +build !js
package viper
+6 -8
View File
@@ -1,19 +1,13 @@
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
// +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
// +build js,wasm
package viper
import (
"fmt"
"runtime"
"errors"
"github.com/fsnotify/fsnotify"
)
func newWatcher() (*watcher, error) {
return &watcher{}, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS)
}
type watcher struct {
Events chan fsnotify.Event
Errors chan error
@@ -30,3 +24,7 @@ func (*watcher) Add(name string) error {
func (*watcher) Remove(name string) error {
return nil
}
func newWatcher() (*watcher, error) {
return &watcher{}, errors.New("fsnotify is not supported on WASM")
}