-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(re)enable map key override for plain YAML
- introduce tree walker/visitor for AST.
- Loading branch information
Showing
6 changed files
with
214 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Copyright 2020 VMware, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package yamlmeta | ||
|
||
import "sort" | ||
|
||
type overrideMapKeys struct{} | ||
|
||
// Visit if `node` is a Map, among its MapItem's that have duplicate keys, removes all but the last. | ||
// This visitor never returns an non-nil error. | ||
func (r *overrideMapKeys) visit(node Node) error { | ||
mapNode, isMap := node.(*Map) | ||
if !isMap { | ||
return nil | ||
} | ||
|
||
keyToIdxs := r.collectIndexesByKey(mapNode) | ||
idxsToRemove := r.selectRedundantIdxs(keyToIdxs) | ||
for _, idx := range idxsToRemove { | ||
mapNode.Items = append(mapNode.Items[:idx], mapNode.Items[idx+1:]...) | ||
} | ||
return nil | ||
} | ||
|
||
// collectIndexesByKey iterates over `mapNode's MapItem's and produces a mapping keys to the index of MapItem within | ||
// this map. The values in this mapping are in ascending order. | ||
func (r *overrideMapKeys) collectIndexesByKey(mapNode *Map) map[interface{}][]int { | ||
keyToIndexes := make(map[interface{}][]int) | ||
for idx, item := range mapNode.Items { | ||
idxs := keyToIndexes[item.Key] | ||
idxs = append(idxs, idx) | ||
keyToIndexes[item.Key] = idxs | ||
} | ||
return keyToIndexes | ||
} | ||
|
||
// selectRedundantIdxs given a mapping from key to list of indexes of MapItem's that have that key, produces a single | ||
// list of indexes that refer to all _but_ the last MapItem. | ||
// Values of `keyToIdxs` are assumed to be in ascending order. | ||
func (r *overrideMapKeys) selectRedundantIdxs(keyToIdxs map[interface{}][]int) []int { | ||
var idxsToRemove []int | ||
for _, idxs := range keyToIdxs { | ||
if len(idxs) == 1 { | ||
continue | ||
} | ||
idxsToRemove = append(idxsToRemove, idxs[:len(idxs)-1]...) | ||
sort.Sort(sort.Reverse(sort.IntSlice(idxsToRemove))) | ||
} | ||
return idxsToRemove | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// Copyright 2020 VMware, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package yamlmeta_test | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/k14s/ytt/pkg/yamlmeta" | ||
) | ||
|
||
func TestMapKeyOverridePlainYAML(t *testing.T) { | ||
t.Run("when no maps have duplicate keys, is a no op", func(t *testing.T) { | ||
docSet := &yamlmeta.DocumentSet{ | ||
Items: []*yamlmeta.Document{{ | ||
Value: &yamlmeta.Map{ | ||
Items: []*yamlmeta.MapItem{ | ||
{Key: "foo", Value: 1}, | ||
{Key: "bar", Value: 2}, | ||
}, | ||
}, | ||
}}, | ||
} | ||
expectedDocSet := docSet.DeepCopyAsNode() | ||
docSet.OverrideMapKeys() | ||
|
||
printer := yamlmeta.NewPrinterWithOpts(os.Stdout, yamlmeta.PrinterOpts{ExcludeRefs: true}) | ||
|
||
result := printer.PrintStr(docSet) | ||
expected := printer.PrintStr(expectedDocSet) | ||
assertEqual(t, result, expected) | ||
}) | ||
t.Run("when there are duplicates, last map item overrides", func(t *testing.T) { | ||
docSet := &yamlmeta.DocumentSet{ | ||
Items: []*yamlmeta.Document{{ | ||
Value: &yamlmeta.Map{ | ||
Items: []*yamlmeta.MapItem{ | ||
{Key: "foo", Value: 1}, | ||
{Key: "foo", Value: 2}, | ||
{Key: "foo", Value: 3}, | ||
{Key: "foo", Value: 4}, | ||
}, | ||
}, | ||
}}, | ||
} | ||
expectedDocSet := &yamlmeta.DocumentSet{ | ||
Items: []*yamlmeta.Document{{ | ||
Value: &yamlmeta.Map{ | ||
Items: []*yamlmeta.MapItem{ | ||
{Key: "foo", Value: 4}, | ||
}, | ||
}, | ||
}}, | ||
} | ||
docSet.OverrideMapKeys() | ||
|
||
printer := yamlmeta.NewPrinterWithOpts(os.Stdout, yamlmeta.PrinterOpts{ExcludeRefs: true}) | ||
|
||
result := printer.PrintStr(docSet) | ||
expected := printer.PrintStr(expectedDocSet) | ||
assertEqual(t, result, expected) | ||
}) | ||
t.Run("when there are multiple keys with duplicates, last map item for each key overrides the others", func(t *testing.T) { | ||
docSet := &yamlmeta.DocumentSet{ | ||
Items: []*yamlmeta.Document{{ | ||
Value: &yamlmeta.Map{ | ||
Items: []*yamlmeta.MapItem{ | ||
{Key: "foo", Value: 1}, | ||
{Key: "bar", Value: 2}, | ||
{Key: "ree", Value: 3}, | ||
{Key: "ree", Value: 4}, | ||
{Key: "foo", Value: 5}, | ||
{Key: "bar", Value: 6}, | ||
}, | ||
}, | ||
}}, | ||
} | ||
expectedDocSet := &yamlmeta.DocumentSet{ | ||
Items: []*yamlmeta.Document{{ | ||
Value: &yamlmeta.Map{ | ||
Items: []*yamlmeta.MapItem{ | ||
{Key: "ree", Value: 4}, | ||
{Key: "foo", Value: 5}, | ||
{Key: "bar", Value: 6}, | ||
}, | ||
}, | ||
}}, | ||
} | ||
docSet.OverrideMapKeys() | ||
|
||
printer := yamlmeta.NewPrinterWithOpts(os.Stdout, yamlmeta.PrinterOpts{ExcludeRefs: true}) | ||
|
||
result := printer.PrintStr(docSet) | ||
expected := printer.PrintStr(expectedDocSet) | ||
assertEqual(t, result, expected) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright 2020 VMware, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package yamlmeta | ||
|
||
// Visitor performs an operation on the given Node while traversing the AST. | ||
// Typically defines the action taken during a Walk(). | ||
type Visitor interface { | ||
visit(Node) error | ||
} | ||
|
||
// Walk traverses the tree starting at `n`, recursively, depth-first, invoking `v` on each node. | ||
// if `v` returns non-nil error, the traversal is aborted. | ||
func Walk(n Node, v Visitor) error { | ||
err := v.visit(n) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, c := range n.GetValues() { | ||
if cn, ok := c.(Node); ok { | ||
err := Walk(cn, v) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
return nil | ||
} |