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 }