- Notifications
You must be signed in to change notification settings - Fork 31
GetAllPrintableProperties will return a map of all printable properties #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
PeterIvanov left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case DecorateAndAddSame shows that a single error can hold several printable values for the same property key. It seems to be an issue that this new method doesn't actually return them the same way.
| } | ||
| } | ||
| | ||
| func (e *Error) GetAllPrintableProperties() map[string]interface{} { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am worried about a public API that returns actual value of properties, not its string formatted view, like Format() does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it and it all depends on the perspective of how this method should be used. If it's just to show the data - maybe. But it would greatly complicate the possible implementation with no obvious reason, especially introducing a risk of the complex casting. As this method would be, IMO, used only internally by the developer to either create some sort of representation of the error (like JSON) or to log the values of properties, I do not see the point of casting them to string. Then he or she can take care of the casting as needed, possibly knowing more of the type to expect as well.
On top of that interface{} is quite gently covered by MarshalJSON and similar methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I fully understood all of your arguments. Presently, I disagree on this point. If the goal of this method is just to expose the same thing that is being added to error message in a more structured way, I fail to see how a string representation is not enough.
On the other hand, returning a raw value of (otherwise possibly inaccessible) property may open the door for all sorts of illicit uses, and I'm not sure it is even worth the effort of carefully considering them all. In such a general purpose library as this, it is best to keep the API as clean as possible, and I would like to avoid adding new ways to access inner data without a very good reason to do so.
| map[string]interface{}{"key1": "a", "key2": 1, "key3": true}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Failure trace is bad for such cases. It is preferable to have a test func and to call it with different arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was quickly made test, but it does cover most of the problem. Please, do pitch in if you have something specific in mind. For now, I wanted to have a PoC with simple coverage just to check, if I am not doing something extremely stupid. And I am used to this as IDE will quickly create for me. VIctim of a habit and shortcuts...
BTW, as I am maybe new to this API - I haven't noticed many scenarios to test here. Just multi-level cause errors if someone is into that and some combinations of properties. You have more experience, so propose which direction you already took. But here a limited number of scenarios is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I analyzed the code further. Currently, only one level of properties will be returned - the top one. If one would like to return properties of the underlying cause(s), one will have to call GetAllPrintableProperties for each cause. Now, I am uncertain whether this is good or bad. For me, probably, good. But I am neither alfa nor omega, don't wanna judge for all. The supplement method I listed in this PR as an example to create a marshalable struct handles that rather well. Hence my comment about prefixing properties, but I am uncertain if that is a good approach here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stack trace in case of failure would be much more helpful if test was written in another way:
func TestFoo(t *testing.T) { case := func(name string, prop Property, value interface{}) { t.Run(tt.name, func(t *testing.T) { errData := Decorate(testType.New(name), "ouch").WithProperty(prop, value) ... } case("foo", RegisterProperty("foo"), nil) case("bar", RegisterProperty("bar"), nil) ... }There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this suggestion is a minor one, bit it goes to making test failure to be a little more readable.
| I believe this topic requires some more thought. Comments above are just those first things that spring to mind. |
| I do agree that it should be discussed, hence my very initial PR. I am not sure about the direction you want to take, but I do see the utility of this method. As the implementation is, as mentioned, initial I am happy to take it somewhere else. But where? For me, the one above – despite some simplifications – is fast, efficient and flexible. If we would like to complicate this further then two risks I see on the horizon:
IMHO KISS it... |
g7r left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the final goal is to marshal the errors, I think we should consider another approach. We can introduce some structure that contains all the public fields that are required to marshal an error. And we need a function that renders an error into that struct. That will clarify the intent and will ease introduction of "unmarshal" operation in future.
| } | ||
| uniq[m.p] = struct{}{} | ||
| strs = append(strs, fmt.Sprintf("%s: %v", m.p.label, m.value)) | ||
| for k, v := range e.GetAllPrintableProperties() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is that maps in Go don't preserve insertion order. Therefore the strings in strs will appear in a kind of randomized order. I don't see any particular problem with that now though but there is no reason to break ordering here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that it might be a problem, but on the other hand I copied that from the existing implementation. It was there, just took it.
I do not see a problem with making it ordered, but currently, it's iterating over properties map, which seems to be a linked list. If that is the case, except duplicated names (again, limited use case currently, as noted in other comments, which for me is still an open question to address), the order should be mostly the same on insertion. Now, the iteration of maps is randomized, but I believe all Go devs know that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still, there is indeed a regression here.
Presently, properties are being printed in reverse order of how they were put into error (skipping overwritten earlier values).
After currently proposed change, they will be printed in random order. This needlessly adds confusion.
This must be fixed. For example, a private method may be introduced to return a resulting slice of a key-value pairs, and then messageFromProperties and the new method would use this result a but differently.
| map[string]interface{}{"key1": "a", "key2": 1, "key3": true}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stack trace in case of failure would be much more helpful if test was written in another way:
func TestFoo(t *testing.T) { case := func(name string, prop Property, value interface{}) { t.Run(tt.name, func(t *testing.T) { errData := Decorate(testType.New(name), "ouch").WithProperty(prop, value) ... } case("foo", RegisterProperty("foo"), nil) case("bar", RegisterProperty("bar"), nil) ... }| Generally, I agree with @g7r, except until now I was about to rather avoid a common struct, leaving a decision on why and how to a developer. Introducing a common OK, we need a decision here I guess. I am happy to take it further and work on it, but I need to have a consensus. For now, IMHO, the best course of action is to make a public function to retrieve a full list of properties, ordered or not, for each level or for all levels combined. On top of that, I can introduce, as in the crude example above, a simple struct with a method to build and retrieve it. All agreed? @g7r and @PeterIvanov? Don't want to step on everyone's toes here, would prefer a more of a consensus here... |
| @uded your position if very reasonable, and thank you for your initiative. |
| Sure, no problem on my end. Currently, we use a fork here and we're rather happy with the implementation as I listed - it's sufficient, covers all of our use cases. I fully understand that the problem is rather heavy-weight, although the implementation is really straight forward. Since I am not a maintainer - I have asked and willing to wait and then help with a final one :-) |
| So, I revisited the behaviour of properties:
Therefore, if we are to expose properties directly, we have 2 options:
If we choose to pursue the second alternative, then it is OK to have |
| } | ||
| uniq[m.p] = struct{}{} | ||
| strs = append(strs, fmt.Sprintf("%s: %v", m.p.label, m.value)) | ||
| for k, v := range e.GetAllPrintableProperties() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still, there is indeed a regression here.
Presently, properties are being printed in reverse order of how they were put into error (skipping overwritten earlier values).
After currently proposed change, they will be printed in random order. This needlessly adds confusion.
This must be fixed. For example, a private method may be introduced to return a resulting slice of a key-value pairs, and then messageFromProperties and the new method would use this result a but differently.
| } | ||
| } | ||
| | ||
| func (e *Error) GetAllPrintableProperties() map[string]interface{} { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I fully understood all of your arguments. Presently, I disagree on this point. If the goal of this method is just to expose the same thing that is being added to error message in a more structured way, I fail to see how a string representation is not enough.
On the other hand, returning a raw value of (otherwise possibly inaccessible) property may open the door for all sorts of illicit uses, and I'm not sure it is even worth the effort of carefully considering them all. In such a general purpose library as this, it is best to keep the API as clean as possible, and I would like to avoid adding new ways to access inner data without a very good reason to do so.
| map[string]interface{}{"key1": "a", "key2": 1, "key3": true}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this suggestion is a minor one, bit it goes to making test failure to be a little more readable.
| I'm closing this PR due to inactivity. |
As discussed in #21, we can simply move some of the code and create a method corresponding to current logic to expose all properties from the error for further processing of errors in situations one would to use them in REST APIs and such.
Simple code like the one below can cover creating a struct from
errorx.Error:This can be used to create a JSON returned to the end customer as an error message. But the code above is just an example, not a production-ready code! Please, review and test before using it as I haven't at all!