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,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