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