commit c60d1b27243f8ec1cbfe34b29d9e2b6f5f72cf58 Author: Sander Schobers Date: Sat Jan 13 18:14:54 2018 +0200 Added ut and utio package (utility packages). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8f6689 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +debug +debug.test \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a868a2e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Sander Schobers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..232f7dc --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# ut package + +The ut package exposes some utilities the subpackage [utio](utio) exposes some IO related utilities. The package is licensed under [MIT](LICENSE). Currently this package is still under development and thus the API may break. + +## API + +Packages: + - [ut](#ut-package) + - [utio](#utio-package) + +### ut package + +**Deopter** + +`Deopter` may be used an action may or may not be executed. Particularly useful in combination with defer. + +Example: +``` +function Open(p string) (*File, error) { + var f, _ = os.Open(p) + var d = NewDeopter(f.Close) + defer d.Invoke() + + var err error + // code that might exit early + if nil != err { + return nil, err + } + + // successful code path + d.Deopt() + return f +} +``` + +**Errors** + +An `Errors` interface that represents multiple errors (but still adheres to the `error` interface) + +``` +type Errors interface { + error + Errs() []error +} +``` + +Combine errors using `ErrCombine` when you want to combine multiple errors as one. Makes use of the `Errors` interface when there is more than one error. +``` +var err error +err = ErrCombine(err, err2) +``` + +### utio package + +Encoder/decoder interfaces and two implementations: + - JSON + - PNG + +``` +type Decoder interface { + Decode(io.Reader) error +} + +type Encoder interface { + Encode(io.Writer) error +} +``` + +Utility methods for reading/writing (with or without encoding) files and strings. + +``` +// File methods +func DecodeFile(string, Decoder) error +func ReadFile(string, ReadFunc) error + +func EncodeFile(string, Encoder) error +func WriteFile(string, WriteFunc) error + +// String methods +func DecodeFromString(string, Decoder) error +func ReadFromString(string, ReadFunc) error + +func EncodeToString(Encoder) (string, error) +func WriteToString(WriteFunc) (string, error) +``` + +Utility methods related to the os/file system. +``` +func Home() (string, error) // returns the user directory +func PathExists(string) bool +func PathDoesNotExist(string) bool +``` + diff --git a/deopter.go b/deopter.go new file mode 100644 index 0000000..8522aa4 --- /dev/null +++ b/deopter.go @@ -0,0 +1,36 @@ +package ut + +// Deopter describes an action that can be opted-out. This can be used in combination with defer to clean up +type Deopter interface { + Add(...DeoptFunc) + Deopt() + Invoke() error +} + +// DeoptFunc describes the action signature for the Deopter. +type DeoptFunc func() error + +// NewDeopter creates a deopter for the functions supplied. +func NewDeopter(fn ...DeoptFunc) Deopter { + return &deopter{fn} +} + +type deopter struct { + fn []DeoptFunc +} + +func (d *deopter) Add(fn ...DeoptFunc) { + d.fn = append(d.fn, fn...) +} + +func (d *deopter) Deopt() { + d.fn = nil +} + +func (d *deopter) Invoke() error { + var err error + for _, fn := range d.fn { + err = ErrCombine(err, fn()) + } + return err +} diff --git a/deopter_test.go b/deopter_test.go new file mode 100644 index 0000000..7d0db1d --- /dev/null +++ b/deopter_test.go @@ -0,0 +1,86 @@ +package ut + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type optional struct { + err error + exec bool +} + +func (o *optional) Invoke() error { + o.exec = true + return o.err +} + +func TestDeopterInvoked(t *testing.T) { + var o = &optional{} + var d = NewDeopter(o.Invoke) + + var err = d.Invoke() + + assert.Nil(t, err) + assert.True(t, o.exec) +} + +func TestDeopterOptOut(t *testing.T) { + var o = &optional{} + var d = NewDeopter(o.Invoke) + + d.Deopt() + var err = d.Invoke() + + assert.Nil(t, err) + assert.False(t, o.exec) +} + +func TestDeopterErrorOnInvocation(t *testing.T) { + var o = &optional{err: errors.New("err")} + var d = NewDeopter(o.Invoke) + + var err = d.Invoke() + + assert.Equal(t, o.err, err) +} + +func TestDeopterMultipleInvocations(t *testing.T) { + var o1, o2 = &optional{}, &optional{} + var d = NewDeopter(o1.Invoke, o2.Invoke) + + var err = d.Invoke() + + assert.Nil(t, err) + assert.True(t, o1.exec) + assert.True(t, o2.exec) +} + +func TestDeopterMultipleErrors(t *testing.T) { + var o1, o2 = &optional{err: errors.New("err1")}, &optional{err: errors.New("err2")} + var d = NewDeopter(o1.Invoke, o2.Invoke) + + var err = d.Invoke() + + assert.NotNil(t, err) + require.Implements(t, (*Errors)(nil), err) + var errs = err.(Errors).Errs() + require.Equal(t, 2, len(errs)) + assert.True(t, o1.exec) + assert.True(t, o2.exec) +} + +func TestDeopterCanAdd(t *testing.T) { + var o1, o2 = &optional{}, &optional{} + var d = NewDeopter(o1.Invoke) + + d.Add(o2.Invoke) + + var err = d.Invoke() + assert.Nil(t, err) + assert.True(t, o1.exec) + assert.True(t, o2.exec) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..a4d28dc --- /dev/null +++ b/errors.go @@ -0,0 +1,54 @@ +package ut + +import ( + "bytes" + "fmt" +) + +var _ Errors = errorAggregate{} + +// Errors exposes slice of errors. +type Errors interface { + error + Errs() []error +} + +type errorAggregate []error + +func (e errorAggregate) Errs() []error { + return e +} + +func (e errorAggregate) Error() string { + var msg = &bytes.Buffer{} + fmt.Fprint(msg, "errors: ") + for i, err := range e { + if 0 < i { + fmt.Fprint(msg, "; ") + } + fmt.Fprint(msg, err) + } + return msg.String() +} + +// ErrCombine combines one or more errors, nil entries are omitted and nil is returned if all given errors are nil. The first argument is expanded if it satisfies the Errors interface. In case of aggregation of errors the return error will satisfy the Errors interface. +func ErrCombine(errs ...error) error { + if 0 < len(errs) { + if e, ok := errs[0].(Errors); ok { + errs = append(e.Errs(), errs[1:]...) + } + } + for i := 0; i < len(errs); i++ { + if nil == errs[i] { + errs = append(errs[:i], errs[i+1:]...) + } else { + i++ + } + } + if 0 == len(errs) { + return nil + } else if 1 == len(errs) { + return errs[0] + } + return errorAggregate(errs) +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..e7f3e1f --- /dev/null +++ b/errors_test.go @@ -0,0 +1,84 @@ +package ut + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestErrCombineNil(t *testing.T) { + assert.Nil(t, ErrCombine(nil)) +} + +func TestErrCombineEmpty(t *testing.T) { + assert.Nil(t, ErrCombine()) +} + +func TestErrCombineErrIsKept(t *testing.T) { + var err = errors.New("err") + + assert.Equal(t, err, ErrCombine(err)) +} + +func TestErrCombineErrIsAssigned(t *testing.T) { + var err = errors.New("err") + + assert.Equal(t, err, ErrCombine(nil, err)) +} + +func TestErrCombineAggregates(t *testing.T) { + var err1 = errors.New("err1") + var err2 = errors.New("err2") + + var err = ErrCombine(err1, err2) + + assert.NotNil(t, err) + require.Implements(t, (*Errors)(nil), err) + var errs = err.(Errors).Errs() + assert.Equal(t, 2, len(errs)) + assert.Equal(t, err1, errs[0]) + assert.Equal(t, err2, errs[1]) +} + +func TestErrCombineErrorContainsBothMessages(t *testing.T) { + var err1 = errors.New("err1") + var err2 = errors.New("err2") + + var err = ErrCombine(err1, err2).Error() + + assert.Contains(t, err, err1.Error()) + assert.Contains(t, err, err2.Error()) +} + +func TestErrCombineExpandsFirstArgument(t *testing.T) { + var err1 = errors.New("err1") + var err2 = errors.New("err2") + var err3 = errors.New("err3") + + var err = ErrCombine(ErrCombine(err1, err2), err3) + + assert.NotNil(t, err) + require.Implements(t, (*Errors)(nil), err) + var errs = err.(Errors).Errs() + assert.Equal(t, 3, len(errs)) + assert.Equal(t, err1, errs[0]) + assert.Equal(t, err2, errs[1]) + assert.Equal(t, err3, errs[2]) +} + +func TestErrCombineDoesNotExpandConsecutiveArguments(t *testing.T) { + var err1 = errors.New("err1") + var err2 = errors.New("err2") + var err3 = errors.New("err3") + + var err = ErrCombine(err1, ErrCombine(err2, err3)) + + assert.NotNil(t, err) + require.Implements(t, (*Errors)(nil), err) + var errs = err.(Errors).Errs() + assert.Equal(t, 2, len(errs)) + assert.Equal(t, err1, errs[0]) + assert.Implements(t, (*Errors)(nil), errs[1]) +} diff --git a/utio/dir.go b/utio/dir.go new file mode 100644 index 0000000..2dbea0f --- /dev/null +++ b/utio/dir.go @@ -0,0 +1,24 @@ +package utio + +import ( + "os" + + homedir "github.com/mitchellh/go-homedir" +) + +// Home gives back the home directory for the current user. +func Home() (string, error) { + return homedir.Dir() +} + +// PathExists tests if the supplied path exists. +func PathExists(path string) bool { + var _, err = os.Stat(path) + return err == nil +} + +// PathDoesNotExist tests if the supplied does not exist. +func PathDoesNotExist(path string) bool { + var _, err = os.Stat(path) + return os.IsNotExist(err) +} diff --git a/utio/file.go b/utio/file.go new file mode 100644 index 0000000..dc01446 --- /dev/null +++ b/utio/file.go @@ -0,0 +1,35 @@ +package utio + +import ( + "os" +) + +// DecodeFile tries to open the file at the specified path and if successful it decodes d. +func DecodeFile(path string, d Decoder) error { + return ReadFile(path, d.Decode) +} + +// ReadFile tries to open the file at the specified path and if successful it invokes fn. +func ReadFile(path string, fn ReadFunc) error { + var f, err = os.Open(path) + if nil != err { + return err + } + defer f.Close() + return fn(f) +} + +// EncodeFile tries to open the file at the specified path and if successful it encodes e. +func EncodeFile(path string, e Encoder) error { + return WriteFile(path, e.Encode) +} + +// WriteFile tries to create the file at the specified path and if successful it invokes fn. +func WriteFile(path string, fn WriteFunc) error { + var f, err = os.Create(path) + if nil != err { + return err + } + defer f.Close() + return fn(f) +} diff --git a/utio/io.go b/utio/io.go new file mode 100644 index 0000000..c9251c4 --- /dev/null +++ b/utio/io.go @@ -0,0 +1,21 @@ +package utio + +import ( + "io" +) + +// ReadFunc defines a read function. +type ReadFunc func(io.Reader) error + +// WriteFunc defines a write function. +type WriteFunc func(io.Writer) error + +// Decoder exposes a decoder for a specific value/type. +type Decoder interface { + Decode(io.Reader) error +} + +// Encoder exposes an encoder for a specific value/type. +type Encoder interface { + Encode(io.Writer) error +} diff --git a/utio/json.go b/utio/json.go new file mode 100644 index 0000000..0616f1b --- /dev/null +++ b/utio/json.go @@ -0,0 +1,27 @@ +package utio + +import ( + "encoding/json" + "io" +) + +// JSONer describes a JSON encoder/decoder. +type JSONer interface { + Encoder + Decoder +} + +// JSON creates a JSON encoder/decoder for v. +func JSON(v interface{}) JSONer { + return &jsoner{v} +} + +type jsoner struct{ v interface{} } + +func (j *jsoner) Decode(r io.Reader) error { + return json.NewDecoder(r).Decode(j.v) +} + +func (j *jsoner) Encode(w io.Writer) error { + return json.NewEncoder(w).Encode(j.v) +} diff --git a/utio/png.go b/utio/png.go new file mode 100644 index 0000000..83ba231 --- /dev/null +++ b/utio/png.go @@ -0,0 +1,55 @@ +package utio + +import ( + "image" + "image/png" + "io" +) + +// PNGer describes a PNG encoder/decoder. +type PNGer interface { + Encoder + Decoder + Image() image.Image +} + +// PNG creates a PNG encoder/decoder for m, m is only required for encoding. +func PNG(m image.Image) PNGer { + return &pnger{m} +} + +type pnger struct { + m image.Image +} + +func (p *pnger) Encode(w io.Writer) error { + return png.Encode(w, p.m) +} + +func (p *pnger) Decode(r io.Reader) error { + var m, err = png.Decode(r) + if nil != err { + return err + } + p.m = m + return nil +} + +func (p *pnger) Image() image.Image { + return p.m +} + +// LoadPNG loads a PNG image from the supplied path. +func LoadPNG(path string) (image.Image, error) { + var p = PNG(nil) + var err = DecodeFile(path, p) + if nil != err { + return nil, err + } + return p.Image(), nil +} + +// SavePNG writes an image as PNG to the supplied path. +func SavePNG(path string, m image.Image) error { + return EncodeFile(path, PNG(m)) +} diff --git a/utio/string.go b/utio/string.go new file mode 100644 index 0000000..6162403 --- /dev/null +++ b/utio/string.go @@ -0,0 +1,28 @@ +package utio + +import "bytes" + +// DecodeFromString d the bytes of s to fn. +func DecodeFromString(s string, d Decoder) error { + return ReadFromString(s, d.Decode) +} + +// ReadFromString supplies the bytes of s to fn. +func ReadFromString(s string, fn ReadFunc) error { + return fn(bytes.NewBufferString(s)) +} + +// EncodeToString returns the encoded string value +func EncodeToString(e Encoder) (string, error) { + return WriteToString(e.Encode) +} + +// WriteToString returns the string value of the data written to fn. +func WriteToString(fn WriteFunc) (string, error) { + var b = &bytes.Buffer{} + var err = fn(b) + if nil != err { + return "", err + } + return b.String(), nil +}