api-client-go/error.go
Neur0toxine 97b1abf470
Correct v2 (#55)
* refactor library and upgrade version
* remove useless environment variable
* remove unsupported versions from go.mod
* fixes for error handling & data types
* different improvements for errors
* fixes for types and tests
* better coverage, fix error with the unmarshalers
2021-10-27 15:49:06 +03:00

193 lines
5.2 KiB
Go

package retailcrm
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
var missingParameterMatcher = regexp.MustCompile(`^Parameter \'([\w\]\[\_\-]+)\' is missing$`)
var (
// ErrMissingCredentials will be returned if no API key was provided to the API.
ErrMissingCredentials = NewAPIError(`apiKey is missing`)
// ErrInvalidCredentials will be returned if provided API key is invalid.
ErrInvalidCredentials = NewAPIError(`wrong "apiKey" value`)
// ErrAccessDenied will be returned in case of "Access denied" error.
ErrAccessDenied = NewAPIError("access denied")
// ErrAccountDoesNotExist will be returned if target system does not exist.
ErrAccountDoesNotExist = NewAPIError("account does not exist")
// ErrValidation will be returned in case of validation errors.
ErrValidation = NewAPIError("validation error")
// ErrMissingParameter will be returned if parameter is missing.
// Underlying error messages list will contain parameter name in the "Name" key.
ErrMissingParameter = NewAPIError("missing parameter")
// ErrGeneric will be returned if error cannot be classified as one of the errors above.
ErrGeneric = NewAPIError("API error")
)
// APIErrorsList struct.
type APIErrorsList map[string]string
// APIError returns when an API error was occurred.
type APIError interface {
error
fmt.Stringer
withWrapped(error) APIError
withErrors(APIErrorsList) APIError
Unwrap() error
Errors() APIErrorsList
}
type apiError struct {
ErrorMsg string `json:"errorMsg,omitempty"`
ErrorsList APIErrorsList `json:"errors,omitempty"`
wrapped error
}
// CreateAPIError from the provided response data. Different error types will be returned depending on the response,
// all of them can be matched using errors.Is. APi errors will always implement APIError interface.
func CreateAPIError(dataResponse []byte) error {
a := &apiError{}
if len(dataResponse) > 0 && dataResponse[0] == '<' {
return ErrAccountDoesNotExist
}
if err := json.Unmarshal(dataResponse, &a); err != nil {
return err
}
var found APIError
switch a.ErrorMsg {
case `"apiKey" is missing.`:
found = ErrMissingCredentials
case `Wrong "apiKey" value.`:
found = ErrInvalidCredentials
case "Access denied.":
found = ErrAccessDenied
case "Account does not exist.":
found = ErrAccountDoesNotExist
case "Errors in the entity format":
fallthrough
case "Validation error":
found = ErrValidation
default:
if param, ok := asMissingParameterErr(a.ErrorMsg); ok {
return a.withWrapped(ErrMissingParameter).withErrors(APIErrorsList{"Name": param})
}
found = ErrGeneric
}
result := NewAPIError(a.ErrorMsg).withWrapped(found)
if len(a.ErrorsList) > 0 {
return result.withErrors(a.ErrorsList)
}
return result
}
// CreateGenericAPIError for the situations when API response cannot be processed, but response was actually received.
func CreateGenericAPIError(message string) APIError {
return NewAPIError(message).withWrapped(ErrGeneric)
}
// NewAPIError returns API error with the provided message.
func NewAPIError(message string) APIError {
return &apiError{ErrorMsg: message}
}
// AsAPIError returns APIError and true if provided error is an APIError or contains wrapped APIError.
// Returns (nil, false) otherwise.
func AsAPIError(err error) (APIError, bool) {
apiErr := unwrapAPIError(err)
return apiErr, apiErr != nil
}
func unwrapAPIError(err error) APIError {
if err == nil {
return nil
}
if apiErr, ok := err.(APIError); ok { // nolint:errorlint
return apiErr
}
wrapper, ok := err.(interface { // nolint:errorlint
Unwrap() error
})
if ok {
return unwrapAPIError(wrapper.Unwrap())
}
return nil
}
// asMissingParameterErr returns true if "Parameter 'name' is missing" error message is provided.
func asMissingParameterErr(message string) (string, bool) {
matches := missingParameterMatcher.FindAllStringSubmatch(message, -1)
if len(matches) == 1 && len(matches[0]) == 2 {
return matches[0][1], true
}
return "", false
}
// Error returns errorMsg field from the response.
func (e *apiError) Error() string {
return e.ErrorMsg
}
// Unwrap returns wrapped error. It is usually one of the predefined types like ErrGeneric or ErrValidation.
// It can be used directly, but it's main purpose is to make errors matchable via errors.Is call.
func (e *apiError) Unwrap() error {
return e.wrapped
}
// Errors returns errors field from the response.
func (e *apiError) Errors() APIErrorsList {
return e.ErrorsList
}
// String returns string representation of an APIError.
func (e *apiError) String() string {
var sb strings.Builder
sb.Grow(256) // nolint:gomnd
sb.WriteString(fmt.Sprintf(`errorMsg: "%s"`, e.Error()))
if len(e.Errors()) > 0 {
i := 0
useIndex := true
errorList := make([]string, len(e.Errors()))
for index, errText := range e.Errors() {
if i == 0 && index == "0" {
useIndex = false
}
if useIndex {
errorList[i] = fmt.Sprintf(`%s: "%s"`, index, errText)
} else {
errorList[i] = errText
}
i++
}
sb.WriteString(", errors: [" + strings.Join(errorList, ", ") + "]")
}
return sb.String()
}
// withWrapped is a wrapped setter.
func (e *apiError) withWrapped(err error) APIError {
e.wrapped = err
return e
}
// withErrors is an ErrorsList setter.
func (e *apiError) withErrors(m APIErrorsList) APIError {
e.ErrorsList = m
return e
}