New domain errors based on the ICU-019 RFC (#760)

pull/774/head
Jim 5 years ago committed by GitHub
parent ea2e0d2be1
commit fb3f11997e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,35 @@
package errors
// Code specifies a code for the error.
type Code uint32
// String will return the Code's Info.Message
func (c Code) String() string {
return c.Info().Message
}
// Info will look up the Code's Info. If the Info is not found, it will return
// Info for an Unknown Code.
func (c Code) Info() Info {
if info, ok := errorCodeInfo[c]; ok {
return info
}
return errorCodeInfo[Unknown]
}
const (
Unknown Code = 0 // Unknown will be equal to a zero value for Codes
// General function errors are reserved Codes 100-999
InvalidParameter Code = 100 // InvalidParameter represents and invalid parameter for an operation.
// DB errors are resevered Codes from 1000-1999
CheckConstraint Code = 1000 // CheckConstraint represents a check constraint error
NotNull Code = 1001 // NotNull represents a value must not be null error
NotUnique Code = 1002 // NotUnique represents a value must be unique error
NotSpecificIntegrity Code = 1003 // NotSpecificIntegrity represents an integrity error that has no specificy domain error code
MissingTable Code = 1004 // Missing table represents an undefined table error
RecordNotFound Code = 1100 // RecordNotFound represents that a record/row was not found matching the criteria
MultipleRecords Code = 1101 // MultipleRecords represents that multiple records/rows were found matching the criteria
)

@ -0,0 +1,79 @@
package errors
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCode_Both_String_Info(t *testing.T) {
t.Parallel()
tests := []struct {
name string
c Code
want Code
}{
{
name: "undefined-code",
c: Code(4294967295),
want: Unknown,
},
{
name: "default-value",
want: Unknown,
},
{
name: "Unknown",
c: Unknown,
want: Unknown,
},
{
name: "InvalidParameter",
c: InvalidParameter,
want: InvalidParameter,
},
{
name: "CheckConstraint",
c: CheckConstraint,
want: CheckConstraint,
},
{
name: "NotNull",
c: NotNull,
want: NotNull,
},
{
name: "NotUnique",
c: NotUnique,
want: NotUnique,
},
{
name: "RecordNotFound",
c: RecordNotFound,
want: RecordNotFound,
},
{
name: "MultipleRecords",
c: MultipleRecords,
want: MultipleRecords,
},
{
name: "NotSpecificIntegrity",
c: NotSpecificIntegrity,
want: NotSpecificIntegrity,
},
{
name: "MissingTable",
c: MissingTable,
want: MissingTable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(errorCodeInfo[tt.want], tt.c.Info())
assert.Equal(errorCodeInfo[tt.want].Message, tt.c.String())
})
}
}

@ -0,0 +1,150 @@
package errors
import (
"errors"
"fmt"
"strings"
"github.com/lib/pq"
)
// Op represents an operation (package.function).
// For example iam.CreateRole
type Op string
// Err provides the ability to specify a Msg, Op, Code and Wrapped error.
// Errs must have a Code and all other fields are optional. We've chosen Err
// over Error for the identifier to support the easy embedding of Errs. Errs
// can be embedded without a conflict between the embedded Err and Err.Error().
type Err struct {
// Code is the error's code, which can be used to get the error's
// errorCodeInfo, which contains the error's Kind and Message
Code Code
// Msg for the error
Msg string
// Op represents the operation raising/propagating an error and is optional
Op Op
// Wrapped is the error which this Err wraps and will be nil if there's no
// error to wrap.
Wrapped error
}
// New creates a new Err and supports the options of:
// WithOp - allows you to specify an optional Op (operation)
// WithMsg() - allows you to specify an optional error msg, if the default
// msg for the error Code is not sufficient.
// WithWrap() - allows you to specify
// an error to wrap
func New(c Code, opt ...Option) error {
opts := GetOpts(opt...)
return &Err{
Code: c,
Op: opts.withOp,
Wrapped: opts.withErrWrapped,
Msg: opts.withErrMsg,
}
}
// Convert will convert the error to a Boundary *Err (returning it as an error)
// and attempt to add a helpful error msg as well. If that's not possible, it
// will return nil
func Convert(e error) *Err {
if e == nil {
return nil
}
if err, ok := e.(*Err); ok {
return err
}
var pqError *pq.Error
if As(e, &pqError) {
if pqError.Code.Class() == "23" { // class of integrity constraint violations
switch pqError.Code {
case "23505": // unique_violation
return New(NotUnique, WithMsg(pqError.Detail), WithWrap(ErrNotUnique)).(*Err)
case "23502": // not_null_violation
msg := fmt.Sprintf("%s must not be empty", pqError.Column)
return New(NotNull, WithMsg(msg), WithWrap(ErrNotNull)).(*Err)
case "23514": // check_violation
msg := fmt.Sprintf("%s constraint failed", pqError.Constraint)
return New(CheckConstraint, WithMsg(msg), WithWrap(ErrCheckConstraint)).(*Err)
default:
return New(NotSpecificIntegrity, WithMsg(pqError.Message)).(*Err)
}
}
if pqError.Code == "42P01" {
return New(MissingTable, WithMsg(pqError.Message)).(*Err)
}
}
// unfortunately, we can't help.
return nil
}
// Info about the Err
func (e *Err) Info() Info {
if e == nil {
return errorCodeInfo[Unknown]
}
return e.Code.Info()
}
// Error satisfies the error interface and returns a string representation of
// the Err
func (e *Err) Error() string {
if e == nil {
return ""
}
var s strings.Builder
if e.Op != "" {
join(&s, ": ", string(e.Op))
}
if e.Msg != "" {
join(&s, ": ", e.Msg)
}
if info, ok := errorCodeInfo[e.Code]; ok {
if e.Msg == "" {
join(&s, ": ", info.Message) // provide a default.
join(&s, ", ", info.Kind.String())
} else {
join(&s, ": ", info.Kind.String())
}
}
join(&s, ": ", fmt.Sprintf("error #%d", e.Code))
if e.Wrapped != nil {
join(&s, ": \n", e.Wrapped.Error())
}
return s.String()
}
func join(str *strings.Builder, delim string, s string) {
if str.Len() == 0 {
_, _ = str.WriteString(s)
return
}
_, _ = str.WriteString(delim + s)
}
// Unwrap implements the errors.Unwrap interface and allows callers to use the
// errors.Is() and errors.As() functions effectively for any wrapped errors.
func (e *Err) Unwrap() error {
if e == nil {
return nil
}
return e.Wrapped
}
// Is the equivalent of the std errors.Is, but allows Devs to only import
// this package for the capability.
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As is the equivalent of the std errors.As, and allows devs to only import
// this package for the capability.
func As(err error, target interface{}) bool {
return errors.As(err, target)
}

@ -0,0 +1,275 @@
package errors_test
import (
"context"
stderrors "errors"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_NewError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
code errors.Code
opt []errors.Option
want error
}{
{
name: "all-options",
code: errors.InvalidParameter,
opt: []errors.Option{
errors.WithOp("alice.Bob"),
errors.WithWrap(errors.ErrRecordNotFound),
errors.WithMsg("test msg"),
},
want: &errors.Err{
Op: "alice.Bob",
Wrapped: errors.ErrRecordNotFound,
Msg: "test msg",
Code: errors.InvalidParameter,
},
},
{
name: "no-options",
opt: nil,
want: &errors.Err{
Code: errors.Unknown,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := errors.New(tt.code, tt.opt...)
require.Error(err)
assert.Equal(tt.want, err)
})
}
}
func TestError_Info(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err *errors.Err
want errors.Code
}{
{
name: "nil",
err: nil,
want: errors.Unknown,
},
{
name: "Unknown",
err: errors.New(errors.Unknown).(*errors.Err),
want: errors.Unknown,
},
{
name: "InvalidParameter",
err: errors.New(errors.InvalidParameter).(*errors.Err),
want: errors.InvalidParameter,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.want.Info(), tt.err.Info())
})
}
}
func TestError_Error(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
want string
}{
{
name: "msg",
err: errors.New(errors.Unknown, errors.WithMsg("test msg")),
want: "test msg: unknown: error #0",
},
{
name: "code",
err: errors.New(errors.CheckConstraint),
want: "constraint check failed, integrity violation: error #1000",
},
{
name: "op-msg-and-code",
err: errors.New(errors.CheckConstraint, errors.WithOp("alice.bob"), errors.WithMsg("test msg")),
want: "alice.bob: test msg: integrity violation: error #1000",
},
{
name: "unknown",
err: errors.New(errors.Unknown),
want: "unknown, unknown: error #0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := tt.err.Error()
assert.Equal(tt.want, got)
})
}
t.Run("nil *Err", func(t *testing.T) {
assert := assert.New(t)
var err *errors.Err
got := err.Error()
assert.Equal("", got)
})
}
func TestError_Unwrap(t *testing.T) {
t.Parallel()
testErr := errors.New(errors.Unknown, errors.WithMsg("test error"))
tests := []struct {
name string
err error
want error
wantIsErr error
}{
{
name: "ErrInvalidParameter",
err: errors.New(errors.InvalidParameter, errors.WithWrap(errors.ErrInvalidParameter)),
want: errors.ErrInvalidParameter,
wantIsErr: errors.ErrInvalidParameter,
},
{
name: "testErr",
err: testErr,
want: nil,
wantIsErr: testErr,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
err := tt.err.(interface {
Unwrap() error
}).Unwrap()
assert.Equal(tt.want, err)
assert.True(stderrors.Is(tt.err, tt.wantIsErr))
})
}
t.Run("nil *Err", func(t *testing.T) {
assert := assert.New(t)
var err *errors.Err
got := err.Unwrap()
assert.Equal(nil, got)
})
}
func TestConvertError(t *testing.T) {
t.Parallel()
const (
createTable = `
create table if not exists test_table (
id bigint generated always as identity primary key,
name text unique,
description text not null,
five text check(length(trim(five)) > 5)
);
`
truncateTable = `truncate test_table;`
insert = `insert into test_table(name, description, five) values (?, ?, ?)`
missingTable = `select * from not_a_defined_table`
)
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
_, err := rw.Exec(ctx, createTable, nil)
require.NoError(t, err)
tests := []struct {
name string
e error
wantErr error
}{
{
name: "nil",
e: nil,
wantErr: nil,
},
{
name: "not-convertible",
e: stderrors.New("test error"),
wantErr: nil,
},
{
name: "NotSpecificIntegrity",
e: &pq.Error{
Code: pq.ErrorCode("23001"),
},
wantErr: errors.New(errors.NotSpecificIntegrity),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := errors.Convert(tt.e)
if tt.wantErr == nil {
assert.Nil(err)
return
}
require.NotNil(err)
assert.Equal(tt.wantErr, err)
})
}
t.Run("ErrCodeUnique", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
_, err := rw.Exec(ctx, truncateTable, nil)
require.NoError(err)
_, err = rw.Exec(ctx, insert, []interface{}{"alice", "coworker", nil})
require.NoError(err)
_, err = rw.Exec(ctx, insert, []interface{}{"alice", "dup coworker", nil})
require.Error(err)
e := errors.Convert(err)
require.NotNil(e)
assert.True(stderrors.Is(e, errors.ErrNotUnique))
assert.Equal("Key (name)=(alice) already exists.: integrity violation: error #1002: \nunique constraint violation: integrity violation: error #1002", e.Error())
})
t.Run("ErrCodeNotNull", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
_, err := rw.Exec(ctx, truncateTable, nil)
require.NoError(err)
_, err = rw.Exec(ctx, insert, []interface{}{"alice", nil, nil})
require.Error(err)
e := errors.Convert(err)
require.NotNil(e)
assert.True(stderrors.Is(e, errors.ErrNotNull))
assert.Equal("description must not be empty: integrity violation: error #1001: \nnot null constraint violated: integrity violation: error #1001", e.Error())
})
t.Run("ErrCodeCheckConstraint", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
_, err := rw.Exec(ctx, truncateTable, nil)
require.NoError(err)
_, err = rw.Exec(ctx, insert, []interface{}{"alice", "coworker", "one"})
require.Error(err)
e := errors.Convert(err)
require.NotNil(e)
assert.True(stderrors.Is(e, errors.ErrCheckConstraint))
assert.Equal("test_table_five_check constraint failed: integrity violation: error #1000: \ncheck constraint violated: integrity violation: error #1000", e.Error())
})
t.Run("MissingTable", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
_, err := rw.Exec(ctx, missingTable, nil)
require.Error(err)
e := errors.Convert(err)
require.NotNil(e)
assert.True(errors.Match(errors.T(errors.MissingTable), e))
assert.Equal("relation \"not_a_defined_table\" does not exist: integrity violation: error #1004", e.Error())
})
}

@ -0,0 +1,47 @@
package errors
type Info struct {
Kind Kind
Message string
}
// errorCodeInfo provides a map of unique Codes (IDs) to their
// corresponding Kind and a default Message.
var errorCodeInfo = map[Code]Info{
Unknown: {
Message: "unknown",
Kind: Other,
},
InvalidParameter: {
Message: "invalid parameter",
Kind: Parameter,
},
CheckConstraint: {
Message: "constraint check failed",
Kind: Integrity,
},
NotNull: {
Message: "must not be empty (null) violation",
Kind: Integrity,
},
NotUnique: {
Message: "must be unique violation",
Kind: Integrity,
},
NotSpecificIntegrity: {
Message: "Integrity violation without specific details",
Kind: Integrity,
},
MissingTable: {
Message: "missing table",
Kind: Integrity,
},
RecordNotFound: {
Message: "record not found",
Kind: Search,
},
MultipleRecords: {
Message: "multiple records",
Kind: Search,
},
}

@ -0,0 +1,91 @@
package errors
import (
"errors"
"github.com/lib/pq"
)
// IsUniqueError returns a boolean indicating whether the error is known to
// report a unique constraint violation.
func IsUniqueError(err error) bool {
if err == nil {
return false
}
var domainErr *Err
if errors.As(err, &domainErr) {
if domainErr.Code == NotUnique {
return true
}
}
var pqError *pq.Error
if errors.As(err, &pqError) {
if pqError.Code == "23505" { // unique_violation
return true
}
}
return false
}
// IsCheckConstraintError returns a boolean indicating whether the error is
// known to report a check constraint violation.
func IsCheckConstraintError(err error) bool {
if err == nil {
return false
}
var domainErr *Err
if errors.As(err, &domainErr) {
if domainErr.Code == CheckConstraint {
return true
}
}
var pqError *pq.Error
if errors.As(err, &pqError) {
if pqError.Code == "23514" { // check_violation
return true
}
}
return false
}
// IsNotNullError returns a boolean indicating whether the error is known
// to report a not-null constraint violation.
func IsNotNullError(err error) bool {
if err == nil {
return false
}
var domainErr *Err
if errors.As(err, &domainErr) {
if domainErr.Code == NotNull {
return true
}
}
var pqError *pq.Error
if errors.As(err, &pqError) {
if pqError.Code == "23502" { // not_null_violation
return true
}
}
return false
}
// IsMissingTableError returns a boolean indicating whether the error is known
// to report a undefined/missing table violation.
func IsMissingTableError(err error) bool {
var pqError *pq.Error
if errors.As(err, &pqError) {
if pqError.Code == "42P01" {
return true
}
}
return false
}

@ -0,0 +1,242 @@
package errors_test
import (
"context"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestError_IsUnique(t *testing.T) {
t.Parallel()
var tests = []struct {
name string
in error
want bool
}{
{
name: "nil-error",
in: nil,
want: false,
},
{
name: "postgres-not-unique",
in: &pq.Error{
Code: pq.ErrorCode("23503"),
},
want: false,
},
{
name: "postgres-is-unique2",
in: &pq.Error{
Code: pq.ErrorCode("23505"),
},
want: true,
},
{
name: "ErrCodeUnique",
in: errors.ErrNotUnique,
want: true,
},
{
name: "wrapped-pq-is-unique",
in: errors.New(errors.NotUnique,
errors.WithWrap(&pq.Error{
Code: pq.ErrorCode("23505"),
}),
),
want: true,
},
{
name: "ErrRecordNotFound",
in: errors.ErrRecordNotFound,
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
err := tt.in
got := errors.IsUniqueError(err)
assert.Equal(tt.want, got)
})
}
}
func TestError_IsCheckConstraint(t *testing.T) {
t.Parallel()
var tests = []struct {
name string
in error
want bool
}{
{
name: "nil-error",
in: nil,
want: false,
},
{
name: "postgres-not-check-constraint",
in: &pq.Error{
Code: pq.ErrorCode("23505"),
},
want: false,
},
{
name: "postgres-is-check-constraint",
in: &pq.Error{
Code: pq.ErrorCode("23514"),
},
want: true,
},
{
name: "ErrCodeCheckConstraint",
in: errors.New(errors.CheckConstraint),
want: true,
},
{
name: "wrapped-pq-is-check-constraint",
in: errors.New(errors.CheckConstraint,
errors.WithWrap(&pq.Error{
Code: pq.ErrorCode("23514"),
}),
),
want: true,
},
{
name: "ErrRecordNotFound",
in: errors.ErrRecordNotFound,
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
err := tt.in
got := errors.IsCheckConstraintError(err)
assert.Equal(tt.want, got)
})
}
}
func TestError_IsNotNullError(t *testing.T) {
t.Parallel()
var tests = []struct {
name string
in error
want bool
}{
{
name: "nil-error",
in: nil,
want: false,
},
{
name: "postgres-is-unique-not-not-null",
in: &pq.Error{
Code: pq.ErrorCode("23505"),
},
want: false,
},
{
name: "postgres-is-check-constraint-not-not-null",
in: &pq.Error{
Code: pq.ErrorCode("23514"),
},
want: false,
},
{
name: "postgres-is-not-null",
in: &pq.Error{
Code: pq.ErrorCode("23502"),
},
want: true,
},
{
name: "ErrCodeNotNull",
in: errors.New(errors.NotNull),
want: true,
},
{
name: "wrapped-pq-is-not-null",
in: errors.New(errors.NotNull,
errors.WithWrap(&pq.Error{
Code: pq.ErrorCode("23502"),
}),
),
want: true,
},
{
name: "ErrRecordNotFound",
in: errors.ErrRecordNotFound,
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
err := tt.in
got := errors.IsNotNullError(err)
assert.Equal(tt.want, got)
})
}
}
func TestError_IsMissingTableError(t *testing.T) {
var tests = []struct {
name string
in error
want bool
}{
{
name: "nil-error",
in: nil,
want: false,
},
{
name: "postgres-is-unique-not-not-null",
in: &pq.Error{
Code: pq.ErrorCode("23505"),
},
want: false,
},
{
name: "postgres-is-check-constraint-not-not-null",
in: &pq.Error{
Code: pq.ErrorCode("23514"),
},
want: false,
},
{
name: "postgres-is-missing-table",
in: &pq.Error{
Code: pq.ErrorCode("42P01"),
},
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
err := tt.in
got := errors.IsMissingTableError(err)
assert.Equal(tt.want, got)
})
}
t.Run("query-missing-table", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
_, err := rw.Query(context.Background(), "select * from non_existent_table", nil)
require.Error(err)
assert.True(errors.IsMissingTableError(err))
})
}

@ -0,0 +1,20 @@
package errors
// Kind specifies the kind of error (unknown, parameter, integrity, etc).
type Kind uint32
const (
Other Kind = iota
Parameter
Integrity
Search
)
func (e Kind) String() string {
return map[Kind]string{
Other: "unknown",
Parameter: "parameter violation",
Integrity: "integrity violation",
Search: "search issue",
}[e]
}

@ -0,0 +1,44 @@
package errors
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKind_String(t *testing.T) {
t.Parallel()
tests := []struct {
name string
e Kind
want string
}{
{
name: "Other",
e: Other,
want: "unknown",
},
{
name: "Parameter",
e: Parameter,
want: "parameter violation",
},
{
name: "Integrity",
e: Integrity,
want: "integrity violation",
},
{
name: "Search",
e: Search,
want: "search issue",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := tt.e.String()
assert.Equal(tt.want, got)
})
}
}

@ -0,0 +1,97 @@
package errors
// Template is useful constructing Match Err templates. Templates allow you to
// match Errs without specifying a Code. In other words, just Match using the
// Errs: Kind, Op, etc.
type Template struct {
Err // Err embedded to support matching Errs
Kind Kind // Kind allows explicit matching on a Template without a Code.
}
// T creates a new Template for matching Errs. Invalid parameters are ignored.
// If more than is one parameter for a given type, only the last one is used.
func T(args ...interface{}) *Template {
t := &Template{}
for _, a := range args {
switch arg := a.(type) {
case Code:
t.Code = arg
case string:
t.Msg = arg
case Op:
t.Op = arg
case *Err: // order is important, this match must before "case error:"
c := *arg
t.Wrapped = &c
case error:
t.Wrapped = arg
case Kind:
t.Kind = arg
default:
// ignore it
}
}
return t
}
// Info about the Template, which is useful when matching a Template's Kind with
// an Err's Kind.
func (t *Template) Info() Info {
if t == nil {
return errorCodeInfo[Unknown]
}
switch {
case t.Code != Unknown:
return t.Code.Info()
case t.Kind != Other:
return Info{
Message: "Unknown",
Kind: t.Kind,
}
default:
return errorCodeInfo[Unknown]
}
}
// Error satisfies the error interface but we intentionally don't return
// anything of value, in an effort to stop users from substituting Templates in
// place of Errs, when creating domain errors.
func (t *Template) Error() string {
return "Template error"
}
// Match the template against the error. The error must be a *Err, or match
// will return false. Matches all non-empty fields of the template against the
// error.
func Match(t *Template, err error) bool {
if t == nil || err == nil {
return false
}
e, ok := err.(*Err)
if !ok {
return false
}
if t.Code != Unknown && t.Code != e.Code {
return false
}
if t.Msg != "" && t.Msg != e.Msg {
return false
}
if t.Op != "" && t.Op != e.Op {
return false
}
if t.Kind != Other && t.Info().Kind != e.Info().Kind {
return false
}
if t.Wrapped != nil {
if wrappedT, ok := t.Wrapped.(*Template); ok {
return Match(wrappedT, e.Wrapped)
}
if e.Wrapped != nil && t.Wrapped.Error() != e.Wrapped.Error() {
return false
}
}
return true
}

@ -0,0 +1,296 @@
package errors
import (
stderrors "errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestT(t *testing.T) {
t.Parallel()
stdErr := stderrors.New("test error")
tests := []struct {
name string
args []interface{}
want *Template
}{
{
name: "all fields",
args: []interface{}{
"test error msg",
Op("alice.Bob"),
InvalidParameter,
stdErr,
Integrity,
},
want: &Template{
Err: Err{
Code: InvalidParameter,
Msg: "test error msg",
Op: "alice.Bob",
Wrapped: stdErr,
},
Kind: Integrity,
},
},
{
name: "Kind only",
args: []interface{}{
Integrity,
},
want: &Template{
Kind: Integrity,
},
},
{
name: "multiple Kinds",
args: []interface{}{
Search,
Integrity,
},
want: &Template{
Kind: Integrity,
},
},
{
name: "ignore",
args: []interface{}{
32,
},
want: &Template{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := T(tt.args...)
assert.Equal(tt.want, got)
})
}
}
func TestTemplate_Info(t *testing.T) {
t.Parallel()
tests := []struct {
name string
template *Template
want Info
}{
{
name: "nil",
template: nil,
want: errorCodeInfo[Unknown],
},
{
name: "Code",
template: T(InvalidParameter),
want: errorCodeInfo[InvalidParameter],
},
{
name: "Code and Kind",
template: T(InvalidParameter, Integrity),
want: errorCodeInfo[InvalidParameter],
},
{
name: "Kind without Code",
template: T(Integrity),
want: Info{Kind: Integrity, Message: "Unknown"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tt.want, tt.template.Info())
})
}
}
func TestTemplate_Error(t *testing.T) {
t.Parallel()
stdErr := stderrors.New("test error")
tests := []struct {
name string
template *Template
}{
{
name: "Kind only",
template: T(Integrity),
},
{
name: "all params",
template: T(
"test error msg",
Op("alice.Bob"),
InvalidParameter,
stdErr,
Integrity,
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := tt.template.Error()
assert.Equal("Template error", got)
})
}
}
func TestMatch(t *testing.T) {
t.Parallel()
stdErr := stderrors.New("test error")
tests := []struct {
name string
template *Template
err error
want bool
}{
{
name: "nil template",
template: nil,
err: New(NotUnique, WithMsg("this thing was must be unique")),
want: false,
},
{
name: "nil err",
template: T(Integrity),
err: nil,
want: false,
},
{
name: "match on Kind only",
template: T(Integrity),
err: New(
NotUnique,
WithMsg("this thing must be unique"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: true,
},
{
name: "no match on Kind only",
template: T(Integrity),
err: New(
RecordNotFound,
WithMsg("this thing is missing"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: false,
},
{
name: "match on Code only",
template: T(NotUnique),
err: New(
NotUnique,
WithMsg("this thing must be unique"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: true,
},
{
name: "no match on Code only",
template: T(NotUnique),
err: New(
RecordNotFound,
WithMsg("this thing is missing"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: false,
},
{
name: "match on Op only",
template: T(Op("alice.Bob")),
err: New(
NotUnique,
WithMsg("this thing must be unique"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: true,
},
{
name: "no match on Op only",
template: T(Op("alice.Alice")),
err: New(
RecordNotFound,
WithMsg("this thing is missing"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: false,
},
{
name: "match on everything",
template: T(
"this thing must be unique",
Integrity,
InvalidParameter,
ErrInvalidFieldMask,
Op("alice.Bob"),
),
err: New(
InvalidParameter,
WithMsg("this thing must be unique"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: true,
},
{
name: "match on Wrapped only",
template: T(ErrInvalidFieldMask),
err: New(
NotUnique,
WithMsg("this thing must be unique"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: true,
},
{
name: "no match on Wrapped only",
template: T(ErrNotUnique),
err: New(
RecordNotFound,
WithMsg("this thing is missing"),
WithOp("alice.Bob"),
WithWrap(ErrInvalidFieldMask),
),
want: false,
},
{
name: "match on Wrapped only stderror",
template: T(stdErr),
err: New(
NotUnique,
WithMsg("this thing must be unique"),
WithOp("alice.Bob"),
WithWrap(stdErr),
),
want: true,
},
{
name: "no match on Wrapped only stderror",
template: T(stderrors.New("no match")),
err: New(
RecordNotFound,
WithMsg("this thing is missing"),
WithOp("alice.Bob"),
WithWrap(stdErr),
),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got := Match(tt.template, tt.err)
assert.Equal(tt.want, got)
})
}
}

@ -0,0 +1,48 @@
package errors
// GetOpts - iterate the inbound Options and return a struct.
func GetOpts(opt ...Option) Options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
}
return opts
}
// Option - how Options are passed as arguments.
type Option func(*Options)
// Options - how Options are represented.
type Options struct {
withErrWrapped error
withErrMsg string
withOp Op
}
func getDefaultOptions() Options {
return Options{}
}
// WithErrCode provides an option to provide an error to wrap when creating a
// new error.
func WithWrap(e error) Option {
return func(o *Options) {
o.withErrWrapped = e
}
}
// WithMsg provides an option to provide a message when creating a new
// error.
func WithMsg(msg string) Option {
return func(o *Options) {
o.withErrMsg = msg
}
}
// WithOp provides an option to provide the operation that's raising/propagating
// the error.
func WithOp(op Op) Option {
return func(o *Options) {
o.withOp = op
}
}

@ -0,0 +1,51 @@
package errors
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test_getOpts provides unit tests for GetOpts and all the options
func Test_getOpts(t *testing.T) {
t.Parallel()
t.Run("WithMsg", func(t *testing.T) {
assert := assert.New(t)
// test default
opts := GetOpts()
testOpts := getDefaultOptions()
testOpts.withErrMsg = ""
assert.Equal(opts, testOpts)
// try setting it
opts = GetOpts(WithMsg("test msg"))
testOpts.withErrMsg = "test msg"
assert.Equal(opts, testOpts)
})
t.Run("WithWrap", func(t *testing.T) {
assert := assert.New(t)
// test default
opts := GetOpts()
testOpts := getDefaultOptions()
testOpts.withErrWrapped = nil
assert.Equal(opts, testOpts)
// try setting it
opts = GetOpts(WithWrap(ErrInvalidParameter))
testOpts.withErrWrapped = ErrInvalidParameter
assert.Equal(opts, testOpts)
})
t.Run("WithOp", func(t *testing.T) {
assert := assert.New(t)
// test default
opts := GetOpts()
testOpts := getDefaultOptions()
testOpts.withOp = ""
assert.Equal(opts, testOpts)
// try setting it
opts = GetOpts(WithOp("alice.bob"))
testOpts.withOp = "alice.bob"
assert.Equal(opts, testOpts)
})
}

@ -0,0 +1,44 @@
package errors
// Errors returned from this package may be tested against these errors
// with errors.Is. Creating new Sentinel type errors like these should be
// deprecated in favor of the new Err type that includes unique Codes and a
// Matching function.
var (
// ErrInvalidPublicId indicates an invalid PublicId.
ErrInvalidPublicId = New(InvalidParameter, WithMsg("invalid publicId"))
// ErrInvalidParameter is returned by create and update methods if
// an attribute on a struct contains illegal or invalid values.
ErrInvalidParameter = New(InvalidParameter, WithMsg("invalid parameter"))
// ErrInvalidFieldMask is returned by update methods if the field mask
// contains unknown fields or fields that cannot be updated.
ErrInvalidFieldMask = New(InvalidParameter, WithMsg("invalid field mask"))
// ErrEmptyFieldMask is returned by update methods if the field mask is
// empty.
ErrEmptyFieldMask = New(InvalidParameter, WithMsg("empty field mask"))
// ErrNotUnique is returned by create and update methods when a write
// to the repository resulted in a unique constraint violation.
ErrNotUnique = New(NotUnique, WithMsg("unique constraint violation"))
// ErrNotNull is returned by methods when a write to the repository resulted
// in a check constraint violation
ErrCheckConstraint = New(CheckConstraint, WithMsg("check constraint violated"))
// ErrNotNull is returned by methods when a write to the repository resulted
// in a not null constraint violation
ErrNotNull = New(NotNull, WithMsg("not null constraint violated"))
// ErrRecordNotFound returns a "record not found" error and it only occurs
// when attempting to read from the database into struct.
// When reading into a slice it won't return this error.
ErrRecordNotFound = New(RecordNotFound, WithMsg("record not found"))
// ErrMultipleRecords is returned by update and delete methods when a
// write to the repository would result in more than one record being
// changed resulting in the transaction being rolled back.
ErrMultipleRecords = New(MultipleRecords, WithMsg("multiple records"))
)
Loading…
Cancel
Save