Skip to content

Commit

Permalink
Merge pull request #5 from LUSHDigital/feature/lush-errors
Browse files Browse the repository at this point in the history
Provide standard LUSH errors
  • Loading branch information
ladydascalie authored Feb 18, 2020
2 parents bc9cc1c + 1e06d48 commit 71113fe
Show file tree
Hide file tree
Showing 6 changed files with 598 additions and 0 deletions.
116 changes: 116 additions & 0 deletions lusherr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# LUSH Core Errors
This package is used to streamline dealing with errors and error messages within the LUSH infrastructure. Using the errors provided by this package will collect a lot of useful debug information about where exactly the error occurred.

## Error Types
These are the standard error types that can be used within a project's domain logic to aid with debugging and error reporting to any of its API consumers.

### Internal Error
`InternalError` can be used to wrap any error e.g. Trying to generate a random UUID, but the generation failed.

```go
id, err := uuid.NewV4()
if err != nil {
return NewInternalError(err)
}
```

### Unauthorized Error
`UnauthorizedError` should be used when an action is performed by a user that they don't have permission to do e.g. Someone tried to access something they were not allowed to according to a permission policy.

```go
if err := policy.Permit(consumer); err != nil {
return NewUnauthorizedError(err)
}
```

### Validation Error
`ValidationError` should be used to detail what user generated information is incorrect and why e.g. Someone set the name field for a user to be empty, but the validation requires it to be present.

```go
type ProductRevision struct {
plu string
}

func (r ProductRevision) validate() error {
if plu == "" {
return NewValidationError("product revision", "plu", fmt.Errorf("must be present"))
}
}
```

### Database Query Error
`DatabaseQueryError` should be used to provide detail about a failed database query e.g. Trying to query the database, but the database rejects the query.

```go
const stmt = `SELECT * FROM user`
rows, err := qu.Query(stmt)
if err != nil {
return nil, NewDatabaseQueryError(stmt, err)
}
```

### Not Found Error
`NotFoundError` should be used when an entity cannot be found e.g. Someone tries to retrieve a user, but the user for the given ID does not exist in the database.

```go
const stmt = `SELECT * FROM user WHERE id = $1`
rows, err := qu.Query(stmt, id)
if err != nil {
switch err {
case sql.ErrNoRows:
return nil, NewNotFoundError("user", id, err)
default:
return nil, NewDatabaseQueryError(stmt, err)
}
}
```

### Not Allowed Error
`NotAllowedError` should be used when an certain action is not allowed e.g. Someone tries to delete something, but the record has been marked as permanent.

```go
if product.permanent {
return NewNotAllowedError(fmt.Errorf("not allowed to remove permanent products"))
}
```

## Locate
Errors produced with the `lusherr` package can be located to return the `runtime.Frame` of where it occurred.

```go
frame, found := lusherr.Locate(err)
if found {
log.Println(err, frame)
} else {
log.Println(err, "frame could not be found")
}
```

### Locator Interface
Implement the [`Locator`](#locator-interface) interface on an error type to return its caller frame to be used in conjunction with the `lusherr` package and associated tooling.

```go
type Locator interface {
Locate() runtime.Frame
}
```

## Pin
Call `Pin` to wrap an error with information about the caller frame of where `Pin` was invoked for an error that does not already carry a caller frame. The resulting error can be cast to [`Locate`](#locator-interface).

```go
func UploadToBucket(w io.Writer) error {
if err := upload(w); err != nil {
return lusherr.Pin(err)
}
}
```

### Pinner Interface
Implement the [`Pinner`](#pinner-interface) interface on an error type to prevent errors that already implements [`Locator`](#locator-interface) to be wrapped multiple times.

```go
type Pinner interface {
Pin(runtime.Frame) error
}
```
17 changes: 17 additions & 0 deletions lusherr/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lusherr

import (
"fmt"
)

// Debug where an error was raised.
func Debug(err error) string {
if err == nil {
return fmt.Sprintf("unknown error")
}
frame, found := Locate(err)
if !found {
return fmt.Sprintf("%v (unknown caller frame)", err)
}
return fmt.Sprintf("%v (%s %s:%d)", err, frame.Function, frame.File, frame.Line)
}
249 changes: 249 additions & 0 deletions lusherr/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package lusherr

import (
"fmt"
"runtime"
)

// NewInternalError builds a generic error.
// e.g. Trying to generate a random UUID, but the generation failed.
func NewInternalError(inner error) error {
return InternalError{
frame: frame(1),
inner: inner,
}
}

// InternalError can be used to wrap any error.
// e.g. Trying to generate a random UUID, but the generation failed.
type InternalError struct {
frame runtime.Frame
inner error
}

func (e InternalError) Error() string {
if e.inner == nil {
return fmt.Sprintf("internal failure")
}
return fmt.Sprintf("internal failure: %v", e.inner)
}

// Unwrap the inner error.
func (e InternalError) Unwrap() error {
return e.inner
}

// Locate the frame of the error.
func (e InternalError) Locate() runtime.Frame {
return e.frame
}

// Pin the error to a caller frame.
func (e InternalError) Pin(frame runtime.Frame) error {
e.frame = frame
return e
}

// NewUnauthorizedError builds a new unauthorized error.
// e.g. Someone tried to access something they were not allowed to according to a permission policy.
func NewUnauthorizedError(inner error) error {
return UnauthorizedError{
frame: frame(1),
inner: inner,
}
}

// UnauthorizedError should be used when an action is performed by a user that they don't have permission to do.
// e.g. Someone tried to access something they were not allowed to according to a permission policy.
type UnauthorizedError struct {
frame runtime.Frame
inner error
}

func (e UnauthorizedError) Error() string {
if e.inner == nil {
return fmt.Sprintf("unauthorized")
}
return fmt.Sprintf("unauthorized: %v", e.inner)
}

// Unwrap the inner error.
func (e UnauthorizedError) Unwrap() error {
return e.inner
}

// Locate the frame of the error.
func (e UnauthorizedError) Locate() runtime.Frame {
return e.frame
}

// Pin the error to a caller frame.
func (e UnauthorizedError) Pin(frame runtime.Frame) error {
e.frame = frame
return e
}

// NewValidationError builds an error for failing to validate a field.
// e.g. Someone set the name field for a user to be empty, but the validation requires it to be present.
func NewValidationError(entity, field string, inner error) error {
return ValidationError{
frame: frame(1),
inner: inner,
Entity: entity,
Field: field,
}
}

// ValidationError should be used to detail what user generated information is incorrect and why.
// e.g. Someone set the name field for a user to be empty, but the validation requires it to be present.
type ValidationError struct {
Entity, Field string
frame runtime.Frame
inner error
}

func (e ValidationError) Error() string {
if e.inner == nil {
return fmt.Sprintf("validation failed for %q on %q", e.Field, e.Entity)
}
return fmt.Sprintf("validation failed for %q on %q: %v", e.Field, e.Entity, e.inner)
}

// Unwrap the inner error.
func (e ValidationError) Unwrap() error {
return e.inner
}

// Locate the frame of the error.
func (e ValidationError) Locate() runtime.Frame {
return e.frame
}

// Pin the error to a caller frame.
func (e ValidationError) Pin(frame runtime.Frame) error {
e.frame = frame
return e
}

// NewDatabaseQueryError builds an error for a failed database query.
// e.g. Trying to query the database, but the database rejects the query.
func NewDatabaseQueryError(query string, inner error) error {
return DatabaseQueryError{
frame: frame(1),
inner: inner,
Query: query,
}
}

// DatabaseQueryError should be used to provide detail about a failed database query.
// e.g. Trying to query the database, but the database rejects the query.
type DatabaseQueryError struct {
Query string
frame runtime.Frame
inner error
}

func (e DatabaseQueryError) Error() string {
if e.inner == nil {
return fmt.Sprintf("database query failed")
}
return fmt.Sprintf("database query failed: %v", e.inner)
}

// Unwrap the inner error.
func (e DatabaseQueryError) Unwrap() error {
return e.inner
}

// Locate the frame of the error.
func (e DatabaseQueryError) Locate() runtime.Frame {
return e.frame
}

// Pin the error to a caller frame.
func (e DatabaseQueryError) Pin(frame runtime.Frame) error {
e.frame = frame
return e
}

// NewNotFoundError builds an error for an entity that cannot be found.
// e.g. Someone tries to retrieve a user, but the user for the given ID does not exist in the database.
func NewNotFoundError(entity string, identifier interface{}, inner error) error {
return NotFoundError{
frame: frame(1),
inner: inner,
Entity: entity,
Identifier: identifier,
}
}

// NotFoundError should be used when an entity cannot be found.
// e.g. Someone tries to retrieve a user, but the user for the given ID does not exist in the database.
type NotFoundError struct {
Entity string
Identifier interface{}
frame runtime.Frame
inner error
}

func (e NotFoundError) Error() string {
if e.inner == nil {
return fmt.Sprintf("cannot find %q (%v)", e.Entity, e.Identifier)
}
return fmt.Sprintf("cannot find %q (%v): %v", e.Entity, e.Identifier, e.inner)
}

// Unwrap the inner error.
func (e NotFoundError) Unwrap() error {
return e.inner
}

// Locate the frame of the error.
func (e NotFoundError) Locate() runtime.Frame {
return e.frame
}

// Pin the error to a caller frame.
func (e NotFoundError) Pin(frame runtime.Frame) error {
e.frame = frame
return e
}

// NewNotAllowedError builds an error for when a certain action is not allowed.
// e.g. Someone tries to delete something, but the record has been marked as permanenet.
func NewNotAllowedError(inner error) error {
return NotAllowedError{
frame: frame(1),
inner: inner,
}
}

// NotAllowedError should be used when an certain action is not allowed.
// e.g. Someone tries to delete something, but the record has been marked as permanent.
type NotAllowedError struct {
frame runtime.Frame
inner error
}

func (e NotAllowedError) Error() string {
if e.inner == nil {
return fmt.Sprintf("action not allowed")
}
return fmt.Sprintf("action not allowed: %v", e.inner)
}

// Unwrap the inner error.
func (e NotAllowedError) Unwrap() error {
return e.inner
}

// Locate the frame of the error.
func (e NotAllowedError) Locate() runtime.Frame {
return e.frame
}

// Pin the error to a caller frame.
func (e NotAllowedError) Pin(frame runtime.Frame) error {
e.frame = frame
return e
}
Loading

0 comments on commit 71113fe

Please sign in to comment.