contracts-lite 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.markdown +80 -0
  3. data/Gemfile +16 -0
  4. data/LICENSE +23 -0
  5. data/README.md +102 -0
  6. data/TODO.markdown +6 -0
  7. data/TUTORIAL.md +747 -0
  8. data/benchmarks/bench.rb +67 -0
  9. data/benchmarks/hash.rb +69 -0
  10. data/benchmarks/invariants.rb +91 -0
  11. data/benchmarks/io.rb +62 -0
  12. data/benchmarks/wrap_test.rb +57 -0
  13. data/contracts.gemspec +13 -0
  14. data/lib/contracts.rb +231 -0
  15. data/lib/contracts/builtin_contracts.rb +541 -0
  16. data/lib/contracts/call_with.rb +97 -0
  17. data/lib/contracts/core.rb +52 -0
  18. data/lib/contracts/decorators.rb +47 -0
  19. data/lib/contracts/engine.rb +26 -0
  20. data/lib/contracts/engine/base.rb +136 -0
  21. data/lib/contracts/engine/eigenclass.rb +50 -0
  22. data/lib/contracts/engine/target.rb +70 -0
  23. data/lib/contracts/error_formatter.rb +121 -0
  24. data/lib/contracts/errors.rb +71 -0
  25. data/lib/contracts/formatters.rb +134 -0
  26. data/lib/contracts/invariants.rb +68 -0
  27. data/lib/contracts/method_handler.rb +195 -0
  28. data/lib/contracts/method_reference.rb +100 -0
  29. data/lib/contracts/support.rb +59 -0
  30. data/lib/contracts/validators.rb +139 -0
  31. data/lib/contracts/version.rb +3 -0
  32. data/script/rubocop +7 -0
  33. data/spec/builtin_contracts_spec.rb +461 -0
  34. data/spec/contracts_spec.rb +748 -0
  35. data/spec/error_formatter_spec.rb +68 -0
  36. data/spec/fixtures/fixtures.rb +710 -0
  37. data/spec/invariants_spec.rb +17 -0
  38. data/spec/module_spec.rb +18 -0
  39. data/spec/override_validators_spec.rb +162 -0
  40. data/spec/ruby_version_specific/contracts_spec_1.9.rb +24 -0
  41. data/spec/ruby_version_specific/contracts_spec_2.0.rb +55 -0
  42. data/spec/ruby_version_specific/contracts_spec_2.1.rb +63 -0
  43. data/spec/spec_helper.rb +102 -0
  44. data/spec/support.rb +10 -0
  45. data/spec/support_spec.rb +21 -0
  46. data/spec/validators_spec.rb +47 -0
  47. metadata +94 -0
@@ -0,0 +1,100 @@
1
+ module Contracts
2
+ # MethodReference represents original method reference that was
3
+ # decorated by contracts.ruby. Used for instance methods.
4
+ class MethodReference
5
+ attr_reader :name
6
+
7
+ # name - name of the method
8
+ # method - method object
9
+ def initialize(name, method)
10
+ @name = name
11
+ @method = method
12
+ end
13
+
14
+ # Returns method_position, delegates to Support.method_position
15
+ def method_position
16
+ Support.method_position(@method)
17
+ end
18
+
19
+ # Makes a method re-definition in proper way
20
+ def make_definition(this, &blk)
21
+ is_private = private?(this)
22
+ is_protected = protected?(this)
23
+ alias_target(this).send(:define_method, name, &blk)
24
+ make_private(this) if is_private
25
+ make_protected(this) if is_protected
26
+ end
27
+
28
+ # Aliases original method to a special unique name, which is known
29
+ # only to this class. Usually done right before re-defining the
30
+ # method.
31
+ def make_alias(this)
32
+ _aliased_name = aliased_name
33
+ original_name = name
34
+
35
+ alias_target(this).class_eval do
36
+ alias_method _aliased_name, original_name
37
+ end
38
+ end
39
+
40
+ # Calls original method on specified `this` argument with
41
+ # specified arguments `args` and block `&blk`.
42
+ def send_to(this, *args, &blk)
43
+ this.send(aliased_name, *args, &blk)
44
+ end
45
+
46
+ private
47
+
48
+ # Makes a method private
49
+ def make_private(this)
50
+ original_name = name
51
+ alias_target(this).class_eval { private original_name }
52
+ end
53
+
54
+ def private?(this)
55
+ this.private_instance_methods.map(&:to_sym).include?(name)
56
+ end
57
+
58
+ def protected?(this)
59
+ this.protected_instance_methods.map(&:to_sym).include?(name)
60
+ end
61
+
62
+ # Makes a method protected
63
+ def make_protected(this)
64
+ original_name = name
65
+ alias_target(this).class_eval { protected original_name }
66
+ end
67
+
68
+ # Returns alias target for instance methods, subject to be
69
+ # overriden in subclasses.
70
+ def alias_target(this)
71
+ this
72
+ end
73
+
74
+ def aliased_name
75
+ @_original_name ||= construct_unique_name
76
+ end
77
+
78
+ def construct_unique_name
79
+ :"__contracts_ruby_original_#{name}_#{Support.unique_id}"
80
+ end
81
+ end
82
+
83
+ # The same as MethodReference, but used for singleton methods.
84
+ class SingletonMethodReference < MethodReference
85
+ private
86
+
87
+ def private?(this)
88
+ this.private_methods.map(&:to_sym).include?(name)
89
+ end
90
+
91
+ def protected?(this)
92
+ this.protected_methods.map(&:to_sym).include?(name)
93
+ end
94
+
95
+ # Return alias target for singleton methods.
96
+ def alias_target(this)
97
+ Support.eigenclass_of this
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,59 @@
1
+ module Contracts
2
+ module Support
3
+ class << self
4
+ def method_position(method)
5
+ return method.method_position if method.is_a?(MethodReference)
6
+
7
+ if RUBY_VERSION =~ /^1\.8/
8
+ if method.respond_to?(:__file__)
9
+ method.__file__ + ":" + method.__line__.to_s
10
+ else
11
+ method.inspect
12
+ end
13
+ else
14
+ file, line = method.source_location
15
+ file + ":" + line.to_s
16
+ end
17
+ end
18
+
19
+ def method_name(method)
20
+ method.is_a?(Proc) ? "Proc" : method.name
21
+ end
22
+
23
+ # Generates unique id, which can be used as a part of identifier
24
+ #
25
+ # Example:
26
+ # Contracts::Support.unique_id # => "i53u6tiw5hbo"
27
+ def unique_id
28
+ # Consider using SecureRandom.hex here, and benchmark which one is better
29
+ (Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36)
30
+ end
31
+
32
+ def contract_id(contract)
33
+ contract.object_id
34
+ end
35
+
36
+ def eigenclass_hierarchy_supported?
37
+ return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
38
+ RUBY_VERSION.to_f > 1.8
39
+ end
40
+
41
+ def eigenclass_of(target)
42
+ class << target; self; end
43
+ end
44
+
45
+ def eigenclass?(target)
46
+ module_eigenclass?(target) ||
47
+ target <= eigenclass_of(Object)
48
+ end
49
+
50
+ private
51
+
52
+ # Module eigenclass can be detected by its ancestor chain
53
+ # containing a Module
54
+ def module_eigenclass?(target)
55
+ target < Module
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,139 @@
1
+ module Contracts
2
+ module Validators
3
+ DEFAULT_VALIDATOR_STRATEGIES = {
4
+ # e.g. lambda {true}
5
+ Proc => lambda { |contract| contract },
6
+
7
+ # e.g. [Num, String]
8
+ # TODO: account for these errors too
9
+ Array => lambda do |contract|
10
+ lambda do |arg|
11
+ return false unless arg.is_a?(Array) && arg.length == contract.length
12
+ arg.zip(contract).all? do |_arg, _contract|
13
+ Contract.valid?(_arg, _contract)
14
+ end
15
+ end
16
+ end,
17
+
18
+ # e.g. { :a => Num, :b => String }
19
+ Hash => lambda do |contract|
20
+ lambda do |arg|
21
+ return false unless arg.is_a?(Hash)
22
+ contract.keys.all? do |k|
23
+ Contract.valid?(arg[k], contract[k])
24
+ end
25
+ end
26
+ end,
27
+
28
+ Range => lambda do |contract|
29
+ lambda do |arg|
30
+ contract.include?(arg)
31
+ end
32
+ end,
33
+
34
+ Regexp => lambda do |contract|
35
+ lambda do |arg|
36
+ arg =~ contract
37
+ end
38
+ end,
39
+
40
+ Contracts::Args => lambda do |contract|
41
+ lambda do |arg|
42
+ Contract.valid?(arg, contract.contract)
43
+ end
44
+ end,
45
+
46
+ Contracts::Func => lambda do |_|
47
+ lambda do |arg|
48
+ arg.is_a?(Method) || arg.is_a?(Proc)
49
+ end
50
+ end,
51
+
52
+ :valid => lambda do |contract|
53
+ lambda { |arg| contract.valid?(arg) }
54
+ end,
55
+
56
+ :class => lambda do |contract|
57
+ lambda { |arg| arg.is_a?(contract) }
58
+ end,
59
+
60
+ :default => lambda do |contract|
61
+ lambda { |arg| contract == arg }
62
+ end
63
+ }.freeze
64
+
65
+ # Allows to override validator with custom one.
66
+ # Example:
67
+ # Contract.override_validator(Array) do |contract|
68
+ # lambda do |arg|
69
+ # # .. implementation for Array contract ..
70
+ # end
71
+ # end
72
+ #
73
+ # Contract.override_validator(:class) do |contract|
74
+ # lambda do |arg|
75
+ # arg.is_a?(contract) || arg.is_a?(RSpec::Mocks::Double)
76
+ # end
77
+ # end
78
+ def override_validator(name, &block)
79
+ validator_strategies[name] = block
80
+ end
81
+
82
+ # This is a little weird. For each contract
83
+ # we pre-make a proc to validate it so we
84
+ # don't have to go through this decision tree every time.
85
+ # Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
86
+ def make_validator!(contract)
87
+ klass = contract.class
88
+ key = if validator_strategies.key?(klass)
89
+ klass
90
+ else
91
+ if contract.respond_to? :valid?
92
+ :valid
93
+ elsif klass == Class || klass == Module
94
+ :class
95
+ else
96
+ :default
97
+ end
98
+ end
99
+
100
+ validator_strategies[key].call(contract)
101
+ end
102
+
103
+ def make_validator(contract)
104
+ contract_id = Support.contract_id(contract)
105
+
106
+ if memoized_validators.key?(contract_id)
107
+ return memoized_validators[contract_id]
108
+ end
109
+
110
+ memoized_validators[contract_id] = make_validator!(contract)
111
+ end
112
+
113
+ # @private
114
+ def reset_validators
115
+ clean_memoized_validators
116
+ restore_validators
117
+ end
118
+
119
+ # @private
120
+ def validator_strategies
121
+ @_validator_strategies ||= restore_validators
122
+ end
123
+
124
+ # @private
125
+ def restore_validators
126
+ @_validator_strategies = DEFAULT_VALIDATOR_STRATEGIES.dup
127
+ end
128
+
129
+ # @private
130
+ def memoized_validators
131
+ @_memoized_validators ||= clean_memoized_validators
132
+ end
133
+
134
+ # @private
135
+ def clean_memoized_validators
136
+ @_memoized_validators = {}
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,3 @@
1
+ module Contracts
2
+ VERSION = "0.14.0"
3
+ end
data/script/rubocop ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if RUBY_VERSION.to_f > 2.1
4
+ puts "running rubocop..."
5
+ puts `bundle exec rubocop -D spec/ lib/`
6
+ exit $?.exitstatus
7
+ end
@@ -0,0 +1,461 @@
1
+ RSpec.describe "Contracts:" do
2
+ before :all do
3
+ @o = GenericExample.new
4
+ end
5
+
6
+ def fails(&some)
7
+ expect { some.call }.to raise_error(ContractError)
8
+ end
9
+
10
+ def passes(&some)
11
+ expect { some.call }.to_not raise_error
12
+ end
13
+
14
+ describe "DescendantOf:" do
15
+ it "should pass for Array" do
16
+ passes { @o.enumerable_descendant_test(Array) }
17
+ end
18
+
19
+ it "should pass for a hash" do
20
+ passes { @o.enumerable_descendant_test(Hash) }
21
+ end
22
+
23
+ it "should fail for a number class" do
24
+ fails { @o.enumerable_descendant_test(Integer) }
25
+ end
26
+
27
+ it "should fail for a non-class" do
28
+ fails { @o.enumerable_descendant_test(1) }
29
+ end
30
+ end
31
+
32
+ describe "Num:" do
33
+ it "should pass for Fixnums" do
34
+ passes { @o.double(2) }
35
+ end
36
+
37
+ it "should pass for Floats" do
38
+ passes { @o.double(2.2) }
39
+ end
40
+
41
+ it "should fail for nil and other data types" do
42
+ fails { @o.double(nil) }
43
+ fails { @o.double(:x) }
44
+ fails { @o.double("x") }
45
+ fails { @o.double(/x/) }
46
+ end
47
+ end
48
+
49
+ describe "Pos:" do
50
+ it "should pass for positive numbers" do
51
+ passes { @o.pos_test(1) }
52
+ passes { @o.pos_test(1.6) }
53
+ end
54
+
55
+ it "should fail for 0" do
56
+ fails { @o.pos_test(0) }
57
+ end
58
+
59
+ it "should fail for negative numbers" do
60
+ fails { @o.pos_test(-1) }
61
+ fails { @o.pos_test(-1.6) }
62
+ end
63
+
64
+ it "should fail for nil and other data types" do
65
+ fails { @o.pos_test(nil) }
66
+ fails { @o.pos_test(:x) }
67
+ fails { @o.pos_test("x") }
68
+ fails { @o.pos_test(/x/) }
69
+ end
70
+ end
71
+
72
+ describe "Neg:" do
73
+ it "should pass for negative numbers" do
74
+ passes { @o.neg_test(-1) }
75
+ passes { @o.neg_test(-1.6) }
76
+ end
77
+
78
+ it "should fail for 0" do
79
+ fails { @o.neg_test(0) }
80
+ end
81
+
82
+ it "should fail for positive numbers" do
83
+ fails { @o.neg_test(1) }
84
+ fails { @o.neg_test(1.6) }
85
+ end
86
+
87
+ it "should fail for nil and other data types" do
88
+ fails { @o.neg_test(nil) }
89
+ fails { @o.neg_test(:x) }
90
+ fails { @o.neg_test("x") }
91
+ fails { @o.neg_test(/x/) }
92
+ end
93
+ end
94
+
95
+ describe "Nat:" do
96
+ it "should pass for 0" do
97
+ passes { @o.nat_test(0) }
98
+ end
99
+
100
+ it "should pass for positive whole numbers" do
101
+ passes { @o.nat_test(1) }
102
+ end
103
+
104
+ it "should fail for positive non-whole numbers" do
105
+ fails { @o.nat_test(1.5) }
106
+ end
107
+
108
+ it "should fail for negative numbers" do
109
+ fails { @o.nat_test(-1) }
110
+ fails { @o.nat_test(-1.6) }
111
+ end
112
+
113
+ it "should fail for nil and other data types" do
114
+ fails { @o.nat_test(nil) }
115
+ fails { @o.nat_test(:x) }
116
+ fails { @o.nat_test("x") }
117
+ fails { @o.nat_test(/x/) }
118
+ end
119
+ end
120
+
121
+ describe "Any:" do
122
+ it "should pass for numbers" do
123
+ passes { @o.show(1) }
124
+ end
125
+ it "should pass for strings" do
126
+ passes { @o.show("bad") }
127
+ end
128
+ it "should pass for procs" do
129
+ passes { @o.show(lambda {}) }
130
+ end
131
+ it "should pass for nil" do
132
+ passes { @o.show(nil) }
133
+ end
134
+ end
135
+
136
+ describe "None:" do
137
+ it "should fail for numbers" do
138
+ fails { @o.fail_all(1) }
139
+ end
140
+ it "should fail for strings" do
141
+ fails { @o.fail_all("bad") }
142
+ end
143
+ it "should fail for procs" do
144
+ fails { @o.fail_all(lambda {}) }
145
+ end
146
+ it "should fail for nil" do
147
+ fails { @o.fail_all(nil) }
148
+ end
149
+ end
150
+
151
+ describe "Or:" do
152
+ it "should pass for nums" do
153
+ passes { @o.num_or_string(1) }
154
+ end
155
+
156
+ it "should pass for strings" do
157
+ passes { @o.num_or_string("bad") }
158
+ end
159
+
160
+ it "should fail for nil" do
161
+ fails { @o.num_or_string(nil) }
162
+ end
163
+ end
164
+
165
+ describe "Xor:" do
166
+ it "should pass for an object with a method :good" do
167
+ passes { @o.xor_test(A.new) }
168
+ end
169
+
170
+ it "should pass for an object with a method :bad" do
171
+ passes { @o.xor_test(B.new) }
172
+ end
173
+
174
+ it "should fail for an object with neither method" do
175
+ fails { @o.xor_test(1) }
176
+ end
177
+
178
+ it "should fail for an object with both methods :good and :bad" do
179
+ fails { @o.xor_test(F.new) }
180
+ end
181
+ end
182
+
183
+ describe "And:" do
184
+ it "should pass for an object of class A that has a method :good" do
185
+ passes { @o.and_test(A.new) }
186
+ end
187
+
188
+ it "should fail for an object that has a method :good but isn't of class A" do
189
+ fails { @o.and_test(F.new) }
190
+ end
191
+ end
192
+
193
+ describe "Enum:" do
194
+ it "should pass for an object that is included" do
195
+ passes { @o.enum_test(:a) }
196
+ end
197
+
198
+ it "should fail for an object that is not included" do
199
+ fails { @o.enum_test(:z) }
200
+ end
201
+ end
202
+
203
+ describe "RespondTo:" do
204
+ it "should pass for an object that responds to :good" do
205
+ passes { @o.responds_test(A.new) }
206
+ end
207
+
208
+ it "should fail for an object that doesn't respond to :good" do
209
+ fails { @o.responds_test(B.new) }
210
+ end
211
+ end
212
+
213
+ describe "Send:" do
214
+ it "should pass for an object that returns true for method :good" do
215
+ passes { @o.send_test(A.new) }
216
+ end
217
+
218
+ it "should fail for an object that returns false for method :good" do
219
+ fails { @o.send_test(F.new) }
220
+ end
221
+ end
222
+
223
+ describe "Exactly:" do
224
+ it "should pass for an object that is exactly a Parent" do
225
+ passes { @o.exactly_test(Parent.new) }
226
+ end
227
+
228
+ it "should fail for an object that inherits from Parent" do
229
+ fails { @o.exactly_test(Child.new) }
230
+ end
231
+
232
+ it "should fail for an object that is not related to Parent at all" do
233
+ fails { @o.exactly_test(A.new) }
234
+ end
235
+ end
236
+
237
+ describe "Eq:" do
238
+ it "should pass for a class" do
239
+ passes { @o.eq_class_test(Foo) }
240
+ end
241
+
242
+ it "should pass for a module" do
243
+ passes { @o.eq_module_test(Bar) }
244
+ end
245
+
246
+ it "should pass for other values" do
247
+ passes { @o.eq_value_test(Baz) }
248
+ end
249
+
250
+ it "should fail when not equal" do
251
+ fails { @o.eq_class_test(Bar) }
252
+ end
253
+
254
+ it "should fail when given instance of class" do
255
+ fails { @o.eq_class_test(Foo.new) }
256
+ end
257
+ end
258
+
259
+ describe "Not:" do
260
+ it "should pass for an argument that isn't nil" do
261
+ passes { @o.not_nil(1) }
262
+ end
263
+
264
+ it "should fail for nil" do
265
+ fails { @o.not_nil(nil) }
266
+ end
267
+ end
268
+
269
+ describe "ArrayOf:" do
270
+ it "should pass for an array of nums" do
271
+ passes { @o.product([1, 2, 3]) }
272
+ end
273
+
274
+ it "should fail for an array with one non-num" do
275
+ fails { @o.product([1, 2, 3, "bad"]) }
276
+ end
277
+
278
+ it "should fail for a non-array" do
279
+ fails { @o.product(1) }
280
+ end
281
+ end
282
+
283
+ describe "RangeOf:" do
284
+ require "date"
285
+ it "should pass for a range of nums" do
286
+ passes { @o.first_in_range_num(3..10) }
287
+ end
288
+
289
+ it "should pass for a range of dates" do
290
+ d1 = Date.today
291
+ d2 = d1 + 18
292
+ passes { @o.first_in_range_date(d1..d2) }
293
+ end
294
+
295
+ it "should fail for a non-range" do
296
+ fails { @o.first_in_range_num("foo") }
297
+ fails { @o.first_in_range_num(:foo) }
298
+ fails { @o.first_in_range_num(5) }
299
+ fails { @o.first_in_range_num(nil) }
300
+ end
301
+
302
+ it "should fail for a range with incorrect data type" do
303
+ fails { @o.first_in_range_num("a".."z") }
304
+ end
305
+
306
+ it "should fail for a badly-defined range" do
307
+ # For some reason, Ruby 2.0.0 allows (date .. number) as a range.
308
+ # Perhaps other Ruby versions do too.
309
+ # Note that (date .. string) gives ArgumentError.
310
+ # This test guards against ranges with inconsistent data types.
311
+ begin
312
+ d1 = Date.today
313
+ fails { @o.first_in_range_date(d1..10) }
314
+ fails { @o.first_in_range_num(d1..10) }
315
+ rescue ArgumentError
316
+ # If Ruby doesn't like the range, we ignore the test.
317
+ :nop
318
+ end
319
+ end
320
+ end
321
+
322
+ describe "SetOf:" do
323
+ it "should pass for a set of nums" do
324
+ passes { @o.product_from_set(Set.new([1, 2, 3])) }
325
+ end
326
+
327
+ it "should fail for an array with one non-num" do
328
+ fails { @o.product_from_set(Set.new([1, 2, 3, "bad"])) }
329
+ end
330
+
331
+ it "should fail for a non-array" do
332
+ fails { @o.product_from_set(1) }
333
+ end
334
+ end
335
+
336
+ describe "Bool:" do
337
+ it "should pass for an argument that is a boolean" do
338
+ passes { @o.bool_test(true) }
339
+ passes { @o.bool_test(false) }
340
+ end
341
+
342
+ it "should fail for nil" do
343
+ fails { @o.bool_test(nil) }
344
+ end
345
+ end
346
+
347
+ describe "Maybe:" do
348
+ it "should pass for nums" do
349
+ expect(@o.maybe_double(1)).to eq(2)
350
+ end
351
+
352
+ it "should pass for nils" do
353
+ expect(@o.maybe_double(nil)).to eq(nil)
354
+ end
355
+
356
+ it "should fail for strings" do
357
+ fails { @o.maybe_double("foo") }
358
+ end
359
+ end
360
+
361
+ describe "KeywordArgs:" do
362
+ it "should pass for exact correct input" do
363
+ passes { @o.person_keywordargs(:name => "calvin", :age => 10) }
364
+ end
365
+
366
+ it "should fail if some keys don't have contracts" do
367
+ fails { @o.person_keywordargs(:name => "calvin", :age => 10, :foo => "bar") }
368
+ end
369
+
370
+ it "should fail if a key with a contract on it isn't provided" do
371
+ fails { @o.person_keywordargs(:name => "calvin") }
372
+ end
373
+
374
+ it "should fail for incorrect input" do
375
+ fails { @o.person_keywordargs(:name => 50, :age => 10) }
376
+ fails { @o.hash_keywordargs(:hash => nil) }
377
+ fails { @o.hash_keywordargs(:hash => 1) }
378
+ end
379
+
380
+ it "should pass if a method is overloaded with non-KeywordArgs" do
381
+ passes { @o.person_keywordargs("name", 10) }
382
+ end
383
+ end
384
+
385
+ describe "Optional:" do
386
+ it "can't be used outside of KeywordArgs" do
387
+ expect do
388
+ BareOptionalContractUsed.new.something(3, 5)
389
+ end.to raise_error(ArgumentError, Contracts::Optional::UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH)
390
+ end
391
+ end
392
+
393
+ describe "HashOf:" do
394
+ it "doesn't allow to specify multiple key-value pairs with pretty syntax" do
395
+ expect do
396
+ Class.new do
397
+ include Contracts::Core
398
+
399
+ Contract Contracts::HashOf[Symbol => String, Contracts::Num => Contracts::Num] => nil
400
+ def something(hash)
401
+ # ...
402
+ end
403
+ end
404
+ end.to raise_error(ArgumentError, "You should provide only one key-value pair to HashOf contract")
405
+ end
406
+
407
+ context "given a fulfilled contract" do
408
+ it { expect(@o.gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
409
+ it { expect(@o.pretty_gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
410
+ end
411
+
412
+ context "given an unfulfilled contract" do
413
+ it { fails { @o.gives_max_value(:panda => "1", :bamboo => "2") } }
414
+ it { fails { @o.gives_max_value(nil) } }
415
+ it { fails { @o.gives_max_value(1) } }
416
+ it { fails { @o.pretty_gives_max_value(:panda => "1", :bamboo => "2") } }
417
+ end
418
+
419
+ describe "#to_s" do
420
+ context "given Symbol => String" do
421
+ it { expect(Contracts::HashOf[Symbol, String].to_s).to eq("Hash<Symbol, String>") }
422
+ end
423
+
424
+ context "given String => Num" do
425
+ it { expect(Contracts::HashOf[String, Contracts::Num].to_s).to eq("Hash<String, Contracts::Builtin::Num>") }
426
+ end
427
+ end
428
+ end
429
+
430
+ describe "StrictHash:" do
431
+ context "when given an exact correct input" do
432
+ it "does not raise an error" do
433
+ passes { @o.strict_person(:name => "calvin", :age => 10) }
434
+ end
435
+ end
436
+
437
+ context "when given an input with correct keys but wrong types" do
438
+ it "raises an error" do
439
+ fails { @o.strict_person(:name => "calvin", :age => "10") }
440
+ end
441
+ end
442
+
443
+ context "when given an input with missing keys" do
444
+ it "raises an error" do
445
+ fails { @o.strict_person(:name => "calvin") }
446
+ end
447
+ end
448
+
449
+ context "when given an input with extra keys" do
450
+ it "raises an error" do
451
+ fails { @o.strict_person(:name => "calvin", :age => "10", :soft => true) }
452
+ end
453
+ end
454
+
455
+ context "when given not a hash" do
456
+ it "raises an error" do
457
+ fails { @o.strict_person(1337) }
458
+ end
459
+ end
460
+ end
461
+ end