mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-27 22:09:53 +02:00
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
This commit is contained in:
@@ -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",
|
||||
})
|
||||
|
113
config/compositeConfig.go
Normal file
113
config/compositeConfig.go
Normal file
@@ -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")
|
||||
}
|
40
config/compositeConfig_test.go
Normal file
40
config/compositeConfig_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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"}
|
||||
|
372
config/defaultConfigProvider.go
Normal file
372
config/defaultConfigProvider.go
Normal file
@@ -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
|
||||
}
|
315
config/defaultConfigProvider_test.go
Normal file
315
config/defaultConfigProvider_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
45
config/docshelper.go
Normal file
45
config/docshelper.go
Normal file
@@ -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)
|
||||
}
|
@@ -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)
|
||||
|
@@ -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")
|
||||
|
||||
|
Reference in New Issue
Block a user