195 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
// Package warnings implements error handling with non-fatal errors (warnings).
 | 
						|
//
 | 
						|
// A recurring pattern in Go programming is the following:
 | 
						|
//
 | 
						|
//  func myfunc(params) error {
 | 
						|
//      if err := doSomething(...); err != nil {
 | 
						|
//          return err
 | 
						|
//      }
 | 
						|
//      if err := doSomethingElse(...); err != nil {
 | 
						|
//          return err
 | 
						|
//      }
 | 
						|
//      if ok := doAnotherThing(...); !ok {
 | 
						|
//          return errors.New("my error")
 | 
						|
//      }
 | 
						|
//      ...
 | 
						|
//      return nil
 | 
						|
//  }
 | 
						|
//
 | 
						|
// This pattern allows interrupting the flow on any received error. But what if
 | 
						|
// there are errors that should be noted but still not fatal, for which the flow
 | 
						|
// should not be interrupted? Implementing such logic at each if statement would
 | 
						|
// make the code complex and the flow much harder to follow.
 | 
						|
//
 | 
						|
// Package warnings provides the Collector type and a clean and simple pattern
 | 
						|
// for achieving such logic. The Collector takes care of deciding when to break
 | 
						|
// the flow and when to continue, collecting any non-fatal errors (warnings)
 | 
						|
// along the way. The only requirement is that fatal and non-fatal errors can be
 | 
						|
// distinguished programmatically; that is a function such as
 | 
						|
//
 | 
						|
//  IsFatal(error) bool
 | 
						|
//
 | 
						|
// must be implemented. The following is an example of what the above snippet
 | 
						|
// could look like using the warnings package:
 | 
						|
//
 | 
						|
//  import "gopkg.in/warnings.v0"
 | 
						|
//
 | 
						|
//  func isFatal(err error) bool {
 | 
						|
//      _, ok := err.(WarningType)
 | 
						|
//      return !ok
 | 
						|
//  }
 | 
						|
//
 | 
						|
//  func myfunc(params) error {
 | 
						|
//      c := warnings.NewCollector(isFatal)
 | 
						|
//      c.FatalWithWarnings = true
 | 
						|
//      if err := c.Collect(doSomething()); err != nil {
 | 
						|
//          return err
 | 
						|
//      }
 | 
						|
//      if err := c.Collect(doSomethingElse(...)); err != nil {
 | 
						|
//          return err
 | 
						|
//      }
 | 
						|
//      if ok := doAnotherThing(...); !ok {
 | 
						|
//          if err := c.Collect(errors.New("my error")); err != nil {
 | 
						|
//              return err
 | 
						|
//          }
 | 
						|
//      }
 | 
						|
//      ...
 | 
						|
//      return c.Done()
 | 
						|
//  }
 | 
						|
//
 | 
						|
// For an example of a non-trivial code base using this library, see
 | 
						|
// gopkg.in/gcfg.v1
 | 
						|
//
 | 
						|
// Rules for using warnings
 | 
						|
//
 | 
						|
//  - ensure that warnings are programmatically distinguishable from fatal
 | 
						|
//    errors (i.e. implement an isFatal function and any necessary error types)
 | 
						|
//  - ensure that there is a single Collector instance for a call of each
 | 
						|
//    exported function
 | 
						|
//  - ensure that all errors (fatal or warning) are fed through Collect
 | 
						|
//  - ensure that every time an error is returned, it is one returned by a
 | 
						|
//    Collector (from Collect or Done)
 | 
						|
//  - ensure that Collect is never called after Done
 | 
						|
//
 | 
						|
// TODO
 | 
						|
//
 | 
						|
//  - optionally limit the number of warnings (e.g. stop after 20 warnings) (?)
 | 
						|
//  - consider interaction with contexts
 | 
						|
//  - go vet-style invocations verifier
 | 
						|
//  - semi-automatic code converter
 | 
						|
//
 | 
						|
package warnings // import "gopkg.in/warnings.v0"
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
)
 | 
						|
 | 
						|
// List holds a collection of warnings and optionally one fatal error.
 | 
						|
type List struct {
 | 
						|
	Warnings []error
 | 
						|
	Fatal    error
 | 
						|
}
 | 
						|
 | 
						|
// Error implements the error interface.
 | 
						|
func (l List) Error() string {
 | 
						|
	b := bytes.NewBuffer(nil)
 | 
						|
	if l.Fatal != nil {
 | 
						|
		fmt.Fprintln(b, "fatal:")
 | 
						|
		fmt.Fprintln(b, l.Fatal)
 | 
						|
	}
 | 
						|
	switch len(l.Warnings) {
 | 
						|
	case 0:
 | 
						|
	// nop
 | 
						|
	case 1:
 | 
						|
		fmt.Fprintln(b, "warning:")
 | 
						|
	default:
 | 
						|
		fmt.Fprintln(b, "warnings:")
 | 
						|
	}
 | 
						|
	for _, err := range l.Warnings {
 | 
						|
		fmt.Fprintln(b, err)
 | 
						|
	}
 | 
						|
	return b.String()
 | 
						|
}
 | 
						|
 | 
						|
// A Collector collects errors up to the first fatal error.
 | 
						|
type Collector struct {
 | 
						|
	// IsFatal distinguishes between warnings and fatal errors.
 | 
						|
	IsFatal func(error) bool
 | 
						|
	// FatalWithWarnings set to true means that a fatal error is returned as
 | 
						|
	// a List together with all warnings so far. The default behavior is to
 | 
						|
	// only return the fatal error and discard any warnings that have been
 | 
						|
	// collected.
 | 
						|
	FatalWithWarnings bool
 | 
						|
 | 
						|
	l    List
 | 
						|
	done bool
 | 
						|
}
 | 
						|
 | 
						|
// NewCollector returns a new Collector; it uses isFatal to distinguish between
 | 
						|
// warnings and fatal errors.
 | 
						|
func NewCollector(isFatal func(error) bool) *Collector {
 | 
						|
	return &Collector{IsFatal: isFatal}
 | 
						|
}
 | 
						|
 | 
						|
// Collect collects a single error (warning or fatal). It returns nil if
 | 
						|
// collection can continue (only warnings so far), or otherwise the errors
 | 
						|
// collected. Collect mustn't be called after the first fatal error or after
 | 
						|
// Done has been called.
 | 
						|
func (c *Collector) Collect(err error) error {
 | 
						|
	if c.done {
 | 
						|
		panic("warnings.Collector already done")
 | 
						|
	}
 | 
						|
	if err == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if c.IsFatal(err) {
 | 
						|
		c.done = true
 | 
						|
		c.l.Fatal = err
 | 
						|
	} else {
 | 
						|
		c.l.Warnings = append(c.l.Warnings, err)
 | 
						|
	}
 | 
						|
	if c.l.Fatal != nil {
 | 
						|
		return c.erorr()
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Done ends collection and returns the collected error(s).
 | 
						|
func (c *Collector) Done() error {
 | 
						|
	c.done = true
 | 
						|
	return c.erorr()
 | 
						|
}
 | 
						|
 | 
						|
func (c *Collector) erorr() error {
 | 
						|
	if !c.FatalWithWarnings && c.l.Fatal != nil {
 | 
						|
		return c.l.Fatal
 | 
						|
	}
 | 
						|
	if c.l.Fatal == nil && len(c.l.Warnings) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	// Note that a single warning is also returned as a List. This is to make it
 | 
						|
	// easier to determine fatal-ness of the returned error.
 | 
						|
	return c.l
 | 
						|
}
 | 
						|
 | 
						|
// FatalOnly returns the fatal error, if any, **in an error returned by a
 | 
						|
// Collector**. It returns nil if and only if err is nil or err is a List
 | 
						|
// with err.Fatal == nil.
 | 
						|
func FatalOnly(err error) error {
 | 
						|
	l, ok := err.(List)
 | 
						|
	if !ok {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return l.Fatal
 | 
						|
}
 | 
						|
 | 
						|
// WarningsOnly returns the warnings **in an error returned by a Collector**.
 | 
						|
func WarningsOnly(err error) []error {
 | 
						|
	l, ok := err.(List)
 | 
						|
	if !ok {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	return l.Warnings
 | 
						|
}
 |