mirror of
https://github.com/spf13/viper.git
synced 2026-09-20 03:18:21 +00:00
Compare commits
@@ -1,2 +1,2 @@
|
|||||||
[*.yml]
|
[{*.yml,*.yaml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🐛 Bug report
|
|
||||||
about: Report a bug to help us improve Viper
|
|
||||||
labels: kind/bug
|
|
||||||
---
|
|
||||||
<!--
|
|
||||||
Thank you for sending a bug report! Here are some tips:
|
|
||||||
|
|
||||||
1. Please fill out the template below to make it easier to debug your problem.
|
|
||||||
2. If you are not sure if it is a bug or not, you can ask in the Gophers slack channel `#viper`.
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Expected behavior (what you expected to happen)**:
|
|
||||||
|
|
||||||
**Actual behavior (what actually happened)**:
|
|
||||||
|
|
||||||
**Repl.it link**:
|
|
||||||
<!-- you can use the following example: https://repl.it/@sagikazarmark/Viper-example -->
|
|
||||||
|
|
||||||
**Code reproducing the issue**:
|
|
||||||
<!-- just to make sure the content of the repl doesn't get lost -->
|
|
||||||
|
|
||||||
```go
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- include configuration file example if necessary -->
|
|
||||||
|
|
||||||
**Environment**:
|
|
||||||
- Viper version:
|
|
||||||
- Config source: <!-- flag, env, file, etc -->
|
|
||||||
- File format: <!-- JSON, YAML, TOML, etc -->
|
|
||||||
|
|
||||||
**Anything else we should know?**:
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
name: 🐛 Bug report
|
||||||
|
description: Report a bug to help us improve Viper
|
||||||
|
labels: [kind/bug]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for submitting a bug report!
|
||||||
|
|
||||||
|
Please fill out the template below to make it easier to debug your problem.
|
||||||
|
|
||||||
|
If you are not sure if it is a bug or not, you can contact us via the available [support channels](https://github.com/spf13/viper/issues/new/choose).
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Preflight Checklist
|
||||||
|
description: Please ensure you've completed all of the following.
|
||||||
|
options:
|
||||||
|
- label: I have searched the [issue tracker](https://www.github.com/spf13/viper/issues) for an issue that matches the one I want to file, without success.
|
||||||
|
required: true
|
||||||
|
- label: I am not looking for support or already pursued the available [support channels](https://github.com/spf13/viper/issues/new/choose) without success.
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: What version of Viper are you using?
|
||||||
|
placeholder: 1.8.1
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Config Source
|
||||||
|
description: What sources do you load configuration from?
|
||||||
|
options:
|
||||||
|
- Manual set
|
||||||
|
- Flags
|
||||||
|
- Environment variables
|
||||||
|
- Files
|
||||||
|
- Remove K/V stores
|
||||||
|
- Defaults
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Format
|
||||||
|
description: Which file formats do you use?
|
||||||
|
options:
|
||||||
|
- JSON
|
||||||
|
- YAML
|
||||||
|
- TOML
|
||||||
|
- Dotenv
|
||||||
|
- HCL
|
||||||
|
- Java properties
|
||||||
|
- INI
|
||||||
|
- Other (specify below)
|
||||||
|
multiple: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Repl.it link
|
||||||
|
description: Complete example on Repl.it reproducing the issue. [Here](https://repl.it/@sagikazarmark/Viper-example) is an example you can use.
|
||||||
|
placeholder: https://repl.it/@sagikazarmark/Viper-example
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Code reproducing the issue
|
||||||
|
description: Please provide a Repl.it link if possible.
|
||||||
|
render: go
|
||||||
|
placeholder: |
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
v := viper.New()
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
err = v.Unmarshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: A clear description of what actually happens.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps To Reproduce
|
||||||
|
description: Steps to reproduce the behavior if it is not self-explanatory.
|
||||||
|
placeholder: |
|
||||||
|
1. In this environment...
|
||||||
|
2. With this config...
|
||||||
|
3. Run '...'
|
||||||
|
4. See error...
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Links? References? Anything that will give us more context about the issue that you are encountering!
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🚀 Feature request
|
|
||||||
about: Suggest an idea for Viper
|
|
||||||
labels: kind/enhancement
|
|
||||||
---
|
|
||||||
<!--
|
|
||||||
Thank you for sending a feature request!
|
|
||||||
Please describe what you would like to change/add and why in detail by filling out the template below.
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
|
||||||
|
|
||||||
**Describe the solution you'd like to see**
|
|
||||||
<!-- A clear and concise description of what would you like to happen. -->
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
<!-- Add any other context or screenshots about the feature request here. -->
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: 🚀 Feature request
|
||||||
|
description: Suggest an idea for Viper
|
||||||
|
labels: [kind/enhancement]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for submitting a feature request!
|
||||||
|
|
||||||
|
Please describe what you would like to change/add and why in detail by filling out the template below.
|
||||||
|
|
||||||
|
If you are not sure if your request fits into Viper, you can contact us via the available [support channels](https://github.com/spf13/viper/issues/new/choose).
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Preflight Checklist
|
||||||
|
description: Please ensure you've completed all of the following.
|
||||||
|
options:
|
||||||
|
- label: I have searched the [issue tracker](https://www.github.com/spf13/viper/issues) for an issue that matches the one I want to file, without success.
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem Description
|
||||||
|
description: A clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: A clear and concise description of what would you like to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternatives Considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Add any other context about the problem here.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
labels:
|
||||||
|
- area/dependencies
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
labels:
|
||||||
|
- area/dependencies
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
comment:
|
comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v3
|
- uses: actions/github-script@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
comment:
|
comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v3
|
- uses: actions/github-script@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search
|
|||||||
viper.AddConfigPath(".") // optionally look for config in the working directory
|
viper.AddConfigPath(".") // optionally look for config in the working directory
|
||||||
err := viper.ReadInConfig() // Find and read the config file
|
err := viper.ReadInConfig() // Find and read the config file
|
||||||
if err != nil { // Handle errors reading the config file
|
if err != nil { // Handle errors reading the config file
|
||||||
panic(fmt.Errorf("Fatal error config file: %s \n", err))
|
panic(fmt.Errorf("Fatal error config file: %w \n", err))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -778,6 +778,15 @@ if err != nil {
|
|||||||
|
|
||||||
Viper uses [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) under the hood for unmarshaling values which uses `mapstructure` tags by default.
|
Viper uses [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) under the hood for unmarshaling values which uses `mapstructure` tags by default.
|
||||||
|
|
||||||
|
### Decoding custom formats
|
||||||
|
|
||||||
|
A frequently requested feature for Viper is adding more value formats and decoders.
|
||||||
|
For example, parsing character (dot, comma, semicolon, etc) separated strings into slices.
|
||||||
|
|
||||||
|
This is already available in Viper using mapstructure decode hooks.
|
||||||
|
|
||||||
|
Read more about the details in [this blog post](https://sagikazarmark.hu/blog/decoding-custom-formats-with-viper/).
|
||||||
|
|
||||||
### Marshalling to string
|
### Marshalling to string
|
||||||
|
|
||||||
You may need to marshal all the settings held in viper into a string rather than write them to a file.
|
You may need to marshal all the settings held in viper into a string rather than write them to a file.
|
||||||
|
|||||||
@@ -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.
|
||||||
+30
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrDecoderNotFound is returned when there is no decoder registered for a format.
|
||||||
|
ErrDecoderNotFound = encodingError("decoder not found for this format")
|
||||||
|
|
||||||
|
// ErrDecoderFormatAlreadyRegistered is returned when an decoder is already registered for a format.
|
||||||
|
ErrDecoderFormatAlreadyRegistered = encodingError("decoder already registered for this format")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecoderRegistry can choose an appropriate Decoder based on the provided format.
|
||||||
|
type DecoderRegistry struct {
|
||||||
|
decoders map[string]Decoder
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDecoderRegistry returns a new, initialized DecoderRegistry.
|
||||||
|
func NewDecoderRegistry() *DecoderRegistry {
|
||||||
|
return &DecoderRegistry{
|
||||||
|
decoders: make(map[string]Decoder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterDecoder registers a Decoder for a format.
|
||||||
|
// Registering a Decoder for an already existing format is not supported.
|
||||||
|
func (e *DecoderRegistry) RegisterDecoder(format string, enc Decoder) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := e.decoders[format]; ok {
|
||||||
|
return ErrDecoderFormatAlreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
e.decoders[format] = enc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode calls the underlying Decoder based on the format.
|
||||||
|
func (e *DecoderRegistry) Decode(format string, b []byte, v interface{}) error {
|
||||||
|
e.mu.RLock()
|
||||||
|
decoder, ok := e.decoders[format]
|
||||||
|
e.mu.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return ErrDecoderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoder.Decode(b, v)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type decoder struct {
|
||||||
|
v interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d decoder) Decode(_ []byte, v interface{}) error {
|
||||||
|
rv := v.(*string)
|
||||||
|
*rv = d.v.(string)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoderRegistry_RegisterDecoder(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
registry := NewDecoderRegistry()
|
||||||
|
|
||||||
|
err := registry.RegisterDecoder("myformat", decoder{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlreadyRegistered", func(t *testing.T) {
|
||||||
|
registry := NewDecoderRegistry()
|
||||||
|
|
||||||
|
err := registry.RegisterDecoder("myformat", decoder{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = registry.RegisterDecoder("myformat", decoder{})
|
||||||
|
if err != ErrDecoderFormatAlreadyRegistered {
|
||||||
|
t.Fatalf("expected ErrDecoderFormatAlreadyRegistered, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoderRegistry_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
registry := NewDecoderRegistry()
|
||||||
|
decoder := decoder{
|
||||||
|
v: "decoded value",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := registry.RegisterDecoder("myformat", decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var v string
|
||||||
|
|
||||||
|
err = registry.Decode("myformat", []byte("some value"), &v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v != "decoded value" {
|
||||||
|
t.Fatalf("expected 'decoded value', got: %#v", v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DecoderNotFound", func(t *testing.T) {
|
||||||
|
registry := NewDecoderRegistry()
|
||||||
|
|
||||||
|
var v string
|
||||||
|
|
||||||
|
err := registry.Decode("myformat", []byte("some value"), &v)
|
||||||
|
if err != ErrDecoderNotFound {
|
||||||
|
t.Fatalf("expected ErrDecoderNotFound, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 interface{}) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrEncoderNotFound is returned when there is no encoder registered for a format.
|
||||||
|
ErrEncoderNotFound = encodingError("encoder not found for this format")
|
||||||
|
|
||||||
|
// ErrEncoderFormatAlreadyRegistered is returned when an encoder is already registered for a format.
|
||||||
|
ErrEncoderFormatAlreadyRegistered = encodingError("encoder already registered for this format")
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncoderRegistry can choose an appropriate Encoder based on the provided format.
|
||||||
|
type EncoderRegistry struct {
|
||||||
|
encoders map[string]Encoder
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoderRegistry returns a new, initialized EncoderRegistry.
|
||||||
|
func NewEncoderRegistry() *EncoderRegistry {
|
||||||
|
return &EncoderRegistry{
|
||||||
|
encoders: make(map[string]Encoder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterEncoder registers an Encoder for a format.
|
||||||
|
// Registering a Encoder for an already existing format is not supported.
|
||||||
|
func (e *EncoderRegistry) RegisterEncoder(format string, enc Encoder) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := e.encoders[format]; ok {
|
||||||
|
return ErrEncoderFormatAlreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
e.encoders[format] = enc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EncoderRegistry) Encode(format string, v interface{}) ([]byte, error) {
|
||||||
|
e.mu.RLock()
|
||||||
|
encoder, ok := e.encoders[format]
|
||||||
|
e.mu.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrEncoderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoder.Encode(v)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
b []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) Encode(_ interface{}) ([]byte, error) {
|
||||||
|
return e.b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoderRegistry_RegisterEncoder(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
registry := NewEncoderRegistry()
|
||||||
|
|
||||||
|
err := registry.RegisterEncoder("myformat", encoder{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AlreadyRegistered", func(t *testing.T) {
|
||||||
|
registry := NewEncoderRegistry()
|
||||||
|
|
||||||
|
err := registry.RegisterEncoder("myformat", encoder{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = registry.RegisterEncoder("myformat", encoder{})
|
||||||
|
if err != ErrEncoderFormatAlreadyRegistered {
|
||||||
|
t.Fatalf("expected ErrEncoderFormatAlreadyRegistered, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoderRegistry_Decode(t *testing.T) {
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
registry := NewEncoderRegistry()
|
||||||
|
encoder := encoder{
|
||||||
|
b: []byte("encoded value"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := registry.RegisterEncoder("myformat", encoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := registry.Encode("myformat", "some value")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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", "some value")
|
||||||
|
if err != ErrEncoderNotFound {
|
||||||
|
t.Fatalf("expected ErrEncoderNotFound, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package encoding
|
||||||
|
|
||||||
|
type encodingError string
|
||||||
|
|
||||||
|
func (e encodingError) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package hcl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl"
|
||||||
|
"github.com/hashicorp/hcl/hcl/printer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for HCL encoding.
|
||||||
|
// TODO: add printer config to the codec?
|
||||||
|
type Codec struct{}
|
||||||
|
|
||||||
|
func (Codec) Encode(v interface{}) ([]byte, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use printer.Format? Is the trailing newline an issue?
|
||||||
|
|
||||||
|
ast, err := hcl.Parse(string(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
err = printer.Fprint(&buf, ast.Node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Codec) Decode(b []byte, v interface{}) error {
|
||||||
|
return hcl.Unmarshal(b, v)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for JSON encoding.
|
||||||
|
type Codec struct{}
|
||||||
|
|
||||||
|
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 interface{}) error {
|
||||||
|
return json.Unmarshal(b, v)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for TOML encoding.
|
||||||
|
type Codec struct{}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package yaml
|
||||||
|
|
||||||
|
import "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
// Codec implements the encoding.Encoder and encoding.Decoder interfaces for YAML encoding.
|
||||||
|
type Codec struct{}
|
||||||
|
|
||||||
|
func (Codec) Encode(v interface{}) ([]byte, error) {
|
||||||
|
return yaml.Marshal(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Codec) Decode(b []byte, v interface{}) error {
|
||||||
|
return yaml.Unmarshal(b, v)
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ package viper
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -36,18 +35,20 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/hashicorp/hcl"
|
|
||||||
"github.com/hashicorp/hcl/hcl/printer"
|
|
||||||
"github.com/magiconair/properties"
|
"github.com/magiconair/properties"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/pelletier/go-toml"
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/subosito/gotenv"
|
"github.com/subosito/gotenv"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
|
"github.com/spf13/viper/internal/encoding"
|
||||||
|
"github.com/spf13/viper/internal/encoding/hcl"
|
||||||
|
"github.com/spf13/viper/internal/encoding/json"
|
||||||
|
"github.com/spf13/viper/internal/encoding/toml"
|
||||||
|
"github.com/spf13/viper/internal/encoding/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigMarshalError happens when failing to marshal the configuration.
|
// ConfigMarshalError happens when failing to marshal the configuration.
|
||||||
@@ -67,8 +68,44 @@ type RemoteResponse struct {
|
|||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
encoderRegistry = encoding.NewEncoderRegistry()
|
||||||
|
decoderRegistry = encoding.NewDecoderRegistry()
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
v = New()
|
v = New()
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := yaml.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("yaml", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("yaml", codec)
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("yml", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("yml", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := json.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("json", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("json", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := toml.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("toml", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("toml", codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
codec := hcl.Codec{}
|
||||||
|
|
||||||
|
encoderRegistry.RegisterEncoder("hcl", codec)
|
||||||
|
decoderRegistry.RegisterDecoder("hcl", codec)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type remoteConfigFactory interface {
|
type remoteConfigFactory interface {
|
||||||
@@ -1153,7 +1190,7 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
|
|||||||
return cast.ToInt(flag.ValueString())
|
return cast.ToInt(flag.ValueString())
|
||||||
case "bool":
|
case "bool":
|
||||||
return cast.ToBool(flag.ValueString())
|
return cast.ToBool(flag.ValueString())
|
||||||
case "stringSlice":
|
case "stringSlice", "stringArray":
|
||||||
s := strings.TrimPrefix(flag.ValueString(), "[")
|
s := strings.TrimPrefix(flag.ValueString(), "[")
|
||||||
s = strings.TrimSuffix(s, "]")
|
s = strings.TrimSuffix(s, "]")
|
||||||
res, _ := readAsCSV(s)
|
res, _ := readAsCSV(s)
|
||||||
@@ -1232,7 +1269,7 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
|
|||||||
return cast.ToInt(flag.ValueString())
|
return cast.ToInt(flag.ValueString())
|
||||||
case "bool":
|
case "bool":
|
||||||
return cast.ToBool(flag.ValueString())
|
return cast.ToBool(flag.ValueString())
|
||||||
case "stringSlice":
|
case "stringSlice", "stringArray":
|
||||||
s := strings.TrimPrefix(flag.ValueString(), "[")
|
s := strings.TrimPrefix(flag.ValueString(), "[")
|
||||||
s = strings.TrimSuffix(s, "]")
|
s = strings.TrimSuffix(s, "]")
|
||||||
res, _ := readAsCSV(s)
|
res, _ := readAsCSV(s)
|
||||||
@@ -1584,35 +1621,12 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
|
|||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.ReadFrom(in)
|
buf.ReadFrom(in)
|
||||||
|
|
||||||
switch strings.ToLower(v.getConfigType()) {
|
switch format := strings.ToLower(v.getConfigType()); format {
|
||||||
case "yaml", "yml":
|
case "yaml", "yml", "json", "toml", "hcl":
|
||||||
if err := yaml.Unmarshal(buf.Bytes(), &c); err != nil {
|
err := decoderRegistry.Decode(format, buf.Bytes(), &c)
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "json":
|
|
||||||
if err := json.Unmarshal(buf.Bytes(), &c); err != nil {
|
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "hcl":
|
|
||||||
obj, err := hcl.Parse(buf.String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConfigParseError{err}
|
return ConfigParseError{err}
|
||||||
}
|
}
|
||||||
if err = hcl.DecodeObject(&c, obj); err != nil {
|
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "toml":
|
|
||||||
tree, err := toml.LoadReader(buf)
|
|
||||||
if err != nil {
|
|
||||||
return ConfigParseError{err}
|
|
||||||
}
|
|
||||||
tmap := tree.ToMap()
|
|
||||||
for k, v := range tmap {
|
|
||||||
c[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
case "dotenv", "env":
|
case "dotenv", "env":
|
||||||
env, err := gotenv.StrictParse(buf)
|
env, err := gotenv.StrictParse(buf)
|
||||||
@@ -1665,26 +1679,13 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
|
|||||||
func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
||||||
c := v.AllSettings()
|
c := v.AllSettings()
|
||||||
switch configType {
|
switch configType {
|
||||||
case "json":
|
case "yaml", "yml", "json", "toml", "hcl":
|
||||||
b, err := json.MarshalIndent(c, "", " ")
|
b, err := encoderRegistry.Encode(configType, c)
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
_, err = f.WriteString(string(b))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConfigMarshalError{err}
|
return ConfigMarshalError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "hcl":
|
_, err = f.WriteString(string(b))
|
||||||
b, err := json.Marshal(c)
|
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
ast, err := hcl.Parse(string(b))
|
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
err = printer.Fprint(f, ast.Node)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ConfigMarshalError{err}
|
return ConfigMarshalError{err}
|
||||||
}
|
}
|
||||||
@@ -1717,25 +1718,6 @@ func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
|||||||
return ConfigMarshalError{err}
|
return ConfigMarshalError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "toml":
|
|
||||||
t, err := toml.TreeFromMap(c)
|
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
s := t.String()
|
|
||||||
if _, err := f.WriteString(s); err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "yaml", "yml":
|
|
||||||
b, err := yaml.Marshal(c)
|
|
||||||
if err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
if _, err = f.WriteString(string(b)); err != nil {
|
|
||||||
return ConfigMarshalError{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ini":
|
case "ini":
|
||||||
keys := v.AllKeys()
|
keys := v.AllKeys()
|
||||||
cfg := ini.Empty()
|
cfg := ini.Empty()
|
||||||
|
|||||||
@@ -917,6 +917,54 @@ func TestBindPFlagsStringSlice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint: dupl
|
||||||
|
func TestBindPFlagsStringArray(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Expected []string
|
||||||
|
Value string
|
||||||
|
}{
|
||||||
|
{[]string{}, ""},
|
||||||
|
{[]string{"jeden"}, "jeden"},
|
||||||
|
{[]string{"dwa,trzy"}, "dwa,trzy"},
|
||||||
|
{[]string{"cztery,\"piec , szesc\""}, "cztery,\"piec , szesc\""},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := New() // create independent Viper object
|
||||||
|
defaultVal := []string{"default"}
|
||||||
|
v.SetDefault("stringarray", defaultVal)
|
||||||
|
|
||||||
|
for _, testValue := range tests {
|
||||||
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||||
|
flagSet.StringArray("stringarray", testValue.Expected, "test")
|
||||||
|
|
||||||
|
for _, changed := range []bool{true, false} {
|
||||||
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
||||||
|
f.Value.Set(testValue.Value)
|
||||||
|
f.Changed = changed
|
||||||
|
})
|
||||||
|
|
||||||
|
err := v.BindPFlags(flagSet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error binding flag set, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestStr struct {
|
||||||
|
StringArray []string
|
||||||
|
}
|
||||||
|
val := &TestStr{}
|
||||||
|
if err := v.Unmarshal(val); err != nil {
|
||||||
|
t.Fatalf("%+#v cannot unmarshal: %s", testValue.Value, err)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
assert.Equal(t, testValue.Expected, val.StringArray)
|
||||||
|
assert.Equal(t, testValue.Expected, v.Get("stringarray"))
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, defaultVal, val.StringArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// nolint: dupl
|
// nolint: dupl
|
||||||
func TestBindPFlagsIntSlice(t *testing.T) {
|
func TestBindPFlagsIntSlice(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user