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