Merge pull request #7964 from hashicorp/dnephin/remove-patch-slice-of-maps-forward-compat
config: Use HookWeakDecodeFromSlice in place of PatchSliceOfMaps
This commit is contained in:
commit
c1feec176f
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/lib"
|
|
||||||
"github.com/hashicorp/consul/lib/decode"
|
"github.com/hashicorp/consul/lib/decode"
|
||||||
"github.com/hashicorp/hcl"
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
@ -48,75 +47,23 @@ func Parse(data string, format string) (c Config, md mapstructure.Metadata, err
|
||||||
return Config{}, mapstructure.Metadata{}, err
|
return Config{}, mapstructure.Metadata{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to be able to report fields which we cannot map as an
|
|
||||||
// error so that users find typos in their configuration quickly. To
|
|
||||||
// achieve this we use the mapstructure library which maps a a raw
|
|
||||||
// map[string]interface{} to a nested structure and reports unused
|
|
||||||
// fields. The input for a mapstructure.Decode expects a
|
|
||||||
// map[string]interface{} as produced by encoding/json.
|
|
||||||
//
|
|
||||||
// The HCL language allows to repeat map keys which forces it to
|
|
||||||
// store nested structs as []map[string]interface{} instead of
|
|
||||||
// map[string]interface{}. This is an ambiguity which makes the
|
|
||||||
// generated structures incompatible with a corresponding JSON
|
|
||||||
// struct. It also does not work well with the mapstructure library.
|
|
||||||
//
|
|
||||||
// In order to still use the mapstructure library to find unused
|
|
||||||
// fields we patch instances of []map[string]interface{} to a
|
|
||||||
// map[string]interface{} before we decode that into a Config
|
|
||||||
// struct.
|
|
||||||
//
|
|
||||||
// However, Config has some fields which are either
|
|
||||||
// []map[string]interface{} or are arrays of structs which
|
|
||||||
// encoding/json will decode to []map[string]interface{}. Therefore,
|
|
||||||
// we need to be able to specify exceptions for this mapping. The
|
|
||||||
// PatchSliceOfMaps() implements that mapping. All fields of type
|
|
||||||
// []map[string]interface{} are mapped to map[string]interface{} if
|
|
||||||
// it contains at most one value. If there is more than one value it
|
|
||||||
// panics. To define exceptions one can specify the nested field
|
|
||||||
// names in dot notation.
|
|
||||||
//
|
|
||||||
// todo(fs): There might be an easier way to achieve the same thing
|
|
||||||
// todo(fs): but this approach works for now.
|
|
||||||
m := lib.PatchSliceOfMaps(raw, []string{
|
|
||||||
"checks",
|
|
||||||
"segments",
|
|
||||||
"service.checks",
|
|
||||||
"services",
|
|
||||||
"services.checks",
|
|
||||||
"watches",
|
|
||||||
"service.connect.proxy.config.upstreams", // Deprecated
|
|
||||||
"services.connect.proxy.config.upstreams", // Deprecated
|
|
||||||
"service.connect.proxy.upstreams",
|
|
||||||
"services.connect.proxy.upstreams",
|
|
||||||
"service.connect.proxy.expose.paths",
|
|
||||||
"services.connect.proxy.expose.paths",
|
|
||||||
"service.proxy.upstreams",
|
|
||||||
"services.proxy.upstreams",
|
|
||||||
"service.proxy.expose.paths",
|
|
||||||
"services.proxy.expose.paths",
|
|
||||||
"acl.tokens.managed_service_provider",
|
|
||||||
|
|
||||||
// Need all the service(s) exceptions also for nested sidecar service.
|
|
||||||
"service.connect.sidecar_service.checks",
|
|
||||||
"services.connect.sidecar_service.checks",
|
|
||||||
"service.connect.sidecar_service.proxy.upstreams",
|
|
||||||
"services.connect.sidecar_service.proxy.upstreams",
|
|
||||||
"service.connect.sidecar_service.proxy.expose.paths",
|
|
||||||
"services.connect.sidecar_service.proxy.expose.paths",
|
|
||||||
}, []string{
|
|
||||||
"config_entries.bootstrap", // completely ignore this tree (fixed elsewhere)
|
|
||||||
})
|
|
||||||
|
|
||||||
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||||
DecodeHook: decode.HookTranslateKeys,
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
Metadata: &md,
|
// decode.HookWeakDecodeFromSlice is only necessary when reading from
|
||||||
Result: &c,
|
// an HCL config file. In the future we could omit it when reading from
|
||||||
|
// JSON configs. It is left here for now to maintain backwards compat
|
||||||
|
// for the unlikely scenario that someone is using malformed JSON configs
|
||||||
|
// and expecting this behaviour to correct their config.
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
|
decode.HookTranslateKeys,
|
||||||
|
),
|
||||||
|
Metadata: &md,
|
||||||
|
Result: &c,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, mapstructure.Metadata{}, err
|
return Config{}, mapstructure.Metadata{}, err
|
||||||
}
|
}
|
||||||
if err := d.Decode(m); err != nil {
|
if err := d.Decode(raw); err != nil {
|
||||||
return Config{}, mapstructure.Metadata{}, err
|
return Config{}, mapstructure.Metadata{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib"
|
|
||||||
"github.com/hashicorp/consul/lib/decode"
|
"github.com/hashicorp/consul/lib/decode"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
@ -102,13 +101,14 @@ type discoveryChainReadResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeDiscoveryChainReadRequest(raw map[string]interface{}) (*discoveryChainReadRequest, error) {
|
func decodeDiscoveryChainReadRequest(raw map[string]interface{}) (*discoveryChainReadRequest, error) {
|
||||||
// lib.TranslateKeys doesn't understand []map[string]interface{} so we have
|
|
||||||
// to do this part first.
|
|
||||||
raw = lib.PatchSliceOfMaps(raw, nil, nil)
|
|
||||||
|
|
||||||
var apiReq discoveryChainReadRequest
|
var apiReq discoveryChainReadRequest
|
||||||
|
// TODO(dnephin): at this time only JSON payloads are read, so it is unlikely
|
||||||
|
// that HookWeakDecodeFromSlice is necessary. It was added while porting
|
||||||
|
// from lib.PatchSliceOfMaps to decode.HookWeakDecodeFromSlice. It may be
|
||||||
|
// safe to remove in the future.
|
||||||
decodeConf := &mapstructure.DecoderConfig{
|
decodeConf := &mapstructure.DecoderConfig{
|
||||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
decode.HookTranslateKeys,
|
decode.HookTranslateKeys,
|
||||||
mapstructure.StringToTimeDurationHookFunc(),
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -284,18 +284,10 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
|
||||||
return nil, fmt.Errorf("Kind value in payload is not a string")
|
return nil, fmt.Errorf("Kind value in payload is not a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
skipWhenPatching, err := ConfigEntryDecodeRulesForKind(entry.GetKind())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// lib.TranslateKeys doesn't understand []map[string]interface{} so we have
|
|
||||||
// to do this part first.
|
|
||||||
raw = lib.PatchSliceOfMaps(raw, skipWhenPatching, nil)
|
|
||||||
|
|
||||||
var md mapstructure.Metadata
|
var md mapstructure.Metadata
|
||||||
decodeConf := &mapstructure.DecoderConfig{
|
decodeConf := &mapstructure.DecoderConfig{
|
||||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
decode.HookTranslateKeys,
|
decode.HookTranslateKeys,
|
||||||
mapstructure.StringToTimeDurationHookFunc(),
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
),
|
),
|
||||||
|
@ -326,54 +318,6 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigEntryDecodeRulesForKind returns rules for 'fixing' config entry key
|
|
||||||
// formats by kind. This is shared between the 'structs' and 'api' variations
|
|
||||||
// of config entries.
|
|
||||||
func ConfigEntryDecodeRulesForKind(kind string) (skipWhenPatching []string, err error) {
|
|
||||||
switch kind {
|
|
||||||
case ProxyDefaults:
|
|
||||||
return []string{
|
|
||||||
"expose.paths",
|
|
||||||
"Expose.Paths",
|
|
||||||
}, nil
|
|
||||||
case ServiceDefaults:
|
|
||||||
return []string{
|
|
||||||
"expose.paths",
|
|
||||||
"Expose.Paths",
|
|
||||||
}, nil
|
|
||||||
case ServiceRouter:
|
|
||||||
return []string{
|
|
||||||
"routes",
|
|
||||||
"Routes",
|
|
||||||
"routes.match.http.header",
|
|
||||||
"Routes.Match.HTTP.Header",
|
|
||||||
"routes.match.http.query_param",
|
|
||||||
"Routes.Match.HTTP.QueryParam",
|
|
||||||
}, nil
|
|
||||||
case ServiceSplitter:
|
|
||||||
return []string{
|
|
||||||
"splits",
|
|
||||||
"Splits",
|
|
||||||
}, nil
|
|
||||||
case ServiceResolver:
|
|
||||||
return nil, nil
|
|
||||||
case IngressGateway:
|
|
||||||
return []string{
|
|
||||||
"listeners",
|
|
||||||
"Listeners",
|
|
||||||
"listeners.services",
|
|
||||||
"Listeners.Services",
|
|
||||||
}, nil
|
|
||||||
case TerminatingGateway:
|
|
||||||
return []string{
|
|
||||||
"services",
|
|
||||||
"Services",
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("kind %q should be explicitly handled here", kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigEntryOp string
|
type ConfigEntryOp string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -58,7 +58,22 @@ type ProxyConfig struct {
|
||||||
// allows caller to choose whether and how to report the error.
|
// allows caller to choose whether and how to report the error.
|
||||||
func ParseProxyConfig(m map[string]interface{}) (ProxyConfig, error) {
|
func ParseProxyConfig(m map[string]interface{}) (ProxyConfig, error) {
|
||||||
var cfg ProxyConfig
|
var cfg ProxyConfig
|
||||||
err := mapstructure.WeakDecode(m, &cfg)
|
decodeConf := &mapstructure.DecoderConfig{
|
||||||
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
|
decode.HookTranslateKeys,
|
||||||
|
),
|
||||||
|
Result: &cfg,
|
||||||
|
WeaklyTypedInput: true,
|
||||||
|
}
|
||||||
|
decoder, err := mapstructure.NewDecoder(decodeConf)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(m); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
// Set defaults (even if error is returned)
|
// Set defaults (even if error is returned)
|
||||||
if cfg.Protocol == "" {
|
if cfg.Protocol == "" {
|
||||||
cfg.Protocol = "tcp"
|
cfg.Protocol = "tcp"
|
||||||
|
@ -103,7 +118,10 @@ type GatewayConfig struct {
|
||||||
func ParseGatewayConfig(m map[string]interface{}) (GatewayConfig, error) {
|
func ParseGatewayConfig(m map[string]interface{}) (GatewayConfig, error) {
|
||||||
var cfg GatewayConfig
|
var cfg GatewayConfig
|
||||||
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||||
DecodeHook: decode.HookTranslateKeys,
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
|
decode.HookTranslateKeys,
|
||||||
|
),
|
||||||
Result: &cfg,
|
Result: &cfg,
|
||||||
WeaklyTypedInput: true,
|
WeaklyTypedInput: true,
|
||||||
})
|
})
|
||||||
|
@ -204,7 +222,11 @@ func (p PassiveHealthCheck) AsOutlierDetection() *envoycluster.OutlierDetection
|
||||||
func ParseUpstreamConfigNoDefaults(m map[string]interface{}) (UpstreamConfig, error) {
|
func ParseUpstreamConfigNoDefaults(m map[string]interface{}) (UpstreamConfig, error) {
|
||||||
var cfg UpstreamConfig
|
var cfg UpstreamConfig
|
||||||
config := &mapstructure.DecoderConfig{
|
config := &mapstructure.DecoderConfig{
|
||||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
|
decode.HookTranslateKeys,
|
||||||
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
|
),
|
||||||
Result: &cfg,
|
Result: &cfg,
|
||||||
WeaklyTypedInput: true,
|
WeaklyTypedInput: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/command/flags"
|
"github.com/hashicorp/consul/command/flags"
|
||||||
"github.com/hashicorp/consul/command/helpers"
|
"github.com/hashicorp/consul/command/helpers"
|
||||||
"github.com/hashicorp/consul/lib"
|
|
||||||
"github.com/hashicorp/consul/lib/decode"
|
"github.com/hashicorp/consul/lib/decode"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
@ -133,18 +131,10 @@ func newDecodeConfigEntry(raw map[string]interface{}) (api.ConfigEntry, error) {
|
||||||
return nil, fmt.Errorf("Kind value in payload is not a string")
|
return nil, fmt.Errorf("Kind value in payload is not a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
skipWhenPatching, err := structs.ConfigEntryDecodeRulesForKind(entry.GetKind())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// lib.TranslateKeys doesn't understand []map[string]interface{} so we have
|
|
||||||
// to do this part first.
|
|
||||||
raw = lib.PatchSliceOfMaps(raw, skipWhenPatching, nil)
|
|
||||||
|
|
||||||
var md mapstructure.Metadata
|
var md mapstructure.Metadata
|
||||||
decodeConf := &mapstructure.DecoderConfig{
|
decodeConf := &mapstructure.DecoderConfig{
|
||||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
decode.HookWeakDecodeFromSlice,
|
||||||
decode.HookTranslateKeys,
|
decode.HookTranslateKeys,
|
||||||
mapstructure.StringToTimeDurationHookFunc(),
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -91,3 +91,44 @@ func canonicalFieldKey(field reflect.StructField) string {
|
||||||
}
|
}
|
||||||
return parts[0]
|
return parts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HookWeakDecodeFromSlice looks for []map[string]interface{} in the source
|
||||||
|
// data. If the target is not a slice or array it attempts to unpack 1 item
|
||||||
|
// out of the slice. If there are more items the source data is left unmodified,
|
||||||
|
// allowing mapstructure to handle and report the decode error caused by
|
||||||
|
// mismatched types.
|
||||||
|
//
|
||||||
|
// If this hook is being used on a "second pass" decode to decode an opaque
|
||||||
|
// configuration into a type, the DecodeConfig should set WeaklyTypedInput=true,
|
||||||
|
// (or another hook) to convert any scalar values into a slice of one value when
|
||||||
|
// the target is a slice. This is necessary because this hook would have converted
|
||||||
|
// the initial slices into single values on the first pass.
|
||||||
|
//
|
||||||
|
// Background
|
||||||
|
//
|
||||||
|
// HCL allows for repeated blocks which forces it to store structures
|
||||||
|
// as []map[string]interface{} instead of map[string]interface{}. This is an
|
||||||
|
// ambiguity which makes the generated structures incompatible with the
|
||||||
|
// corresponding JSON data.
|
||||||
|
//
|
||||||
|
// This hook allows config to be read from the HCL format into a raw structure,
|
||||||
|
// and later decoded into a strongly typed structure.
|
||||||
|
func HookWeakDecodeFromSlice(from, to reflect.Type, data interface{}) (interface{}, error) {
|
||||||
|
if from.Kind() == reflect.Slice && (to.Kind() == reflect.Slice || to.Kind() == reflect.Array) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch d := data.(type) {
|
||||||
|
case []map[string]interface{}:
|
||||||
|
switch {
|
||||||
|
case len(d) == 0:
|
||||||
|
return nil, nil
|
||||||
|
case len(d) == 1:
|
||||||
|
return d[0], nil
|
||||||
|
default:
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -205,3 +206,69 @@ type nested struct {
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHookWeakDecodeFromSlice_DoesNotModifySliceTargets(t *testing.T) {
|
||||||
|
source := `
|
||||||
|
slice {
|
||||||
|
name = "first"
|
||||||
|
}
|
||||||
|
slice {
|
||||||
|
name = "second"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
target := &nested{}
|
||||||
|
err := decodeHCLToMapStructure(source, target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &nested{
|
||||||
|
Slice: []Item{{Name: "first"}, {Name: "second"}},
|
||||||
|
}
|
||||||
|
require.Equal(t, target, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHCLToMapStructure(source string, target interface{}) error {
|
||||||
|
raw := map[string]interface{}{}
|
||||||
|
err := hcl.Decode(&raw, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
md := new(mapstructure.Metadata)
|
||||||
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||||
|
DecodeHook: HookWeakDecodeFromSlice,
|
||||||
|
Metadata: md,
|
||||||
|
Result: target,
|
||||||
|
})
|
||||||
|
return decoder.Decode(&raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookWeakDecodeFromSlice_ErrorsWithMultipleNestedBlocks(t *testing.T) {
|
||||||
|
source := `
|
||||||
|
item {
|
||||||
|
name = "first"
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
name = "second"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
target := &nested{}
|
||||||
|
err := decodeHCLToMapStructure(source, target)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "'Item' expected a map, got 'slice'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHookWeakDecodeFromSlice_UnpacksNestedBlocks(t *testing.T) {
|
||||||
|
source := `
|
||||||
|
item {
|
||||||
|
name = "first"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
target := &nested{}
|
||||||
|
err := decodeHCLToMapStructure(source, target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &nested{
|
||||||
|
Item: Item{Name: "first"},
|
||||||
|
}
|
||||||
|
require.Equal(t, target, expected)
|
||||||
|
}
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func PatchSliceOfMaps(m map[string]interface{}, skip []string, skipTree []string) map[string]interface{} {
|
|
||||||
lowerSkip := make([]string, len(skip))
|
|
||||||
lowerSkipTree := make([]string, len(skipTree))
|
|
||||||
|
|
||||||
for i, val := range skip {
|
|
||||||
lowerSkip[i] = strings.ToLower(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, val := range skipTree {
|
|
||||||
lowerSkipTree[i] = strings.ToLower(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
return patchValue("", m, lowerSkip, lowerSkipTree).(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func patchValue(name string, v interface{}, skip []string, skipTree []string) interface{} {
|
|
||||||
switch x := v.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
if len(x) == 0 {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
mm := make(map[string]interface{})
|
|
||||||
for k, v := range x {
|
|
||||||
key := k
|
|
||||||
if name != "" {
|
|
||||||
key = name + "." + k
|
|
||||||
}
|
|
||||||
mm[k] = patchValue(key, v, skip, skipTree)
|
|
||||||
}
|
|
||||||
return mm
|
|
||||||
|
|
||||||
case []interface{}:
|
|
||||||
if len(x) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strSliceContains(name, skipTree) {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
if strSliceContains(name, skip) {
|
|
||||||
for i, y := range x {
|
|
||||||
x[i] = patchValue(name, y, skip, skipTree)
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
if _, ok := x[0].(map[string]interface{}); !ok {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
if len(x) > 1 {
|
|
||||||
panic(fmt.Sprintf("%s: []map[string]interface{} with more than one element not supported: %s", name, v))
|
|
||||||
}
|
|
||||||
return patchValue(name, x[0], skip, skipTree)
|
|
||||||
|
|
||||||
case []map[string]interface{}:
|
|
||||||
if len(x) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strSliceContains(name, skipTree) {
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
if strSliceContains(name, skip) {
|
|
||||||
for i, y := range x {
|
|
||||||
x[i] = patchValue(name, y, skip, skipTree).(map[string]interface{})
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
if len(x) > 1 {
|
|
||||||
panic(fmt.Sprintf("%s: []map[string]interface{} with more than one element not supported: %s", name, v))
|
|
||||||
}
|
|
||||||
return patchValue(name, x[0], skip, skipTree)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func strSliceContains(s string, v []string) bool {
|
|
||||||
lower := strings.ToLower(s)
|
|
||||||
for _, vv := range v {
|
|
||||||
if lower == vv {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func parse(s string) map[string]interface{} {
|
|
||||||
var m map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(s), &m); err != nil {
|
|
||||||
panic(s + ":" + err.Error())
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPatchSliceOfMaps(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
in, out string
|
|
||||||
skip []string
|
|
||||||
skipTree []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: `{"a":{"b":"c"}}`,
|
|
||||||
out: `{"a":{"b":"c"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: `{"a":[{"b":"c"}]}`,
|
|
||||||
out: `{"a":{"b":"c"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: `{"a":[{"b":[{"c":"d"}]}]}`,
|
|
||||||
out: `{"a":{"b":{"c":"d"}}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: `{"a":[{"b":"c"}]}`,
|
|
||||||
out: `{"a":[{"b":"c"}]}`,
|
|
||||||
skip: []string{"a"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: `{
|
|
||||||
"Services": [
|
|
||||||
{
|
|
||||||
"checks": [
|
|
||||||
{
|
|
||||||
"header": [
|
|
||||||
{"a":"b"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
out: `{
|
|
||||||
"Services": [
|
|
||||||
{
|
|
||||||
"checks": [
|
|
||||||
{
|
|
||||||
"header": {"a":"b"}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`,
|
|
||||||
skip: []string{"services", "services.checks"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// inspired by the 'config_entries.bootstrap.*' structure for configs
|
|
||||||
in: `
|
|
||||||
{
|
|
||||||
"a": [
|
|
||||||
{
|
|
||||||
"b": [
|
|
||||||
{
|
|
||||||
"c": "val1",
|
|
||||||
"d": {
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
"e": [
|
|
||||||
{
|
|
||||||
"super": "duper"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
out: `
|
|
||||||
{
|
|
||||||
"a": {
|
|
||||||
"b": [
|
|
||||||
{
|
|
||||||
"c": "val1",
|
|
||||||
"d": {
|
|
||||||
"foo": "bar"
|
|
||||||
},
|
|
||||||
"e": [
|
|
||||||
{
|
|
||||||
"super": "duper"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
skipTree: []string{"a.b"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
desc := fmt.Sprintf("%02d: %s -> %s skip: %v", i, tt.in, tt.out, tt.skip)
|
|
||||||
t.Run(desc, func(t *testing.T) {
|
|
||||||
out := PatchSliceOfMaps(parse(tt.in), tt.skip, tt.skipTree)
|
|
||||||
if got, want := out, parse(tt.out); !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("\ngot %#v\nwant %#v", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue