Added ut and utio package (utility packages).

This commit is contained in:
Sander Schobers 2018-01-13 18:14:54 +02:00
commit c60d1b2724
13 changed files with 567 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vscode
debug
debug.test

21
LICENSE Normal file
View File

@ -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.

93
README.md Normal file
View File

@ -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
```

36
deopter.go Normal file
View File

@ -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
}

86
deopter_test.go Normal file
View File

@ -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)
}

54
errors.go Normal file
View File

@ -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)
}

84
errors_test.go Normal file
View File

@ -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])
}

24
utio/dir.go Normal file
View File

@ -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)
}

35
utio/file.go Normal file
View File

@ -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)
}

21
utio/io.go Normal file
View File

@ -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
}

27
utio/json.go Normal file
View File

@ -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)
}

55
utio/png.go Normal file
View File

@ -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))
}

28
utio/string.go Normal file
View File

@ -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
}