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 }