Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mxmauro committed Dec 14, 2022
1 parent 95d60ca commit 2334b80
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 3 deletions.
37 changes: 37 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/*
3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Expand Down Expand Up @@ -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.
Expand Down
61 changes: 59 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

80 changes: 80 additions & 0 deletions atomic.go
Original file line number Diff line number Diff line change
@@ -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
}
50 changes: 50 additions & 0 deletions atomic_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
54 changes: 54 additions & 0 deletions augmented.go
Original file line number Diff line number Diff line change
@@ -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
}
24 changes: 24 additions & 0 deletions augmented_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/randlabs/go-exterror

go 1.19
22 changes: 22 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions helpers_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 2334b80

Please sign in to comment.