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 }