errortree

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 21, 2018 License: MIT Imports: 3 Imported by: 9

README

go-errortree

GoDoc Build Status codecov Go Report Card

github.com/speijnik/go-errortree provides functionality for working with errors structured as a tree.

Structuring errors in such a way may be desired when, for example, validating structured input such as a configuration file with multiple sections.

A corresponding example can be found in the example_config_validation_test.go file.

The code is released under the terms of the MIT license.

Documentation

Overview

Package errortree provides primitives for working with errors in tree structure

errortree is intended to be used in places where errors are generated from an arbitrary tree structure, like the validation of a configuration file. This allows adding additional context as to why an error has happened in a clean and structured way.

errortree fully supports nesting of multiple trees, including simplified retrieval of errors which, among other things, should help remove repeated boilerplate code from unit tests.

Example
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"os"

	"github.com/speijnik/go-errortree"
)

// ErrOptionMissing indicates that a configuration option is missing
var ErrOptionMissing = errors.New("Configuration option is missing")

type NetworkConfiguration struct {
	ListenAddress string
	MaxClients    uint
}

// Validate validates the network configuration
func (c *NetworkConfiguration) Validate() (err error) {
	if c.ListenAddress == "" {
		err = errortree.Add(err, "ListenAddress", ErrOptionMissing)
	} else if _, _, splitErr := net.SplitHostPort(c.ListenAddress); splitErr != nil {
		err = errortree.Add(err, "ListenAddress", errors.New("Must be in host:port format"))
	}

	if c.MaxClients < 1 {
		err = errortree.Add(err, "MaxClients", errors.New("Must be at least 1"))
	}

	return
}

type StorageConfiguration struct {
	DataDirectory string
}

func (c *StorageConfiguration) Validate() (err error) {
	if c.DataDirectory == "" {
		err = errortree.Add(err, "DataDirectory", ErrOptionMissing)
	} else if fileInfo, statErr := os.Stat(c.DataDirectory); statErr != nil {
		err = errortree.Add(err, "DataDirectory", errors.New("Directory does not exist"))
	} else if !fileInfo.IsDir() {
		err = errortree.Add(err, "DataDirectory", errors.New("Not a directory"))
	}
	return
}

// Configuration represents a configuration struct
// which may be filled from a file.
//
// It provides a Validate function which ensures that the configuration
// is correct and can be used.
type Configuration struct {
	// Network configuration
	Network NetworkConfiguration

	// Storage configuration
	Storage StorageConfiguration
}

func (c *Configuration) Validate() (err error) {
	err = errortree.Add(err, "Network", c.Network.Validate())
	err = errortree.Add(err, "Storage", c.Storage.Validate())

	return
}

func main() {
	// Initialize an empty configuration. Validating this configuration should give us three errors:
	c := Configuration{}

	fmt.Println("[0] " + c.Validate().Error())

	// Now set some values and validate again
	c.Network.ListenAddress = "[[:8080"
	c.Network.MaxClients = 1
	c.Storage.DataDirectory = "/non-existing"

	fmt.Println("[1] " + c.Validate().Error())

	// Set the data directory to a temporary file (!) and validate again
	f, err := ioutil.TempFile("", "go-errortree-test-")
	if err != nil {
		panic(err)
	}
	defer os.Remove(f.Name())
	defer f.Close()

	c.Network.ListenAddress = "0.0.0.0:8080"
	c.Storage.DataDirectory = f.Name()

	fmt.Println("[2] " + c.Validate().Error())

	// Fix everything and run again
	tempDir, err := ioutil.TempDir("", "go-errortree-test-")
	if err != nil {
		panic(err)
	}
	defer os.Remove(tempDir)

	c.Storage.DataDirectory = tempDir

	if err := c.Validate(); err == nil {
		fmt.Println("[3] Config OK")
	}

}
Output:

[0] 3 errors occurred:

* Network:ListenAddress: Configuration option is missing
* Network:MaxClients: Must be at least 1
* Storage:DataDirectory: Configuration option is missing
[1] 2 errors occurred:

* Network:ListenAddress: Must be in host:port format
* Storage:DataDirectory: Directory does not exist
[2] 1 error occurred:

* Storage:DataDirectory: Not a directory
[3] Config OK

Index

Examples

Constants

View Source
const DefaultDelimiter = ":"

DefaultDelimiter defines the delimiter that is by default used for representing paths to nested Errors

Variables

This section is empty.

Functions

func Add

func Add(parent error, key string, err error) error

Add adds an error under a given key to the provided tree.

This function panics if the key is already present in the tree. Otherwise it behaves like Set.

Example
var err error

// Using Add on a nil-error automatically creates an error tree and adds the desired error
err = errortree.Add(err, "test", errors.New("test error"))
fmt.Println(err.Error())
Output:

1 error occurred:

* test: test error
Example (Duplicate)
// Add an error with the key "test"
err := errortree.Add(nil, "test", errors.New("test error"))

// Recover from the panic, this will the output we expect below
defer func() {
	if r := recover(); r != nil {
		fmt.Println(r)
	}
}()

errortree.Add(err, "test", errors.New("key re-used"))
Output:

Cannot add error: key test exists.
Example (Nested)
// Create an error which will acts as the child for our top-level error
childError := errortree.Add(nil, "test0", errors.New("child error"))
// Add another error to our child
childError = errortree.Add(childError, "test1", errors.New("another child error"))

// Create the top-level error, adding the child in the process
err := errortree.Add(nil, "child", childError)
// Add another top-level error
err = errortree.Add(err, "second", errors.New("top-level error"))

fmt.Println(err.Error())
Output:

3 errors occurred:

* child:test0: child error
* child:test1: another child error
* second: top-level error

func Flatten

func Flatten(err error) map[string]error

Flatten returns the error tree in flattened form.

Each error inside the complete tree is stored under its full key. The full key is constructed from the each error's path inside the tree and joined together with the tree's delimiter.

Example
tree := &errortree.Tree{
	Errors: map[string]error{
		"a": errors.New("top-level"),
		"b": &errortree.Tree{
			Errors: map[string]error{
				"c": errors.New("nested"),
			},
		},
	},
}

flattened := errortree.Flatten(tree)
// Sort keys alphabetically so we get reproducible output
keys := errortree.Keys(tree)
for _, key := range keys {
	fmt.Println("key: " + key + ", value: " + flattened[key].Error())
}
Output:

key: a, value: top-level
key: b:c, value: nested

func Get

func Get(err error, key string, path ...string) error

Get retrieves the error for the given key from the provided error. The path parameter may be used for specifying a nested error's key.

If the error is not an errortree.Tree or the child cannot be found on the exact path this function returns nil.

Example
tree := &errortree.Tree{
	Errors: map[string]error{
		"a":    errors.New("top-level"),
		"test": errors.New("test"),
		"b": &errortree.Tree{
			Errors: map[string]error{
				"c": errors.New("nested"),
			},
		},
	},
}

// Get can be used to retrieve an error by its key
fmt.Println(errortree.Get(tree, "a"))

// Nested retrieval is supported as well
fmt.Println(errortree.Get(tree, "b", "c"))
Output:

top-level
nested
Example (Nested)
tree := &errortree.Tree{
	Errors: map[string]error{
		"a":    errors.New("top-level"),
		"test": errors.New("test"),
		"b": &errortree.Tree{
			Errors: map[string]error{
				"c": errors.New("nested"),
			},
		},
	},
}

// Get tries to resolve the path exactly and returns nil if
// the path does not exist
fmt.Println(errortree.Get(tree, "b", "non-existent"))
Output:

<nil>
Example (Non_tree)
// Get returns nil if the passed error is not a tree

fmt.Println(errortree.Get(errors.New("test"), "key"))
Output:

<nil>

func GetAny

func GetAny(err error, key string, path ...string) error

GetAny retrieves the error for a given key from the tree. The path parameter may be used for specifying a nested error's key.

This function returns the most-specific match:

If the provided error is not an errortree.Tree, the provided error is returned. If at any step the path cannot be fully followed, the previous error on the path will be returned.

Example (Nested)
// When GetAny does not encounter an exact match in the tree, it returns the most-specific match

tree := &errortree.Tree{
	Errors: map[string]error{
		"a":    errors.New("top-level"),
		"test": errors.New("test"),
		"b": &errortree.Tree{
			Errors: map[string]error{
				"c": errors.New("nested"),
			},
		},
	},
}

// Get tries to resolve the path exactly and returns nil if
// the path does not exist
fmt.Println(errortree.GetAny(tree, "b", "non-existent"))
Output:

1 error occurred:

* c: nested
Example (Non_tree)
// GetAny always returns the error it got passed even if it is not a tree

fmt.Println(errortree.GetAny(errors.New("test"), "key"))
Output:

test

func Keys

func Keys(err error) []string

Keys returns all error keys present in a given tree.

The value returned by this function is an alphabetically sorted, flattened list of all keys in a tree of Tree structs.

The delimiter configured for the top-level tree is guaranteed to be used throughout the complete tree.

Example
tree := &errortree.Tree{
	Errors: map[string]error{
		"a":    errors.New("top-level"),
		"test": errors.New("test"),
		"b": &errortree.Tree{
			Errors: map[string]error{
				"c": errors.New("nested"),
			},
		},
	},
}

fmt.Println(strings.Join(errortree.Keys(tree), ", "))
Output:

a, b:c, test

func Set

func Set(parent error, key string, err error) error

Set creates or replaces an error under a given key in a tree.

The parent value may be nil, in which case a new *Tree is created, to which the key is added and the new *Tree is returned. Otherwise the *Tree to which the key was added is returned.

Example
var err error

// Using Set on a nil-error automatically creates an error tree and adds the desired error
err = errortree.Set(err, "test", errors.New("test error"))
fmt.Println(err.Error())
Output:

1 error occurred:

* test: test error
Example (Duplicate)
// Add an error with the key "test"
err := errortree.Add(nil, "test", errors.New("test error"))

// Call Set on the key, which will override it
err = errortree.Set(err, "test", errors.New("key re-used"))
fmt.Println(err.Error())
Output:

1 error occurred:

* test: key re-used

func SimpleFormatter

func SimpleFormatter(errorMap map[string]error) string

SimpleFormatter provides a simple Formatter which returns a message indicating how many Errors occurred and details for every error. The reported Errors are sorted alphabetically by key.

Types

type Formatter

type Formatter func(map[string]error) string

Formatter defines the Formatter type This function can expected that the provided map contains a flattened map of all Errors

type Tree

type Tree struct {
	// Errors holds the tree's items
	Errors map[string]error
	// Delimiter specifies the tree's delimiter for building nested paths
	Delimiter string
	// Formatter specifies the formatter to use when Error is invoked
	Formatter Formatter
}

Tree is an error type which acts as a container for storing multiple errors in a tree structure.

func GetTree

func GetTree(err error) (tree *Tree, isTree bool)

GetTree returns the tree for a given error.

func New

func New() *Tree

New returns a new error tree.

func (*Tree) Error

func (t *Tree) Error() string

func (*Tree) ErrorOrNil

func (t *Tree) ErrorOrNil() error

ErrorOrNil returns nil if the tree is empty or the tree itself otherwise.

func (*Tree) WrappedErrors

func (t *Tree) WrappedErrors() []error

WrappedErrors returns the errors wrapped by the tree.

The ordering of the returned errors is determined by the alphabetical ordering of the corresponding error keys.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL