SEBA-705 better error reporting

Change-Id: Id461c6efe2d0b7ab9c0d1ddb72482d10899b16fe
diff --git a/error/error.go b/error/error.go
new file mode 100644
index 0000000..c30096b
--- /dev/null
+++ b/error/error.go
@@ -0,0 +1,344 @@
+/*
+ * 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 error
+
+/*  Cordctl error classes
+
+	The basic idea is to throw specific error classes, so it's easier to test for them rather than doing string
+	comparisons or other ad hoc mechanisms for determining the type of error. This decouples the human
+	readable text of an error from programmatic testing of error type.
+
+	We differentiate between errors that we want to generate brief output, such as for example a
+	user mistyping a model name, versus errors that we want to generate additional context. This prevents
+	overwhelming a user with voluminous output for a simple mistake. A command-line option may be provided
+	to force full error output should it be desired.
+
+	Additionally, an added benefit is ease of maintenance and localisation, by locating all error text
+	in one place.
+
+	To return an error, for example:
+
+		return WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
+
+	To check to see if a specific error was returned, either of the following are acceptable:
+
+		_, ok := err.(*ChecksumMismatchError)
+		...
+
+		switch err.(type) {
+		case *ChecksumMismatchError:
+        ...
+*/
+
+import (
+	"bytes"
+	"fmt"
+	go_errors "github.com/go-errors/errors"
+	"os"
+	"runtime"
+	"strings"
+)
+
+const (
+	MaxStackDepth = 50
+)
+
+// Prefix applied to all error messages. Initialized in module init() function from os.Args[0]
+var prefix string
+
+/* CordCtlError is the interface for errors created by cordctl.
+ *    ShouldDumpStack()
+ *        Returns false for well-understood problems such as invalid user input where a brief error message is sufficient
+ *		  Returns true for poorly-understood / unexpected problems where a full dump context may be useful
+ *    Stack()
+ *        Returns a string containing the stack trace where the error occurred
+ *        Only useful if WithStackTrace() was called on the error
+ */
+
+type CordCtlError interface {
+	error
+	ShouldDumpStack() bool
+	Stack() string
+	AddStackTrace(skip int)
+}
+
+/* BaseError supports attaching stack traces to errors
+ *    Borrowed the technique from github.com/go-errors. Decided against using go-errors directly since it requires
+ *    wrapping our error classes. Instead, incorporated the stack trace directly into our error class.
+ */
+
+type BaseError struct {
+	stack  []uintptr
+	frames []go_errors.StackFrame
+}
+
+func (f *BaseError) AddStackTrace(skip int) {
+	stack := make([]uintptr, MaxStackDepth)
+	length := runtime.Callers(2+skip, stack[:])
+	f.stack = stack[:length]
+}
+
+func (f *BaseError) Stack() string {
+	buf := bytes.Buffer{}
+
+	for _, frame := range f.StackFrames() {
+		buf.WriteString(frame.String())
+	}
+
+	return string(buf.Bytes())
+}
+
+func (f *BaseError) StackFrames() []go_errors.StackFrame {
+	if f.frames == nil {
+		f.frames = make([]go_errors.StackFrame, len(f.stack))
+
+		for i, pc := range f.stack {
+			f.frames[i] = go_errors.NewStackFrame(pc)
+		}
+	}
+
+	return f.frames
+}
+
+// ***************************************************************************
+// UserError is composed into Errors that are due to user input
+
+type UserError struct {
+	BaseError
+}
+
+func (f UserError) ShouldDumpStack() bool {
+	return false
+}
+
+// **************************************************************************
+// TransferError is composed into Errors that are due to failures in transfers
+
+type TransferError struct {
+	BaseError
+}
+
+func (f TransferError) ShouldDumpStack() bool {
+	return false
+}
+
+// ***************************************************************************
+// UnexpectedError is things that we don't expect to happen. They should
+// generate maximum error context, to provide useful information for developer
+// diagnosis.
+
+type UnexpectedError struct {
+	BaseError
+}
+
+func (f UnexpectedError) ShouldDumpStack() bool {
+	return true
+}
+
+// ***************************************************************************
+// Specific error classes follow
+
+// Checksum mismatch when downloading or uploading a file
+type ChecksumMismatchError struct {
+	TransferError
+	Name     string // (optional) Name of file
+	Expected string
+	Actual   string
+}
+
+func (f ChecksumMismatchError) Error() string {
+	if f.Name != "" {
+		return fmt.Sprintf("%s %s: checksum mismatch (actual=%s, expected=%s)", prefix, f.Name, f.Expected, f.Actual)
+	} else {
+		return fmt.Sprintf("%s: checksum mismatch (actual=%s, expected=%s)", prefix, f.Expected, f.Actual)
+	}
+}
+
+// User specified a model type that is not valid
+type UnknownModelTypeError struct {
+	UserError
+	Name string // Name of model
+}
+
+func (f UnknownModelTypeError) Error() string {
+	return fmt.Sprintf("%s: Model %s does not exist. Use `cordctl modeltype list` to get a list of available models", prefix, f.Name)
+}
+
+// User specified a model state that is not valid
+type UnknownModelStateError struct {
+	UserError
+	Name string // Name of state
+}
+
+func (f UnknownModelStateError) Error() string {
+	return fmt.Sprintf("%s: Model state %s does not exist", prefix, f.Name)
+}
+
+// Command requires a filter be specified
+type FilterRequiredError struct {
+	UserError
+}
+
+func (f FilterRequiredError) Error() string {
+	return "Filter required. Use either an ID, --filter, or --all to specify which models to operate on"
+}
+
+// Command was aborted by the user
+type AbortedError struct {
+	UserError
+}
+
+func (f AbortedError) Error() string {
+	return "Aborted"
+}
+
+// Command was aborted by the user
+type NoMatchError struct {
+	UserError
+}
+
+func (f NoMatchError) Error() string {
+	return "No Match"
+}
+
+// User specified a field name that is not valid
+type FieldDoesNotExistError struct {
+	UserError
+	ModelName string
+	FieldName string
+}
+
+func (f FieldDoesNotExistError) Error() string {
+	return fmt.Sprintf("%s: Model %s does not have field %s", prefix, f.ModelName, f.FieldName)
+}
+
+// User specified a query string that is not properly formatted
+type IllegalQueryError struct {
+	UserError
+	Query string
+}
+
+func (f IllegalQueryError) Error() string {
+	return fmt.Sprintf("%s: Illegal query string %s", prefix, f.Query)
+}
+
+// We failed to type convert something that we thought should have converted
+type TypeConversionError struct {
+	UnexpectedError
+	Source      string
+	Destination string
+}
+
+func (f TypeConversionError) Error() string {
+	return fmt.Sprintf("%s: Failed to type convert from %s to %s", prefix, f.Source, f.Destination)
+}
+
+// Version did not match a constraint
+type VersionConstraintError struct {
+	UserError
+	Name       string
+	Version    string
+	Constraint string
+}
+
+func (f VersionConstraintError) Error() string {
+	return fmt.Sprintf("%s: %s version %s did not match constraint '%s'", prefix, f.Name, f.Version, f.Constraint)
+}
+
+// A model was not found
+type ModelNotFoundError struct {
+	UserError
+	ModelName string
+	Id        int32
+	Queries   map[string]string
+}
+
+func (f ModelNotFoundError) Error() string {
+	if f.Queries != nil {
+		return fmt.Sprintf("%s: %s query %v not Found", prefix, f.ModelName, f.Queries)
+	} else {
+		return fmt.Sprintf("%s: Model %s id %d not Found", prefix, f.ModelName, f.Id)
+	}
+}
+
+// InvalidInputError is a catch-all for user mistakes that aren't covered elsewhere
+type InvalidInputError struct {
+	UserError
+	Message string
+}
+
+func (f InvalidInputError) Error() string {
+	return fmt.Sprintf("%s: %s", prefix, f.Message)
+}
+
+func NewInvalidInputError(format string, params ...interface{}) *InvalidInputError {
+	msg := fmt.Sprintf(format, params...)
+	err := &InvalidInputError{Message: msg}
+	err.AddStackTrace(2)
+	return err
+}
+
+// InternalError is a catch-all for errors that don't fit somewhere else
+type InternalError struct {
+	UnexpectedError
+	Message string
+}
+
+func (f InternalError) Error() string {
+	return fmt.Sprintf("%s: %s", prefix, f.Message)
+}
+
+func NewInternalError(format string, params ...interface{}) *InternalError {
+	msg := fmt.Sprintf(format, params...)
+	err := &InternalError{Message: msg}
+	err.AddStackTrace(2)
+	return err
+}
+
+// ***************************************************************************
+// Global exported function declarations
+
+// Attach a stack trace to an error. The error passed in must be a pointer to an error structure for the
+// CordCtlError interface to match.
+func WithStackTrace(err CordCtlError) error {
+	err.AddStackTrace(2)
+	return err
+}
+
+// Set the prefix rather than using os.Args[0]. This is useful for testing.
+func SetPrefix(s string) {
+	prefix = s
+}
+
+// Convert an RPC error into a Cord Error
+func RpcErrorWithIdToCordError(err error, modelName string, id int32) error {
+	if err == nil {
+		return err
+	}
+	if strings.Contains(err.Error(), "rpc error: code = NotFound") {
+		err := &ModelNotFoundError{ModelName: modelName, Id: id}
+		err.AddStackTrace(2)
+		return err
+	}
+	return err
+}
+
+// Module initialization. Automatically defaults prefix to program name
+func init() {
+	prefix = os.Args[0]
+}
diff --git a/error/error_test.go b/error/error_test.go
new file mode 100644
index 0000000..072b74e
--- /dev/null
+++ b/error/error_test.go
@@ -0,0 +1,81 @@
+/*
+ * 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 error
+
+import (
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func init() {
+	SetPrefix("cordctl")
+}
+
+func TestGenericError(t *testing.T) {
+	var err error
+
+	err = fmt.Errorf("Some error")
+
+	// Type conversion from `error` to ChecksumMismatchError should fail
+	_, ok := err.(ChecksumMismatchError)
+	assert.False(t, ok)
+
+	// Type conversion from `error` to CordCtlError should fail
+	_, ok = err.(CordCtlError)
+	assert.False(t, ok)
+}
+
+func TestChecksumMismatchError(t *testing.T) {
+	var err error
+
+	err = WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
+
+	//assert.Equal(t, err.(*ChecksumMismatchError).Stack(), "foo")
+
+	// Check that the Error() function returns the right text
+	assert.Equal(t, err.Error(), "cordctl: checksum mismatch (actual=456, expected=123)")
+
+	// Type conversion from `error` to ChecksumMismatchError should succeed
+	_, ok := err.(*ChecksumMismatchError)
+	assert.True(t, ok)
+
+	// Type switch is another way of doing the same
+	switch err.(type) {
+	case *ChecksumMismatchError:
+		// do nothing
+	case CordCtlError:
+		assert.Fail(t, "Should have used the ChecksumMismatchError case instead")
+	default:
+		assert.Fail(t, "Wrong part of switch statement was called")
+	}
+
+	// Type conversion from `error` to CordCtlError should succeed
+	cce, ok := err.(CordCtlError)
+	assert.True(t, ok)
+
+	// ShouldDumpStack() returned from a ChecksumMismatchError should be false
+	assert.False(t, cce.ShouldDumpStack())
+}
+
+func TestUnknownModelTypeError(t *testing.T) {
+	var err error
+
+	err = WithStackTrace(&UnknownModelTypeError{Name: "foo"})
+
+	_ = err
+}