egonSchiele / contracts.ruby

Contracts for Ruby.
http://egonschiele.github.com/contracts.ruby
BSD 2-Clause "Simplified" License
1.44k stars 82 forks source link

Per-Contract assertions #222

Open kentfredric opened 8 years ago

kentfredric commented 8 years ago

I'm trying to duct tape together the ability to pass type constraints to an accessor/constructor generator toolkit (eg: https://seanohalpin.lighthouseapp.com/projects/26673-doodle/tickets/12-permit-inline-must ), but I'm getting stuck working out how to get the data back.

Its important to me that if I pass such a contract to anything, there's no way obvious to me to extract the right context and error messages from the contract.

If you passed such a contract to a tool like this, the tool would indeed know /what/ its validating.

But only the contract itself can explain /why/ it failed.

Here, the only obvious usecase that is documented that is remotely like this is documented as such:

data = parse(user_input)
unless Contract.valid?(data, HashOf[String,Nat])
  raise UserInputError.new(user_input)
end

But this doesn't serve to be helpful, because UserInputError doesn't contain any information about why it failed.

So if you had an abstracted interface that worked like this ( only a simplified example to elide over the complexity of getter/setter/initialiser generation )

def add_rule( key, contract ) 
    self.rules[key] = contract
end
def set_key( key, value )
    unless Contract.valid?( value, self.rules[key] )
       raise UserInputError.new(user_input)
    end
    self.data[key] = value
end

Here there is no obvious apparent way to express the nature of the failure, only the name of the contract that emitted it.

And this becomes much less useful in the event of all/any contracts where metadata about the individual sub-rules that failed is something that can only be resolved by the contract itself, not the caller

You could rely on .to_s giving a semi-useful result, but its not capable of interpolating the actual value in the result, or any sub parts of the value that may be specific to the reason for the failure.

For instance, if you had a StringWithLength constraint, it wouldn't necessarily be obvious if the reason for a given value failing was a result of it not being a string, or the result of the string not being long enough.

It also seems to me it would be "useful" to be able to call assertions on single parameters in a similar manner if you don't like the amount of magic in stipulating a secondary signature before the method definition, ala:

def set_foo(key)
   Constraints::Int.assert(key, :name => 'key', :method => 'set_foo' )
end

Mostly because this will be significantly less surprising to a person causally reading the code and is not familiar with the pre-method-call signature.

Context: I'm trying to port some of the things I've found useful in Perl's "Moose" Type-Constraint system, and its generally helpful to be able to do this. Contracts is the closest thing so far, so I figured I might at least suggest changes that improve the number of places it can be used instead of attempting to write my own.

Its rather exhaustive list of per-constraint methods can be found here: https://metacpan.org/pod/Moose::Meta::TypeConstraint

Thanks in advance.

waterlink commented 8 years ago

Hi @kentfredric!

Do you have a certain API for contracts in mind?

Given the fact, that we already have some sort of Validator abstraction, which is used under the hood by Contract.valid?(...), most probably, it is feasible to make such API:

data = parse(user_input)
validator = Contracts::Validator.from(HashOf[String => Nat])
validation_result = validator.validate(data)
unless validation_result.ok?
  # use `validation_result.error`
end

What do you think would validation_result.error look like? Options I see:

Last means, that a :klass validator will provide a message Expected #<...> to be a User, but any custom contract can provide its own message through a special method (maybe, #failure_message). Unfortunately, that means, that such a contract will have to be stateful - think how rspec matchers are implemented - the same thing.

pvdvreede commented 8 years ago

I am using contracts for its assertion and validation capabilities against a recursive ruby hash. It is to try and completely validate an AWS Cloudformation template. So I to would like an API like you have suggested above @waterlink .

Contracts.ruby works great for creating a Type system I need, the only issue is that the Contract.valid? method only tells me if the contract failed, not why, so in the case of a ruby hash that has numerous other hashes and custom types inside it I have no way of knowing (or reporting to the user) what is not valid underneath the top level Cloudformation type.

For my case it would be good to have the validation_result have an errors object to allow for multiple errors if this is possible, such that I can print out all the errors at once to the user. Each error in the array could be a string that is derived from the failed contract's to_s method if this is possible?

Alternatively, I do like the failure callback system that is already in place, so if there was a way to call that on each contract under the root contract as they failed then the user could tweak what they needed that way, rather than just having the single error/errors property on a validation_result.