seinshah/gerrors

View on GitHub
gerror.go

Summary

Maintainability
A
35 mins
Test Coverage
package gerrors

import (
    "bytes"
    "errors"
    "fmt"
    "strconv"
    "strings"

    "google.golang.org/genproto/googleapis/rpc/errdetails"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

const (
    // MetadataIdentifier is the ket for accessing the error identifier.
    MetadataIdentifier = "_identifier"

    // MetadataErrorCode is the key for accessing the gerrors internal
    // or customized error code.
    MetadataErrorCode = "_error_code"

    // MetadataDefaultMessage is the key for accessing the default gerrors core
    // error message.
    MetadataDefaultMessage = "_default_message"

    // MetadataOriginalError is the key for accessing the original error
    // which was used during initializing GeneralError.
    MetadataOriginalError = "_original_error"
)

// GeneralError is the error type defined, controlled, and handled by gerrors package.
type GeneralError struct {
    originalError error
    coreError     CoreError
    formatter     *Formatter
    details       *errdetails.ErrorInfo
}

// tplData is used to populate each error's information and then parse the template.
type tplData struct {
    Identifier     string
    ErrorCode      string
    GrpcErrorCode  string
    Message        string
    DefaultMessage string
    Labels         map[string]string
}

// errNoOriginalError is set as the input error whenever there is no original error.
var errNoOriginalError = errors.New("no original error")

// GrpcError accepts an error of gerrors.GeneralError type and returns a gRPC error
// by translating the error to gRPC error and attach all labels as the metadata.
// It supports [Google's AIP 193].
// If the input is not of [GeneralError] type, it smply returns a gRPC error
// with [google.golang.org/grpc/codes.Unknown] error code with the input error
// message as the message.
// If the receiver is receiving the error in gRPC error format, you can check
// [this blog post] on how to parse the error and extract the information from it.
//
// [Google's AIP 193]: https://google.aip.dev/193
// [this blog post]: https://jbrandhorst.com/post/grpc-errors
func GrpcError(err error) error {
    var finalErr *GeneralError

    if !errors.As(err, &finalErr) {
        return status.Error(codes.Unknown, err.Error())
    }

    grpcErr, ok := finalErr.coreError.(CoreGRPCError)

    if !ok {
        return status.Error(codes.Unknown, finalErr.Error())
    }

    st := status.New(grpcErr.GetGRPCCode(), finalErr.Error())

    finalStatus, err := st.WithDetails(finalErr.details)
    if err != nil {
        finalStatus = st
    }

    return finalStatus.Err()
}

// New creates a new [GeneralError] instance using the provided formatter.
// inputErr is the error that is triggered prior to the creation of the error
// and it can be nil. If it's nil, the final error message will be the code's
// default message.
// Any new error can have a list of key values as the metadata. These key values
// will be appended to the formatter's default labels.
// If the formatter has a logger, it will also log the error at Error level.
func (f *Formatter) New(inputErr error, code Code, metadataKeyValues ...any) *GeneralError {
    err := f.createError(inputErr, code, metadataKeyValues...)

    err.log(f.logger, LogLevelError, err.MetadataSlice())

    return err
}

// NewWithLogLevel is the same as New, but it allows the caller to control the log level.
// If the logger is not provided to the formatter, this method is exactly the same as New
// where logging will be ignored.
func (f *Formatter) NewWithLogLevel(
    inputErr error,
    code Code,
    level LogLevel,
    metadataKeyValues ...any,
) *GeneralError {
    err := f.createError(inputErr, code, metadataKeyValues...)

    err.log(f.logger, level, err.MetadataSlice())

    return err
}

func (f *Formatter) createError(inputErr error, code Code, metadataKeyValues ...any) *GeneralError {
    if inputErr == nil {
        inputErr = errNoOriginalError
    }

    err := &GeneralError{
        originalError: inputErr,
        coreError:     f.coreDataLookup.Lookup(code),
        formatter:     f,
        details:       nil,
    }

    err.generateDetails(metadataKeyValues, f.labels)

    return err
}

// Error allows GeneralError to implement the error interface.
// It uses the formatter template and different information of the GeneralError
// to generate the error message.
func (ge *GeneralError) Error() string {
    var buf bytes.Buffer

    data := ge.getTemplateData()

    if err := ge.formatter.template.Execute(&buf, data); err != nil {
        return fmt.Sprintf("failed to execute template: %s (original error: %s)", err.Error(), data.Message)
    }

    return buf.String()
}

// Metadata returns all the combined labels of the given GeneralError.
func (ge *GeneralError) Metadata() map[string]string {
    return ge.details.GetMetadata()
}

// Grpc is the method defined on GeneralError which returns the gRPC error.
// See Grpc function for more details.
func (ge *GeneralError) Grpc() error {
    return GrpcError(ge)
}

func (ge *GeneralError) generateDetails(metadataKeyValues []any, defaultLabels map[string]string) {
    metadata := make(map[string]string)

    for k, v := range defaultLabels {
        metadata[k] = v
    }

    metadata[MetadataIdentifier] = ge.coreError.GetIdentifier()
    metadata[MetadataErrorCode] = strconv.Itoa(int(ge.coreError.GetInternalCode()))
    metadata[MetadataDefaultMessage] = ge.coreError.GetDefaultMessage()
    metadata[MetadataOriginalError] = ge.originalError.Error()

    for index := 0; index < len(metadataKeyValues); index += 2 {
        key, val, ok := ge.formatter.getStringifiedKeyValue(metadataKeyValues, index)
        if !ok {
            continue
        }

        metadata[key] = val
    }

    ge.details = &errdetails.ErrorInfo{
        Reason:   strings.ReplaceAll(strings.ToUpper(ge.coreError.GetIdentifier()), " ", "_"),
        Metadata: metadata,
    }
}

func (ge *GeneralError) getTemplateData() tplData {
    msg := ge.coreError.GetDefaultMessage()
    if !errors.Is(ge.originalError, errNoOriginalError) {
        msg = ge.originalError.Error()
    }

    var grpcCode string

    coreg, ok := ge.coreError.(CoreGRPCError)
    if ok {
        grpcCode = strconv.Itoa(int(coreg.GetGRPCCode()))
    }

    return tplData{
        Identifier:     ge.coreError.GetIdentifier(),
        ErrorCode:      strconv.Itoa(int(ge.coreError.GetInternalCode())),
        GrpcErrorCode:  grpcCode,
        Message:        msg,
        DefaultMessage: ge.coreError.GetDefaultMessage(),
        Labels:         ge.details.GetMetadata(),
    }
}

func (ge *GeneralError) log(logger logger, level LogLevel, metadata []any) {
    if logger == nil || level == LogLevelOff {
        return
    }

    // nolint: exhaustive
    switch level {
    case LogLevelTrace:
        l, ok := logger.(traceLogger)
        if !ok {
            return
        }

        l.Trace(ge.Error(), metadata...)
    case LogLevelDebug:
        l, ok := logger.(debugLogger)
        if !ok {
            return
        }

        l.Debug(ge.Error(), metadata...)
    case LogLevelInfo:
        l, ok := logger.(infoLogger)
        if !ok {
            return
        }

        l.Info(ge.Error(), metadata...)
    case LogLevelWarn:
        l, ok := logger.(warnLogger)
        if !ok {
            return
        }

        l.Warn(ge.Error(), metadata...)
    default:
        logger.Error(
            ge.originalError,
            ge.Error(),
            metadata...,
        )
    }
}

func (ge *GeneralError) MetadataSlice() []any {
    s := make([]any, len(ge.details.GetMetadata())*2)
    index := 0

    for k, v := range ge.details.GetMetadata() {
        s[index], s[index+1] = k, v
        index += 2
    }

    return s
}