From 2334b8077aaf556264556ac40ea4224cdc72fb71 Mon Sep 17 00:00:00 2001 From: Mauro Leggieri Date: Wed, 14 Dec 2022 17:43:56 -0300 Subject: [PATCH] Initial commit --- .gitattributes | 37 ++++++++++++++++++++++ .gitignore | 25 +++++++++++++++ LICENSE | 3 +- README.md | 61 ++++++++++++++++++++++++++++++++++-- atomic.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++ atomic_test.go | 50 +++++++++++++++++++++++++++++ augmented.go | 54 ++++++++++++++++++++++++++++++++ augmented_test.go | 24 ++++++++++++++ go.mod | 3 ++ helpers.go | 22 +++++++++++++ helpers_test.go | 17 ++++++++++ 11 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 atomic.go create mode 100644 atomic_test.go create mode 100644 augmented.go create mode 100644 augmented_test.go create mode 100644 go.mod create mode 100644 helpers.go create mode 100644 helpers_test.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b894d3d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,37 @@ +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# Force the following filetypes to have unix eols, so Windows does not break them +*.* text eol=lf + +# Windows forced line-endings +/.idea/* text eol=crlf + +# +## These files are binary and should be left untouched +# + +# (binary is a macro for -text -diff) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary +*.flv binary +*.fla binary +*.swf binary +*.gz binary +*.zip binary +*.7z binary +*.ttf binary +*.eot binary +*.woff binary +*.pyc binary +*.pdf binary +*.ez binary +*.bz2 binary +*.swp binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af72e14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +*.~* + +*.log +*.swp +.idea +.vscode +*.patch +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +.DS_Store +app +demo + +vendor/* diff --git a/LICENSE b/LICENSE index 261eeb9..80c12c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -186,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright (C) 2022 RandLabs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 85bf1a2..3094d38 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# go-atomic-error -An error object that can be set only once and implements context behavior +# go-exterror + +Extended error routines. + +## AugmentedError + +An error object which wraps another one and adds supports for extended fields. + + +```golang +package main + +import ( + "errors" + + "github.com/randlabs/go-exterror" +) + +func main() { + err := exterror.NewAugmentedError( + errors.New("wrapped error"), + "some example message", map[string]interface{}{ + "value2": 1, + "value1": "hello", + }, + ) + + //... +} +``` +## AtomicError + +An error object that can be set only once and implements context behavior. + +```golang +package main + +import ( + "errors" + + "github.com/randlabs/go-exterror" +) + +func main() { + err := exterror.NewAtomicError() + + // Set an error in a separate go routine + go func() { + err.Set(errors.New("error")) + }() + + // Wait for the error to be set + <-err.Done() +} +``` + +## License +See `LICENSE` file for details. + diff --git a/atomic.go b/atomic.go new file mode 100644 index 0000000..6a08153 --- /dev/null +++ b/atomic.go @@ -0,0 +1,80 @@ +package exterror + +import ( + "sync" + "time" +) + +// ----------------------------------------------------------------------------- + +// AtomicError is a thread-safe error object. It also implements Context behavior +type AtomicError struct { + mtx sync.RWMutex + err error + done chan struct{} + doneOnce sync.Once +} + +// ----------------------------------------------------------------------------- + +// NewAtomicError creates a new thread safe error object. +func NewAtomicError() *AtomicError { + e := &AtomicError{ + mtx: sync.RWMutex{}, + done: make(chan struct{}), + doneOnce: sync.Once{}, + } + return e +} + +// Set stores the passed error if the current is nil and completes the context. +func (x *AtomicError) Set(err error) bool { + changed := false + + if err != nil { + x.mtx.Lock() + if x.err == nil { + changed = true + x.err = err + } + x.mtx.Unlock() + + if changed { + x.doneOnce.Do(func() { + close(x.done) + }) + } + } + + return changed +} + +// Deadline returns the time when work done on behalf of this context +// should be canceled. There is no Deadline for a AtomicError. +func (*AtomicError) Deadline() (deadline time.Time, ok bool) { + return +} + +// Done returns a channel that's closed when work done on behalf of this +// context should be canceled. +func (x *AtomicError) Done() <-chan struct{} { + return x.done +} + +// Value returns the value associated with this context for key, or nil +// if no value is associated with key. +func (*AtomicError) Value(_ interface{}) interface{} { + return nil +} + +// Err returns nil if Done is not yet closed. +// If Done is closed, Err returns a non-nil error explaining why: +// Canceled if the context was canceled +// or DeadlineExceeded if the context's deadline passed. +// After Err returns a non-nil error, successive calls to Err return the same error. +func (x *AtomicError) Err() error { + x.mtx.RLock() + defer x.mtx.RUnlock() + + return x.err +} diff --git a/atomic_test.go b/atomic_test.go new file mode 100644 index 0000000..0799f44 --- /dev/null +++ b/atomic_test.go @@ -0,0 +1,50 @@ +package exterror_test + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/randlabs/go-exterror" +) + +// ----------------------------------------------------------------------------- + +func TestAtomicError(t *testing.T) { + wg := sync.WaitGroup{} + + err := exterror.NewAtomicError() + + // Simulate two go-routines setting an error simultaneously + wg.Add(2) + go func() { + err.Set(errors.New("error 1")) + wg.Done() + }() + + go func() { + err.Set(errors.New("error 2")) + wg.Done() + }() + + wg.Wait() + + if err.Err().Error() != "error 1" && err.Err().Error() != "error 2" { + t.FailNow() + } +} + +func TestAtomicErrorContext(t *testing.T) { + err := exterror.NewAtomicError() + + go func() { + err.Set(errors.New("error")) + }() + + select { + case <-err.Done(): + case <-time.After(5 * time.Second): + t.FailNow() + } +} diff --git a/augmented.go b/augmented.go new file mode 100644 index 0000000..b888ecf --- /dev/null +++ b/augmented.go @@ -0,0 +1,54 @@ +package exterror + +import ( + "fmt" + "sort" +) + +// ----------------------------------------------------------------------------- + +// AugmentedError is just an error with extended data. +type AugmentedError struct { + Message string + Fields map[string]interface{} + Err error // Underlying error that occurred during the operation. +} + +// ----------------------------------------------------------------------------- + +// NewAugmentedError creates a new AugmentedError. +func NewAugmentedError(wrappedErr error, text string, fields map[string]interface{}) *AugmentedError { + e := AugmentedError{} + e.Message = text + e.Fields = fields + e.Err = wrappedErr + return &e +} + +// Unwrap returns the underlying error. +func (e *AugmentedError) Unwrap() error { + return e.Err +} + +// Error returns a string representation of the error. +func (e *AugmentedError) Error() string { + if e == nil { + return "" + } + s := e.Message + if e.Fields != nil { + keys := make([]string, 0, len(e.Fields)) + for k := range e.Fields { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + s += fmt.Sprintf(" [%s=%v]", k, e.Fields[k]) + } + } + if e.Err != nil { + s += " [err=" + e.Err.Error() + "]" + } + return s +} diff --git a/augmented_test.go b/augmented_test.go new file mode 100644 index 0000000..28ff03d --- /dev/null +++ b/augmented_test.go @@ -0,0 +1,24 @@ +package exterror_test + +import ( + "errors" + "testing" + + "github.com/randlabs/go-exterror" +) + +// ----------------------------------------------------------------------------- + +func TestAugmentedError(t *testing.T) { + err := exterror.NewAugmentedError( + errors.New("dummy wrapped error"), + "dummy message error", map[string]interface{}{ + "value2": 1000, + "value1": "hello", + }, + ) + + if err.Error() != "dummy message error [value1=hello] [value2=1000] [err=dummy wrapped error]" { + t.FailNow() + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..729f9e4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/randlabs/go-exterror + +go 1.19 diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..e876287 --- /dev/null +++ b/helpers.go @@ -0,0 +1,22 @@ +package exterror + +import ( + "net" +) + +// ----------------------------------------------------------------------------- + +// IsNetworkError returns true if the provided error object is related to a network error. +func IsNetworkError(err error) bool { + if err != nil { + switch err.(type) { + case net.Error: + return true + case *net.OpError: + return true + case *net.DNSError: + return true + } + } + return false +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..9e4e12c --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,17 @@ +package exterror_test + +import ( + "net" + "testing" + + "github.com/randlabs/go-exterror" +) + +// ----------------------------------------------------------------------------- + +func TestIsNetworkError(t *testing.T) { + _, err := net.LookupIP("non-existent-domain.123") + if !exterror.IsNetworkError(err) { + t.FailNow() + } +}