Skip to content
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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

mindreframer
Copy link
Contributor

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)

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)
@rous-gg
Copy link

rous-gg commented Dec 21, 2016

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.

@mindreframer
Copy link
Contributor Author

here is an example how the error message would look like:

ParamContractError:
       Contract violation for argument 1 of 1:
       Expected: (KeywordArgs[{:guid=>String, :occurred_at=>DateTime, :created_by_id=>Contracts::Builtin::Num}])
       Actual: {:guid=>"4fea1b56-3218-4c4c-a2ce-6db42d8e445f", :occurred_at=>"1", :test1=>1}
       Missing Contract: {:test1=>1}
       Invalid Args: [{:occurred_at=>"1", :contract=>DateTime}]
       Missing Args: {:created_by_id=>Contracts::Builtin::Num}
       Value guarded in: Events::AbstractEvent::initialize
       With Contract: KeywordArgs => NilClass

Copy link
Owner

@egonSchiele egonSchiele left a 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)
Copy link
Owner

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|
Copy link
Owner

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

Copy link
Contributor Author

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) }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Contributor Author

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
Copy link
Owner

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)
Copy link
Owner

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:

  1. 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.

  2. 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?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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?

  2. 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.

Copy link

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

Copy link
Contributor Author

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
Copy link
Owner

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@egonSchiele
Copy link
Owner

Hey all,
Sorry for my late response here. @mindreframer could you update the data hash? I like that better than running the contracts twice.

@mindreframer
Copy link
Contributor Author

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

  • less backwards-compatibility (drop support for rubies 1.8.7- 1.9.3)
  • faster tests: remove cucumber tests, because they are kinda fragile and expensive to maintain
  • increase the code climate index (now at 3.9), that required quite some refactoring
  • reduce cyclomatic complexity as much as possible, that means all the nested if-else blocks were refactored to be much flatter

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 Contracts and maybe you will like some parts of my changes and use them.

Best,
Roman

@egonSchiele
Copy link
Owner

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.

@mindreframer
Copy link
Contributor Author

@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: https://github.com/contracts.ruby/contracts
Then you could post a "looking for maintainer(s) for contracts" post on http://www.rubyflow.com/ and link to a github issue with the same title... Some people with more time than you and active Ruby investment might be interested. Pick somebody you would trust to keep the project alive.

Also there are 23 watchers on the repo. )))

But well... That is maybe something you should figure out for yourself :)))

Best,
Roman

@mindreframer
Copy link
Contributor Author

here is another project where the original maintainer looked for new maintainers:

fgrehm/vagrant-cachier#143 (comment)

@egonSchiele
Copy link
Owner

good idea!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants