Import of https://github.com/ciena/voltctl at commit 40d61fbf3f910ed4017cf67c9c79e8e1f82a33a5
Change-Id: I8464c59e60d76cb8612891db3303878975b5416c
diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go
new file mode 100644
index 0000000..5aa24ad
--- /dev/null
+++ b/pkg/filter/filter.go
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2019-present Ciena Corporation
+ *
+ * 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 filter
+
+import (
+ "fmt"
+ "reflect"
+ "regexp"
+ "strings"
+)
+
+type Operation int
+
+const (
+ UK Operation = iota
+ EQ
+ NE
+ GT
+ LT
+ GE
+ LE
+ RE
+)
+
+func toOp(op string) Operation {
+ switch op {
+ case "=":
+ return EQ
+ case "!=":
+ return NE
+ case ">":
+ return GT
+ case "<":
+ return LT
+ case ">=":
+ return GE
+ case "<=":
+ return LE
+ case "~":
+ return RE
+ default:
+ return UK
+ }
+}
+
+type FilterTerm struct {
+ Op Operation
+ Value string
+ re *regexp.Regexp
+}
+
+type Filter map[string]FilterTerm
+
+var termRE = regexp.MustCompile("^\\s*([a-zA-Z_][.a-zA-Z0-9_]*)\\s*(~|<=|>=|<|>|!=|=)\\s*(.+)\\s*$")
+
+// Parse parses a comma separated list of filter terms
+func Parse(spec string) (Filter, error) {
+ filter := make(map[string]FilterTerm)
+ terms := strings.Split(spec, ",")
+ var err error
+
+ // Each term is in the form <key><op><value>
+ for _, term := range terms {
+ parts := termRE.FindAllStringSubmatch(term, -1)
+ if parts == nil {
+ return nil, fmt.Errorf("Unable to parse filter term '%s'", term)
+ }
+ ft := FilterTerm{
+ Op: toOp(parts[0][2]),
+ Value: parts[0][3],
+ }
+ if ft.Op == RE {
+ ft.re, err = regexp.Compile(ft.Value)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to parse regexp filter value '%s'", ft.Value)
+ }
+ }
+ filter[parts[0][1]] = ft
+ }
+ return filter, nil
+}
+
+func (f Filter) Process(data interface{}) (interface{}, error) {
+ slice := reflect.ValueOf(data)
+ if slice.Kind() != reflect.Slice {
+ if f.Evaluate(data) {
+ return data, nil
+ }
+ return nil, nil
+ }
+
+ var result []interface{}
+
+ for i := 0; i < slice.Len(); i++ {
+ if f.Evaluate(slice.Index(i).Interface()) {
+ result = append(result, slice.Index(i).Interface())
+ }
+ }
+
+ return result, nil
+}
+
+func (f Filter) Evaluate(item interface{}) bool {
+ val := reflect.ValueOf(item)
+
+ for k, v := range f {
+ field := val.FieldByName(k)
+ if !field.IsValid() {
+ return false
+ }
+
+ switch v.Op {
+ case RE:
+ if !v.re.MatchString(fmt.Sprintf("%v", field)) {
+ return false
+ }
+ case EQ:
+ // This seems to work for most comparisons
+ if fmt.Sprintf("%v", field) != v.Value {
+ return false
+ }
+ default:
+ // For unsupported operations, always pass
+ }
+ }
+ return true
+}
diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go
new file mode 100644
index 0000000..c4c02f5
--- /dev/null
+++ b/pkg/filter/filter_test.go
@@ -0,0 +1,214 @@
+package filter
+
+import (
+ "testing"
+)
+
+type TestFilterStruct struct {
+ One string
+ Two string
+ Three string
+}
+
+func TestFilterList(t *testing.T) {
+ f, err := Parse("One=a,Two=b")
+ if err != nil {
+ t.Errorf("Unable to parse filter: %s", err.Error())
+ }
+
+ data := []interface{}{
+ TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "c",
+ },
+ TestFilterStruct{
+ One: "1",
+ Two: "2",
+ Three: "3",
+ },
+ TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "z",
+ },
+ }
+
+ r, _ := f.Process(data)
+
+ if _, ok := r.([]interface{}); !ok {
+ t.Errorf("Expected list, but didn't get one")
+ }
+
+ if len(r.([]interface{})) != 2 {
+ t.Errorf("Expected %d got %d", 2, len(r.([]interface{})))
+ }
+
+ if r.([]interface{})[0] != data[0] {
+ t.Errorf("Filtered list did not match, item %d", 0)
+ }
+ if r.([]interface{})[1] != data[2] {
+ t.Errorf("Filtered list did not match, item %d", 1)
+ }
+}
+
+func TestFilterItem(t *testing.T) {
+ f, err := Parse("One=a,Two=b")
+ if err != nil {
+ t.Errorf("Unable to parse filter: %s", err.Error())
+ }
+
+ data := TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "c",
+ }
+
+ r, _ := f.Process(data)
+
+ if r == nil {
+ t.Errorf("Expected item, got nil")
+ }
+
+ if _, ok := r.([]interface{}); ok {
+ t.Errorf("Expected item, but got list")
+ }
+}
+
+func TestGoodFilters(t *testing.T) {
+ var f Filter
+ var err error
+ f, err = Parse("One=a,Two=b")
+ if err != nil {
+ t.Errorf("1. Unable to parse filter: %s", err.Error())
+ }
+ if len(f) != 2 ||
+ f["One"].Value != "a" ||
+ f["One"].Op != EQ ||
+ f["Two"].Value != "b" ||
+ f["Two"].Op != EQ {
+ t.Errorf("1. Filter did not parse correctly")
+ }
+
+ f, err = Parse("One=a")
+ if err != nil {
+ t.Errorf("2. Unable to parse filter: %s", err.Error())
+ }
+ if len(f) != 1 ||
+ f["One"].Value != "a" ||
+ f["One"].Op != EQ {
+ t.Errorf("2. Filter did not parse correctly")
+ }
+
+ f, err = Parse("One<a")
+ if err != nil {
+ t.Errorf("3. Unable to parse filter: %s", err.Error())
+ }
+ if len(f) != 1 ||
+ f["One"].Value != "a" ||
+ f["One"].Op != LT {
+ t.Errorf("3. Filter did not parse correctly")
+ }
+
+ f, err = Parse("One!=a")
+ if err != nil {
+ t.Errorf("4. Unable to parse filter: %s", err.Error())
+ }
+ if len(f) != 1 ||
+ f["One"].Value != "a" ||
+ f["One"].Op != NE {
+ t.Errorf("4. Filter did not parse correctly")
+ }
+}
+
+func TestBadFilters(t *testing.T) {
+ _, err := Parse("One%a")
+ if err == nil {
+ t.Errorf("Parsed filter when it shouldn't have")
+ }
+}
+
+func TestSingleRecord(t *testing.T) {
+ f, err := Parse("One=d")
+ if err != nil {
+ t.Errorf("Unable to parse filter: %s", err.Error())
+ }
+
+ data := TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "c",
+ }
+
+ r, err := f.Process(data)
+ if err != nil {
+ t.Errorf("Error processing data")
+ }
+
+ if r != nil {
+ t.Errorf("expected no results, got some")
+ }
+}
+
+// Invalid fields are ignored (i.e. an error is returned, but need to
+// cover the code path in tests
+func TestInvalidField(t *testing.T) {
+ f, err := Parse("Four=a")
+ if err != nil {
+ t.Errorf("Unable to parse filter: %s", err.Error())
+ }
+
+ data := TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "c",
+ }
+
+ r, err := f.Process(data)
+ if err != nil {
+ t.Errorf("Error processing data")
+ }
+
+ if r != nil {
+ t.Errorf("expected no results, got some")
+ }
+}
+
+func TestREFilter(t *testing.T) {
+ var f Filter
+ var err error
+ f, err = Parse("One~a")
+ if err != nil {
+ t.Errorf("Unable to parse RE expression")
+ }
+ if len(f) != 1 {
+ t.Errorf("filter parsed incorrectly")
+ }
+
+ data := []interface{}{
+ TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "c",
+ },
+ TestFilterStruct{
+ One: "1",
+ Two: "2",
+ Three: "3",
+ },
+ TestFilterStruct{
+ One: "a",
+ Two: "b",
+ Three: "z",
+ },
+ }
+
+ f.Process(data)
+}
+
+func TestBadRE(t *testing.T) {
+ _, err := Parse("One~(qs*")
+ if err == nil {
+ t.Errorf("Expected RE parse error, got none")
+ }
+}