contracts 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +13 -5
- data/CHANGELOG.markdown +8 -0
- data/Gemfile +3 -0
- data/README.md +14 -10
- data/TUTORIAL.md +34 -1
- data/benchmarks/io.rb +3 -3
- data/cucumber.yml +1 -0
- data/features/README.md +17 -0
- data/features/basics/functype.feature +71 -0
- data/features/basics/simple_example.feature +210 -0
- data/features/builtin_contracts/README.md +22 -0
- data/features/builtin_contracts/and.feature +103 -0
- data/features/builtin_contracts/any.feature +44 -0
- data/features/builtin_contracts/args.feature +1 -0
- data/features/builtin_contracts/array_of.feature +1 -0
- data/features/builtin_contracts/bool.feature +64 -0
- data/features/builtin_contracts/enum.feature +1 -0
- data/features/builtin_contracts/eq.feature +1 -0
- data/features/builtin_contracts/exactly.feature +1 -0
- data/features/builtin_contracts/func.feature +1 -0
- data/features/builtin_contracts/hash_of.feature +1 -0
- data/features/builtin_contracts/keyword_args.feature +1 -0
- data/features/builtin_contracts/maybe.feature +1 -0
- data/features/builtin_contracts/nat.feature +115 -0
- data/features/builtin_contracts/neg.feature +115 -0
- data/features/builtin_contracts/none.feature +145 -0
- data/features/builtin_contracts/not.feature +1 -0
- data/features/builtin_contracts/num.feature +64 -0
- data/features/builtin_contracts/or.feature +83 -0
- data/features/builtin_contracts/pos.feature +116 -0
- data/features/builtin_contracts/range_of.feature +1 -0
- data/features/builtin_contracts/respond_to.feature +78 -0
- data/features/builtin_contracts/send.feature +147 -0
- data/features/builtin_contracts/set_of.feature +1 -0
- data/features/builtin_contracts/xor.feature +99 -0
- data/features/support/env.rb +6 -0
- data/lib/contracts.rb +1 -1
- data/lib/contracts/builtin_contracts.rb +356 -351
- data/lib/contracts/core.rb +11 -2
- data/lib/contracts/formatters.rb +2 -2
- data/lib/contracts/validators.rb +6 -0
- data/lib/contracts/version.rb +1 -1
- data/script/cucumber +5 -0
- data/script/docs-release +3 -0
- data/script/docs-staging +3 -0
- data/spec/builtin_contracts_spec.rb +1 -1
- data/spec/contracts_spec.rb +29 -0
- data/spec/fixtures/fixtures.rb +12 -0
- data/spec/validators_spec.rb +25 -3
- metadata +42 -9
@@ -0,0 +1,147 @@
|
|
1
|
+
Feature: Send
|
2
|
+
|
3
|
+
Takes a variable number of method names as symbols. Given an argument, all of
|
4
|
+
those methods are called on the argument one by one. If they all return true,
|
5
|
+
the contract passes.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
Contract C::Send[:valid?, :has_items?] => C::ArrayOf[Item]
|
9
|
+
```
|
10
|
+
|
11
|
+
This contract will pass only if:
|
12
|
+
`arg.valid? == true && arg.has_items? == true`,
|
13
|
+
where `arg` is the first argument.
|
14
|
+
|
15
|
+
Background:
|
16
|
+
Given a file named "item.rb" with:
|
17
|
+
"""ruby
|
18
|
+
Item = Struct.new(:name, :cost)
|
19
|
+
Item::DEFAULT = Item["default", 0]
|
20
|
+
"""
|
21
|
+
|
22
|
+
Given a file named "send_usage.rb" with:
|
23
|
+
"""ruby
|
24
|
+
require "contracts"
|
25
|
+
C = Contracts
|
26
|
+
require "./item"
|
27
|
+
|
28
|
+
class FetchItemCommand
|
29
|
+
include Contracts::Core
|
30
|
+
|
31
|
+
Contract C::Send[:valid?, :has_items?] => C::ArrayOf[Item]
|
32
|
+
def call(subject)
|
33
|
+
([Item::DEFAULT] + subject.items).uniq
|
34
|
+
end
|
35
|
+
end
|
36
|
+
"""
|
37
|
+
|
38
|
+
Scenario: All methods return `true`
|
39
|
+
Given a file named "box.rb" with:
|
40
|
+
"""ruby
|
41
|
+
class Box
|
42
|
+
def valid?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_items?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def items
|
51
|
+
[Item["cat", 599.99]]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
require "./send_usage"
|
56
|
+
p FetchItemCommand.new.call(Box.new)
|
57
|
+
"""
|
58
|
+
When I run `ruby box.rb`
|
59
|
+
Then output should contain:
|
60
|
+
"""
|
61
|
+
[#<struct Item name="default", cost=0>, #<struct Item name="cat", cost=599.99>]
|
62
|
+
"""
|
63
|
+
|
64
|
+
Scenario: When second method returns `false`
|
65
|
+
Given a file named "cat.rb" with:
|
66
|
+
"""ruby
|
67
|
+
class Cat
|
68
|
+
def valid?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_items?
|
73
|
+
false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
require "./send_usage"
|
78
|
+
p FetchItemCommand.new.call(Cat.new)
|
79
|
+
"""
|
80
|
+
When I run `ruby cat.rb`
|
81
|
+
Then output should contain:
|
82
|
+
"""
|
83
|
+
: Contract violation for argument 1 of 1: (ParamContractError)
|
84
|
+
Expected: (a value that returns true for all of [:valid?, :has_items?]),
|
85
|
+
"""
|
86
|
+
And output should contain:
|
87
|
+
"""
|
88
|
+
Actual: #<Cat
|
89
|
+
"""
|
90
|
+
|
91
|
+
Scenario: When first method returns `false`
|
92
|
+
Given a file named "invalid.rb" with:
|
93
|
+
"""ruby
|
94
|
+
class Invalid
|
95
|
+
def valid?
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
def has_items?
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def items
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
require "./send_usage"
|
109
|
+
p FetchItemCommand.new.call(Invalid.new)
|
110
|
+
"""
|
111
|
+
When I run `ruby invalid.rb`
|
112
|
+
Then output should contain:
|
113
|
+
"""
|
114
|
+
: Contract violation for argument 1 of 1: (ParamContractError)
|
115
|
+
Expected: (a value that returns true for all of [:valid?, :has_items?]),
|
116
|
+
"""
|
117
|
+
And output should contain:
|
118
|
+
"""
|
119
|
+
Actual: #<Invalid
|
120
|
+
"""
|
121
|
+
|
122
|
+
Scenario: When all methods return `false`
|
123
|
+
Given a file named "nothing.rb" with:
|
124
|
+
"""ruby
|
125
|
+
class Nothing
|
126
|
+
def valid?
|
127
|
+
false
|
128
|
+
end
|
129
|
+
|
130
|
+
def has_items?
|
131
|
+
false
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
require "./send_usage"
|
136
|
+
p FetchItemCommand.new.call(Nothing.new)
|
137
|
+
"""
|
138
|
+
When I run `ruby nothing.rb`
|
139
|
+
Then output should contain:
|
140
|
+
"""
|
141
|
+
: Contract violation for argument 1 of 1: (ParamContractError)
|
142
|
+
Expected: (a value that returns true for all of [:valid?, :has_items?]),
|
143
|
+
"""
|
144
|
+
And output should contain:
|
145
|
+
"""
|
146
|
+
Actual: #<Nothing
|
147
|
+
"""
|
@@ -0,0 +1 @@
|
|
1
|
+
Feature: SetOf (TODO)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
Feature: Xor
|
2
|
+
|
3
|
+
Takes a variable number of contracts. The contract passes if one and only one
|
4
|
+
of the contracts pass.
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
Contract C::Xor[Float, C::Neg] => String
|
8
|
+
```
|
9
|
+
|
10
|
+
This example will validate first argument of a method and accept either
|
11
|
+
`Float` or natural integer, but not both.
|
12
|
+
|
13
|
+
Background:
|
14
|
+
Given a file named "xor_usage.rb" with:
|
15
|
+
"""ruby
|
16
|
+
require "contracts"
|
17
|
+
C = Contracts
|
18
|
+
|
19
|
+
class Example
|
20
|
+
include Contracts::Core
|
21
|
+
|
22
|
+
Contract C::Xor[Float, C::Neg] => String
|
23
|
+
def strange_number(number)
|
24
|
+
number.to_i.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
"""
|
28
|
+
|
29
|
+
Scenario: Accepts float
|
30
|
+
Given a file named "accepts_float.rb" with:
|
31
|
+
"""ruby
|
32
|
+
require "./xor_usage"
|
33
|
+
puts Example.new.strange_number(3.7)
|
34
|
+
"""
|
35
|
+
When I run `ruby accepts_float.rb`
|
36
|
+
Then output should contain:
|
37
|
+
"""
|
38
|
+
3
|
39
|
+
"""
|
40
|
+
|
41
|
+
Scenario: Accepts negative integer
|
42
|
+
Given a file named "accepts_negative_integer.rb" with:
|
43
|
+
"""ruby
|
44
|
+
require "./xor_usage"
|
45
|
+
puts Example.new.strange_number(-7)
|
46
|
+
"""
|
47
|
+
When I run `ruby accepts_negative_integer.rb`
|
48
|
+
Then output should contain:
|
49
|
+
"""
|
50
|
+
-7
|
51
|
+
"""
|
52
|
+
|
53
|
+
Scenario: Rejects negative float
|
54
|
+
Given a file named "rejects_negative_float.rb" with:
|
55
|
+
"""ruby
|
56
|
+
require "./xor_usage"
|
57
|
+
puts Example.new.strange_number(-3.5)
|
58
|
+
"""
|
59
|
+
When I run `ruby rejects_negative_float.rb`
|
60
|
+
Then output should contain:
|
61
|
+
"""
|
62
|
+
: Contract violation for argument 1 of 1: (ParamContractError)
|
63
|
+
Expected: (Float xor Neg),
|
64
|
+
Actual: -3.5
|
65
|
+
Value guarded in: Example::strange_number
|
66
|
+
With Contract: Xor => String
|
67
|
+
"""
|
68
|
+
|
69
|
+
Scenario: Rejects positive integer
|
70
|
+
Given a file named "rejects_positive_integer.rb" with:
|
71
|
+
"""ruby
|
72
|
+
require "./xor_usage"
|
73
|
+
puts Example.new.strange_number(9)
|
74
|
+
"""
|
75
|
+
When I run `ruby rejects_positive_integer.rb`
|
76
|
+
Then output should contain:
|
77
|
+
"""
|
78
|
+
: Contract violation for argument 1 of 1: (ParamContractError)
|
79
|
+
Expected: (Float xor Neg),
|
80
|
+
Actual: 9
|
81
|
+
Value guarded in: Example::strange_number
|
82
|
+
With Contract: Xor => String
|
83
|
+
"""
|
84
|
+
|
85
|
+
Scenario: Rejects other values
|
86
|
+
Given a file named "rejects_other.rb" with:
|
87
|
+
"""ruby
|
88
|
+
require "./xor_usage"
|
89
|
+
puts Example.new.strange_number(:foo)
|
90
|
+
"""
|
91
|
+
When I run `ruby rejects_other.rb`
|
92
|
+
Then output should contain:
|
93
|
+
"""
|
94
|
+
: Contract violation for argument 1 of 1: (ParamContractError)
|
95
|
+
Expected: (Float xor Neg),
|
96
|
+
Actual: :foo
|
97
|
+
Value guarded in: Example::strange_number
|
98
|
+
With Contract: Xor => String
|
99
|
+
"""
|
data/lib/contracts.rb
CHANGED
@@ -108,7 +108,7 @@ class Contract < Contracts::Decorator
|
|
108
108
|
def to_s
|
109
109
|
args = args_contracts.map { |c| pretty_contract(c) }.join(", ")
|
110
110
|
ret = pretty_contract(ret_contract)
|
111
|
-
("#{args} => #{ret}").gsub("Contracts::", "")
|
111
|
+
("#{args} => #{ret}").gsub("Contracts::Builtin::", "")
|
112
112
|
end
|
113
113
|
|
114
114
|
# Given a hash, prints out a failure message.
|
@@ -19,460 +19,465 @@ require "set"
|
|
19
19
|
# The contract is <tt>Contract Num, Num, Num</tt>.
|
20
20
|
# That says that the +add+ function takes two numbers and returns a number.
|
21
21
|
module Contracts
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
# Check that an argument is a positive number.
|
30
|
-
class Pos
|
31
|
-
def self.valid? val
|
32
|
-
val && val.is_a?(Numeric) && val > 0
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Check that an argument is a negative number.
|
37
|
-
class Neg
|
38
|
-
def self.valid? val
|
39
|
-
val && val.is_a?(Numeric) && val < 0
|
22
|
+
module Builtin
|
23
|
+
# Check that an argument is +Numeric+.
|
24
|
+
class Num
|
25
|
+
def self.valid? val
|
26
|
+
val.is_a? Numeric
|
27
|
+
end
|
40
28
|
end
|
41
|
-
end
|
42
29
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
30
|
+
# Check that an argument is a positive number.
|
31
|
+
class Pos
|
32
|
+
def self.valid? val
|
33
|
+
val && val.is_a?(Numeric) && val > 0
|
34
|
+
end
|
47
35
|
end
|
48
|
-
end
|
49
36
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
37
|
+
# Check that an argument is a negative number.
|
38
|
+
class Neg
|
39
|
+
def self.valid? val
|
40
|
+
val && val.is_a?(Numeric) && val < 0
|
41
|
+
end
|
54
42
|
end
|
55
|
-
end
|
56
43
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
44
|
+
# Check that an argument is a natural number.
|
45
|
+
class Nat
|
46
|
+
def self.valid? val
|
47
|
+
val && val.is_a?(Integer) && val >= 0
|
48
|
+
end
|
61
49
|
end
|
62
|
-
end
|
63
50
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# New: <tt>Or[param1, param2]</tt>
|
70
|
-
#
|
71
|
-
# Of course, <tt>.new</tt> still works.
|
72
|
-
class CallableClass
|
73
|
-
include ::Contracts::Formatters
|
74
|
-
def self.[](*vals)
|
75
|
-
new(*vals)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# Takes a variable number of contracts.
|
80
|
-
# The contract passes if any of the contracts pass.
|
81
|
-
# Example: <tt>Or[Fixnum, Float]</tt>
|
82
|
-
class Or < CallableClass
|
83
|
-
def initialize(*vals)
|
84
|
-
@vals = vals
|
51
|
+
# Passes for any argument.
|
52
|
+
class Any
|
53
|
+
def self.valid? val
|
54
|
+
true
|
55
|
+
end
|
85
56
|
end
|
86
57
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
58
|
+
# Fails for any argument.
|
59
|
+
class None
|
60
|
+
def self.valid? val
|
61
|
+
false
|
91
62
|
end
|
92
63
|
end
|
93
64
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
65
|
+
# Use this when you are writing your own contract classes.
|
66
|
+
# Allows your contract to be called with <tt>[]</tt> instead of <tt>.new</tt>:
|
67
|
+
#
|
68
|
+
# Old: <tt>Or.new(param1, param2)</tt>
|
69
|
+
#
|
70
|
+
# New: <tt>Or[param1, param2]</tt>
|
71
|
+
#
|
72
|
+
# Of course, <tt>.new</tt> still works.
|
73
|
+
class CallableClass
|
74
|
+
include ::Contracts::Formatters
|
75
|
+
def self.[](*vals)
|
76
|
+
new(*vals)
|
77
|
+
end
|
98
78
|
end
|
99
|
-
end
|
100
79
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
80
|
+
# Takes a variable number of contracts.
|
81
|
+
# The contract passes if any of the contracts pass.
|
82
|
+
# Example: <tt>Or[Fixnum, Float]</tt>
|
83
|
+
class Or < CallableClass
|
84
|
+
def initialize(*vals)
|
85
|
+
@vals = vals
|
86
|
+
end
|
108
87
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
88
|
+
def valid?(val)
|
89
|
+
@vals.any? do |contract|
|
90
|
+
res, _ = Contract.valid?(val, contract)
|
91
|
+
res
|
92
|
+
end
|
113
93
|
end
|
114
|
-
results.count(true) == 1
|
115
|
-
end
|
116
94
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
95
|
+
def to_s
|
96
|
+
@vals[0, @vals.size-1].map do |x|
|
97
|
+
InspectWrapper.create(x)
|
98
|
+
end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s
|
99
|
+
end
|
121
100
|
end
|
122
|
-
end
|
123
101
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
102
|
+
# Takes a variable number of contracts.
|
103
|
+
# The contract passes if exactly one of those contracts pass.
|
104
|
+
# Example: <tt>Xor[Fixnum, Float]</tt>
|
105
|
+
class Xor < CallableClass
|
106
|
+
def initialize(*vals)
|
107
|
+
@vals = vals
|
108
|
+
end
|
131
109
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
110
|
+
def valid?(val)
|
111
|
+
results = @vals.map do |contract|
|
112
|
+
res, _ = Contract.valid?(val, contract)
|
113
|
+
res
|
114
|
+
end
|
115
|
+
results.count(true) == 1
|
136
116
|
end
|
137
|
-
end
|
138
117
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
118
|
+
def to_s
|
119
|
+
@vals[0, @vals.size-1].map do |x|
|
120
|
+
InspectWrapper.create(x)
|
121
|
+
end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s
|
122
|
+
end
|
143
123
|
end
|
144
|
-
end
|
145
124
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
end
|
125
|
+
# Takes a variable number of contracts.
|
126
|
+
# The contract passes if all contracts pass.
|
127
|
+
# Example: <tt>And[Fixnum, Float]</tt>
|
128
|
+
class And < CallableClass
|
129
|
+
def initialize(*vals)
|
130
|
+
@vals = vals
|
131
|
+
end
|
154
132
|
|
155
|
-
|
156
|
-
|
157
|
-
|
133
|
+
def valid?(val)
|
134
|
+
@vals.all? do |contract|
|
135
|
+
res, _ = Contract.valid?(val, contract)
|
136
|
+
res
|
137
|
+
end
|
158
138
|
end
|
159
|
-
end
|
160
139
|
|
161
|
-
|
162
|
-
|
140
|
+
def to_s
|
141
|
+
@vals[0, @vals.size-1].map do |x|
|
142
|
+
InspectWrapper.create(x)
|
143
|
+
end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s
|
144
|
+
end
|
163
145
|
end
|
164
|
-
end
|
165
146
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
end
|
147
|
+
# Takes a variable number of method names as symbols.
|
148
|
+
# The contract passes if the argument responds to all
|
149
|
+
# of those methods.
|
150
|
+
# Example: <tt>RespondTo[:password, :credit_card]</tt>
|
151
|
+
class RespondTo < CallableClass
|
152
|
+
def initialize(*meths)
|
153
|
+
@meths = meths
|
154
|
+
end
|
175
155
|
|
176
|
-
|
177
|
-
|
178
|
-
|
156
|
+
def valid?(val)
|
157
|
+
@meths.all? do |meth|
|
158
|
+
val.respond_to? meth
|
159
|
+
end
|
179
160
|
end
|
180
|
-
end
|
181
161
|
|
182
|
-
|
183
|
-
|
162
|
+
def to_s
|
163
|
+
"a value that responds to #{@meths.inspect}"
|
164
|
+
end
|
184
165
|
end
|
185
|
-
end
|
186
166
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
167
|
+
# Takes a variable number of method names as symbols.
|
168
|
+
# Given an argument, all of those methods are called
|
169
|
+
# on the argument one by one. If they all return true,
|
170
|
+
# the contract passes.
|
171
|
+
# Example: <tt>Send[:valid?]</tt>
|
172
|
+
class Send < CallableClass
|
173
|
+
def initialize(*meths)
|
174
|
+
@meths = meths
|
175
|
+
end
|
194
176
|
|
195
|
-
|
196
|
-
|
197
|
-
|
177
|
+
def valid?(val)
|
178
|
+
@meths.all? do |meth|
|
179
|
+
val.send(meth)
|
180
|
+
end
|
181
|
+
end
|
198
182
|
|
199
|
-
|
200
|
-
|
183
|
+
def to_s
|
184
|
+
"a value that returns true for all of #{@meths.inspect}"
|
185
|
+
end
|
201
186
|
end
|
202
|
-
end
|
203
187
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
end
|
188
|
+
# Takes a class +A+. If argument is object of type +A+, the contract passes.
|
189
|
+
# If it is a subclass of A (or not related to A in any way), it fails.
|
190
|
+
# Example: <tt>Exactly[Numeric]</tt>
|
191
|
+
class Exactly < CallableClass
|
192
|
+
def initialize(cls)
|
193
|
+
@cls = cls
|
194
|
+
end
|
212
195
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
end
|
196
|
+
def valid?(val)
|
197
|
+
val.class == @cls
|
198
|
+
end
|
217
199
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
class Eq < CallableClass
|
222
|
-
def initialize(value)
|
223
|
-
@value = value
|
200
|
+
def to_s
|
201
|
+
"exactly #{@cls.inspect}"
|
202
|
+
end
|
224
203
|
end
|
225
204
|
|
226
|
-
|
227
|
-
|
228
|
-
|
205
|
+
# Takes a list of values, e.g. +[:a, :b, :c]+. If argument is included in
|
206
|
+
# the list, the contract passes.
|
207
|
+
#
|
208
|
+
# Example: <tt>Enum[:a, :b, :c]</tt>?
|
209
|
+
class Enum < CallableClass
|
210
|
+
def initialize(*vals)
|
211
|
+
@vals = vals
|
212
|
+
end
|
229
213
|
|
230
|
-
|
231
|
-
|
214
|
+
def valid?(val)
|
215
|
+
@vals.include? val
|
216
|
+
end
|
232
217
|
end
|
233
|
-
end
|
234
218
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
219
|
+
# Takes a value +v+. If the argument is +.equal+ to +v+, the contract passes,
|
220
|
+
# otherwise the contract fails.
|
221
|
+
# Example: <tt>Eq[Class]</tt>
|
222
|
+
class Eq < CallableClass
|
223
|
+
def initialize(value)
|
224
|
+
@value = value
|
225
|
+
end
|
242
226
|
|
243
|
-
|
244
|
-
|
245
|
-
res, _ = Contract.valid?(val, contract)
|
246
|
-
!res
|
227
|
+
def valid?(val)
|
228
|
+
@value.equal?(val)
|
247
229
|
end
|
248
|
-
end
|
249
230
|
|
250
|
-
|
251
|
-
|
231
|
+
def to_s
|
232
|
+
"to be equal to #{@value.inspect}"
|
233
|
+
end
|
252
234
|
end
|
253
|
-
end
|
254
235
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
def initialize(collection_class, contract)
|
263
|
-
@collection_class = collection_class
|
264
|
-
@contract = contract
|
265
|
-
end
|
236
|
+
# Takes a variable number of contracts. The contract
|
237
|
+
# passes if all of those contracts fail for the given argument.
|
238
|
+
# Example: <tt>Not[nil]</tt>
|
239
|
+
class Not < CallableClass
|
240
|
+
def initialize(*vals)
|
241
|
+
@vals = vals
|
242
|
+
end
|
266
243
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
244
|
+
def valid?(val)
|
245
|
+
@vals.all? do |contract|
|
246
|
+
res, _ = Contract.valid?(val, contract)
|
247
|
+
!res
|
248
|
+
end
|
272
249
|
end
|
273
|
-
end
|
274
250
|
|
275
|
-
|
276
|
-
|
251
|
+
def to_s
|
252
|
+
"a value that is none of #{@vals.inspect}"
|
253
|
+
end
|
277
254
|
end
|
278
255
|
|
279
|
-
|
280
|
-
|
256
|
+
# @private
|
257
|
+
# Takes a collection(responds to :each) type and a contract.
|
258
|
+
# The related argument must be of specified collection type.
|
259
|
+
# Checks the contract against every element of the collection.
|
260
|
+
# If it passes for all elements, the contract passes.
|
261
|
+
# Example: <tt>CollectionOf[Array, Num]</tt>
|
262
|
+
class CollectionOf < CallableClass
|
263
|
+
def initialize(collection_class, contract)
|
281
264
|
@collection_class = collection_class
|
282
|
-
@
|
265
|
+
@contract = contract
|
283
266
|
end
|
284
267
|
|
285
|
-
def
|
286
|
-
|
287
|
-
|
268
|
+
def valid?(vals)
|
269
|
+
return false unless vals.is_a?(@collection_class)
|
270
|
+
vals.all? do |val|
|
271
|
+
res, _ = Contract.valid?(val, @contract)
|
272
|
+
res
|
273
|
+
end
|
288
274
|
end
|
289
275
|
|
290
|
-
|
291
|
-
|
292
|
-
|
276
|
+
def to_s
|
277
|
+
"a collection #{@collection_class} of #{@contract}"
|
278
|
+
end
|
293
279
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
# Takes a contract. The related argument must be a set.
|
301
|
-
# Checks the contract against every element of the set.
|
302
|
-
# If it passes for all elements, the contract passes.
|
303
|
-
# Example: <tt>SetOf[Num]</tt>
|
304
|
-
SetOf = CollectionOf::Factory.new(Set)
|
305
|
-
|
306
|
-
# Used for <tt>*args</tt> (variadic functions). Takes a contract
|
307
|
-
# and uses it to validate every element passed in
|
308
|
-
# through <tt>*args</tt>.
|
309
|
-
# Example: <tt>Args[Or[String, Num]]</tt>
|
310
|
-
class Args < CallableClass
|
311
|
-
attr_reader :contract
|
312
|
-
def initialize(contract)
|
313
|
-
@contract = contract
|
314
|
-
end
|
280
|
+
class Factory
|
281
|
+
def initialize(collection_class, &before_new)
|
282
|
+
@collection_class = collection_class
|
283
|
+
@before_new = before_new
|
284
|
+
end
|
315
285
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
286
|
+
def new(contract)
|
287
|
+
@before_new && @before_new.call
|
288
|
+
CollectionOf.new(@collection_class, contract)
|
289
|
+
end
|
320
290
|
|
321
|
-
|
322
|
-
|
323
|
-
val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
291
|
+
alias_method :[], :new
|
292
|
+
end
|
324
293
|
end
|
325
|
-
end
|
326
294
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
295
|
+
# Takes a contract. The related argument must be an array.
|
296
|
+
# Checks the contract against every element of the array.
|
297
|
+
# If it passes for all elements, the contract passes.
|
298
|
+
# Example: <tt>ArrayOf[Num]</tt>
|
299
|
+
ArrayOf = CollectionOf::Factory.new(Array)
|
300
|
+
|
301
|
+
# Takes a contract. The related argument must be a set.
|
302
|
+
# Checks the contract against every element of the set.
|
303
|
+
# If it passes for all elements, the contract passes.
|
304
|
+
# Example: <tt>SetOf[Num]</tt>
|
305
|
+
SetOf = CollectionOf::Factory.new(Set)
|
306
|
+
|
307
|
+
# Used for <tt>*args</tt> (variadic functions). Takes a contract
|
308
|
+
# and uses it to validate every element passed in
|
309
|
+
# through <tt>*args</tt>.
|
310
|
+
# Example: <tt>Args[Or[String, Num]]</tt>
|
311
|
+
class Args < CallableClass
|
312
|
+
attr_reader :contract
|
313
|
+
def initialize(contract)
|
314
|
+
@contract = contract
|
315
|
+
end
|
333
316
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
Contract.valid?(val.last, @contract)
|
317
|
+
def to_s
|
318
|
+
"Args[#{@contract}]"
|
319
|
+
end
|
338
320
|
end
|
339
321
|
|
340
|
-
|
341
|
-
|
322
|
+
class Bool
|
323
|
+
def self.valid? val
|
324
|
+
val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
325
|
+
end
|
342
326
|
end
|
343
|
-
end
|
344
327
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
def initialize(key, value = nil)
|
352
|
-
if value
|
353
|
-
@key = key
|
354
|
-
@value = value
|
355
|
-
else
|
356
|
-
validate_hash(key)
|
357
|
-
@key = key.keys.first
|
358
|
-
@value = key[@key]
|
328
|
+
# Use this to specify a Range object of a particular datatype.
|
329
|
+
# Example: <tt>RangeOf[Nat]</tt>, <tt>RangeOf[Date]</tt>, ...
|
330
|
+
class RangeOf < CallableClass
|
331
|
+
def initialize(contract)
|
332
|
+
@contract = contract
|
359
333
|
end
|
360
|
-
end
|
361
334
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
335
|
+
def valid?(val)
|
336
|
+
val.is_a?(Range) &&
|
337
|
+
Contract.valid?(val.first, @contract) &&
|
338
|
+
Contract.valid?(val.last, @contract)
|
339
|
+
end
|
366
340
|
|
367
|
-
|
341
|
+
def to_s
|
342
|
+
"a range of #{@contract}"
|
343
|
+
end
|
368
344
|
end
|
369
345
|
|
370
|
-
|
371
|
-
|
372
|
-
|
346
|
+
# Use this to specify the Hash characteristics. Takes two contracts,
|
347
|
+
# one for hash keys and one for hash values.
|
348
|
+
# Example: <tt>HashOf[Symbol, String]</tt>
|
349
|
+
class HashOf < CallableClass
|
350
|
+
INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract"
|
351
|
+
|
352
|
+
def initialize(key, value = nil)
|
353
|
+
if value
|
354
|
+
@key = key
|
355
|
+
@value = value
|
356
|
+
else
|
357
|
+
validate_hash(key)
|
358
|
+
@key = key.keys.first
|
359
|
+
@value = key[@key]
|
360
|
+
end
|
361
|
+
end
|
373
362
|
|
374
|
-
|
363
|
+
def valid?(hash)
|
364
|
+
return false unless hash.is_a?(Hash)
|
365
|
+
keys_match = hash.keys.map { |k| Contract.valid?(k, @key) }.all?
|
366
|
+
vals_match = hash.values.map { |v| Contract.valid?(v, @value) }.all?
|
375
367
|
|
376
|
-
|
377
|
-
|
378
|
-
end
|
379
|
-
end
|
368
|
+
[keys_match, vals_match].all?
|
369
|
+
end
|
380
370
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
end
|
371
|
+
def to_s
|
372
|
+
"Hash<#{@key}, #{@value}>"
|
373
|
+
end
|
374
|
+
|
375
|
+
private
|
387
376
|
|
388
|
-
|
389
|
-
|
390
|
-
options.all? do |key, contract|
|
391
|
-
Optional._valid?(hash, key, contract)
|
377
|
+
def validate_hash(hash)
|
378
|
+
fail ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1
|
392
379
|
end
|
393
380
|
end
|
394
381
|
|
395
|
-
|
396
|
-
|
397
|
-
|
382
|
+
# Use this for specifying contracts for keyword arguments
|
383
|
+
# Example: <tt>KeywordArgs[ e: Range, f: Optional[Num] ]</tt>
|
384
|
+
class KeywordArgs < CallableClass
|
385
|
+
def initialize(options)
|
386
|
+
@options = options
|
387
|
+
end
|
398
388
|
|
399
|
-
|
400
|
-
|
401
|
-
|
389
|
+
def valid?(hash)
|
390
|
+
return false unless hash.keys - options.keys == []
|
391
|
+
options.all? do |key, contract|
|
392
|
+
Optional._valid?(hash, key, contract)
|
393
|
+
end
|
394
|
+
end
|
402
395
|
|
403
|
-
|
396
|
+
def to_s
|
397
|
+
"KeywordArgs[#{options}]"
|
398
|
+
end
|
404
399
|
|
405
|
-
|
406
|
-
|
400
|
+
def inspect
|
401
|
+
to_s
|
402
|
+
end
|
407
403
|
|
408
|
-
|
409
|
-
# Example: <tt>Optional[Num]</tt>
|
410
|
-
class Optional < CallableClass
|
411
|
-
UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH =
|
412
|
-
"Unable to use Optional contract outside of KeywordArgs contract"
|
404
|
+
private
|
413
405
|
|
414
|
-
|
415
|
-
return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional)
|
416
|
-
contract.within_opt_hash!
|
417
|
-
!hash.key?(key) || Contract.valid?(hash[key], contract)
|
406
|
+
attr_reader :options
|
418
407
|
end
|
419
408
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
409
|
+
# Use this for specifying optional keyword argument
|
410
|
+
# Example: <tt>Optional[Num]</tt>
|
411
|
+
class Optional < CallableClass
|
412
|
+
UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH =
|
413
|
+
"Unable to use Optional contract outside of KeywordArgs contract"
|
424
414
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
415
|
+
def self._valid?(hash, key, contract)
|
416
|
+
return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional)
|
417
|
+
contract.within_opt_hash!
|
418
|
+
!hash.key?(key) || Contract.valid?(hash[key], contract)
|
419
|
+
end
|
429
420
|
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
421
|
+
def initialize(contract)
|
422
|
+
@contract = contract
|
423
|
+
@within_opt_hash = false
|
424
|
+
end
|
434
425
|
|
435
|
-
|
436
|
-
|
437
|
-
|
426
|
+
def within_opt_hash!
|
427
|
+
@within_opt_hash = true
|
428
|
+
self
|
429
|
+
end
|
438
430
|
|
439
|
-
|
440
|
-
|
441
|
-
|
431
|
+
def valid?(value)
|
432
|
+
ensure_within_opt_hash
|
433
|
+
Contract.valid?(value, contract)
|
434
|
+
end
|
442
435
|
|
443
|
-
|
436
|
+
def to_s
|
437
|
+
"Optional[#{formatted_contract}]"
|
438
|
+
end
|
444
439
|
|
445
|
-
|
440
|
+
def inspect
|
441
|
+
to_s
|
442
|
+
end
|
446
443
|
|
447
|
-
|
448
|
-
return if within_opt_hash
|
449
|
-
fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
|
450
|
-
end
|
444
|
+
private
|
451
445
|
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
446
|
+
attr_reader :contract, :within_opt_hash
|
447
|
+
|
448
|
+
def ensure_within_opt_hash
|
449
|
+
return if within_opt_hash
|
450
|
+
fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH
|
451
|
+
end
|
456
452
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
class Maybe < Or
|
461
|
-
def initialize(*vals)
|
462
|
-
super(*(vals + [nil]))
|
453
|
+
def formatted_contract
|
454
|
+
Formatters::InspectWrapper.create(contract)
|
455
|
+
end
|
463
456
|
end
|
464
457
|
|
465
|
-
|
466
|
-
|
458
|
+
# Takes a Contract.
|
459
|
+
# The contract passes if the contract passes or the given value is nil.
|
460
|
+
# Maybe(foo) is equivalent to Or[foo, nil].
|
461
|
+
class Maybe < Or
|
462
|
+
def initialize(*vals)
|
463
|
+
super(*(vals + [nil]))
|
464
|
+
end
|
465
|
+
|
466
|
+
def include_proc?
|
467
|
+
@vals.include? Proc
|
468
|
+
end
|
467
469
|
end
|
468
|
-
end
|
469
470
|
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
471
|
+
# Used to define contracts on functions passed in as arguments.
|
472
|
+
# Example: <tt>Func[Num => Num] # the function should take a number and return a number</tt>
|
473
|
+
class Func < CallableClass
|
474
|
+
attr_reader :contracts
|
475
|
+
def initialize(*contracts)
|
476
|
+
@contracts = contracts
|
477
|
+
end
|
476
478
|
end
|
477
479
|
end
|
480
|
+
|
481
|
+
# Users can still include `Contracts::Core` & `Contracts::Builtin`
|
482
|
+
include Builtin
|
478
483
|
end
|