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")
+	}
+}