go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/errutil/exception.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package errutil
     9  
    10  import (
    11  	"bytes"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  )
    16  
    17  // Exception is an error with a stack trace.
    18  //
    19  // It also can have an optional cause, it implements `Exception`
    20  type Exception struct {
    21  	// Class disambiguates between errors, it can be used to identify the type of the error.
    22  	Class error
    23  	// Message adds further detail to the error, and shouldn't be used for disambiguation.
    24  	Message string
    25  	// Inner holds the original error in cases where we're wrapping an error with a stack trace.
    26  	Inner error
    27  	// StackTrace is the call stack frames used to create the stack output.
    28  	StackTrace StackTrace
    29  }
    30  
    31  // Format allows for conditional expansion in printf statements
    32  // based on the token and flags used.
    33  //
    34  //	%+v : class + message + stack
    35  //	%v, %c : class
    36  //	%m : message
    37  //	%t : stack
    38  func (e *Exception) Format(s fmt.State, verb rune) {
    39  	switch verb {
    40  	case 'v':
    41  		if e.Class != nil && len(e.Class.Error()) > 0 {
    42  			fmt.Fprint(s, e.Class.Error())
    43  		}
    44  		if len(e.Message) > 0 {
    45  			fmt.Fprint(s, "; "+e.Message)
    46  		}
    47  		if s.Flag('+') && e.StackTrace != nil {
    48  			e.StackTrace.Format(s, verb)
    49  		}
    50  		if e.Inner != nil {
    51  			if typed, ok := e.Inner.(fmt.Formatter); ok {
    52  				fmt.Fprint(s, "\n")
    53  				typed.Format(s, verb)
    54  			} else {
    55  				fmt.Fprintf(s, "\n%v", e.Inner)
    56  			}
    57  		}
    58  		return
    59  	case 'c':
    60  		fmt.Fprint(s, e.Class.Error())
    61  	case 'i':
    62  		if e.Inner != nil {
    63  			if typed, ok := e.Inner.(fmt.Formatter); ok {
    64  				typed.Format(s, verb)
    65  			} else {
    66  				fmt.Fprintf(s, "%v", e.Inner)
    67  			}
    68  		}
    69  	case 'm':
    70  		fmt.Fprint(s, e.Message)
    71  	case 'q':
    72  		fmt.Fprintf(s, "%q", e.Message)
    73  	}
    74  }
    75  
    76  // Error implements the `error` interface.
    77  // It returns the exception class, without any of the other supporting context like the stack trace.
    78  // To fetch the stack trace, use .String().
    79  func (e *Exception) Error() string {
    80  	return e.Class.Error()
    81  }
    82  
    83  // Decompose breaks the exception down to be marshaled into an intermediate format.
    84  func (e *Exception) Decompose() map[string]interface{} {
    85  	values := map[string]interface{}{}
    86  	values["Class"] = e.Class.Error()
    87  	values["Message"] = e.Message
    88  	if e.StackTrace != nil {
    89  		values["StackTrace"] = e.StackTrace.Strings()
    90  	}
    91  	if e.Inner != nil {
    92  		if typed, isTyped := e.Inner.(*Exception); isTyped {
    93  			values["Inner"] = typed.Decompose()
    94  		} else {
    95  			values["Inner"] = e.Inner.Error()
    96  		}
    97  	}
    98  	return values
    99  }
   100  
   101  // MarshalJSON is a custom json marshaler.
   102  func (e *Exception) MarshalJSON() ([]byte, error) {
   103  	return json.Marshal(e.Decompose())
   104  }
   105  
   106  // UnmarshalJSON is a custom json unmarshaler.
   107  func (e *Exception) UnmarshalJSON(contents []byte) error {
   108  	// try first as a string ...
   109  	var class string
   110  	if tryErr := json.Unmarshal(contents, &class); tryErr == nil {
   111  		e.Class = Class(class)
   112  		return nil
   113  	}
   114  
   115  	// try an object ...
   116  	values := make(map[string]json.RawMessage)
   117  	if err := json.Unmarshal(contents, &values); err != nil {
   118  		return New(err)
   119  	}
   120  
   121  	if class, ok := values["Class"]; ok {
   122  		var classString string
   123  		if err := json.Unmarshal([]byte(class), &classString); err != nil {
   124  			return New(err)
   125  		}
   126  		e.Class = Class(classString)
   127  	}
   128  
   129  	if message, ok := values["Message"]; ok {
   130  		if err := json.Unmarshal([]byte(message), &e.Message); err != nil {
   131  			return New(err)
   132  		}
   133  	}
   134  
   135  	if inner, ok := values["Inner"]; ok {
   136  		var innerClass string
   137  		if tryErr := json.Unmarshal([]byte(inner), &class); tryErr == nil {
   138  			e.Inner = Class(innerClass)
   139  		}
   140  		var innerEx Exception
   141  		if tryErr := json.Unmarshal([]byte(inner), &innerEx); tryErr == nil {
   142  			e.Inner = &innerEx
   143  		}
   144  	}
   145  	if stack, ok := values["StackTrace"]; ok {
   146  		var stackStrings []string
   147  		if err := json.Unmarshal([]byte(stack), &stackStrings); err != nil {
   148  			return New(err)
   149  		}
   150  		e.StackTrace = StackStrings(stackStrings)
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  // String returns a fully formed string representation of the ex.
   157  // It's equivalent to calling sprintf("%+v", ex).
   158  func (e *Exception) String() string {
   159  	s := new(bytes.Buffer)
   160  	if e.Class != nil && len(e.Class.Error()) > 0 {
   161  		fmt.Fprintf(s, "%s", e.Class)
   162  	}
   163  	if len(e.Message) > 0 {
   164  		fmt.Fprint(s, " "+e.Message)
   165  	}
   166  	if e.StackTrace != nil {
   167  		fmt.Fprint(s, " "+e.StackTrace.String())
   168  	}
   169  	return s.String()
   170  }
   171  
   172  // Unwrap returns the inner error if it exists.
   173  // Enables error chaining and calling errors.Is/As to
   174  // match on inner errors.
   175  func (e *Exception) Unwrap() error {
   176  	return e.Inner
   177  }
   178  
   179  // Is returns true if the target error matches the Ex.
   180  // Enables errors.Is on Ex classes when an error
   181  // is wrapped using Ex.
   182  func (e *Exception) Is(target error) bool {
   183  	return Is(e, target)
   184  }
   185  
   186  // As delegates to the errors.As to match on the Ex class.
   187  func (e *Exception) As(target interface{}) bool {
   188  	return errors.As(e.Class, target)
   189  }