Merge "[VOL-2736]host and port should be specified as a single argument not as two separate arguments"
diff --git a/VERSION b/VERSION
index b08cf5b..0664a8f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.1.5-dev
+1.1.6
diff --git a/internal/pkg/commands/devices.go b/internal/pkg/commands/devices.go
index b6864f6..3068902 100644
--- a/internal/pkg/commands/devices.go
+++ b/internal/pkg/commands/devices.go
@@ -41,7 +41,10 @@
   ADMINSTATE:    {{.AdminState}}
   OPERSTATUS:    {{.OperStatus}}
   CONNECTSTATUS: {{.ConnectStatus}}`
-	DEFAULT_DEVICE_VALUE_GET_FORMAT = "table{{.Name}}\t{{.Result}}"
+	DEFAULT_DEVICE_PM_CONFIG_GET_FORMAT         = "table{{.DefaultFreq}}\t{{.Grouped}}\t{{.FreqOverride}}"
+	DEFAULT_DEVICE_PM_CONFIG_METRIC_LIST_FORMAT = "table{{.Name}}\t{{.Type}}\t{{.Enabled}}\t{{.SampleFreq}}"
+	DEFAULT_DEVICE_PM_CONFIG_GROUP_LIST_FORMAT  = "table{{.GroupName}}\t{{.Enabled}}\t{{.GroupFreq}}"
+	DEFAULT_DEVICE_VALUE_GET_FORMAT             = "table{{.Name}}\t{{.Result}}"
 )
 
 type DeviceList struct {
@@ -57,6 +60,8 @@
 
 type DeviceId string
 
+type MetricName string
+type GroupName string
 type PortNum uint32
 type ValueFlag string
 
@@ -119,6 +124,71 @@
 	} `positional-args:"yes"`
 }
 
+type DevicePmConfigsGet struct {
+	ListOutputOptions
+	Args struct {
+		Id DeviceId `positional-arg-name:"DEVICE_ID" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigMetricList struct {
+	ListOutputOptions
+	Args struct {
+		Id DeviceId `positional-arg-name:"DEVICE_ID" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigGroupList struct {
+	ListOutputOptions
+	Args struct {
+		Id DeviceId `positional-arg-name:"DEVICE_ID" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigGroupMetricList struct {
+	ListOutputOptions
+	Args struct {
+		Id    DeviceId  `positional-arg-name:"DEVICE_ID" required:"yes"`
+		Group GroupName `positional-arg-name:"GROUP_NAME" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigFrequencySet struct {
+	OutputOptions
+	Args struct {
+		Frequency uint32   `positional-arg-name:"FREQUENCY" required:"yes"`
+		Id        DeviceId `positional-arg-name:"DEVICE_ID" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigMetricEnable struct {
+	Args struct {
+		Id      DeviceId     `positional-arg-name:"DEVICE_ID" required:"yes"`
+		Metrics []MetricName `positional-arg-name:"METRIC_NAME" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigMetricDisable struct {
+	Args struct {
+		Id      DeviceId     `positional-arg-name:"DEVICE_ID" required:"yes"`
+		Metrics []MetricName `positional-arg-name:"METRIC_NAME" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigGroupEnable struct {
+	Args struct {
+		Id     DeviceId    `positional-arg-name:"DEVICE_ID" required:"yes"`
+		Groups []GroupName `positional-arg-name:"GROUP_NAME" required:"yes"`
+	} `positional-args:"yes"`
+}
+
+type DevicePmConfigGroupDisable struct {
+	Args struct {
+		Id     DeviceId    `positional-arg-name:"DEVICE_ID" required:"yes"`
+		Groups []GroupName `positional-arg-name:"GROUP_NAME" required:"yes"`
+	} `positional-args:"yes"`
+}
+
 type DeviceGetExtValue struct {
 	ListOutputOptions
 	Args struct {
@@ -143,6 +213,25 @@
 	Value   struct {
 		Get DeviceGetExtValue `command:"get"`
 	} `command:"value"`
+	PmConfig struct {
+		Get       DevicePmConfigsGet `command:"get"`
+		Frequency struct {
+			Set DevicePmConfigFrequencySet `command:"set"`
+		} `command:"frequency"`
+		Metric struct {
+			List    DevicePmConfigMetricList    `command:"list"`
+			Enable  DevicePmConfigMetricEnable  `command:"enable"`
+			Disable DevicePmConfigMetricDisable `command:"disable"`
+		} `command:"metric"`
+		Group struct {
+			List    DevicePmConfigGroupList    `command:"list"`
+			Enable  DevicePmConfigGroupEnable  `command:"enable"`
+			Disable DevicePmConfigGroupDisable `command:"disable"`
+		} `command:"group"`
+		GroupMetric struct {
+			List DevicePmConfigGroupMetricList `command:"list"`
+		} `command:"groupmetric"`
+	} `command:"pmconfig"`
 }
 
 var deviceOpts = DeviceOpts{}
@@ -153,6 +242,109 @@
 	}
 }
 
+func (i *MetricName) Complete(match string) []flags.Completion {
+	conn, err := NewConnection()
+	if err != nil {
+		return nil
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	var deviceId string
+found:
+	for i := len(os.Args) - 1; i >= 0; i -= 1 {
+		switch os.Args[i] {
+		case "enable":
+			fallthrough
+		case "disable":
+			if len(os.Args) > i+1 {
+				deviceId = os.Args[i+1]
+			} else {
+				return nil
+			}
+			break found
+		default:
+		}
+	}
+
+	if len(deviceId) == 0 {
+		return nil
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(deviceId)}
+
+	pmconfigs, err := client.ListDevicePmConfigs(ctx, &id)
+
+	if err != nil {
+		return nil
+	}
+
+	list := make([]flags.Completion, 0)
+	for _, metrics := range pmconfigs.Metrics {
+		if strings.HasPrefix(metrics.Name, match) {
+			list = append(list, flags.Completion{Item: metrics.Name})
+		}
+	}
+
+	return list
+}
+
+func (i *GroupName) Complete(match string) []flags.Completion {
+	conn, err := NewConnection()
+	if err != nil {
+		return nil
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	var deviceId string
+found:
+	for i := len(os.Args) - 1; i >= 0; i -= 1 {
+		switch os.Args[i] {
+		case "list":
+			fallthrough
+		case "enable":
+			fallthrough
+		case "disable":
+			if len(os.Args) > i+1 {
+				deviceId = os.Args[i+1]
+			} else {
+				return nil
+			}
+			break found
+		default:
+		}
+	}
+
+	if len(deviceId) == 0 {
+		return nil
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(deviceId)}
+
+	pmconfigs, err := client.ListDevicePmConfigs(ctx, &id)
+
+	if err != nil {
+		return nil
+	}
+
+	list := make([]flags.Completion, 0)
+	for _, group := range pmconfigs.Groups {
+		if strings.HasPrefix(group.GroupName, match) {
+			list = append(list, flags.Completion{Item: group.GroupName})
+		}
+	}
+	return list
+}
+
 func (i *PortNum) Complete(match string) []flags.Completion {
 	conn, err := NewConnection()
 	if err != nil {
@@ -473,9 +665,6 @@
 	if outputFormat == "" {
 		outputFormat = GetCommandOptionWithDefault("device-ports", "format", DEFAULT_DEVICE_PORTS_FORMAT)
 	}
-	if options.Quiet {
-		outputFormat = "{{.Id}}"
-	}
 
 	orderBy := options.OrderBy
 	if orderBy == "" {
@@ -592,6 +781,406 @@
 	return nil
 }
 
+func (options *DevicePmConfigsGet) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = GetCommandOptionWithDefault("device-pm-configs", "format", DEFAULT_DEVICE_PM_CONFIG_GET_FORMAT)
+	}
+
+	orderBy := options.OrderBy
+	if orderBy == "" {
+		orderBy = GetCommandOptionWithDefault("device-pm-configs", "order", "")
+	}
+
+	result := CommandResult{
+		Format:    format.Format(outputFormat),
+		Filter:    options.Filter,
+		OrderBy:   orderBy,
+		OutputAs:  toOutputType(options.OutputAs),
+		NameLimit: options.NameLimit,
+		Data:      pmConfigs,
+	}
+
+	GenerateOutput(&result)
+	return nil
+
+}
+
+func (options *DevicePmConfigMetricList) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	if !pmConfigs.Grouped {
+		for _, metric := range pmConfigs.Metrics {
+			if metric.SampleFreq == 0 {
+				metric.SampleFreq = pmConfigs.DefaultFreq
+			}
+		}
+
+	}
+
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = GetCommandOptionWithDefault("device-pm-configs", "format", DEFAULT_DEVICE_PM_CONFIG_METRIC_LIST_FORMAT)
+	}
+
+	orderBy := options.OrderBy
+	if orderBy == "" {
+		orderBy = GetCommandOptionWithDefault("device-pm-configs", "order", "")
+	}
+
+	result := CommandResult{
+		Format:    format.Format(outputFormat),
+		Filter:    options.Filter,
+		OrderBy:   orderBy,
+		OutputAs:  toOutputType(options.OutputAs),
+		NameLimit: options.NameLimit,
+		Data:      pmConfigs.Metrics,
+	}
+
+	GenerateOutput(&result)
+	return nil
+
+}
+
+func (options *DevicePmConfigMetricEnable) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	if !pmConfigs.Grouped {
+		for _, metric := range pmConfigs.Metrics {
+			for _, mName := range options.Args.Metrics {
+				if string(mName) == metric.Name && !metric.Enabled {
+					metric.Enabled = true
+					_, err := client.UpdateDevicePmConfigs(ctx, pmConfigs)
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (options *DevicePmConfigMetricDisable) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	if !pmConfigs.Grouped {
+		for _, metric := range pmConfigs.Metrics {
+			for _, mName := range options.Args.Metrics {
+				if string(mName) == metric.Name && metric.Enabled {
+					metric.Enabled = false
+					_, err := client.UpdateDevicePmConfigs(ctx, pmConfigs)
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (options *DevicePmConfigGroupEnable) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	if pmConfigs.Grouped {
+		for _, group := range pmConfigs.Groups {
+			for _, gName := range options.Args.Groups {
+				if string(gName) == group.GroupName && !group.Enabled {
+					group.Enabled = true
+					_, err := client.UpdateDevicePmConfigs(ctx, pmConfigs)
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (options *DevicePmConfigGroupDisable) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	if pmConfigs.Grouped {
+		for _, group := range pmConfigs.Groups {
+			for _, gName := range options.Args.Groups {
+				if string(gName) == group.GroupName && group.Enabled {
+					group.Enabled = false
+					_, err := client.UpdateDevicePmConfigs(ctx, pmConfigs)
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (options *DevicePmConfigGroupList) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	if pmConfigs.Grouped {
+		for _, group := range pmConfigs.Groups {
+			if group.GroupFreq == 0 {
+				group.GroupFreq = pmConfigs.DefaultFreq
+			}
+		}
+
+	}
+
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = GetCommandOptionWithDefault("device-pm-configs", "format", DEFAULT_DEVICE_PM_CONFIG_GROUP_LIST_FORMAT)
+	}
+
+	orderBy := options.OrderBy
+	if orderBy == "" {
+		orderBy = GetCommandOptionWithDefault("device-pm-configs", "order", "")
+	}
+
+	result := CommandResult{
+		Format:    format.Format(outputFormat),
+		Filter:    options.Filter,
+		OrderBy:   orderBy,
+		OutputAs:  toOutputType(options.OutputAs),
+		NameLimit: options.NameLimit,
+		Data:      pmConfigs.Groups,
+	}
+
+	GenerateOutput(&result)
+	return nil
+
+}
+
+func (options *DevicePmConfigGroupMetricList) Execute(args []string) error {
+
+	var metrics []*voltha.PmConfig
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	for _, groups := range pmConfigs.Groups {
+
+		if string(options.Args.Group) == groups.GroupName {
+			for _, metric := range groups.Metrics {
+				if metric.SampleFreq == 0 && groups.GroupFreq == 0 {
+					metric.SampleFreq = pmConfigs.DefaultFreq
+				} else {
+					metric.SampleFreq = groups.GroupFreq
+				}
+			}
+			metrics = groups.Metrics
+		}
+	}
+
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = GetCommandOptionWithDefault("device-pm-configs", "format", DEFAULT_DEVICE_PM_CONFIG_METRIC_LIST_FORMAT)
+	}
+
+	orderBy := options.OrderBy
+	if orderBy == "" {
+		orderBy = GetCommandOptionWithDefault("device-pm-configs", "order", "")
+	}
+
+	result := CommandResult{
+		Format:    format.Format(outputFormat),
+		Filter:    options.Filter,
+		OrderBy:   orderBy,
+		OutputAs:  toOutputType(options.OutputAs),
+		NameLimit: options.NameLimit,
+		Data:      metrics,
+	}
+
+	GenerateOutput(&result)
+	return nil
+
+}
+
+func (options *DevicePmConfigFrequencySet) Execute(args []string) error {
+
+	conn, err := NewConnection()
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	client := voltha.NewVolthaServiceClient(conn)
+
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	id := voltha.ID{Id: string(options.Args.Id)}
+
+	pmConfigs, err := client.ListDevicePmConfigs(ctx, &id)
+	if err != nil {
+		return err
+	}
+
+	pmConfigs.DefaultFreq = options.Args.Frequency
+
+	_, err = client.UpdateDevicePmConfigs(ctx, pmConfigs)
+	if err != nil {
+		return err
+	}
+
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = GetCommandOptionWithDefault("device-pm-configs", "format", DEFAULT_DEVICE_PM_CONFIG_GET_FORMAT)
+	}
+	if options.Quiet {
+		outputFormat = "{{.Id}}"
+	}
+
+	result := CommandResult{
+		Format:    format.Format(outputFormat),
+		OutputAs:  toOutputType(options.OutputAs),
+		NameLimit: options.NameLimit,
+		Data:      pmConfigs,
+	}
+
+	GenerateOutput(&result)
+	return nil
+
+}
+
 type ReturnValueRow struct {
 	Name   string      `json:"name"`
 	Result interface{} `json:"result"`
diff --git a/internal/pkg/commands/log.go b/internal/pkg/commands/log.go
index b142804..52ab6cb 100644
--- a/internal/pkg/commands/log.go
+++ b/internal/pkg/commands/log.go
@@ -624,7 +624,7 @@
 	if len(options.Args.Component) == 0 {
 		componentList, err = cm.RetrieveComponentList(ctx, config.ConfigTypeLogLevel)
 		if err != nil {
-			return fmt.Errorf("Unable to retrieve list of voltha components : %s ", err)
+			return fmt.Errorf("Unable to retrieve list of voltha components : %s \nIs ETCD available at %s:%d?", err, host, port)
 		}
 	} else {
 		componentList = toStringArray(options.Args.Component)
diff --git a/internal/pkg/commands/logicaldevices.go b/internal/pkg/commands/logicaldevices.go
index e59f69d..3a5dd66 100644
--- a/internal/pkg/commands/logicaldevices.go
+++ b/internal/pkg/commands/logicaldevices.go
@@ -21,13 +21,14 @@
 	"github.com/golang/protobuf/ptypes/empty"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/opencord/voltctl/pkg/format"
+	"github.com/opencord/voltha-protos/v3/go/openflow_13"
 	"github.com/opencord/voltha-protos/v3/go/voltha"
 	"strings"
 )
 
 const (
 	DEFAULT_LOGICAL_DEVICE_FORMAT         = "table{{ .Id }}\t{{printf \"%016x\" .DatapathId}}\t{{.RootDeviceId}}\t{{.Desc.SerialNum}}\t{{.SwitchFeatures.NBuffers}}\t{{.SwitchFeatures.NTables}}\t{{printf \"0x%08x\" .SwitchFeatures.Capabilities}}"
-	DEFAULT_LOGICAL_DEVICE_PORT_FORMAT    = "table{{.Id}}\t{{.DeviceId}}\t{{.DevicePortNo}}\t{{.RootPort}}\t{{.Openflow.PortNo}}\t{{.Openflow.HwAddr}}\t{{.Openflow.Name}}\t{{.Openflow.State}}\t{{.Openflow.Features.Current}}\t{{.Openflow.Bitrate.Current}}"
+	DEFAULT_LOGICAL_DEVICE_PORT_FORMAT    = "table{{.Id}}\t{{.DeviceId}}\t{{.DevicePortNo}}\t{{.RootPort}}\t{{.OfpPortStats.PortNo}}\t{{.OfpPort.HwAddr}}\t{{.OfpPort.Name}}\t{{printf \"0x%08x\" .OfpPort.State}}\t{{printf \"0x%08x\" .OfpPort.Curr}}\t{{.OfpPort.CurrSpeed}}"
 	DEFAULT_LOGICAL_DEVICE_INSPECT_FORMAT = `ID: {{.Id}}
   DATAPATHID: {{.DatapathId}}
   ROOTDEVICEID: {{.RootDeviceId}}
@@ -173,6 +174,16 @@
 		return err
 	}
 
+	// ensure no nil pointers
+	for _, v := range ports.Items {
+		if v.OfpPortStats == nil {
+			v.OfpPortStats = &openflow_13.OfpPortStats{}
+		}
+		if v.OfpPort == nil {
+			v.OfpPort = &openflow_13.OfpPort{}
+		}
+	}
+
 	outputFormat := CharReplacer.Replace(options.Format)
 	if outputFormat == "" {
 		outputFormat = GetCommandOptionWithDefault("logical-device-ports", "format", DEFAULT_LOGICAL_DEVICE_PORT_FORMAT)
diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go
index f566d94..e704790 100644
--- a/pkg/filter/filter.go
+++ b/pkg/filter/filter.go
@@ -17,6 +17,7 @@
 
 import (
 	"fmt"
+	"log"
 	"reflect"
 	"regexp"
 	"strings"
@@ -138,37 +139,65 @@
 	return true
 }
 
-func (f Filter) Evaluate(item interface{}) bool {
-	val := reflect.ValueOf(item)
-
+func (f Filter) EvaluateTerm(k string, v FilterTerm, val reflect.Value, recurse bool) bool {
 	// If we have been given a pointer, then deference it
 	if val.Kind() == reflect.Ptr {
 		val = reflect.Indirect(val)
 	}
 
-	for k, v := range f {
-		field := val.FieldByName(k)
+	// If the user gave us an explicitly named dotted field, then split it
+	if strings.Contains(k, ".") {
+		parts := strings.SplitN(k, ".", 2)
+		field := val.FieldByName(parts[0])
 		if !field.IsValid() {
+			log.Printf("Failed to find dotted field %s while filtering\n", parts[0])
 			return false
 		}
+		return f.EvaluateTerm(parts[1], v, field, false)
+	}
 
-		if (field.Kind() == reflect.Slice) || (field.Kind() == reflect.Array) {
-			// For an array, check to see if any item matches
-			someMatch := false
-			for i := 0; i < field.Len(); i++ {
-				arrayElem := field.Index(i)
-				if testField(v, arrayElem) {
-					someMatch = true
-				}
-			}
-			if !someMatch {
-				return false
-			}
-		} else {
-			if !testField(v, field) {
-				return false
+	field := val.FieldByName(k)
+	if !field.IsValid() {
+		log.Printf("Failed to find field %s while filtering\n", k)
+		return false
+	}
+
+	if (field.Kind() == reflect.Slice) || (field.Kind() == reflect.Array) {
+		// For an array, check to see if any item matches
+		someMatch := false
+		for i := 0; i < field.Len(); i++ {
+			arrayElem := field.Index(i)
+			if testField(v, arrayElem) {
+				someMatch = true
 			}
 		}
+		if !someMatch {
+			//if recurse && val.Kind() == reflect.Struct {
+			//    TODO: implement automatic recursion when the user did not
+			//          use a dotted notation. Go through the list of fields
+			//          in the struct, recursively check each one.
+			//}
+			return false
+		}
+	} else {
+		if !testField(v, field) {
+			return false
+		}
 	}
+
+	return true
+}
+
+func (f Filter) Evaluate(item interface{}) bool {
+	val := reflect.ValueOf(item)
+
+	for k, v := range f {
+		matches := f.EvaluateTerm(k, v, val, true)
+		if !matches {
+			// If any of the filter fail, the overall match fails
+			return false
+		}
+	}
+
 	return true
 }
diff --git a/pkg/order/order.go b/pkg/order/order.go
index 715b302..0f324cd 100644
--- a/pkg/order/order.go
+++ b/pkg/order/order.go
@@ -92,6 +92,15 @@
 	sort.SliceStable(data, func(i, j int) bool {
 		left := reflect.ValueOf(slice.Index(i).Interface())
 		right := reflect.ValueOf(slice.Index(j).Interface())
+
+		if left.Kind() == reflect.Ptr {
+			left = reflect.Indirect(left)
+		}
+
+		if right.Kind() == reflect.Ptr {
+			right = reflect.Indirect(right)
+		}
+
 		for _, term := range s {
 			fleft := left.FieldByName(term.Name)
 			fright := right.FieldByName(term.Name)