pkg/multierror/prefixed.go (112 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package multierror import ( "errors" "fmt" "runtime" "sort" "sync" "github.com/hashicorp/go-multierror" ) type asMulti interface { Multierror() *Prefixed } // FormatFunc defines a format function which should format a slice of errors // into a string. type FormatFunc func(es []error) string // Prefixed is a multierror which will prefix the error output message with the // specified prefix. The output will be sorted so the errors can be asserted on // test functions in case the multierror has been used in different goroutines. // This structure is concurrently safe, avoid using value semantics. type Prefixed struct { Prefix string Errors []error FormatFunc FormatFunc // SkipPrefixing when set to true subsequently appended errors needn't be // prefixed. This is particularly useful multierrors which contain JSON // marshaleable errors. SkipPrefixing bool mu sync.RWMutex } // NewPrefixed creates a new pointer to Prefixed. func NewPrefixed(prefix string, errs ...error) *Prefixed { return &Prefixed{Prefix: prefix, Errors: unpackErrors(prefix, errs...)} } // NewJSONPrefixed returns a new pointer to Prefixed with the right config for // JSON packed errors to not be prefixed. func NewJSONPrefixed(prefix string, errs ...error) *Prefixed { return &Prefixed{ Prefix: prefix, Errors: unpackErrors(prefix, errs...), SkipPrefixing: true, FormatFunc: JSONFormatFunc, } } // Append appends a number of errors to the current instance of Prefixed. It'll // unwrap any wrapped errors in the form of *Prefixed or *multierror.Error. func (p *Prefixed) Append(errs ...error) *Prefixed { defer p.mu.Unlock() p.mu.Lock() p.Errors = append(p.Errors, unpackErrors(p.Prefix, errs...)...) return p } // ErrorOrNil either returns nil when the type is nil or when there's no Errors. // Otherwise, the type is returned. func (p *Prefixed) ErrorOrNil() error { defer p.mu.RUnlock() p.mu.RLock() if len(p.Errors) > 0 { return p } return nil } // Error returns the stored slice of error formatted using a set FormatFunc or // multierror.ListFormatFunc when no FormatFunc is specified. func (p *Prefixed) Error() string { defer p.mu.Unlock() p.mu.Lock() if len(p.Errors) == 0 { return "" } // Sort the errors so there's some consistency on the output. sort.SliceStable(p.Errors, func(i, j int) bool { return p.Errors[i].Error() < p.Errors[j].Error() }) if p.FormatFunc == nil { p.FormatFunc = wrapPrefix(p.Prefix, multierror.ListFormatFunc) } return p.FormatFunc(p.Errors) } func wrapPrefix(prefix string, f FormatFunc) FormatFunc { return func(es []error) string { return fmt.Sprint(prefix, ": ", f(es)) } } func unpackErrors(prefix string, errs ...error) []error { result := make([]error, 0, len(errs)) for _, err := range errs { if err == nil { continue } result = append(result, unpack(prefix, err)...) } return result } func unpack(prefix string, err error) []error { // Handles wrapped apierror.Error, this intermediary interface is // needed since the apierror package imports multierror.Prefixed. if m, ok := err.(asMulti); ok { if e := m.Multierror(); e != nil { return handleNestedPrefixed(prefix, e) } } var e *Prefixed if errors.As(err, &e) { return handleNestedPrefixed(prefix, e) } var hashiErr *multierror.Error if errors.As(err, &hashiErr) { return hashiErr.Errors } return []error{err} } func handleNestedPrefixed(prefix string, e *Prefixed) []error { if prefix == e.Prefix || e.SkipPrefixing { return e.Errors } return prefixIndividualErrors(e) } func prefixIndividualErrors(prefixed *Prefixed) []error { var result = make([]error, 0, len(prefixed.Errors)) for _, errElement := range prefixed.Errors { if prefixed.Prefix == "" { // Calling it with skip 3 since there's 3 internal calls which must // be skipped until the external call can surface. if pc, _, _, ok := runtime.Caller(3); ok { if details := runtime.FuncForPC(pc); details != nil { prefixed.Prefix = details.Name() } } } result = append(result, errors.New( fmt.Sprint(prefixed.Prefix, ": ", errElement.Error()), )) } return result }