// Copyright 2023 Harness, Inc. // // 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 gitrpc import ( "errors" "fmt" "github.com/harness/gitness/gitrpc/rpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var ( ErrNoParamsProvided = ErrInvalidArgumentf("params not provided") ) const ( conflictFilesKey = "conflict_files" pathKey = "path" ) type Status string const ( StatusConflict Status = "conflict" StatusInternal Status = "internal" StatusInvalidArgument Status = "invalid" StatusNotFound Status = "not_found" StatusPathNotFound Status = "path_not_found" StatusNotImplemented Status = "not_implemented" StatusUnauthorized Status = "unauthorized" StatusFailed Status = "failed" StatusPreconditionFailed Status = "precondition_failed" StatusNotMergeable Status = "not_mergeable" StatusAborted Status = "aborted" ) type Error struct { // Machine-readable status code. Status Status // Human-readable error message. Message string // Details Details map[string]any } // Error implements the error interface. func (e *Error) Error() string { return e.Message } // ErrorStatus unwraps an gitrpc error and returns its code. // Non-application errors always return StatusInternal. func ErrorStatus(err error) Status { var ( e *Error ) if err == nil { return "" } if errors.As(err, &e) { return e.Status } return StatusInternal } // ErrorMessage unwraps an gitrpc error and returns its message. // Non-gitrpc errors always return "Internal error". func ErrorMessage(err error) string { var ( e *Error ) if err == nil { return "" } if errors.As(err, &e) { return e.Message } return "Internal error." } // ErrorDetails unwraps an gitrpc error and returns its details. // Non-gitrpc errors always return nil. func ErrorDetails(err error) map[string]any { var ( e *Error ) if err == nil { return nil } if errors.As(err, &e) { return e.Details } return nil } // NewError is a factory function to return an Error with a given status and message. func NewError(code Status, message string) *Error { return &Error{ Status: code, Message: message, } } // NewError is a factory function to return an Error with a given status, message and details. func NewErrorWithDetails(code Status, message string, details map[string]any) *Error { err := NewError(code, message) err.Details = details return err } // Errorf is a helper function to return an Error with a given status and formatted message. func Errorf(code Status, format string, args ...interface{}) *Error { return &Error{ Status: code, Message: fmt.Sprintf(format, args...), } } // ErrInvalidArgumentf is a helper function to return an invalid argument Error. func ErrInvalidArgumentf(format string, args ...interface{}) *Error { return Errorf(StatusInvalidArgument, format, args...) } func processRPCErrorf(err error, format string, args ...interface{}) error { if errors.Is(err, &Error{}) { return err } // create fallback error returned if we can't map it fallbackMsg := fmt.Sprintf(format, args...) fallbackErr := NewError(StatusInternal, fallbackMsg) // ensure it's an rpc error st, ok := status.FromError(err) if !ok { return fallbackErr } msg := st.Message() switch { case st.Code() == codes.AlreadyExists: return NewError(StatusConflict, msg) case st.Code() == codes.NotFound: code := StatusNotFound details := make(map[string]any) for _, detail := range st.Details() { switch t := detail.(type) { case *rpc.PathNotFoundError: code = StatusPathNotFound details[pathKey] = t.Path default: } } if len(details) > 0 { return NewErrorWithDetails(code, msg, details) } return NewError(code, msg) case st.Code() == codes.InvalidArgument: return NewError(StatusInvalidArgument, msg) case st.Code() == codes.FailedPrecondition: code := StatusPreconditionFailed details := make(map[string]any) for _, detail := range st.Details() { switch t := detail.(type) { case *rpc.MergeConflictError: details[conflictFilesKey] = t.ConflictingFiles code = StatusNotMergeable default: } } if len(details) > 0 { return NewErrorWithDetails(code, msg, details) } return NewError(code, msg) default: return fallbackErr } } func AsConflictFilesError(err error) (files []string) { details := ErrorDetails(err) object, ok := details[conflictFilesKey] if ok { files, _ = object.([]string) } return } // AsPathNotFoundError returns the path that wasn't found in case that's the error. func AsPathNotFoundError(err error) (path string) { details := ErrorDetails(err) object, ok := details[pathKey] if ok { path, _ = object.(string) } return }