// Source: https://github.com/distribution/distribution // Copyright 2014 https://github.com/distribution/distribution Authors // // Licensed 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 errcode import ( "encoding/json" "fmt" "strings" ) // ErrorCoder is the base interface for ErrorCode and Error allowing // users of each to just call ErrorCode to get the real ID of each. type ErrorCoder interface { ErrorCode() CodeError } // CodeError represents the error type. The errors are serialized via strings // and the integer format may change and should *never* be exported. type CodeError int var _ error = CodeError(0) // ErrorCode just returns itself. func (ec CodeError) ErrorCode() CodeError { return ec } // Error returns the ID/Value. func (ec CodeError) Error() string { return strings.ToLower(strings.ReplaceAll(ec.String(), "_", " ")) } // Descriptor returns the descriptor for the error code. func (ec CodeError) Descriptor() ErrorDescriptor { d, ok := errorCodeToDescriptors[ec] if !ok { return ErrCodeUnknown.Descriptor() } return d } // String returns the canonical identifier for this error code. func (ec CodeError) String() string { return ec.Descriptor().Value } // Message returned the human-readable error message for this error code. func (ec CodeError) Message() string { return ec.Descriptor().Message } // MarshalText encodes the receiver into UTF-8-encoded text and returns the // result. func (ec CodeError) MarshalText() (text []byte, err error) { return []byte(ec.String()), nil } // UnmarshalText decodes the form generated by MarshalText. func (ec *CodeError) UnmarshalText(text []byte) error { desc, ok := idToDescriptors[string(text)] if !ok { desc = ErrCodeUnknown.Descriptor() } *ec = desc.Code return nil } // WithMessage creates a new Error struct based on the passed-in info and // overrides the Message property. func (ec CodeError) WithMessage(message string) Error { return Error{ Code: ec, Message: message, } } // WithDetail creates a new Error struct based on the passed-in info and // set the Detail property appropriately. func (ec CodeError) WithDetail(detail interface{}) Error { return Error{ Code: ec, Message: ec.Message(), }.WithDetail(detail) } // WithArgs creates a new Error struct and sets the Args slice. func (ec CodeError) WithArgs(args ...interface{}) Error { return Error{ Code: ec, Message: ec.Message(), }.WithArgs(args...) } // Error provides a wrapper around ErrorCode with extra Details provided. type Error struct { Code CodeError `json:"code"` Message string `json:"message"` Detail interface{} `json:"detail,omitempty"` } var _ error = Error{} // ErrorCode returns the ID/Value of this Error. func (e Error) ErrorCode() CodeError { return e.Code } // Error returns a human readable representation of the error. func (e Error) Error() string { return fmt.Sprintf("%s: %s", e.Code.Error(), e.Message) } // WithDetail will return a new Error, based on the current one, but with // some Detail info added. func (e Error) WithDetail(detail interface{}) Error { return Error{ Code: e.Code, Message: e.Message, Detail: detail, } } // WithArgs uses the passed-in list of interface{} as the substitution // variables in the Error's Message string, but returns a new Error. func (e Error) WithArgs(args ...interface{}) Error { return Error{ Code: e.Code, Message: fmt.Sprintf(e.Code.Message(), args...), Detail: e.Detail, } } // ErrorDescriptor provides relevant information about a given error code. type ErrorDescriptor struct { // Code is the error code that this descriptor describes. Code CodeError // Value provides a unique, string key, often captilized with // underscores, to identify the error code. This value is used as the // keyed value when serializing api errors. Value string // Message is a short, human readable description of the error condition // included in API responses. Message string // Description provides a complete account of the errors purpose, suitable // for use in documentation. Description string // HTTPStatusCode provides the http status code that is associated with // this error condition. HTTPStatusCode int } // ParseErrorCode returns the value by the string error code. // `ErrorCodeUnknown` will be returned if the error is not known. func ParseErrorCode(value string) CodeError { ed, ok := idToDescriptors[value] if ok { return ed.Code } return ErrCodeUnknown } // Errors provides the envelope for multiple errors and a few sugar methods // for use within the application. type Errors []error var _ error = Errors{} func (errs Errors) Error() string { switch len(errs) { case 0: return "" case 1: return errs[0].Error() default: msg := "errors:\n" for _, err := range errs { msg += err.Error() + "\n" } return msg } } // Len returns the current number of errors. func (errs Errors) Len() int { return len(errs) } // MarshalJSON converts slice of error, ErrorCode or Error into a // slice of Error - then serializes. func (errs Errors) MarshalJSON() ([]byte, error) { var tmpErrs struct { Errors []Error `json:"errors,omitempty"` } for _, daErr := range errs { var err Error switch daErr := daErr.(type) { case CodeError: err = daErr.WithDetail(nil) case Error: err = daErr default: err = ErrCodeUnknown.WithDetail(daErr) } // If the Error struct was setup and they forgot to set the // Message field (meaning its "") then grab it from the ErrCode msg := err.Message if msg == "" { msg = err.Code.Message() } tmpErr := Error{ Code: err.Code, Message: msg, Detail: err.Detail, } // if the detail contains error extract the error message // otherwise json.Marshal will not serialize it at all // https://github.com/golang/go/issues/10748 if detail, ok := tmpErr.Detail.(error); ok { tmpErr.Detail = detail.Error() } tmpErrs.Errors = append(tmpErrs.Errors, tmpErr) } return json.Marshal(tmpErrs) } // UnmarshalJSON deserializes []Error and then converts it into slice of // Error or ErrorCode. func (errs *Errors) UnmarshalJSON(data []byte) error { var tmpErrs struct { Errors []Error } if err := json.Unmarshal(data, &tmpErrs); err != nil { return err } var newErrs Errors for _, daErr := range tmpErrs.Errors { // If Message is empty or exactly matches the Code's message string // then just use the Code, no need for a full Error struct if daErr.Detail == nil && (daErr.Message == "" || daErr.Message == daErr.Code.Message()) { // Error's w/o details get converted to ErrorCode newErrs = append(newErrs, daErr.Code) } else { // Error's w/ details are untouched newErrs = append( newErrs, Error{ Code: daErr.Code, Message: daErr.Message, Detail: daErr.Detail, }, ) } } *errs = newErrs return nil }