Skip to content

Commit 8c1c5a6

Browse files
committed
Merge pull request #159 from waterlink/feature/override_validator
Refactor validator strategies + Add ability to override validators
2 parents 25828d8 + 88375aa commit 8c1c5a6

File tree

6 files changed

+429
-141
lines changed

6 files changed

+429
-141
lines changed

TUTORIAL.md

+30
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,36 @@ end
509509

510510
If your failure callback returns `false`, the method that the contract is guarding will not be called (the default behaviour).
511511

512+
## Providing your own custom validators
513+
514+
This can be done with `Contract.override_validator`:
515+
516+
```ruby
517+
# Make contracts accept all RSpec doubles
518+
Contract.override_validator(:class) do |contract|
519+
lambda do |arg|
520+
arg.is_a?(RSpec::Mocks::Double) ||
521+
arg.is_a?(contract)
522+
end
523+
end
524+
```
525+
526+
The block you provide should always return lambda accepting one argument - validated argument. Block itself accepts contract as an argument.
527+
528+
Possible validator overrides:
529+
530+
- `override_validator(MyCustomContract)` - allows to add some special behaviour for custom contracts,
531+
- `override_validator(Proc)` - e.g. `lambda { true }`,
532+
- `override_validator(Array)` - e.g. `[Num, String]`,
533+
- `override_validator(Hash)` - e.g. `{ :a => Num, :b => String }`,
534+
- `override_validator(Contracts::Args)` - e.g. `Args[Num]`,
535+
- `override_validator(Contracts::Func)` - e.g. `Func[Num => Num]`,
536+
- `override_validator(:valid)` - allows to override how contracts that respond to `:valid?` are handled,
537+
- `override_validator(:class)` - allows to override how class/module contract constants are handled,
538+
- `override_validator(:default)` - otherwise, raw value contracts.
539+
540+
Default validators can be found here: [lib/contracts/validators.rb](https://github.com/egonSchiele/contracts.ruby/blob/master/lib/contracts/validators.rb).
541+
512542
## Disabling contracts
513543

514544
If you want to disable contracts, set the `NO_CONTRACTS` environment variable. This will disable contracts and you won't have a performance hit. Pattern matching will still work if you disable contracts in this way! With NO_CONTRACTS only pattern-matching contracts are defined.

lib/contracts.rb

+10-141
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
require "contracts/support"
88
require "contracts/engine"
99
require "contracts/method_handler"
10+
require "contracts/validators"
11+
require "contracts/call_with"
1012

1113
module Contracts
1214
def self.included(base)
@@ -58,7 +60,15 @@ def functype(funcname)
5860
# Contract [contract names] => return_value
5961
#
6062
# This class also provides useful callbacks and a validation method.
63+
#
64+
# For #make_validator and related logic see file
65+
# lib/contracts/validators.rb
66+
# For #call_with and related logic see file
67+
# lib/contracts/call_with.rb
6168
class Contract < Contracts::Decorator
69+
extend Contracts::Validators
70+
include Contracts::CallWith
71+
6272
# Default implementation of failure_callback. Provided as a block to be able
6373
# to monkey patch #failure_callback only temporary and then switch it back.
6474
# First important usage - for specs.
@@ -209,54 +219,6 @@ def self.valid?(arg, contract)
209219
make_validator(contract)[arg]
210220
end
211221

212-
# This is a little weird. For each contract
213-
# we pre-make a proc to validate it so we
214-
# don't have to go through this decision tree every time.
215-
# Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
216-
def self.make_validator(contract)
217-
# if is faster than case!
218-
klass = contract.class
219-
if klass == Proc
220-
# e.g. lambda {true}
221-
contract
222-
elsif klass == Array
223-
# e.g. [Num, String]
224-
# TODO: account for these errors too
225-
lambda do |arg|
226-
return false unless arg.is_a?(Array) && arg.length == contract.length
227-
arg.zip(contract).all? do |_arg, _contract|
228-
Contract.valid?(_arg, _contract)
229-
end
230-
end
231-
elsif klass == Hash
232-
# e.g. { :a => Num, :b => String }
233-
lambda do |arg|
234-
return false unless arg.is_a?(Hash)
235-
contract.keys.all? do |k|
236-
Contract.valid?(arg[k], contract[k])
237-
end
238-
end
239-
elsif klass == Contracts::Args
240-
lambda do |arg|
241-
Contract.valid?(arg, contract.contract)
242-
end
243-
elsif klass == Contracts::Func
244-
lambda do |arg|
245-
arg.is_a?(Method) || arg.is_a?(Proc)
246-
end
247-
else
248-
# classes and everything else
249-
# e.g. Fixnum, Num
250-
if contract.respond_to? :valid?
251-
lambda { |arg| contract.valid?(arg) }
252-
elsif klass == Class || klass == Module
253-
lambda { |arg| arg.is_a?(contract) }
254-
else
255-
lambda { |arg| contract == arg }
256-
end
257-
end
258-
end
259-
260222
def [](*args, &blk)
261223
call(*args, &blk)
262224
end
@@ -287,99 +249,6 @@ def maybe_append_options! args, blk
287249
end
288250
end
289251

290-
def call_with(this, *args, &blk)
291-
args << blk if blk
292-
293-
# Explicitly append blk=nil if nil != Proc contract violation anticipated
294-
maybe_append_block!(args, blk)
295-
296-
# Explicitly append options={} if Hash contract is present
297-
maybe_append_options!(args, blk)
298-
299-
# Loop forward validating the arguments up to the splat (if there is one)
300-
(@args_contract_index || args.size).times do |i|
301-
contract = args_contracts[i]
302-
arg = args[i]
303-
validator = @args_validators[i]
304-
305-
unless validator && validator[arg]
306-
return unless Contract.failure_callback(:arg => arg,
307-
:contract => contract,
308-
:class => klass,
309-
:method => method,
310-
:contracts => self,
311-
:arg_pos => i+1,
312-
:total_args => args.size,
313-
:return_value => false)
314-
end
315-
316-
if contract.is_a?(Contracts::Func)
317-
args[i] = Contract.new(klass, arg, *contract.contracts)
318-
end
319-
end
320-
321-
# If there is a splat loop backwards to the lower index of the splat
322-
# Once we hit the splat in this direction set its upper index
323-
# Keep validating but use this upper index to get the splat validator.
324-
if @args_contract_index
325-
splat_upper_index = @args_contract_index
326-
(args.size - @args_contract_index).times do |i|
327-
arg = args[args.size - 1 - i]
328-
329-
if args_contracts[args_contracts.size - 1 - i].is_a?(Contracts::Args)
330-
splat_upper_index = i
331-
end
332-
333-
# Each arg after the spat is found must use the splat validator
334-
j = i < splat_upper_index ? i : splat_upper_index
335-
contract = args_contracts[args_contracts.size - 1 - j]
336-
validator = @args_validators[args_contracts.size - 1 - j]
337-
338-
unless validator && validator[arg]
339-
return unless Contract.failure_callback(:arg => arg,
340-
:contract => contract,
341-
:class => klass,
342-
:method => method,
343-
:contracts => self,
344-
:arg_pos => i-1,
345-
:total_args => args.size,
346-
:return_value => false)
347-
end
348-
349-
if contract.is_a?(Contracts::Func)
350-
args[args.size - 1 - i] = Contract.new(klass, arg, *contract.contracts)
351-
end
352-
end
353-
end
354-
355-
# If we put the block into args for validating, restore the args
356-
args.slice!(-1) if blk
357-
result = if method.respond_to?(:call)
358-
# proc, block, lambda, etc
359-
method.call(*args, &blk)
360-
else
361-
# original method name referrence
362-
method.send_to(this, *args, &blk)
363-
end
364-
365-
unless @ret_validator[result]
366-
Contract.failure_callback(:arg => result,
367-
:contract => ret_contract,
368-
:class => klass,
369-
:method => method,
370-
:contracts => self,
371-
:return_value => true)
372-
end
373-
374-
this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
375-
376-
if ret_contract.is_a?(Contracts::Func)
377-
result = Contract.new(klass, result, *ret_contract.contracts)
378-
end
379-
380-
result
381-
end
382-
383252
# Used to determine type of failure exception this contract should raise in case of failure
384253
def failure_exception
385254
if @pattern_match

lib/contracts/call_with.rb

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
module Contracts
2+
module CallWith
3+
def call_with(this, *args, &blk)
4+
args << blk if blk
5+
6+
# Explicitly append blk=nil if nil != Proc contract violation anticipated
7+
maybe_append_block!(args, blk)
8+
9+
# Explicitly append options={} if Hash contract is present
10+
maybe_append_options!(args, blk)
11+
12+
# Loop forward validating the arguments up to the splat (if there is one)
13+
(@args_contract_index || args.size).times do |i|
14+
contract = args_contracts[i]
15+
arg = args[i]
16+
validator = @args_validators[i]
17+
18+
unless validator && validator[arg]
19+
return unless Contract.failure_callback(:arg => arg,
20+
:contract => contract,
21+
:class => klass,
22+
:method => method,
23+
:contracts => self,
24+
:arg_pos => i+1,
25+
:total_args => args.size,
26+
:return_value => false)
27+
end
28+
29+
if contract.is_a?(Contracts::Func)
30+
args[i] = Contract.new(klass, arg, *contract.contracts)
31+
end
32+
end
33+
34+
# If there is a splat loop backwards to the lower index of the splat
35+
# Once we hit the splat in this direction set its upper index
36+
# Keep validating but use this upper index to get the splat validator.
37+
if @args_contract_index
38+
splat_upper_index = @args_contract_index
39+
(args.size - @args_contract_index).times do |i|
40+
arg = args[args.size - 1 - i]
41+
42+
if args_contracts[args_contracts.size - 1 - i].is_a?(Contracts::Args)
43+
splat_upper_index = i
44+
end
45+
46+
# Each arg after the spat is found must use the splat validator
47+
j = i < splat_upper_index ? i : splat_upper_index
48+
contract = args_contracts[args_contracts.size - 1 - j]
49+
validator = @args_validators[args_contracts.size - 1 - j]
50+
51+
unless validator && validator[arg]
52+
return unless Contract.failure_callback(:arg => arg,
53+
:contract => contract,
54+
:class => klass,
55+
:method => method,
56+
:contracts => self,
57+
:arg_pos => i-1,
58+
:total_args => args.size,
59+
:return_value => false)
60+
end
61+
62+
if contract.is_a?(Contracts::Func)
63+
args[args.size - 1 - i] = Contract.new(klass, arg, *contract.contracts)
64+
end
65+
end
66+
end
67+
68+
# If we put the block into args for validating, restore the args
69+
args.slice!(-1) if blk
70+
result = if method.respond_to?(:call)
71+
# proc, block, lambda, etc
72+
method.call(*args, &blk)
73+
else
74+
# original method name referrence
75+
method.send_to(this, *args, &blk)
76+
end
77+
78+
unless @ret_validator[result]
79+
Contract.failure_callback(:arg => result,
80+
:contract => ret_contract,
81+
:class => klass,
82+
:method => method,
83+
:contracts => self,
84+
:return_value => true)
85+
end
86+
87+
this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
88+
89+
if ret_contract.is_a?(Contracts::Func)
90+
result = Contract.new(klass, result, *ret_contract.contracts)
91+
end
92+
93+
result
94+
end
95+
end
96+
end

lib/contracts/support.rb

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def unique_id
2929
(Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36)
3030
end
3131

32+
def contract_id(contract)
33+
contract.object_id
34+
end
35+
3236
def eigenclass_hierarchy_supported?
3337
return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
3438
RUBY_VERSION.to_f > 1.8

0 commit comments

Comments
 (0)