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