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// Rules for using warnings
61//
62//  - ensure that warnings are programmatically distinguishable from fatal
63//    errors (i.e. implement an isFatal function and any necessary error types)
64//  - ensure that there is a single Collector instance for a call of each
65//    exported function
66//  - ensure that all errors (fatal or warning) are fed through Collect
67//  - ensure that every time an error is returned, it is one returned by a
68//    Collector (from Collect or Done)
69//  - ensure that Collect is never called after Done
70//
71// TODO
72//
73//  - optionally limit the number of warnings (e.g. stop after 20 warnings) (?)
74//  - consider interaction with contexts
75//  - go vet-style invocations verifier
76//  - semi-automatic code converter
77//
78package warnings // import "gopkg.in/warnings.v0"
79
80import (
81	"bytes"
82	"fmt"
83)
84
85// List holds a collection of warnings and optionally one fatal error.
86type List struct {
87	Warnings []error
88	Fatal    error
89}
90
91// Error implements the error interface.
92func (l List) Error() string {
93	b := bytes.NewBuffer(nil)
94	if l.Fatal != nil {
95		fmt.Fprintln(b, "fatal:")
96		fmt.Fprintln(b, l.Fatal)
97	}
98	switch len(l.Warnings) {
99	case 0:
100	// nop
101	case 1:
102		fmt.Fprintln(b, "warning:")
103	default:
104		fmt.Fprintln(b, "warnings:")
105	}
106	for _, err := range l.Warnings {
107		fmt.Fprintln(b, err)
108	}
109	return b.String()
110}
111
112// A Collector collects errors up to the first fatal error.
113type Collector struct {
114	// IsFatal distinguishes between warnings and fatal errors.
115	IsFatal func(error) bool
116	// FatalWithWarnings set to true means that a fatal error is returned as
117	// a List together with all warnings so far. The default behavior is to
118	// only return the fatal error and discard any warnings that have been
119	// collected.
120	FatalWithWarnings bool
121
122	l    List
123	done bool
124}
125
126// NewCollector returns a new Collector; it uses isFatal to distinguish between
127// warnings and fatal errors.
128func NewCollector(isFatal func(error) bool) *Collector {
129	return &Collector{IsFatal: isFatal}
130}
131
132// Collect collects a single error (warning or fatal). It returns nil if
133// collection can continue (only warnings so far), or otherwise the errors
134// collected. Collect mustn't be called after the first fatal error or after
135// Done has been called.
136func (c *Collector) Collect(err error) error {
137	if c.done {
138		panic("warnings.Collector already done")
139	}
140	if err == nil {
141		return nil
142	}
143	if c.IsFatal(err) {
144		c.done = true
145		c.l.Fatal = err
146	} else {
147		c.l.Warnings = append(c.l.Warnings, err)
148	}
149	if c.l.Fatal != nil {
150		return c.erorr()
151	}
152	return nil
153}
154
155// Done ends collection and returns the collected error(s).
156func (c *Collector) Done() error {
157	c.done = true
158	return c.erorr()
159}
160
161func (c *Collector) erorr() error {
162	if !c.FatalWithWarnings && c.l.Fatal != nil {
163		return c.l.Fatal
164	}
165	if c.l.Fatal == nil && len(c.l.Warnings) == 0 {
166		return nil
167	}
168	// Note that a single warning is also returned as a List. This is to make it
169	// easier to determine fatal-ness of the returned error.
170	return c.l
171}
172
173// FatalOnly returns the fatal error, if any, **in an error returned by a
174// Collector**. It returns nil if and only if err is nil or err is a List
175// with err.Fatal == nil.
176func FatalOnly(err error) error {
177	l, ok := err.(List)
178	if !ok {
179		return err
180	}
181	return l.Fatal
182}
183
184// WarningsOnly returns the warnings **in an error returned by a Collector**.
185func WarningsOnly(err error) []error {
186	l, ok := err.(List)
187	if !ok {
188		return nil
189	}
190	return l.Warnings
191}
192