diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..9448661 --- /dev/null +++ b/errors.go @@ -0,0 +1,118 @@ +package errors + +import ( + "bytes" + "fmt" +) + +type Error struct { + // The kind of error + Kind Kind `json:"kind"` + // The method or function being invoked + Method Method `json:"method"` + // The username of the user attempting the operation + Username Username `json:"username"` + // The error message + Message Message `json:"message"` + // Nested Error + Err error `json:"err"` +} + +func E(args ...interface{}) error { + if len(args) == 0 { + return nil + } + e := &Error{} + for _, arg := range args { + switch arg := arg.(type) { + case Kind: + e.Kind = arg + case Method: + e.Method = arg + case Username: + e.Username = arg + case Message: + e.Message = arg + case string: + if e.Message == "" { + e.Message = Message(arg) + } + case *Error: + copy := *arg + e.Err = © + case error: + e.Err = arg + default: + return Errorf("unknown type %T with value %v in error call", arg, arg) + } + } + return e +} + +func (e *Error) isZero() bool { + return e.Method == "" && e.Username == "" && e.Message == "" && e.Kind == 0 && e.Err == nil +} + +// pad appends str to the buffer if the buffer already has some data. +func pad(b *bytes.Buffer, str string) { + if b.Len() == 0 { + return + } + b.WriteString(str) +} + +func (e *Error) Error() string { + b := new(bytes.Buffer) + if e.Method != "" { + b.WriteString(string(e.Method)) + } + if e.Username != "" { + pad(b, ": ") + b.WriteString(string(e.Username)) + } + if e.Kind != 0 { + pad(b, ": ") + b.WriteString(e.Kind.String()) + } + if e.Message != "" { + pad(b, ": ") + b.WriteString(string(e.Message)) + } + if e.Err != nil { + // Indent to new line if it is another Error + if prevErr, ok := e.Err.(*Error); ok { + if !prevErr.isZero() { + pad(b, ":\n\t") + b.WriteString(string(e.Err.Error())) + } + } else { // Just print it out if a standard error + pad(b, ": ") + b.WriteString(string(e.Err.Error())) + } + } + if b.Len() == 0 { + return "no error message" + } + return b.String() +} + +// Errorf is a wrapper that calls E with just a message formatted with the args +// this allows for only importing this library for all error handling +func Errorf(f string, args ...interface{}) error { + return E(fmt.Sprintf(f, args...)) +} + +// Returns True if err is of type Error and of the given Kind +func Is(k Kind, err error) bool { + e, ok := err.(*Error) + if !ok { + return false + } + if e.Kind != Other { + return e.Kind == k + } + if e.Err != nil { + return Is(k, e.Err) + } + return false +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c9f94e4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/dustinpianalto/errors + +go 1.17 diff --git a/types.go b/types.go new file mode 100644 index 0000000..5823c96 --- /dev/null +++ b/types.go @@ -0,0 +1,39 @@ +package errors + +type Username string +type Method string +type Message string +type Kind uint16 + +const ( + Other Kind = iota // Unknown error or something that doesn't fit other categories + Internal // Internal error that should not be shown to user + Invalid // Operation is not permitted for this type of item + Permission // Permission denied + IO // External IO error + Conflict // The item already exists + NotFound // The item does not exist + Malformed // The request format is not valid +) + +func (k Kind) String() string { + switch k { + case Other: + return "other error" + case Internal: + return "internal error" + case Invalid: + return "invalid operation" + case Permission: + return "permission denied" + case IO: + return "I/O error" + case Conflict: + return "item already exists" + case NotFound: + return "item does not exist" + case Malformed: + return "malformed request" + } + return "unknown type" +}