Chromium Code Reviews| Index: common/errors/tags.go |
| diff --git a/common/errors/tags.go b/common/errors/tags.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..fc584bf80d950c71566808c2a947e6648266e57b |
| --- /dev/null |
| +++ b/common/errors/tags.go |
| @@ -0,0 +1,235 @@ |
| +// Copyright 2017 The LUCI Authors. All rights reserved. |
|
iannucci
2017/06/24 00:01:11
This is where the bulk of the new logic lives.
|
| +// Use of this source code is governed under the Apache License, Version 2.0 |
| +// that can be found in the LICENSE file. |
| + |
| +package errors |
| + |
| +import ( |
| + "fmt" |
| + "reflect" |
| + "sync" |
| +) |
| + |
| +type ( |
| + tagKey int |
| + |
| + // TagKey objects are used for doing map lookups in GetTags, as well as |
| + // checking for the presence and/or value of a tag on a specific error. |
| + // |
| + // The return values of NewBoolTag and NewTagger should be used directly as |
| + // TagKeys. |
| + TagKey interface { |
| + isTagKey() |
| + |
| + // returns true if the error contains a tag with this key. |
| + In(err error) bool |
| + |
| + // returns the value associated with this tag, if this error is tagged with |
| + // this key. |
| + ValueIn(err error) (val interface{}, ok bool) |
| + } |
| + |
| + // TagValue represents a (tag, value) to be used with Annotate.Tag, or may be |
| + // applied to an error directly with the Apply method. |
| + TagValue interface { |
| + getKey() tagKey |
| + getValue() interface{} |
| + |
| + // Applies this TagValue to the provided error. This is equivalent to |
| + // Annotate(err).Tag(tagValue).Err() |
| + Apply(err error) error |
| + } |
| + |
| + tagValue struct { |
| + key tagKey |
| + value interface{} |
| + } |
| + |
| + // BoolTag is both a TagKey and a TagValue (whose data value is nil). This is |
| + // obtained from NewBoolTag. |
| + BoolTag interface { |
|
dnj
2017/06/24 14:53:55
I'd:
1) Make the NewBoolTag method BoolTag.
2) Hav
iannucci
2017/06/24 20:16:10
Done.
|
| + TagKey |
| + TagValue |
| + } |
| + |
| + // Tagger has a With method to mint TagValues with associated with its |
| + // underlying tag. This is obtained from NewTagger. |
| + Tagger interface { |
| + TagKey |
| + With(value interface{}) TagValue |
| + } |
| + |
| + tagDetail struct { |
| + description string |
| + requiresValue bool |
| + validate func(v interface{}) |
| + } |
| +) |
| + |
| +var ( |
| + tagsLock sync.RWMutex |
| + curTag tagKey |
| + tags = map[tagKey]tagDetail{} |
| +) |
| + |
| +func getTagDetail(key tagKey) tagDetail { |
| + tagsLock.RLock() |
| + detail, ok := tags[key] |
| + tagsLock.RUnlock() |
| + if !ok { |
| + panic("unknown tag") |
| + } |
| + return detail |
| +} |
| + |
| +// tagKey is actually secretly both a TagKey, a TagValue, and a Tagger. This is |
| +// done so that the tags map can be a very simple, direct, int lookup. |
| +// |
| +// Technically a user could cast a Tagger to a BoolTag, which would be strange, |
| +// however preventing this cast makes the whole thing much more complex. |
|
dnj
2017/06/24 14:53:55
(Unless you delete the BoolTag interface)
iannucci
2017/06/24 20:16:10
They could still cast it to TagValue
|
| +func (t tagKey) isTagKey() {} |
| +func (t tagKey) getKey() tagKey { return t } |
| +func (t tagKey) getValue() interface{} { return nil } |
| +func (t tagKey) String() string { |
| + return fmt.Sprintf("errors.TagKey{%q}", getTagDetail(t).description) |
| +} |
| + |
| +func (t tagKey) Apply(err error) error { |
| + detail := getTagDetail(t) |
| + if detail.requiresValue { |
| + panic(fmt.Sprintf("tag %q requires a value", detail.description)) |
| + } |
| + if err == nil { |
| + return nil |
| + } |
| + a := &Annotator{err, stackContext{FrameInfo: stackFrameInfoForError(1, err)}} |
|
dnj
2017/06/24 14:53:54
We're dong a full captureStack every time we apply
iannucci
2017/06/24 20:16:09
If you apply it singly, then yes, if you apply man
|
| + return a.Tag(t).Err() |
| +} |
| + |
| +func (t tagKey) With(val interface{}) TagValue { |
|
dnj
2017/06/24 14:53:55
I don't like the binary "Apply" XOR "With" as an i
iannucci
2017/06/24 20:16:10
I actually don't understand this comment at all...
|
| + detail := getTagDetail(t) |
| + if !detail.requiresValue { |
| + panic(fmt.Sprintf("tag %q doesn't allow a value", detail.description)) |
| + } |
| + |
| + switch k := reflect.ValueOf(val).Kind(); { |
| + case k < reflect.Array || k == reflect.String: |
|
dnj
2017/06/24 14:53:54
I think it would be better to just enumerate the "
iannucci
2017/06/24 20:16:09
ALL OF THIS CODE IS GONE NOW!
|
| + default: |
| + panic("Tagger.With(value) only takes simple types for `value`, not " + k.String()) |
|
dnj
2017/06/24 14:53:55
Why? I get that you're trying to not have this be
iannucci
2017/06/24 20:16:10
I was trying to prevent people from putting mutabl
|
| + } |
| + if detail.validate != nil { |
|
dnj
2017/06/24 14:53:55
Similar concerns for validation, as either validat
iannucci
2017/06/24 20:16:10
Done.
|
| + detail.validate(val) |
| + } |
| + return tagValue{t, val} |
| +} |
| + |
| +func (t tagKey) In(err error) bool { |
| + _, ok := t.ValueIn(err) |
| + return ok |
| +} |
| + |
| +// ValueIn implements TagKey. |
|
dnj
2017/06/24 14:53:55
Comment not needed, or comment other methods unifo
iannucci
2017/06/24 20:16:10
Acknowledged.
|
| +func (t tagKey) ValueIn(err error) (value interface{}, ok bool) { |
|
dnj
2017/06/24 14:53:55
Is it OK that this traverses MultiError? Probably,
iannucci
2017/06/24 20:16:09
yes, I have a test for it too
|
| + Walk(err, func(err error) bool { |
| + if sc, isSC := err.(stackContexter); isSC { |
| + if value, ok = sc.stackContext().Tags[t]; ok { |
| + return false |
| + } |
| + } |
| + return true |
| + }) |
| + return |
| +} |
| + |
| +func (t tagValue) getKey() tagKey { return t.key } |
| +func (t tagValue) getValue() interface{} { return t.value } |
| +func (t tagValue) Apply(err error) error { |
| + if err == nil { |
| + return nil |
| + } |
| + a := &Annotator{err, stackContext{FrameInfo: stackFrameInfoForError(1, err)}} |
|
dnj
2017/06/24 14:53:55
(Same, full stack trace seems like a not great thi
iannucci
2017/06/24 20:16:10
If you need to apply more than one tag, use the An
|
| + return a.Tag(t).Err() |
| +} |
| + |
| +// NewBoolTag creates a new tag with a fixed "nil" value. |
| +// |
| +// You should use this in the top level context of your package like: |
| +// var myTag = errors.NewBoolTag("this error is a user error") |
| +// |
| +// You could then use it like: |
| +// err = myTag.Apply(err) |
| +// myTag.In(err) // == true |
| +// myTag.ValueIn(err) // == (nil, true) |
| +func NewBoolTag(description string) BoolTag { |
|
dnj
2017/06/24 14:53:55
Note that there's nothing stopping someone from na
iannucci
2017/06/24 20:16:10
That's why I had "New" here. It would also mean th
|
| + tagsLock.Lock() |
| + defer tagsLock.Unlock() |
| + |
| + t := curTag + 1 |
| + curTag++ |
| + tags[t] = tagDetail{description: description} |
| + |
| + return t |
| +} |
| + |
| +// NewTagger creates a new Tagger which can be added to errors with |
| +// a corresponding value. |
| +// |
| +// Values used with this tag are must be a simple type (i.e. has a reflect.Kind |
| +// which is a base data type like bool, string, or int). |
| +// |
| +// The `validate` function is an optional argument which allows you to further |
| +// validate values assocaited with this tag. The function should panic if the |
| +// validation fails. |
| +// |
| +// You should use this in the top level context of your package like: |
| +// type CodeType int |
| +// var CodeTag = errors.NewTagger( |
| +// "has a response code", func(v interface) { |
| +// v.(CodeType) // panics if v is not CodeType |
| +// }) |
| +// |
| +// You could then use it like: |
| +// err = CodeTag.With(CodeType(100)).Apply(err) |
| +// CodeTag.In(err) // == true |
| +// CodeTag.ValueIn(err) // == (CodeType(100), true) |
| +func NewTagger(description string, validate ...func(v interface{})) Tagger { |
| + var valFn func(interface{}) |
| + switch len(validate) { |
| + case 0: |
| + case 1: |
| + valFn = validate[0] |
| + default: |
| + panic("NewTagger takes exactly 0 or 1 validate function") |
| + } |
| + |
| + tagsLock.Lock() |
| + defer tagsLock.Unlock() |
| + |
| + t := curTag + 1 |
| + curTag++ |
| + tags[t] = tagDetail{description, true, valFn} |
| + |
| + return t |
| +} |
| + |
| +// GetTags returns a map of all TagKeys set in this error to their value. |
| +// |
| +// A nil value means that the tag is present, but has a nil associated value. |
| +// |
| +// This is done in a depth-first traversal of the error stack, with the |
| +// most-recently-set value of the tag taking precendence. |
| +func GetTags(err error) map[TagKey]interface{} { |
| + ret := map[TagKey]interface{}{} |
| + Walk(err, func(err error) bool { |
| + if sc, ok := err.(stackContexter); ok { |
| + ctx := sc.stackContext() |
| + for k, v := range ctx.Tags { |
| + if _, ok := ret[k]; !ok { |
| + ret[k] = v |
| + } |
| + } |
| + } |
| + return true |
| + }) |
| + return ret |
| +} |