main
Raw Download raw file
  1// Package warnings implements error handling with non-fatal errors (warnings).
  2//
  3// A recurring pattern in Go programming is the following:
  4//
  5//  func myfunc(params) error {
  6//      if err := doSomething(...); err != nil {
  7//          return err
  8//      }
  9//      if err := doSomethingElse(...); err != nil {
 10//          return err
 11//      }
 12//      if ok := doAnotherThing(...); !ok {
 13//          return errors.New("my error")
 14//      }
 15//      ...
 16//      return nil
 17//  }
 18//
 19// This pattern allows interrupting the flow on any received error. But what if
 20// there are errors that should be noted but still not fatal, for which the flow
 21// should not be interrupted? Implementing such logic at each if statement would
 22// make the code complex and the flow much harder to follow.
 23//
 24// Package warnings provides the Collector type and a clean and simple pattern
 25// for achieving such logic. The Collector takes care of deciding when to break
 26// the flow and when to continue, collecting any non-fatal errors (warnings)
 27// along the way. The only requirement is that fatal and non-fatal errors can be
 28// distinguished programmatically; that is a function such as
 29//
 30//  IsFatal(error) bool
 31//
 32// must be implemented. The following is an example of what the above snippet
 33// could look like using the warnings package:
 34//
 35//  import "gopkg.in/warnings.v0"
 36//
 37//  func isFatal(err error) bool {
 38//      _, ok := err.(WarningType)
 39//      return !ok
 40//  }
 41//
 42//  func myfunc(params) error {
 43//      c := warnings.NewCollector(isFatal)
 44//      c.FatalWithWarnings = true
 45//      if err := c.Collect(doSomething()); err != nil {
 46//          return err
 47//      }
 48//      if err := c.Collect(doSomethingElse(...)); err != nil {
 49//          return err
 50//      }
 51//      if ok := doAnotherThing(...); !ok {
 52//          if err := c.Collect(errors.New("my error")); err != nil {
 53//              return err
 54//          }
 55//      }
 56//      ...
 57//      return c.Done()
 58//  }
 59//
 60// For an example of a non-trivial code base using this library, see
 61// gopkg.in/gcfg.v1
 62//
 63// Rules for using warnings
 64//
 65//  - ensure that warnings are programmatically distinguishable from fatal
 66//    errors (i.e. implement an isFatal function and any necessary error types)
 67//  - ensure that there is a single Collector instance for a call of each
 68//    exported function
 69//  - ensure that all errors (fatal or warning) are fed through Collect
 70//  - ensure that every time an error is returned, it is one returned by a
 71//    Collector (from Collect or Done)
 72//  - ensure that Collect is never called after Done
 73//
 74// TODO
 75//
 76//  - optionally limit the number of warnings (e.g. stop after 20 warnings) (?)
 77//  - consider interaction with contexts
 78//  - go vet-style invocations verifier
 79//  - semi-automatic code converter
 80//
 81package warnings // import "gopkg.in/warnings.v0"
 82
 83import (
 84	"bytes"
 85	"fmt"
 86)
 87
 88// List holds a collection of warnings and optionally one fatal error.
 89type List struct {
 90	Warnings []error
 91	Fatal    error
 92}
 93
 94// Error implements the error interface.
 95func (l List) Error() string {
 96	b := bytes.NewBuffer(nil)
 97	if l.Fatal != nil {
 98		fmt.Fprintln(b, "fatal:")
 99		fmt.Fprintln(b, l.Fatal)
100	}
101	switch len(l.Warnings) {
102	case 0:
103	// nop
104	case 1:
105		fmt.Fprintln(b, "warning:")
106	default:
107		fmt.Fprintln(b, "warnings:")
108	}
109	for _, err := range l.Warnings {
110		fmt.Fprintln(b, err)
111	}
112	return b.String()
113}
114
115// A Collector collects errors up to the first fatal error.
116type Collector struct {
117	// IsFatal distinguishes between warnings and fatal errors.
118	IsFatal func(error) bool
119	// FatalWithWarnings set to true means that a fatal error is returned as
120	// a List together with all warnings so far. The default behavior is to
121	// only return the fatal error and discard any warnings that have been
122	// collected.
123	FatalWithWarnings bool
124
125	l    List
126	done bool
127}
128
129// NewCollector returns a new Collector; it uses isFatal to distinguish between
130// warnings and fatal errors.
131func NewCollector(isFatal func(error) bool) *Collector {
132	return &Collector{IsFatal: isFatal}
133}
134
135// Collect collects a single error (warning or fatal). It returns nil if
136// collection can continue (only warnings so far), or otherwise the errors
137// collected. Collect mustn't be called after the first fatal error or after
138// Done has been called.
139func (c *Collector) Collect(err error) error {
140	if c.done {
141		panic("warnings.Collector already done")
142	}
143	if err == nil {
144		return nil
145	}
146	if c.IsFatal(err) {
147		c.done = true
148		c.l.Fatal = err
149	} else {
150		c.l.Warnings = append(c.l.Warnings, err)
151	}
152	if c.l.Fatal != nil {
153		return c.erorr()
154	}
155	return nil
156}
157
158// Done ends collection and returns the collected error(s).
159func (c *Collector) Done() error {
160	c.done = true
161	return c.erorr()
162}
163
164func (c *Collector) erorr() error {
165	if !c.FatalWithWarnings && c.l.Fatal != nil {
166		return c.l.Fatal
167	}
168	if c.l.Fatal == nil && len(c.l.Warnings) == 0 {
169		return nil
170	}
171	// Note that a single warning is also returned as a List. This is to make it
172	// easier to determine fatal-ness of the returned error.
173	return c.l
174}
175
176// FatalOnly returns the fatal error, if any, **in an error returned by a
177// Collector**. It returns nil if and only if err is nil or err is a List
178// with err.Fatal == nil.
179func FatalOnly(err error) error {
180	l, ok := err.(List)
181	if !ok {
182		return err
183	}
184	return l.Fatal
185}
186
187// WarningsOnly returns the warnings **in an error returned by a Collector**.
188func WarningsOnly(err error) []error {
189	l, ok := err.(List)
190	if !ok {
191		return nil
192	}
193	return l.Warnings
194}