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,67 @@
1
+ require "./lib/contracts"
2
+ require "benchmark"
3
+ require "rubygems"
4
+ require "method_profiler"
5
+ require "ruby-prof"
6
+
7
+ include Contracts
8
+
9
+ def add a, b
10
+ a + b
11
+ end
12
+
13
+ Contract Num, Num => Num
14
+ def contracts_add a, b
15
+ a + b
16
+ end
17
+
18
+ def explicit_add a, b
19
+ fail unless a.is_a?(Numeric)
20
+ fail unless b.is_a?(Numeric)
21
+ c = a + b
22
+ fail unless c.is_a?(Numeric)
23
+ c
24
+ end
25
+
26
+ def benchmark
27
+ Benchmark.bm 30 do |x|
28
+ x.report "testing add" do
29
+ 1_000_000.times do |_|
30
+ add(rand(1000), rand(1000))
31
+ end
32
+ end
33
+ x.report "testing contracts add" do
34
+ 1_000_000.times do |_|
35
+ contracts_add(rand(1000), rand(1000))
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def profile
42
+ profilers = []
43
+ profilers << MethodProfiler.observe(Contract)
44
+ profilers << MethodProfiler.observe(Object)
45
+ profilers << MethodProfiler.observe(Contracts::MethodDecorators)
46
+ profilers << MethodProfiler.observe(Contracts::Decorator)
47
+ profilers << MethodProfiler.observe(Contracts::Support)
48
+ profilers << MethodProfiler.observe(UnboundMethod)
49
+ 10_000.times do |_|
50
+ contracts_add(rand(1000), rand(1000))
51
+ end
52
+ profilers.each { |p| puts p.report }
53
+ end
54
+
55
+ def ruby_prof
56
+ RubyProf.start
57
+ 100_000.times do |_|
58
+ contracts_add(rand(1000), rand(1000))
59
+ end
60
+ result = RubyProf.stop
61
+ printer = RubyProf::FlatPrinter.new(result)
62
+ printer.print(STDOUT)
63
+ end
64
+
65
+ benchmark
66
+ profile
67
+ ruby_prof if ENV["FULL_BENCH"] # takes some time
@@ -0,0 +1,69 @@
1
+ require "./lib/contracts"
2
+ require "benchmark"
3
+ require "rubygems"
4
+ require "method_profiler"
5
+ require "ruby-prof"
6
+
7
+ include Contracts
8
+
9
+ def add opts
10
+ opts[:a] + opts[:b]
11
+ end
12
+
13
+ Contract ({ :a => Num, :b => Num}) => Num
14
+ def contracts_add opts
15
+ opts[:a] + opts[:b]
16
+ end
17
+
18
+ def explicit_add opts
19
+ a = opts[:a]
20
+ b = opts[:b]
21
+ fail unless a.is_a?(Numeric)
22
+ fail unless b.is_a?(Numeric)
23
+ c = a + b
24
+ fail unless c.is_a?(Numeric)
25
+ c
26
+ end
27
+
28
+ def benchmark
29
+ Benchmark.bm 30 do |x|
30
+ x.report "testing add" do
31
+ 1_000_000.times do |_|
32
+ add(:a => rand(1000), :b => rand(1000))
33
+ end
34
+ end
35
+ x.report "testing contracts add" do
36
+ 1_000_000.times do |_|
37
+ contracts_add(:a => rand(1000), :b => rand(1000))
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def profile
44
+ profilers = []
45
+ profilers << MethodProfiler.observe(Contract)
46
+ profilers << MethodProfiler.observe(Object)
47
+ profilers << MethodProfiler.observe(Contracts::MethodDecorators)
48
+ profilers << MethodProfiler.observe(Contracts::Decorator)
49
+ profilers << MethodProfiler.observe(Contracts::Support)
50
+ profilers << MethodProfiler.observe(UnboundMethod)
51
+ 10_000.times do |_|
52
+ contracts_add(:a => rand(1000), :b => rand(1000))
53
+ end
54
+ profilers.each { |p| puts p.report }
55
+ end
56
+
57
+ def ruby_prof
58
+ RubyProf.start
59
+ 100_000.times do |_|
60
+ contracts_add(:a => rand(1000), :b => rand(1000))
61
+ end
62
+ result = RubyProf.stop
63
+ printer = RubyProf::FlatPrinter.new(result)
64
+ printer.print(STDOUT)
65
+ end
66
+
67
+ benchmark
68
+ profile
69
+ ruby_prof if ENV["FULL_BENCH"] # takes some time
@@ -0,0 +1,91 @@
1
+ require "./lib/contracts"
2
+ require "benchmark"
3
+ require "rubygems"
4
+ require "method_profiler"
5
+ require "ruby-prof"
6
+
7
+ class Obj
8
+ include Contracts
9
+
10
+ attr_accessor :value
11
+ def initialize value
12
+ @value = value
13
+ end
14
+
15
+ Contract Num, Num => Num
16
+ def contracts_add a, b
17
+ a + b
18
+ end
19
+ end
20
+
21
+ class ObjWithInvariants
22
+ include Contracts
23
+ include Contracts::Invariants
24
+
25
+ invariant(:value_not_nil) { value != nil }
26
+ invariant(:value_not_string) { !value.is_a?(String) }
27
+
28
+ attr_accessor :value
29
+ def initialize value
30
+ @value = value
31
+ end
32
+
33
+ Contract Num, Num => Num
34
+ def contracts_add a, b
35
+ a + b
36
+ end
37
+ end
38
+
39
+ def benchmark
40
+ obj = Obj.new(3)
41
+ obj_with_invariants = ObjWithInvariants.new(3)
42
+
43
+ Benchmark.bm 30 do |x|
44
+ x.report "testing contracts add" do
45
+ 1_000_000.times do |_|
46
+ obj.contracts_add(rand(1000), rand(1000))
47
+ end
48
+ end
49
+ x.report "testing contracts add with invariants" do
50
+ 1_000_000.times do |_|
51
+ obj_with_invariants.contracts_add(rand(1000), rand(1000))
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def profile
58
+ obj_with_invariants = ObjWithInvariants.new(3)
59
+
60
+ profilers = []
61
+ profilers << MethodProfiler.observe(Contract)
62
+ profilers << MethodProfiler.observe(Object)
63
+ profilers << MethodProfiler.observe(Contracts::Support)
64
+ profilers << MethodProfiler.observe(Contracts::Invariants)
65
+ profilers << MethodProfiler.observe(Contracts::Invariants::InvariantExtension)
66
+ profilers << MethodProfiler.observe(UnboundMethod)
67
+
68
+ 10_000.times do |_|
69
+ obj_with_invariants.contracts_add(rand(1000), rand(1000))
70
+ end
71
+
72
+ profilers.each { |p| puts p.report }
73
+ end
74
+
75
+ def ruby_prof
76
+ RubyProf.start
77
+
78
+ obj_with_invariants = ObjWithInvariants.new(3)
79
+
80
+ 100_000.times do |_|
81
+ obj_with_invariants.contracts_add(rand(1000), rand(1000))
82
+ end
83
+
84
+ result = RubyProf.stop
85
+ printer = RubyProf::FlatPrinter.new(result)
86
+ printer.print(STDOUT)
87
+ end
88
+
89
+ benchmark
90
+ profile
91
+ ruby_prof if ENV["FULL_BENCH"] # takes some time
data/benchmarks/io.rb ADDED
@@ -0,0 +1,62 @@
1
+ require "./lib/contracts"
2
+ require "benchmark"
3
+ require "rubygems"
4
+ require "method_profiler"
5
+ require "ruby-prof"
6
+ require "open-uri"
7
+
8
+ include Contracts
9
+
10
+ def download url
11
+ open("http://www.#{url}/").read
12
+ end
13
+
14
+ Contract String => String
15
+ def contracts_download url
16
+ open("http://www.#{url}").read
17
+ end
18
+
19
+ @urls = %w{google.com bing.com}
20
+
21
+ def benchmark
22
+ Benchmark.bm 30 do |x|
23
+ x.report "testing download" do
24
+ 100.times do |_|
25
+ download(@urls.sample)
26
+ end
27
+ end
28
+ x.report "testing contracts download" do
29
+ 100.times do |_|
30
+ contracts_download(@urls.sample)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def profile
37
+ profilers = []
38
+ profilers << MethodProfiler.observe(Contract)
39
+ profilers << MethodProfiler.observe(Object)
40
+ profilers << MethodProfiler.observe(Contracts::MethodDecorators)
41
+ profilers << MethodProfiler.observe(Contracts::Decorator)
42
+ profilers << MethodProfiler.observe(Contracts::Support)
43
+ profilers << MethodProfiler.observe(UnboundMethod)
44
+ 10.times do |_|
45
+ contracts_download(@urls.sample)
46
+ end
47
+ profilers.each { |p| puts p.report }
48
+ end
49
+
50
+ def ruby_prof
51
+ RubyProf.start
52
+ 10.times do |_|
53
+ contracts_download(@urls.sample)
54
+ end
55
+ result = RubyProf.stop
56
+ printer = RubyProf::FlatPrinter.new(result)
57
+ printer.print(STDOUT)
58
+ end
59
+
60
+ benchmark
61
+ profile
62
+ ruby_prof if ENV["FULL_BENCH"] # takes some time
@@ -0,0 +1,57 @@
1
+ require "benchmark"
2
+
3
+ module Wrapper
4
+ def self.extended(klass)
5
+ klass.class_eval do
6
+ @@methods = {}
7
+ def self.methods
8
+ @@methods
9
+ end
10
+ def self.set_method k, v
11
+ @@methods[k] = v
12
+ end
13
+ end
14
+ end
15
+
16
+ def method_added name
17
+ return if methods.include?(name)
18
+ puts "#{name} added"
19
+ set_method(name, instance_method(name))
20
+ class_eval %{
21
+ def #{name}(*args)
22
+ self.class.methods[#{name.inspect}].bind(self).call(*args)
23
+ end
24
+ }, __FILE__, __LINE__ + 1
25
+ end
26
+ end
27
+
28
+ class NotWrapped
29
+ def add a, b
30
+ a + b
31
+ end
32
+ end
33
+
34
+ class Wrapped
35
+ extend ::Wrapper
36
+ def add a, b
37
+ a + b
38
+ end
39
+ end
40
+
41
+ w = Wrapped.new
42
+ nw = NotWrapped.new
43
+ # p w.add(1, 4)
44
+ # exit
45
+ # 30 is the width of the output column
46
+ Benchmark.bm 30 do |x|
47
+ x.report "wrapped" do
48
+ 100_000.times do |_|
49
+ w.add(rand(1000), rand(1000))
50
+ end
51
+ end
52
+ x.report "not wrapped" do
53
+ 100_000.times do |_|
54
+ nw.add(rand(1000), rand(1000))
55
+ end
56
+ end
57
+ end
data/contracts.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.join(__FILE__, "../lib/contracts/version"))
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "contracts-lite"
5
+ s.version = Contracts::VERSION
6
+ s.summary = "Contracts for Ruby. (fork)"
7
+ s.description = "This library provides contracts for Ruby. Contracts let you clearly express how your code behaves, and free you from writing tons of boilerplate, defensive code."
8
+ s.author = "Aditya Bhargava"
9
+ s.email = "bluemangroupie@gmail.com"
10
+ s.files = `git ls-files`.split("\n")
11
+ s.homepage = "http://github.com/ddd-ruby/contracts.ruby"
12
+ s.license = "BSD-2-Clause"
13
+ end
data/lib/contracts.rb ADDED
@@ -0,0 +1,231 @@
1
+ require "contracts/builtin_contracts"
2
+ require "contracts/decorators"
3
+ require "contracts/errors"
4
+ require "contracts/error_formatter"
5
+ require "contracts/formatters"
6
+ require "contracts/invariants"
7
+ require "contracts/method_reference"
8
+ require "contracts/support"
9
+ require "contracts/engine"
10
+ require "contracts/method_handler"
11
+ require "contracts/validators"
12
+ require "contracts/call_with"
13
+ require "contracts/core"
14
+
15
+ module Contracts
16
+ def self.included(base)
17
+ base.send(:include, Core)
18
+ end
19
+
20
+ def self.extended(base)
21
+ base.send(:extend, Core)
22
+ end
23
+ end
24
+
25
+ # This is the main Contract class. When you write a new contract, you'll
26
+ # write it as:
27
+ #
28
+ # Contract [contract names] => return_value
29
+ #
30
+ # This class also provides useful callbacks and a validation method.
31
+ #
32
+ # For #make_validator and related logic see file
33
+ # lib/contracts/validators.rb
34
+ # For #call_with and related logic see file
35
+ # lib/contracts/call_with.rb
36
+ class Contract < Contracts::Decorator
37
+ extend Contracts::Validators
38
+ include Contracts::CallWith
39
+
40
+ # Default implementation of failure_callback. Provided as a block to be able
41
+ # to monkey patch #failure_callback only temporary and then switch it back.
42
+ # First important usage - for specs.
43
+ DEFAULT_FAILURE_CALLBACK = proc do |data|
44
+ if data[:return_value]
45
+ # this failed on the return contract
46
+ fail ReturnContractError.new(failure_msg(data), data)
47
+ else
48
+ # this failed for a param contract
49
+ fail data[:contracts].failure_exception.new(failure_msg(data), data)
50
+ end
51
+ end
52
+
53
+ attr_reader :args_contracts, :ret_contract, :klass, :method
54
+ def initialize(klass, method, *contracts)
55
+ unless contracts.last.is_a?(Hash)
56
+ unless contracts.one?
57
+ fail %{
58
+ It looks like your contract for #{method.name} doesn't have a return
59
+ value. A contract should be written as `Contract arg1, arg2 =>
60
+ return_value`.
61
+ }.strip
62
+ end
63
+ contracts = [nil => contracts[-1]]
64
+ end
65
+
66
+ # internally we just convert that return value syntax back to an array
67
+ @args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys
68
+
69
+ @ret_contract = contracts[-1].values[0]
70
+
71
+ @args_validators = args_contracts.map do |contract|
72
+ Contract.make_validator(contract)
73
+ end
74
+
75
+ @args_contract_index = args_contracts.index do |contract|
76
+ contract.is_a? Contracts::Args
77
+ end
78
+
79
+ @ret_validator = Contract.make_validator(ret_contract)
80
+
81
+ @pattern_match = false
82
+
83
+ # == @has_proc_contract
84
+ last_contract = args_contracts.last
85
+ is_a_proc = last_contract.is_a?(Class) && (last_contract <= Proc || last_contract <= Method)
86
+ maybe_a_proc = last_contract.is_a?(Contracts::Maybe) && last_contract.include_proc?
87
+
88
+ @has_proc_contract = is_a_proc || maybe_a_proc || last_contract.is_a?(Contracts::Func)
89
+
90
+ # ====
91
+
92
+ # == @has_options_contract
93
+ last_contract = args_contracts.last
94
+ penultimate_contract = args_contracts[-2]
95
+ @has_options_contract = if @has_proc_contract
96
+ penultimate_contract.is_a?(Hash) || penultimate_contract.is_a?(Contracts::Builtin::KeywordArgs)
97
+ else
98
+ last_contract.is_a?(Hash) || last_contract.is_a?(Contracts::Builtin::KeywordArgs)
99
+ end
100
+ # ===
101
+
102
+ @klass, @method = klass, method
103
+ end
104
+
105
+ def pretty_contract c
106
+ c.is_a?(Class) ? c.name : c.class.name
107
+ end
108
+
109
+ def to_s
110
+ args = args_contracts.map { |c| pretty_contract(c) }.join(", ")
111
+ ret = pretty_contract(ret_contract)
112
+ ("#{args} => #{ret}").gsub("Contracts::Builtin::", "")
113
+ end
114
+
115
+ # Given a hash, prints out a failure message.
116
+ # This function is used by the default #failure_callback method
117
+ # and uses the hash passed into the failure_callback method.
118
+ def self.failure_msg(data)
119
+ Contracts::ErrorFormatters.failure_msg(data)
120
+ end
121
+
122
+ # Callback for when a contract fails. By default it raises
123
+ # an error and prints detailed info about the contract that
124
+ # failed. You can also monkeypatch this callback to do whatever
125
+ # you want...log the error, send you an email, print an error
126
+ # message, etc.
127
+ #
128
+ # Example of monkeypatching:
129
+ #
130
+ # def Contract.failure_callback(data)
131
+ # puts "You had an error!"
132
+ # puts failure_msg(data)
133
+ # exit
134
+ # end
135
+ def self.failure_callback(data, use_pattern_matching = true)
136
+ if data[:contracts].pattern_match? && use_pattern_matching
137
+ return DEFAULT_FAILURE_CALLBACK.call(data)
138
+ end
139
+
140
+ fetch_failure_callback.call(data)
141
+ end
142
+
143
+ # Used to override failure_callback without monkeypatching.
144
+ #
145
+ # Takes: block parameter, that should accept one argument - data.
146
+ #
147
+ # Example usage:
148
+ #
149
+ # Contract.override_failure_callback do |data|
150
+ # puts "You had an error"
151
+ # puts failure_msg(data)
152
+ # exit
153
+ # end
154
+ def self.override_failure_callback(&blk)
155
+ @failure_callback = blk
156
+ end
157
+
158
+ # Used to restore default failure callback
159
+ def self.restore_failure_callback
160
+ @failure_callback = DEFAULT_FAILURE_CALLBACK
161
+ end
162
+
163
+ def self.fetch_failure_callback
164
+ @failure_callback ||= DEFAULT_FAILURE_CALLBACK
165
+ end
166
+
167
+ # Used to verify if an argument satisfies a contract.
168
+ #
169
+ # Takes: an argument and a contract.
170
+ #
171
+ # Returns: a tuple: [Boolean, metadata]. The boolean indicates
172
+ # whether the contract was valid or not. If it wasn't, metadata
173
+ # contains some useful information about the failure.
174
+ def self.valid?(arg, contract)
175
+ make_validator(contract)[arg]
176
+ end
177
+
178
+ def [](*args, &blk)
179
+ call(*args, &blk)
180
+ end
181
+
182
+ def call(*args, &blk)
183
+ call_with(nil, *args, &blk)
184
+ end
185
+
186
+ # if we specified a proc in the contract but didn't pass one in,
187
+ # it's possible we are going to pass in a block instead. So lets
188
+ # append a nil to the list of args just so it doesn't fail.
189
+
190
+ # a better way to handle this might be to take this into account
191
+ # before throwing a "mismatched # of args" error.
192
+ # returns true if it appended nil
193
+ def maybe_append_block! args, blk
194
+ return false unless @has_proc_contract && !blk &&
195
+ (@args_contract_index || args.size < args_contracts.size)
196
+ args << nil
197
+ true
198
+ end
199
+
200
+ # Same thing for when we have named params but didn't pass any in.
201
+ # returns true if it appended nil
202
+ def maybe_append_options! args, blk
203
+ return false unless @has_options_contract
204
+ if @has_proc_contract && (args_contracts[-2].is_a?(Hash) || args_contracts[-2].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-2].is_a?(Hash)
205
+ args.insert(-2, {})
206
+ elsif (args_contracts[-1].is_a?(Hash) || args_contracts[-1].is_a?(Contracts::Builtin::KeywordArgs)) && !args[-1].is_a?(Hash)
207
+ args << {}
208
+ end
209
+ true
210
+ end
211
+
212
+ # Used to determine type of failure exception this contract should raise in case of failure
213
+ def failure_exception
214
+ if pattern_match?
215
+ PatternMatchingError
216
+ else
217
+ ParamContractError
218
+ end
219
+ end
220
+
221
+ # @private
222
+ # Used internally to mark contract as pattern matching contract
223
+ def pattern_match!
224
+ @pattern_match = true
225
+ end
226
+
227
+ # Used to determine if contract is a pattern matching contract
228
+ def pattern_match?
229
+ @pattern_match == true
230
+ end
231
+ end