aboutsummaryrefslogtreecommitdiffhomepage
path: root/config
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2021-06-09 10:58:18 +0200
committerBjørn Erik Pedersen <[email protected]>2021-06-14 17:00:32 +0200
commitd392893cd73dc00c927f342778f6dca9628d328e (patch)
treee2ea3eec09f36b7122ecdbc498c3c130e240e85c /config
parenta886dd53b80322e1edf924f2ede4d4ea037c5baf (diff)
downloadhugo-d392893cd73dc00c927f342778f6dca9628d328e.tar.gz
hugo-d392893cd73dc00c927f342778f6dca9628d328e.zip
Misc config loading fixes
The main motivation behind this is simplicity and correctnes, but the new small config library is also faster: ``` BenchmarkDefaultConfigProvider/Viper-16 252418 4546 ns/op 2720 B/op 30 allocs/op BenchmarkDefaultConfigProvider/Custom-16 450756 2651 ns/op 1008 B/op 6 allocs/op ``` Fixes #8633 Fixes #8618 Fixes #8630 Updates #8591 Closes #6680 Closes #5192
Diffstat (limited to 'config')
-rw-r--r--config/commonConfig_test.go4
-rw-r--r--config/compositeConfig.go113
-rw-r--r--config/compositeConfig_test.go40
-rw-r--r--config/configLoader.go23
-rw-r--r--config/configProvider.go5
-rw-r--r--config/configProvider_test.go3
-rw-r--r--config/defaultConfigProvider.go372
-rw-r--r--config/defaultConfigProvider_test.go315
-rw-r--r--config/docshelper.go45
-rw-r--r--config/privacy/privacyConfig_test.go3
-rw-r--r--config/services/servicesConfig_test.go4
11 files changed, 897 insertions, 30 deletions
diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go
index d4273277a..55767913f 100644
--- a/config/commonConfig_test.go
+++ b/config/commonConfig_test.go
@@ -21,14 +21,12 @@ import (
"github.com/gohugoio/hugo/common/types"
qt "github.com/frankban/quicktest"
-
- "github.com/spf13/viper"
)
func TestBuild(t *testing.T) {
c := qt.New(t)
- v := viper.New()
+ v := New()
v.Set("build", map[string]interface{}{
"useResourceCacheWhen": "always",
})
diff --git a/config/compositeConfig.go b/config/compositeConfig.go
new file mode 100644
index 000000000..c68419533
--- /dev/null
+++ b/config/compositeConfig.go
@@ -0,0 +1,113 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "github.com/gohugoio/hugo/common/maps"
+)
+
+// NewCompositeConfig creates a new composite Provider with a read-only base
+// and a writeable layer.
+func NewCompositeConfig(base, layer Provider) Provider {
+ return &compositeConfig{
+ base: base,
+ layer: layer,
+ }
+}
+
+// compositeConfig contains a read only config base with
+// a possibly writeable config layer on top.
+type compositeConfig struct {
+ base Provider
+ layer Provider
+}
+
+func (c *compositeConfig) GetBool(key string) bool {
+ if c.layer.IsSet(key) {
+ return c.layer.GetBool(key)
+ }
+ return c.base.GetBool(key)
+}
+
+func (c *compositeConfig) GetInt(key string) int {
+ if c.layer.IsSet(key) {
+ return c.layer.GetInt(key)
+ }
+ return c.base.GetInt(key)
+}
+
+func (c *compositeConfig) Merge(key string, value interface{}) {
+ c.layer.Merge(key, value)
+}
+
+func (c *compositeConfig) GetParams(key string) maps.Params {
+ if c.layer.IsSet(key) {
+ return c.layer.GetParams(key)
+ }
+ return c.base.GetParams(key)
+}
+
+func (c *compositeConfig) GetStringMap(key string) map[string]interface{} {
+ if c.layer.IsSet(key) {
+ return c.layer.GetStringMap(key)
+ }
+ return c.base.GetStringMap(key)
+}
+
+func (c *compositeConfig) GetStringMapString(key string) map[string]string {
+ if c.layer.IsSet(key) {
+ return c.layer.GetStringMapString(key)
+ }
+ return c.base.GetStringMapString(key)
+}
+
+func (c *compositeConfig) GetStringSlice(key string) []string {
+ if c.layer.IsSet(key) {
+ return c.layer.GetStringSlice(key)
+ }
+ return c.base.GetStringSlice(key)
+}
+
+func (c *compositeConfig) Get(key string) interface{} {
+ if c.layer.IsSet(key) {
+ return c.layer.Get(key)
+ }
+ return c.base.Get(key)
+}
+
+func (c *compositeConfig) IsSet(key string) bool {
+ if c.layer.IsSet(key) {
+ return true
+ }
+ return c.base.IsSet(key)
+}
+
+func (c *compositeConfig) GetString(key string) string {
+ if c.layer.IsSet(key) {
+ return c.layer.GetString(key)
+ }
+ return c.base.GetString(key)
+}
+
+func (c *compositeConfig) Set(key string, value interface{}) {
+ c.layer.Set(key, value)
+}
+
+func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
+ panic("not supported")
+}
+
+func (c *compositeConfig) SetDefaultMergeStrategy() {
+ panic("not supported")
+}
diff --git a/config/compositeConfig_test.go b/config/compositeConfig_test.go
new file mode 100644
index 000000000..60644102f
--- /dev/null
+++ b/config/compositeConfig_test.go
@@ -0,0 +1,40 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestCompositeConfig(t *testing.T) {
+ c := qt.New(t)
+
+ c.Run("Set and get", func(c *qt.C) {
+ base, layer := New(), New()
+ cfg := NewCompositeConfig(base, layer)
+
+ layer.Set("a1", "av")
+ base.Set("b1", "bv")
+ cfg.Set("c1", "cv")
+
+ c.Assert(cfg.Get("a1"), qt.Equals, "av")
+ c.Assert(cfg.Get("b1"), qt.Equals, "bv")
+ c.Assert(cfg.Get("c1"), qt.Equals, "cv")
+ c.Assert(cfg.IsSet("c1"), qt.IsTrue)
+ c.Assert(layer.IsSet("c1"), qt.IsTrue)
+ c.Assert(base.IsSet("c1"), qt.IsFalse)
+ })
+}
diff --git a/config/configLoader.go b/config/configLoader.go
index 6d94f0b79..0998b1bef 100644
--- a/config/configLoader.go
+++ b/config/configLoader.go
@@ -20,7 +20,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
- "github.com/spf13/viper"
)
var (
@@ -43,15 +42,11 @@ func IsValidConfigFilename(filename string) bool {
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
func FromConfigString(config, configType string) (Provider, error) {
- v := newViper()
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
if err != nil {
return nil, err
}
-
- v.MergeConfigMap(m)
-
- return v, nil
+ return NewFrom(m), nil
}
// FromFile loads the configuration from the given filename.
@@ -60,15 +55,7 @@ func FromFile(fs afero.Fs, filename string) (Provider, error) {
if err != nil {
return nil, err
}
-
- v := newViper()
-
- err = v.MergeConfigMap(m)
- if err != nil {
- return nil, err
- }
-
- return v, nil
+ return NewFrom(m), nil
}
// FromFileToMap is the same as FromFile, but it returns the config values
@@ -116,9 +103,3 @@ func init() {
func RenameKeys(m map[string]interface{}) {
keyAliases.Rename(m)
}
-
-func newViper() *viper.Viper {
- v := viper.New()
-
- return v
-}
diff --git a/config/configProvider.go b/config/configProvider.go
index 928bf948a..92206ca9e 100644
--- a/config/configProvider.go
+++ b/config/configProvider.go
@@ -14,6 +14,7 @@
package config
import (
+ "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
)
@@ -22,11 +23,15 @@ type Provider interface {
GetString(key string) string
GetInt(key string) int
GetBool(key string) bool
+ GetParams(key string) maps.Params
GetStringMap(key string) map[string]interface{}
GetStringMapString(key string) map[string]string
GetStringSlice(key string) []string
Get(key string) interface{}
Set(key string, value interface{})
+ Merge(key string, value interface{})
+ SetDefaultMergeStrategy()
+ WalkParams(walkFn func(params ...KeyParams) bool)
IsSet(key string) bool
}
diff --git a/config/configProvider_test.go b/config/configProvider_test.go
index d9fff56b6..0afba1e58 100644
--- a/config/configProvider_test.go
+++ b/config/configProvider_test.go
@@ -17,12 +17,11 @@ import (
"testing"
qt "github.com/frankban/quicktest"
- "github.com/spf13/viper"
)
func TestGetStringSlicePreserveString(t *testing.T) {
c := qt.New(t)
- cfg := viper.New()
+ cfg := New()
s := "This is a string"
sSlice := []string{"This", "is", "a", "slice"}
diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go
new file mode 100644
index 000000000..d9c9db7f1
--- /dev/null
+++ b/config/defaultConfigProvider.go
@@ -0,0 +1,372 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "sync"
+
+ "github.com/spf13/cast"
+
+ "github.com/gohugoio/hugo/common/maps"
+)
+
+var (
+
+ // ConfigRootKeysSet contains all of the config map root keys.
+ // TODO(bep) use this for something (docs etc.)
+ ConfigRootKeysSet = map[string]bool{
+ "build": true,
+ "caches": true,
+ "frontmatter": true,
+ "languages": true,
+ "imaging": true,
+ "markup": true,
+ "mediatypes": true,
+ "menus": true,
+ "minify": true,
+ "module": true,
+ "outputformats": true,
+ "params": true,
+ "permalinks": true,
+ "related": true,
+ "sitemap": true,
+ "taxonomies": true,
+ }
+
+ // ConfigRootKeys is a sorted version of ConfigRootKeysSet.
+ ConfigRootKeys []string
+)
+
+func init() {
+ for k := range ConfigRootKeysSet {
+ ConfigRootKeys = append(ConfigRootKeys, k)
+ }
+ sort.Strings(ConfigRootKeys)
+}
+
+// New creates a Provider backed by an empty maps.Params.
+func New() Provider {
+ return &defaultConfigProvider{
+ root: make(maps.Params),
+ }
+}
+
+// NewFrom creates a Provider backed by params.
+func NewFrom(params maps.Params) Provider {
+ maps.PrepareParams(params)
+ return &defaultConfigProvider{
+ root: params,
+ }
+}
+
+// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
+// All methods are thread safe.
+type defaultConfigProvider struct {
+ mu sync.RWMutex
+ root maps.Params
+
+ keyCache sync.Map
+}
+
+func (c *defaultConfigProvider) Get(k string) interface{} {
+ if k == "" {
+ return c.root
+ }
+ c.mu.RLock()
+ key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
+ if m == nil {
+ return nil
+ }
+ v := m[key]
+ c.mu.RUnlock()
+ return v
+}
+
+func (c *defaultConfigProvider) GetBool(k string) bool {
+ v := c.Get(k)
+ return cast.ToBool(v)
+}
+
+func (c *defaultConfigProvider) GetInt(k string) int {
+ v := c.Get(k)
+ return cast.ToInt(v)
+}
+
+func (c *defaultConfigProvider) IsSet(k string) bool {
+ var found bool
+ c.mu.RLock()
+ key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
+ if m != nil {
+ _, found = m[key]
+ }
+ c.mu.RUnlock()
+ return found
+}
+
+func (c *defaultConfigProvider) GetString(k string) string {
+ v := c.Get(k)
+ return cast.ToString(v)
+}
+
+func (c *defaultConfigProvider) GetParams(k string) maps.Params {
+ v := c.Get(k)
+ if v == nil {
+ return nil
+ }
+ return v.(maps.Params)
+}
+
+func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} {
+ v := c.Get(k)
+ return maps.ToStringMap(v)
+}
+
+func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
+ v := c.Get(k)
+ return maps.ToStringMapString(v)
+}
+
+func (c *defaultConfigProvider) GetStringSlice(k string) []string {
+ v := c.Get(k)
+ return cast.ToStringSlice(v)
+}
+
+func (c *defaultConfigProvider) Set(k string, v interface{}) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ k = strings.ToLower(k)
+
+ if k == "" {
+ if p, ok := maps.ToParamsAndPrepare(v); ok {
+ // Set the values directly in root.
+ c.root.Set(p)
+ } else {
+ c.root[k] = v
+ }
+
+ return
+ }
+
+ switch vv := v.(type) {
+ case map[string]interface{}:
+ var p maps.Params = vv
+ v = p
+ maps.PrepareParams(p)
+ }
+
+ key, m := c.getNestedKeyAndMap(k, true)
+
+ if existing, found := m[key]; found {
+ if p1, ok := existing.(maps.Params); ok {
+ if p2, ok := v.(maps.Params); ok {
+ p1.Set(p2)
+ return
+ }
+ }
+ }
+
+ m[key] = v
+}
+
+func (c *defaultConfigProvider) Merge(k string, v interface{}) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ k = strings.ToLower(k)
+
+ if k == "" {
+ rs, f := c.root.GetMergeStrategy()
+ if f && rs == maps.ParamsMergeStrategyNone {
+ // The user has set a "no merge" strategy on this,
+ // nothing more to do.
+ return
+ }
+
+ if p, ok := maps.ToParamsAndPrepare(v); ok {
+ // As there may be keys in p not in root, we need to handle
+ // those as a special case.
+ for kk, vv := range p {
+ if pp, ok := vv.(maps.Params); ok {
+ if ppp, ok := c.root[kk]; ok {
+ ppp.(maps.Params).Merge(pp)
+ } else {
+ // We need to use the default merge strategy for
+ // this key.
+ np := make(maps.Params)
+ strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
+ np.SetDefaultMergeStrategy(strategy)
+ np.Merge(pp)
+ if len(np) > 0 {
+ c.root[kk] = np
+ }
+ }
+ }
+ }
+ // Merge the rest.
+ c.root.Merge(p)
+ } else {
+ panic(fmt.Sprintf("unsupported type %T received in Merge", v))
+ }
+
+ return
+ }
+
+ switch vv := v.(type) {
+ case map[string]interface{}:
+ var p maps.Params = vv
+ v = p
+ maps.PrepareParams(p)
+ }
+
+ key, m := c.getNestedKeyAndMap(k, true)
+
+ if existing, found := m[key]; found {
+ if p1, ok := existing.(maps.Params); ok {
+ if p2, ok := v.(maps.Params); ok {
+ p1.Merge(p2)
+ }
+ }
+ } else {
+ m[key] = v
+ }
+}
+
+func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
+ var walk func(params ...KeyParams)
+ walk = func(params ...KeyParams) {
+ if walkFn(params...) {
+ return
+ }
+ p1 := params[len(params)-1]
+ i := len(params)
+ for k, v := range p1.Params {
+ if p2, ok := v.(maps.Params); ok {
+ paramsplus1 := make([]KeyParams, i+1)
+ copy(paramsplus1, params)
+ paramsplus1[i] = KeyParams{Key: k, Params: p2}
+ walk(paramsplus1...)
+ }
+ }
+ }
+ walk(KeyParams{Key: "", Params: c.root})
+}
+
+func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
+ if len(params) == 0 {
+ return maps.ParamsMergeStrategyNone
+ }
+
+ var (
+ strategy maps.ParamsMergeStrategy
+ prevIsRoot bool
+ curr = params[len(params)-1]
+ )
+
+ if len(params) > 1 {
+ prev := params[len(params)-2]
+ prevIsRoot = prev.Key == ""
+
+ // Inherit from parent (but not from the root unless it's set by user).
+ s, found := prev.Params.GetMergeStrategy()
+ if !prevIsRoot && !found {
+ panic("invalid state, merge strategy not set on parent")
+ }
+ if found || !prevIsRoot {
+ strategy = s
+ }
+ }
+
+ switch curr.Key {
+ case "":
+ // Don't set a merge strategy on the root unless set by user.
+ // This will be handled as a special case.
+ case "params":
+ strategy = maps.ParamsMergeStrategyDeep
+ case "outputformats", "mediatypes":
+ if prevIsRoot {
+ strategy = maps.ParamsMergeStrategyShallow
+ }
+ case "menus":
+ isMenuKey := prevIsRoot
+ if !isMenuKey {
+ // Can also be set below languages.
+ // root > languages > en > menus
+ if len(params) == 4 && params[1].Key == "languages" {
+ isMenuKey = true
+ }
+ }
+ if isMenuKey {
+ strategy = maps.ParamsMergeStrategyShallow
+ }
+ default:
+ if strategy == "" {
+ strategy = maps.ParamsMergeStrategyNone
+ }
+ }
+
+ return strategy
+}
+
+type KeyParams struct {
+ Key string
+ Params maps.Params
+}
+
+func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
+ c.WalkParams(func(params ...KeyParams) bool {
+ if len(params) == 0 {
+ return false
+ }
+ p := params[len(params)-1].Params
+ var found bool
+ if _, found = p.GetMergeStrategy(); found {
+ // Set by user.
+ return false
+ }
+ strategy := c.determineMergeStrategy(params...)
+ if strategy != "" {
+ p.SetDefaultMergeStrategy(strategy)
+ }
+ return false
+ })
+
+}
+
+func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
+ var parts []string
+ v, ok := c.keyCache.Load(key)
+ if ok {
+ parts = v.([]string)
+ } else {
+ parts = strings.Split(key, ".")
+ c.keyCache.Store(key, parts)
+ }
+ current := c.root
+ for i := 0; i < len(parts)-1; i++ {
+ next, found := current[parts[i]]
+ if !found {
+ if create {
+ next = make(maps.Params)
+ current[parts[i]] = next
+ } else {
+ return "", nil
+ }
+ }
+ current = next.(maps.Params)
+ }
+ return parts[len(parts)-1], current
+}
diff --git a/config/defaultConfigProvider_test.go b/config/defaultConfigProvider_test.go
new file mode 100644
index 000000000..834165d96
--- /dev/null
+++ b/config/defaultConfigProvider_test.go
@@ -0,0 +1,315 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/spf13/viper"
+
+ "github.com/gohugoio/hugo/common/para"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestDefaultConfigProvider(t *testing.T) {
+ c := qt.New(t)
+
+ c.Run("Set and get", func(c *qt.C) {
+ cfg := New()
+ var k string
+ var v interface{}
+
+ k, v = "foo", "bar"
+ cfg.Set(k, v)
+ c.Assert(cfg.Get(k), qt.Equals, v)
+ c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v)
+ c.Assert(cfg.GetString(k), qt.Equals, v)
+
+ k, v = "foo", 42
+ cfg.Set(k, v)
+ c.Assert(cfg.Get(k), qt.Equals, v)
+ c.Assert(cfg.GetInt(k), qt.Equals, v)
+
+ c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+ "foo": 42,
+ })
+ })
+
+ c.Run("Set and get map", func(c *qt.C) {
+ cfg := New()
+
+ cfg.Set("foo", map[string]interface{}{
+ "bar": "baz",
+ })
+
+ c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
+ "bar": "baz",
+ })
+
+ c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")})
+ c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")})
+ })
+
+ c.Run("Set and get nested", func(c *qt.C) {
+ cfg := New()
+
+ cfg.Set("a", map[string]interface{}{
+ "B": "bv",
+ })
+ cfg.Set("a.c", "cv")
+
+ c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
+ "b": "bv",
+ "c": "cv",
+ })
+ c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
+
+ cfg.Set("b.a", "av")
+ c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
+ "a": "av",
+ })
+
+ cfg.Set("b", map[string]interface{}{
+ "b": "bv",
+ })
+
+ c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
+ "a": "av",
+ "b": "bv",
+ })
+
+ cfg = New()
+
+ cfg.Set("a", "av")
+
+ cfg.Set("", map[string]interface{}{
+ "a": "av2",
+ "b": "bv2",
+ })
+
+ c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+ "a": "av2",
+ "b": "bv2",
+ })
+
+ cfg = New()
+
+ cfg.Set("a", "av")
+
+ cfg.Set("", map[string]interface{}{
+ "b": "bv2",
+ })
+
+ c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+ "a": "av",
+ "b": "bv2",
+ })
+
+ cfg = New()
+
+ cfg.Set("", map[string]interface{}{
+ "foo": map[string]interface{}{
+ "a": "av",
+ },
+ })
+
+ cfg.Set("", map[string]interface{}{
+ "foo": map[string]interface{}{
+ "b": "bv2",
+ },
+ })
+
+ c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
+ "a": "av",
+ "b": "bv2",
+ })
+ })
+
+ c.Run("Merge default strategy", func(c *qt.C) {
+ cfg := New()
+
+ cfg.Set("a", map[string]interface{}{
+ "B": "bv",
+ })
+
+ cfg.Merge("a", map[string]interface{}{
+ "B": "bv2",
+ "c": "cv2",
+ })
+
+ c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
+ "b": "bv",
+ "c": "cv2",
+ })
+
+ cfg = New()
+
+ cfg.Set("a", "av")
+
+ cfg.Merge("", map[string]interface{}{
+ "a": "av2",
+ "b": "bv2",
+ })
+
+ c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
+ "a": "av",
+ "b": "bv2",
+ })
+ })
+
+ c.Run("Merge shallow", func(c *qt.C) {
+ cfg := New()
+
+ cfg.Set("a", map[string]interface{}{
+ "_merge": "shallow",
+ "B": "bv",
+ "c": map[string]interface{}{
+ "b": "bv",
+ },
+ })
+
+ cfg.Merge("a", map[string]interface{}{
+ "c": map[string]interface{}{
+ "d": "dv2",
+ },
+ "e": "ev2",
+ })
+
+ c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
+ "e": "ev2",
+ "_merge": maps.ParamsMergeStrategyShallow,
+ "b": "bv",
+ "c": maps.Params{
+ "b": "bv",
+ },
+ })
+ })
+
+ c.Run("IsSet", func(c *qt.C) {
+ cfg := New()
+
+ cfg.Set("a", map[string]interface{}{
+ "B": "bv",
+ })
+
+ c.Assert(cfg.IsSet("A"), qt.IsTrue)
+ c.Assert(cfg.IsSet("a.b"), qt.IsTrue)
+ c.Assert(cfg.IsSet("z"), qt.IsFalse)
+ })
+
+ c.Run("Para", func(c *qt.C) {
+ cfg := New()
+ p := para.New(4)
+ r, _ := p.Start(context.Background())
+
+ setAndGet := func(k string, v int) error {
+ vs := strconv.Itoa(v)
+ cfg.Set(k, v)
+ err := errors.New("get failed")
+ if cfg.Get(k) != v {
+ return err
+ }
+ if cfg.GetInt(k) != v {
+ return err
+ }
+ if cfg.GetString(k) != vs {
+ return err
+ }
+ if !cfg.IsSet(k) {
+ return err
+ }
+ return nil
+ }
+
+ for i := 0; i < 20; i++ {
+ i := i
+ r.Run(func() error {
+ const v = 42
+ k := fmt.Sprintf("k%d", i)
+ if err := setAndGet(k, v); err != nil {
+ return err
+ }
+
+ m := maps.Params{
+ "new": 42,
+ }
+
+ cfg.Merge("", m)
+
+ return nil
+ })
+ }
+
+ c.Assert(r.Wait(), qt.IsNil)
+ })
+}
+
+func BenchmarkDefaultConfigProvider(b *testing.B) {
+ type cfger interface {
+ Get(key string) interface{}
+ Set(key string, value interface{})
+ IsSet(key string) bool
+ }
+
+ newMap := func() map[string]interface{} {
+ return map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c": 32,
+ "d": 43,
+ },
+ },
+ "b": 62,
+ }
+ }
+
+ runMethods := func(b *testing.B, cfg cfger) {
+ m := newMap()
+ cfg.Set("mymap", m)
+ cfg.Set("num", 32)
+ if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) {
+ b.Fatal("IsSet failed")
+ }
+
+ if cfg.Get("num") != 32 {
+ b.Fatal("Get failed")
+ }
+
+ if cfg.Get("mymap.a.b.c") != 32 {
+ b.Fatal("Get failed")
+ }
+ }
+
+ b.Run("Viper", func(b *testing.B) {
+ v := viper.New()
+ for i := 0; i < b.N; i++ {
+ runMethods(b, v)
+ }
+ })
+
+ b.Run("Custom", func(b *testing.B) {
+ cfg := New()
+ for i := 0; i < b.N; i++ {
+ runMethods(b, cfg)
+ }
+ })
+}
diff --git a/config/docshelper.go b/config/docshelper.go
new file mode 100644
index 000000000..336a0dc16
--- /dev/null
+++ b/config/docshelper.go
@@ -0,0 +1,45 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/docshelper"
+)
+
+// This is is just some helpers used to create some JSON used in the Hugo docs.
+func init() {
+ docsProvider := func() docshelper.DocProvider {
+
+ cfg := New()
+ for _, configRoot := range ConfigRootKeys {
+ cfg.Set(configRoot, make(maps.Params))
+ }
+ lang := maps.Params{
+ "en": maps.Params{
+ "menus": maps.Params{},
+ "params": maps.Params{},
+ },
+ }
+ cfg.Set("languages", lang)
+ cfg.SetDefaultMergeStrategy()
+
+ configHelpers := map[string]interface{}{
+ "mergeStrategy": cfg.Get(""),
+ }
+ return docshelper.DocProvider{"config": configHelpers}
+ }
+
+ docshelper.AddDocProviderFunc(docsProvider)
+}
diff --git a/config/privacy/privacyConfig_test.go b/config/privacy/privacyConfig_test.go
index 0fb599c0a..c17ce713d 100644
--- a/config/privacy/privacyConfig_test.go
+++ b/config/privacy/privacyConfig_test.go
@@ -18,7 +18,6 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
- "github.com/spf13/viper"
)
func TestDecodeConfigFromTOML(t *testing.T) {
@@ -94,7 +93,7 @@ PrivacyENhanced = true
func TestDecodeConfigDefault(t *testing.T) {
c := qt.New(t)
- pc, err := DecodeConfig(viper.New())
+ pc, err := DecodeConfig(config.New())
c.Assert(err, qt.IsNil)
c.Assert(pc, qt.Not(qt.IsNil))
c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, false)
diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go
index 6e979b999..d7a52ba4f 100644
--- a/config/services/servicesConfig_test.go
+++ b/config/services/servicesConfig_test.go
@@ -18,7 +18,7 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
- "github.com/spf13/viper"
+
)
func TestDecodeConfigFromTOML(t *testing.T) {
@@ -55,7 +55,7 @@ disableInlineCSS = true
func TestUseSettingsFromRootIfSet(t *testing.T) {
c := qt.New(t)
- cfg := viper.New()
+ cfg := config.New()
cfg.Set("disqusShortname", "root_short")
cfg.Set("googleAnalytics", "ga_root")