From 804aa1bcb5e72d6f6eefd8254d81953d6edbacc4 Mon Sep 17 00:00:00 2001 From: Erik Kristensen Date: Thu, 28 Mar 2024 15:22:49 -0600 Subject: [PATCH] feat(filter): add In and NotIn operators (#44) * feat(filter): add In and NotIn operators * test: add additional tests * chore: updating some comments * chore(golangci-lint): fix violations --- pkg/filter/filter.go | 56 +++++++++++++++++++++++++--- pkg/filter/filter_test.go | 77 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 2f617ef..9aee616 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -4,6 +4,7 @@ package filter import ( "fmt" "regexp" + "slices" "strconv" "strings" "time" @@ -22,6 +23,8 @@ const ( DateOlderThan Type = "dateOlderThan" Suffix Type = "suffix" Prefix Type = "prefix" + NotIn Type = "NotIn" + In Type = "In" Global = "__global__" ) @@ -48,6 +51,7 @@ func (f Filters) Get(resourceType string) []Filter { return filters } +// Validate checks if the filters are valid or not and returns an error if they are not func (f Filters) Validate() error { for resourceType, filters := range f { for _, filter := range filters { @@ -60,7 +64,8 @@ func (f Filters) Validate() error { return nil } -// Append appends the filters from f2 to f +// Append appends the filters from f2 to f. This is primarily used to append filters from a preset +// to a set of filters that were defined on a resource type. func (f Filters) Append(f2 Filters) { for resourceType, filter := range f2 { f[resourceType] = append(f[resourceType], filter...) @@ -84,6 +89,9 @@ type Filter struct { // Value is the value to filter on Value string + // Values allows for multiple values to be specified for a filter + Values []string + // Invert is a flag to invert the filter Invert string } @@ -138,6 +146,12 @@ func (f *Filter) Match(o string) (bool, error) { case Suffix: return strings.HasSuffix(o, f.Value), nil + case In: + return slices.Contains(f.Values, o), nil + + case NotIn: + return !slices.Contains(f.Values, o), nil + default: return false, fmt.Errorf("unknown type %s", f.Type) } @@ -152,20 +166,50 @@ func (f *Filter) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } - m := map[string]string{} + m := map[string]interface{}{} err := unmarshal(m) if err != nil { fmt.Println("%%%%%%%%") return err } - f.Type = Type(m["type"]) - f.Value = m["value"] - f.Property = m["property"] - f.Invert = m["invert"] + f.Type = Type(m["type"].(string)) + + if m["value"] == nil { + f.Value = "" + } else { + f.Value = m["value"].(string) + } + + if m["values"] == nil { + f.Values = []string{} + } else { + interfaceSlice := m["values"].([]interface{}) + stringSlice := make([]string, len(interfaceSlice)) + for i, v := range interfaceSlice { + str, _ := v.(string) + stringSlice[i] = str + } + + f.Values = stringSlice + } + + if m["property"] == nil { + f.Property = "" + } else { + f.Property = m["property"].(string) + } + + if m["invert"] == nil { + f.Invert = "" + } else { + f.Invert = m["invert"].(string) + } + return nil } +// NewExactFilter creates a new filter that matches the exact value func NewExactFilter(value string) Filter { return Filter{ Type: Exact, diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index 97429ae..196dbd8 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -61,12 +61,12 @@ func TestFilter_GlobalYAML(t *testing.T) { expected := filter.Filters{ "Resource1": []filter.Filter{ - {Property: "prop3", Type: filter.Exact, Value: "value3"}, - {Property: "prop1", Type: filter.Exact, Value: "value1"}, + {Property: "prop3", Type: filter.Exact, Value: "value3", Values: []string{}}, + {Property: "prop1", Type: filter.Exact, Value: "value1", Values: []string{}}, }, "Resource2": []filter.Filter{ - {Property: "prop3", Type: filter.Exact, Value: "value3"}, - {Property: "prop2", Type: filter.Exact, Value: "value2"}, + {Property: "prop3", Type: filter.Exact, Value: "value3", Values: []string{}}, + {Property: "prop2", Type: filter.Exact, Value: "value2", Values: []string{}}, }, } @@ -217,6 +217,24 @@ func TestFilter_UnmarshalFilter(t *testing.T) { match: []string{"12345-somesuffix"}, error: true, }, + { + name: "invert", + yaml: `{"type":"exact","value":"foo","invert":"true"}`, + match: []string{"foo"}, + mismatch: []string{"bar", "baz"}, + }, + { + name: "not-in", + yaml: `{"type":"NotIn","values":["foo","bar"]}`, + match: []string{"baz", "qux"}, + mismatch: []string{"foo", "bar"}, + }, + { + name: "in", + yaml: `{"type":"In","values":["foo","bar"]}`, + match: []string{"foo", "bar"}, + mismatch: []string{"baz", "qux"}, + }, } for _, tc := range cases { @@ -274,6 +292,45 @@ func TestFilter_Merge(t *testing.T) { }, } + // Merge the two Filters objects + f1.Merge(f2) + + // Create the expected result + expected := filter.Filters{ + "resource1": []filter.Filter{ + {Property: "prop1", Type: filter.Exact, Value: "value1"}, + {Property: "prop2", Type: filter.Glob, Value: "value2"}, + }, + "resource2": []filter.Filter{ + {Property: "prop3", Type: filter.Regex, Value: "value3"}, + }, + } + + validateErr := expected.Validate() + assert.NoError(t, validateErr) + + // Check if the result is as expected + if !reflect.DeepEqual(f1, expected) { + t.Errorf("Merge() = %v, want %v", f1, expected) + } +} + +func TestFilter_Append(t *testing.T) { + // Create two Filters objects + f1 := filter.Filters{ + "resource1": []filter.Filter{ + {Property: "prop1", Type: filter.Exact, Value: "value1"}, + }, + } + f2 := filter.Filters{ + "resource1": []filter.Filter{ + {Property: "prop2", Type: filter.Glob, Value: "value2"}, + }, + "resource2": []filter.Filter{ + {Property: "prop3", Type: filter.Regex, Value: "value3"}, + }, + } + // Append the two Filters objects f1.Append(f2) @@ -297,6 +354,18 @@ func TestFilter_Merge(t *testing.T) { } } +func TestFilter_EmptyType(t *testing.T) { + f := filter.Filter{ + Property: "Name", + Type: "", + Value: "anything", + } + + match, err := f.Match("anything") + assert.NoError(t, err) + assert.True(t, match) +} + func TestFilter_ValidateError(t *testing.T) { filters := filter.Filters{ "resource1": []filter.Filter{