contracts-lite 0.14.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.
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