-
Notifications
You must be signed in to change notification settings - Fork 82
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
custom error formatter for KeywordArgs #246
base: master
Are you sure you want to change the base?
custom error formatter for KeywordArgs #246
Conversation
problem: - it is impossible to quickly understand the contract violation with big hashes solution: - provide custom error formatter just for keywordargs, that gives you extra information about: - Missing Contract definition (for unspecified keys in contract) - Invalid Args (validation failures) - Missing Args (required values, that are missing)
This is a really good feature to have. Right now it's really hard to understand what is wrong with keyword args contract and you spend a lot of time making that diff in mind. |
here is an example how the error message would look like:
|
run error_formatter_spec only for rubies > 1.8
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.
Very useful, thanks. Nice to see tests too. I added some comments, take a look.
DefaultErrorFormatter | ||
end | ||
|
||
def self.keysword_args?(data) |
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.
typo, should be keyword_args
def missing_args_info | ||
@missing_args_info ||= begin | ||
missing_keys = contract_options.keys - arg.keys | ||
contract_options.select do |key, _value| |
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.
you can use _
instead of _value
since that var isn't used
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.
done
def missing_contract_info | ||
@missing_contract_info ||= begin | ||
contract_keys = contract_options.keys | ||
arg.select { |key, _value| !contract_keys.include?(key) } |
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.
same
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.
done
else | ||
value.is_a?(contract) | ||
end | ||
rescue |
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.
why was this rescue needed?
|
||
def check_contract(contract, value) | ||
if contract.respond_to?(:valid?) | ||
contract.valid?(value) |
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 don't like the idea of calling the valid
method of a contract again, to display the error message, for a couple of reasons:
-
It breaks the idea that contracts are only run once...so if someone happens to write a contract that takes a long time to check, they wouldn't expect the error message to take time to render.
-
Error rendering should ideally be very simple. It shouldn't be possible to generate an error while rendering the error message. But code like this means we'll be running arbitrary code by other folks (anyone can write their own class with a valid? functions), which could fail for whatever reason. Maybe that is why you added the
rescue
?
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.
-
Right now data object does not contain information about specific invalid keyword args keys. it's really hard to make a diff in mind to find them. in most situations contracts are really simple one (type checks, range checks etc) and they do not take much time to execute. Actually we can move that to config so developers will be able to enable/disable this failure msg calculation globally. The main purpose for it is to save time when working in test and dev modes. What do you think if we move that to config?
-
We create a lot of custom contracts in our applications utilising :valid? method. You can both return false in it or raise ParamContractError. This is why we've added rescue to it.
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.
Let's take a look at the example:
Invalid Args: [{:occurred_at=>"1", :contract=>DateTime}]
One line of output tells us
- which argument is wrong
- argument value
- contract
This should help developers a lot
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 you have expensive logic in your contract validators, you are doing it wrong. Elixir for example allows only some selected bits of logic to be run in function guards, to make sure that your function call is not burdened by accidental side-effects from guards. Sure, here it is theoretically possible to do anything, but that responsibility should be up to the user of the library. On the other hand, it would be possible to modify the data
hash to include all the information about failed/missing validations, then we would not need to run validations twice for error message generation... Life is about tradeoffs... @egonSchiele Which alternative would you prefer?
end | ||
|
||
describe "self.class_for" do | ||
it "returns the right formatter for passed in data" do |
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.
this block needs a test inside it
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.
done
Hey all, |
Hey @egonSchiele, good to hear from you! Because the discussion here ended kinda abruptly and I liked the idea of contracts for Ruby, I have continued my work on a fork here: https://github.com/ddd-ruby/contracts.ruby Many changes I have introduced would be too disruptive and would be probably rejected here after a longer discussion, and honestly, I would not even blame you for that :). You as maintainer of this project should be more conservative towards changes. I tried to simplify contracts from the DX perspective, that means
Overall the resulting codebase should be simpler and a bit easier to maintain / contribute to. One gotcha: the refactoring made it a bit slower, it is roughly 10% slower in trivial benchmarks that the original implementation. My current fork satisfies my needs and I have already invested enough efforts into it. If you are interested in some of those changes, you might consider merging some parts yourself... I really hope this does not appear offensive or rude to you. I just did not have the patience to wait for an undefinite time period for a decision from your side. My brain just does not work like this. :P Overall thanks for Best, |
No problem Roman, it is not offensive at all :) I don't work in Ruby these days, so it is a bit of overhead for me to maintain the project, which is why my response took a while. I'm really glad to see you like the project ... you have made a ton of commits! I wish I had more time to work on contracts, I would definitely look at your changes. The codebase is quite complex so I would like to simplify it also. |
@egonSchiele thx for staying cool! If you don't work in Ruby anymore, maybe you could look around for another maintainer / co-maintainer with rights to merge / push to Rubygems? That way contracts would not suffer a slow death... A separate Github team would be a nice touch, like: Also there are 23 watchers on the repo. ))) But well... That is maybe something you should figure out for yourself :))) Best, |
here is another project where the original maintainer looked for new maintainers: |
good idea! |
problem:
solution: