From a68ddf1f2ab010a82408e98fd706d519ebd86d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Thu, 9 Jun 2022 15:04:11 +0200 Subject: [PATCH] Add a resoruce indexing utility struct The struct indexes resources which lowers set search complexity from linear to constant. --- pkg/resource/resource.go | 80 +++++++++++++++++++++++++++++++++++ pkg/resource/resource_test.go | 78 ++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 pkg/resource/resource.go create mode 100644 pkg/resource/resource_test.go diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go new file mode 100644 index 000000000..787762abc --- /dev/null +++ b/pkg/resource/resource.go @@ -0,0 +1,80 @@ +package resource + +type ( + IndexNode struct { + children map[string]*IndexNode + + values []interface{} + } +) + +func NewIndex() *IndexNode { + return &IndexNode{ + children: make(map[string]*IndexNode), + values: make([]interface{}, 0), + } +} + +func (in *IndexNode) Add(value interface{}, pp ...[]string) { + if in == nil { + *in = *NewIndex() + } + + if len(pp) == 1 { + for _, identifier := range pp[0] { + if identifier == "*" { + in.values = append(in.values, value) + } else { + nn := in.children[identifier] + if nn == nil { + nn = NewIndex() + in.children[identifier] = nn + } + + nn.values = append(nn.values, value) + } + } + return + } + + p := pp[0] + for _, identifier := range p { + if identifier == "*" { + in.values = append(in.values, value) + continue + } + + if _, ok := in.children[identifier]; !ok { + in.children[identifier] = NewIndex() + } + + in.children[identifier].Add(value, pp[1:]...) + } +} + +func (di *IndexNode) Collect(pp ...[]string) (out []interface{}) { + if len(pp) == 0 { + if len(di.values) > 0 { + out = append(out, di.values...) + } + return + } + + p := pp[0] + for _, identifier := range p { + skip := false + if _, ok := di.children[identifier]; ok { + aux := di.children[identifier].Collect(pp[1:]...) + out = append(out, aux...) + + skip = len(aux) > 0 + } + + out = append(out, di.values...) + if skip { + break + } + } + + return +} diff --git a/pkg/resource/resource_test.go b/pkg/resource/resource_test.go new file mode 100644 index 000000000..56085296b --- /dev/null +++ b/pkg/resource/resource_test.go @@ -0,0 +1,78 @@ +package resource + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func pathHelper(pp ...string) (out [][]string) { + for _, p := range pp { + out = append(out, []string{p}) + } + return +} + +func TestSimplePath(t *testing.T) { + n := NewIndex() + + n.Add(1, pathHelper("a")...) + + out := n.Collect(pathHelper("a")...) + require.Len(t, out, 1) + require.Equal(t, 1, out[0]) +} + +func TestSimpleLongPath(t *testing.T) { + n := NewIndex() + + n.Add(1, pathHelper("a", "b", "c")...) + + out := n.Collect(pathHelper("a", "b", "c")...) + require.Len(t, out, 1) + require.Equal(t, 1, out[0]) +} + +func TestWildSimplePath(t *testing.T) { + n := NewIndex() + + n.Add(1, pathHelper("a", "*")...) + + out := n.Collect(pathHelper("a", "b")...) + require.Len(t, out, 1) + require.Equal(t, 1, out[0]) +} + +func TestComplex(t *testing.T) { + n := NewIndex() + + n.Add(1, pathHelper("resource", "lvl1.id1", "lvl2.id2", "res.id")...) + n.Add(2, pathHelper("resource", "lvl1.id1", "lvl2.id2", "*")...) + n.Add(3, pathHelper("resource", "lvl1.id1", "lvl2.id2", "*")...) + n.Add(4, pathHelper("resource", "*", "*", "*")...) + + var out []interface{} + + t.Run("full path", func(t *testing.T) { + out = n.Collect(pathHelper("resource", "lvl1.id1", "lvl2.id2", "res.id")...) + require.Len(t, out, 4) + require.Equal(t, []interface{}{1, 2, 3, 4}, out) + }) + + t.Run("full path; id not matching", func(t *testing.T) { + out = n.Collect(pathHelper("resource", "lvl1.id1", "lvl2.id2", "invalid")...) + require.Len(t, out, 3) + require.Equal(t, []interface{}{2, 3, 4}, out) + }) + + t.Run("partial matching path 1", func(t *testing.T) { + out = n.Collect(pathHelper("resource", "invald", "invalid", "invalid")...) + require.Len(t, out, 1) + require.Equal(t, []interface{}{4}, out) + }) + + t.Run("not matching path", func(t *testing.T) { + out = n.Collect(pathHelper("invalid", "invald", "invalid", "invalid")...) + require.Len(t, out, 0) + }) +}