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

     1  /*
     2  
     3  Copyright (c) 2022 - 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  // 	%+v : class + message + stack
   108  // 	%v, %c : class
   109  // 	%m : message
   110  // 	%t : stack
   111  func (e *Ex) Format(s fmt.State, verb rune) {
   112  	switch verb {
   113  	case 'v':
   114  		if e.Class != nil && len(e.Class.Error()) > 0 {
   115  			fmt.Fprint(s, e.Class.Error())
   116  		}
   117  		if len(e.Message) > 0 {
   118  			fmt.Fprint(s, "; "+e.Message)
   119  		}
   120  		if s.Flag('+') && e.StackTrace != nil {
   121  			e.StackTrace.Format(s, verb)
   122  		}
   123  		if e.Inner != nil {
   124  			if typed, ok := e.Inner.(fmt.Formatter); ok {
   125  				fmt.Fprint(s, "\n")
   126  				typed.Format(s, verb)
   127  			} else {
   128  				fmt.Fprintf(s, "\n%v", e.Inner)
   129  			}
   130  		}
   131  		return
   132  	case 'c':
   133  		fmt.Fprint(s, e.Class.Error())
   134  	case 'i':
   135  		if e.Inner != nil {
   136  			if typed, ok := e.Inner.(fmt.Formatter); ok {
   137  				typed.Format(s, verb)
   138  			} else {
   139  				fmt.Fprintf(s, "%v", e.Inner)
   140  			}
   141  		}
   142  	case 'm':
   143  		fmt.Fprint(s, e.Message)
   144  	case 'q':
   145  		fmt.Fprintf(s, "%q", e.Message)
   146  	}
   147  }
   148  
   149  // Error implements the `error` interface.
   150  // It returns the exception class, without any of the other supporting context like the stack trace.
   151  // To fetch the stack trace, use .String().
   152  func (e *Ex) Error() string {
   153  	return e.Class.Error()
   154  }
   155  
   156  // Decompose breaks the exception down to be marshaled into an intermediate format.
   157  func (e *Ex) Decompose() map[string]interface{} {
   158  	values := map[string]interface{}{}
   159  	values["Class"] = e.Class.Error()
   160  	values["Message"] = e.Message
   161  	if e.StackTrace != nil {
   162  		values["StackTrace"] = e.StackTrace.Strings()
   163  	}
   164  	if e.Inner != nil {
   165  		if typed, isTyped := e.Inner.(*Ex); isTyped {
   166  			values["Inner"] = typed.Decompose()
   167  		} else {
   168  			values["Inner"] = e.Inner.Error()
   169  		}
   170  	}
   171  	return values
   172  }
   173  
   174  // MarshalJSON is a custom json marshaler.
   175  func (e *Ex) MarshalJSON() ([]byte, error) {
   176  	return json.Marshal(e.Decompose())
   177  }
   178  
   179  // UnmarshalJSON is a custom json unmarshaler.
   180  func (e *Ex) UnmarshalJSON(contents []byte) error {
   181  	// try first as a string ...
   182  	var class string
   183  	if tryErr := json.Unmarshal(contents, &class); tryErr == nil {
   184  		e.Class = Class(class)
   185  		return nil
   186  	}
   187  
   188  	// try an object ...
   189  	values := make(map[string]json.RawMessage)
   190  	if err := json.Unmarshal(contents, &values); err != nil {
   191  		return New(err)
   192  	}
   193  
   194  	if class, ok := values["Class"]; ok {
   195  		var classString string
   196  		if err := json.Unmarshal([]byte(class), &classString); err != nil {
   197  			return New(err)
   198  		}
   199  		e.Class = Class(classString)
   200  	}
   201  
   202  	if message, ok := values["Message"]; ok {
   203  		if err := json.Unmarshal([]byte(message), &e.Message); err != nil {
   204  			return New(err)
   205  		}
   206  	}
   207  
   208  	if inner, ok := values["Inner"]; ok {
   209  		var innerClass string
   210  		if tryErr := json.Unmarshal([]byte(inner), &class); tryErr == nil {
   211  			e.Inner = Class(innerClass)
   212  		}
   213  		innerEx := Ex{}
   214  		if tryErr := json.Unmarshal([]byte(inner), &innerEx); tryErr == nil {
   215  			e.Inner = &innerEx
   216  		}
   217  	}
   218  	if stack, ok := values["StackTrace"]; ok {
   219  		var stackStrings []string
   220  		if err := json.Unmarshal([]byte(stack), &stackStrings); err != nil {
   221  			return New(err)
   222  		}
   223  		e.StackTrace = StackStrings(stackStrings)
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  // String returns a fully formed string representation of the ex.
   230  // It's equivalent to calling sprintf("%+v", ex).
   231  func (e *Ex) String() string {
   232  	s := new(bytes.Buffer)
   233  	if e.Class != nil && len(e.Class.Error()) > 0 {
   234  		fmt.Fprintf(s, "%s", e.Class)
   235  	}
   236  	if len(e.Message) > 0 {
   237  		fmt.Fprint(s, " "+e.Message)
   238  	}
   239  	if e.StackTrace != nil {
   240  		fmt.Fprint(s, " "+e.StackTrace.String())
   241  	}
   242  	return s.String()
   243  }
   244  
   245  // Unwrap returns the inner error if it exists.
   246  // Enables error chaining and calling errors.Is/As to
   247  // match on inner errors.
   248  func (e *Ex) Unwrap() error {
   249  	return e.Inner
   250  }
   251  
   252  // Is returns true if the target error matches the Ex.
   253  // Enables errors.Is on Ex classes when an error
   254  // is wrapped using Ex.
   255  func (e *Ex) Is(target error) bool {
   256  	return Is(e, target)
   257  }
   258  
   259  // As delegates to the errors.As to match on the Ex class.
   260  func (e *Ex) As(target interface{}) bool {
   261  	return errors.As(e.Class, target)
   262  }