diff --git a/err_impl.go b/err_impl.go index 5d5cf9a..a68e51c 100644 --- a/err_impl.go +++ b/err_impl.go @@ -114,6 +114,22 @@ func (err *e) Unwrap() error { return err.wrappedErr } +func (err *e) V(key string) (any, bool) { + for err := error(err); err != nil; err = errors.Unwrap(err) { + ee, ok := err.(*e) + if !ok { + continue + } + + for _, kv := range ee.kvs { + if kv.K == key { + return kv.V, true + } + } + } + return nil, false +} + func (err *e) stackTrace() StackFrames { var out StackFrames for err := error(err); err != nil; err = errors.Unwrap(err) { diff --git a/errors.go b/errors.go index 318a4ac..8b21784 100644 --- a/errors.go +++ b/errors.go @@ -48,3 +48,38 @@ func StackTrace(err error) StackFrames { } return ee.stackTrace() } + +// V returns a typed value for the kvs of an error. Type conversion +// can be used to convert the output value. We do not distinguish +// between a purposeful value and key not found. With the +// single return param, we can do the following to convert it to a +// more specific type: +// +// err := errors.New("simple msg", errors.KVs("int", 1)) +// i, ok := errors.V(err, "int).(int) +// +// Note: this will take the first matching key. If you are interested +// in obtaining a key's value from a wrapped error collides with a +// parent's key value, then you can manually unwrap the error and call V +// on it to skip the parent field. +// +// TODO: +// - food for thought, we could change the V funcs signature +// to allow for a generic type to be provided, however... it +// feels both premature and limiting in the event you don't +// care about the type. If we get an ask for that, we can provide +// guidance for this via the comment above and perhaps some example +// code. +func V(err error, key string) any { + if err == nil { + return nil + } + + fielder, ok := err.(interface{ V(key string) (any, bool) }) + if !ok { + return nil + } + + raw, _ := fielder.V(key) + return raw +} diff --git a/errors_test.go b/errors_test.go index cf8223b..a18b5e7 100644 --- a/errors_test.go +++ b/errors_test.go @@ -184,6 +184,33 @@ func Test_Errors(t *testing.T) { } } +func TestV(t *testing.T) { + type foo struct { + i int + } + + t.Run("key val pairs are should be accessible", func(t *testing.T) { + err := errors.New("simple msg", errors.KVs("bool", true, "str", "string", "float", 3.14, "int", 1, "foo", foo{i: 3})) + + eqV(t, err, "bool", true) + eqV(t, err, "str", "string") + eqV(t, err, "float", 3.14) + eqV(t, err, "int", 1) + eqV(t, err, "foo", foo{i: 3}) + + if v := errors.V(err, "non existent"); v != nil { + t.Errorf("unexpected value returned:\n\t\tgot:\t%#v", v) + } + }) + + t.Run("when parent error kv pair collides with wrapped error will take parent kv pair", func(t *testing.T) { + err := errors.New("simple msg", errors.KVs("str", "initial")) + err = errors.Wrap(err, errors.KVs("str", "wrapped")) + + eqV(t, err, "str", "wrapped") + }) +} + func eq[T comparable](t *testing.T, want, got T) bool { t.Helper() @@ -194,6 +221,14 @@ func eq[T comparable](t *testing.T, want, got T) bool { return matches } +func eqV[T comparable](t *testing.T, err error, key string, want T) bool { + t.Helper() + + got, ok := errors.V(err, key).(T) + must(t, eq(t, true, ok)) + return eq(t, want, got) +} + func eqFields(t *testing.T, want, got []any) bool { t.Helper()