github.com/blend/go-sdk@v1.20240719.1/ex/ex.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package ex
     9  
    10  import (
    11  	"bytes"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  )
    16  
    17  var (
    18  	_ error          = (*Ex)(nil)
    19  	_ fmt.Formatter  = (*Ex)(nil)
    20  	_ json.Marshaler = (*Ex)(nil)
    21  )
    22  
    23  // New returns a new exception with a call stack.
    24  // Pragma: this violates the rule that you should take interfaces and return
    25  // concrete types intentionally; it is important for the semantics of typed pointers and nil
    26  // for this to return an interface because (*Ex)(nil) != nil, but (error)(nil) == nil.
    27  func New(class interface{}, options ...Option) Exception {
    28  	return NewWithStackDepth(class, DefaultNewStartDepth, options...)
    29  }
    30  
    31  // NewWithStackDepth creates a new exception with a given start point of the stack.
    32  func NewWithStackDepth(class interface{}, startDepth int, options ...Option) Exception {
    33  	if class == nil {
    34  		return nil
    35  	}
    36  
    37  	var ex *Ex
    38  	switch typed := class.(type) {
    39  	case *Ex:
    40  		if typed == nil {
    41  			return nil
    42  		}
    43  		ex = typed
    44  	case error:
    45  		if typed == nil {
    46  			return nil
    47  		}
    48  
    49  		ex = &Ex{
    50  			Class:      typed,
    51  			Inner:      errors.Unwrap(typed),
    52  			StackTrace: Callers(startDepth),
    53  		}
    54  	case string:
    55  		ex = &Ex{
    56  			Class:      Class(typed),
    57  			StackTrace: Callers(startDepth),
    58  		}
    59  	default:
    60  		ex = &Ex{
    61  			Class:      Class(fmt.Sprint(class)),
    62  			StackTrace: Callers(startDepth),
    63  		}
    64  	}
    65  	for _, option := range options {
    66  		option(ex)
    67  	}
    68  	return ex
    69  }
    70  
    71  // Ex is an error with a stack trace.
    72  // It also can have an optional cause, it implements `Exception`
    73  type Ex struct {
    74  	// Class disambiguates between errors, it can be used to identify the type of the error.
    75  	Class error
    76  	// Message adds further detail to the error, and shouldn't be used for disambiguation.
    77  	Message string
    78  	// Inner holds the original error in cases where we're wrapping an error with a stack trace.
    79  	Inner error
    80  	// StackTrace is the call stack frames used to create the stack output.
    81  	StackTrace StackTrace
    82  }
    83  
    84  // WithMessage sets the exception message.
    85  // Deprecation notice: This method is included as a migraition path from v2, and will be removed after v3.
    86  func (e *Ex) WithMessage(args ...interface{}) Exception {
    87  	e.Message = fmt.Sprint(args...)
    88  	return e
    89  }
    90  
    91  // WithMessagef sets the exception message based on a format and arguments.
    92  // Deprecation notice: This method is included as a migration path from v2, and will be removed after v3.
    93  func (e *Ex) WithMessagef(format string, args ...interface{}) Exception {
    94  	e.Message = fmt.Sprintf(format, args...)
    95  	return e
    96  }
    97  
    98  // WithInner sets the inner ex.
    99  // Deprecation notice: This method is included as a migraition path from v2, and will be removed after v3.
   100  func (e *Ex) WithInner(err error) Exception {
   101  	e.Inner = NewWithStackDepth(err, DefaultNewStartDepth)
   102  	return e
   103  }
   104  
   105  // Format allows for conditional expansion in printf statements
   106  // based on the token and flags used.
   107  //
   108  //	%+v : class + message + stack
   109  //	%v, %c : class
   110  //	%m : message
   111  //	%t : stack
   112  func (e *Ex) Format(s fmt.State, verb rune) {
   113  	switch verb {
   114  	case 'v':
   115  		if e.Class != nil && len(e.Class.Error()) > 0 {
   116  			fmt.Fprint(s, e.Class.Error())
   117  		}
   118  		if len(e.Message) > 0 {
   119  			fmt.Fprint(s, "; "+e.Message)
   120  		}
   121  		if s.Flag('+') && e.StackTrace != nil {
   122  			e.StackTrace.Format(s, verb)
   123  		}
   124  		if e.Inner != nil {
   125  			if typed, ok := e.Inner.(fmt.Formatter); ok {
   126  				fmt.Fprint(s, "\n")
   127  				typed.Format(s, verb)
   128  			} else {
   129  				fmt.Fprintf(s, "\n%v", e.Inner)
   130  			}
   131  		}
   132  		return
   133  	case 'c':
   134  		fmt.Fprint(s, e.Class.Error())
   135  	case 'i':
   136  		if e.Inner != nil {
   137  			if typed, ok := e.Inner.(fmt.Formatter); ok {
   138  				typed.Format(s, verb)
   139  			} else {
   140  				fmt.Fprintf(s, "%v", e.Inner)
   141  			}
   142  		}
   143  	case 'm':
   144  		fmt.Fprint(s, e.Message)
   145  	case 'q':
   146  		fmt.Fprintf(s, "%q", e.Message)
   147  	}
   148  }
   149  
   150  // Error implements the `error` interface.
   151  // It returns the exception class, without any of the other supporting context like the stack trace.
   152  // To fetch the stack trace, use .String().
   153  func (e *Ex) Error() string {
   154  	return e.Class.Error()
   155  }
   156  
   157  // Decompose breaks the exception down to be marshaled into an intermediate format.
   158  func (e *Ex) Decompose() map[string]interface{} {
   159  	values := map[string]interface{}{}
   160  	values["Class"] = e.Class.Error()
   161  	values["Message"] = e.Message
   162  	if e.StackTrace != nil {
   163  		values["StackTrace"] = e.StackTrace.Strings()
   164  	}
   165  	if e.Inner != nil {
   166  		if typed, isTyped := e.Inner.(*Ex); isTyped {
   167  			values["Inner"] = typed.Decompose()
   168  		} else {
   169  			values["Inner"] = e.Inner.Error()
   170  		}
   171  	}
   172  	return values
   173  }
   174  
   175  // MarshalJSON is a custom json marshaler.
   176  func (e *Ex) MarshalJSON() ([]byte, error) {
   177  	return json.Marshal(e.Decompose())
   178  }
   179  
   180  // UnmarshalJSON is a custom json unmarshaler.
   181  func (e *Ex) UnmarshalJSON(contents []byte) error {
   182  	// try first as a string ...
   183  	var class string
   184  	if tryErr := json.Unmarshal(contents, &class); tryErr == nil {
   185  		e.Class = Class(class)
   186  		return nil
   187  	}
   188  
   189  	// try an object ...
   190  	values := make(map[string]json.RawMessage)
   191  	if err := json.Unmarshal(contents, &values); err != nil {
   192  		return New(err)
   193  	}
   194  
   195  	if class, ok := values["Class"]; ok {
   196  		var classString string
   197  		if err := json.Unmarshal([]byte(class), &classString); err != nil {
   198  			return New(err)
   199  		}
   200  		e.Class = Class(classString)
   201  	}
   202  
   203  	if message, ok := values["Message"]; ok {
   204  		if err := json.Unmarshal([]byte(message), &e.Message); err != nil {
   205  			return New(err)
   206  		}
   207  	}
   208  
   209  	if inner, ok := values["Inner"]; ok {
   210  		var innerClass string
   211  		if tryErr := json.Unmarshal([]byte(inner), &class); tryErr == nil {
   212  			e.Inner = Class(innerClass)
   213  		}
   214  		innerEx := Ex{}
   215  		if tryErr := json.Unmarshal([]byte(inner), &innerEx); tryErr == nil {
   216  			e.Inner = &innerEx
   217  		}
   218  	}
   219  	if stack, ok := values["StackTrace"]; ok {
   220  		var stackStrings []string
   221  		if err := json.Unmarshal([]byte(stack), &stackStrings); err != nil {
   222  			return New(err)
   223  		}
   224  		e.StackTrace = StackStrings(stackStrings)
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // String returns a fully formed string representation of the ex.
   231  // It's equivalent to calling sprintf("%+v", ex).
   232  func (e *Ex) String() string {
   233  	s := new(bytes.Buffer)
   234  	if e.Class != nil && len(e.Class.Error()) > 0 {
   235  		fmt.Fprintf(s, "%s", e.Class)
   236  	}
   237  	if len(e.Message) > 0 {
   238  		fmt.Fprint(s, " "+e.Message)
   239  	}
   240  	if e.StackTrace != nil {
   241  		fmt.Fprint(s, " "+e.StackTrace.String())
   242  	}
   243  	return s.String()
   244  }
   245  
   246  // Unwrap returns the inner error if it exists.
   247  // Enables error chaining and calling errors.Is/As to
   248  // match on inner errors.
   249  func (e *Ex) Unwrap() error {
   250  	return e.Inner
   251  }
   252  
   253  // Is returns true if the target error matches the Ex.
   254  // Enables errors.Is on Ex classes when an error
   255  // is wrapped using Ex.
   256  func (e *Ex) Is(target error) bool {
   257  	if e == nil || e.Class == nil {
   258  		return false
   259  	}
   260  	// If target is a multi-error, never try to convert it to *Ex.
   261  	if _, ok := target.(Multi); ok {
   262  		return errors.Is(e.Class, target)
   263  	}
   264  	// Try to convert target to *Ex.
   265  	if targetTyped := As(target); targetTyped != nil {
   266  		if targetTyped.Class == nil {
   267  			return false
   268  		}
   269  		return e.Class == targetTyped.Class
   270  	}
   271  	// Otherwise, use the native `errors.Is()`.
   272  	return errors.Is(e.Class, target)
   273  }
   274  
   275  // As delegates to the errors.As to match on the Ex class.
   276  func (e *Ex) As(target interface{}) bool {
   277  	return errors.As(e.Class, target)
   278  }