SEBA-704 implement sorting

Change-Id: Ic790b01b36d6bad9f79e064bc04f77a22ddfd396
diff --git a/commands/command.go b/commands/command.go
index 4a38d5b..bdd7dfb 100644
--- a/commands/command.go
+++ b/commands/command.go
@@ -20,6 +20,7 @@
 	"encoding/json"
 	"fmt"
 	"github.com/opencord/cordctl/format"
+	"github.com/opencord/cordctl/order"
 	"google.golang.org/grpc"
 	"gopkg.in/yaml.v2"
 	"io"
@@ -95,6 +96,11 @@
 	OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
 }
 
+type ListOutputOptions struct {
+	OutputOptions
+	OrderBy string `short:"r" long:"orderby" default:"" description:"Specify the sort order of the results"`
+}
+
 func toOutputType(in string) OutputType {
 	switch in {
 	case "table":
@@ -110,6 +116,7 @@
 
 type CommandResult struct {
 	Format   format.Format
+	OrderBy  string
 	OutputAs OutputType
 	Data     interface{}
 }
@@ -191,17 +198,28 @@
 
 func GenerateOutput(result *CommandResult) {
 	if result != nil && result.Data != nil {
+		data := result.Data
+		if result.OrderBy != "" {
+			s, err := order.Parse(result.OrderBy)
+			if err != nil {
+				panic(err)
+			}
+			data, err = s.Process(data)
+			if err != nil {
+				panic(err)
+			}
+		}
 		if result.OutputAs == OUTPUT_TABLE {
 			tableFormat := format.Format(result.Format)
-			tableFormat.Execute(OutputStream, true, result.Data)
+			tableFormat.Execute(OutputStream, true, data)
 		} else if result.OutputAs == OUTPUT_JSON {
-			asJson, err := json.Marshal(&result.Data)
+			asJson, err := json.Marshal(&data)
 			if err != nil {
 				panic(err)
 			}
 			fmt.Fprintf(OutputStream, "%s", asJson)
 		} else if result.OutputAs == OUTPUT_YAML {
-			asYaml, err := yaml.Marshal(&result.Data)
+			asYaml, err := yaml.Marshal(&data)
 			if err != nil {
 				panic(err)
 			}
@@ -228,3 +246,23 @@
 
 	GenerateOutput(&result)
 }
+
+// Applies common output options to format and generate output
+func FormatAndGenerateListOutput(options *ListOutputOptions, default_format string, quiet_format string, data interface{}) {
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = default_format
+	}
+	if options.Quiet {
+		outputFormat = quiet_format
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+		OrderBy:  options.OrderBy,
+	}
+
+	GenerateOutput(&result)
+}
diff --git a/commands/models.go b/commands/models.go
index fd820b2..bd6552f 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -35,7 +35,7 @@
 type ModelNameString string
 
 type ModelList struct {
-	OutputOptions
+	ListOutputOptions
 	ShowHidden      bool   `long:"showhidden" description:"Show hidden fields in default output"`
 	ShowFeedback    bool   `long:"showfeedback" description:"Show feedback fields in default output"`
 	ShowBookkeeping bool   `long:"showbookkeeping" description:"Show bookkeeping fields in default output"`
@@ -220,7 +220,7 @@
 		}
 	}
 
-	FormatAndGenerateOutput(&options.OutputOptions, default_format.String(), "{{.id}}", data)
+	FormatAndGenerateListOutput(&options.ListOutputOptions, default_format.String(), "{{.id}}", data)
 
 	return nil
 }
diff --git a/commands/services.go b/commands/services.go
index e46dac6..e4a11cf 100644
--- a/commands/services.go
+++ b/commands/services.go
@@ -28,7 +28,7 @@
 )
 
 type ServiceList struct {
-	OutputOptions
+	ListOutputOptions
 }
 
 type ServiceListOutput struct {
@@ -88,7 +88,7 @@
 		data[i].State = val.GetFieldByName("state").(string)
 	}
 
-	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SERVICE_FORMAT, "{{.Name}}", data)
+	FormatAndGenerateListOutput(&options.ListOutputOptions, DEFAULT_SERVICE_FORMAT, "{{.Name}}", data)
 
 	return nil
 }
diff --git a/order/order.go b/order/order.go
new file mode 100644
index 0000000..ef19321
--- /dev/null
+++ b/order/order.go
@@ -0,0 +1,185 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original 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 order
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+	"strings"
+)
+
+type Operation int
+
+const (
+	ASC Operation = iota
+	DSC
+)
+
+type SortTerm struct {
+	Op   Operation
+	Name string
+}
+
+func (o Operation) String() string {
+	switch o {
+	default:
+		fallthrough
+	case ASC:
+		return "ASC"
+	case DSC:
+		return "DSC"
+	}
+}
+
+type Sorter []SortTerm
+
+func split(term string) SortTerm {
+	st := SortTerm{}
+	if len(term) > 0 {
+		switch term[0] {
+		case '+':
+			fallthrough
+		case '>':
+			st.Op = ASC
+			st.Name = term[1:]
+		case '-':
+			fallthrough
+		case '<':
+			st.Op = DSC
+			st.Name = term[1:]
+		default:
+			st.Op = ASC
+			st.Name = term
+		}
+	} else {
+		st.Op = ASC
+		st.Name = term
+	}
+	return st
+}
+
+// Parse parses a comma separated list of filter terms
+func Parse(spec string) (Sorter, error) {
+	terms := strings.Split(spec, ",")
+	s := make([]SortTerm, 0)
+	for _, term := range terms {
+		s = append(s, split(term))
+	}
+
+	return s, nil
+}
+
+func (s Sorter) Process(data interface{}) (interface{}, error) {
+	slice := reflect.ValueOf(data)
+	if slice.Kind() != reflect.Slice {
+		return data, nil
+	}
+
+	sort.SliceStable(data, func(i, j int) bool {
+		left := reflect.ValueOf(slice.Index(i).Interface())
+		right := reflect.ValueOf(slice.Index(j).Interface())
+		for _, term := range s {
+			var fleft, fright reflect.Value
+			if left.Kind() == reflect.Map {
+				fleft = left.MapIndex(reflect.ValueOf(term.Name))
+				fright = right.MapIndex(reflect.ValueOf(term.Name))
+			} else {
+				fleft = left.FieldByName(term.Name)
+				fright = right.FieldByName(term.Name)
+			}
+			switch fleft.Kind() {
+			case reflect.Uint:
+				fallthrough
+			case reflect.Uint8:
+				fallthrough
+			case reflect.Uint16:
+				fallthrough
+			case reflect.Uint32:
+				fallthrough
+			case reflect.Uint64:
+				ileft := fleft.Uint()
+				iright := fright.Uint()
+				switch term.Op {
+				case ASC:
+					if ileft < iright {
+						return true
+					} else if ileft > iright {
+						return false
+					}
+				case DSC:
+					if ileft > iright {
+						return true
+					} else if ileft < iright {
+						return false
+					}
+				}
+			case reflect.Int:
+				fallthrough
+			case reflect.Int8:
+				fallthrough
+			case reflect.Int16:
+				fallthrough
+			case reflect.Int32:
+				fallthrough
+			case reflect.Int64:
+				ileft := fleft.Int()
+				iright := fright.Int()
+				switch term.Op {
+				case ASC:
+					if ileft < iright {
+						return true
+					} else if ileft > iright {
+						return false
+					}
+				case DSC:
+					if ileft > iright {
+						return true
+					} else if ileft < iright {
+						return false
+					}
+				}
+			default:
+				var sleft, sright string
+				if left.Kind() == reflect.Map {
+					sleft = fmt.Sprintf("%v", left.MapIndex(reflect.ValueOf(term.Name)))
+					sright = fmt.Sprintf("%v", right.MapIndex(reflect.ValueOf(term.Name)))
+				} else {
+					sleft = fmt.Sprintf("%v", left.FieldByName(term.Name))
+					sright = fmt.Sprintf("%v", right.FieldByName(term.Name))
+				}
+				diff := strings.Compare(sleft, sright)
+				if term.Op != DSC {
+					if diff == -1 {
+						return true
+					} else if diff == 1 {
+						return false
+					}
+				} else {
+					if diff == 1 {
+						return true
+					} else if diff == -1 {
+						return false
+					}
+				}
+			}
+		}
+		return false
+	})
+
+	return data, nil
+}
diff --git a/order/order_test.go b/order/order_test.go
new file mode 100644
index 0000000..b136824
--- /dev/null
+++ b/order/order_test.go
@@ -0,0 +1,83 @@
+/*
+ * 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 order
+
+import (
+	"fmt"
+	"testing"
+)
+
+type SortTestStruct struct {
+	One   string
+	Two   string
+	Three uint64
+}
+
+var testList = []SortTestStruct{
+	{
+		One:   "a",
+		Two:   "x",
+		Three: 10,
+	},
+	{
+		One:   "a",
+		Two:   "c",
+		Three: 1,
+	},
+	{
+		One:   "a",
+		Two:   "b",
+		Three: 2,
+	},
+	{
+		One:   "a",
+		Two:   "a",
+		Three: 3,
+	},
+	{
+		One:   "b",
+		Two:   "a",
+		Three: 3,
+	},
+}
+
+func TestSort(t *testing.T) {
+	s, err := Parse("+One,-Two")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	//fmt.Printf("%#v\n", s)
+	o, err := s.Process(testList)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	fmt.Printf("END: %#v\n", o)
+}
+
+func TestSortInt(t *testing.T) {
+	s, err := Parse("Three,One")
+	if err != nil {
+		t.Errorf("Unable to parse sort specification")
+	}
+	//fmt.Printf("%#v\n", s)
+	o, err := s.Process(testList)
+	if err != nil {
+		t.Errorf("Sort failed: %s", err.Error())
+	}
+
+	fmt.Printf("END: %#v\n", o)
+}