ruby_contracts 0.2.5 → 0.3.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.
- data/lib/ruby_contracts/contract.rb +19 -0
- data/lib/ruby_contracts/dsl.rb +131 -0
- data/lib/ruby_contracts/input_type.rb +41 -0
- data/lib/ruby_contracts/list.rb +11 -0
- data/lib/ruby_contracts/output_type.rb +16 -0
- data/lib/ruby_contracts/postcondition.rb +22 -0
- data/lib/ruby_contracts/precondition.rb +22 -0
- data/lib/ruby_contracts/version.rb +1 -1
- data/lib/ruby_contracts.rb +7 -159
- data/test/inheritance_test.rb +9 -0
- metadata +9 -2
@@ -0,0 +1,19 @@
|
|
1
|
+
module Contracts
|
2
|
+
class Contract
|
3
|
+
def satisfied?(context, arguments, result=nil)
|
4
|
+
raise "Contract.satisfied? must be implemented in subclasses."
|
5
|
+
end
|
6
|
+
|
7
|
+
def message
|
8
|
+
raise "Contract.message must be implemented in subclasses."
|
9
|
+
end
|
10
|
+
|
11
|
+
def before?
|
12
|
+
raise "Contract.before? must be implemented in subclasses."
|
13
|
+
end
|
14
|
+
|
15
|
+
def after?
|
16
|
+
raise "Contract.after? must be implemented in subclasses."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Contracts
|
2
|
+
module DSL
|
3
|
+
def self.included(base)
|
4
|
+
if ENV['ENABLE_ASSERTION']
|
5
|
+
base.extend Contracts::DSL::ClassMethods
|
6
|
+
base.__contracts_initialize
|
7
|
+
else
|
8
|
+
base.extend Contracts::DSL::EmptyClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def inherited(subclass)
|
14
|
+
super
|
15
|
+
subclass.__contracts_initialize
|
16
|
+
end
|
17
|
+
|
18
|
+
def __contracts_initialize
|
19
|
+
@__contracts = Contracts::List.new
|
20
|
+
@__contracts_for = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def __contracts_for(name, current_contracts=nil)
|
24
|
+
if @__contracts_for.has_key?(name) && !current_contracts
|
25
|
+
@__contracts_for[name]
|
26
|
+
else
|
27
|
+
contracts = ancestors[1..-1].reverse.reduce([]) do |c, klass|
|
28
|
+
ancestor_hash = klass.instance_variable_get('@__contracts_for')
|
29
|
+
c += ancestor_hash[name] if ancestor_hash && ancestor_hash.has_key?(name)
|
30
|
+
c
|
31
|
+
end
|
32
|
+
contracts << current_contracts if current_contracts
|
33
|
+
@__contracts_for[name] = contracts
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def __contract_failure!(name, message, result, *args)
|
38
|
+
args.pop if args.last.kind_of?(Proc)
|
39
|
+
raise Contracts::Error.new("#{self}##{name}(#{args.join ', '}) => #{result || "?"} ; #{message}.")
|
40
|
+
end
|
41
|
+
|
42
|
+
def type(options)
|
43
|
+
@__contracts << Contracts::InputType.new(options[:in].kind_of?(Array) ? options[:in] : [options[:in]]) if options.has_key?(:in)
|
44
|
+
@__contracts << Contracts::OutputType.new(options[:out]) if options.has_key?(:out)
|
45
|
+
end
|
46
|
+
|
47
|
+
def pre(message=nil, &block)
|
48
|
+
@__contracts << Contracts::Precondition.new(message, block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def post(message=nil, &block)
|
52
|
+
@__contracts << Contracts::Postcondition.new(message, block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_added(name)
|
56
|
+
super
|
57
|
+
|
58
|
+
return if @__skip_other_contracts_definitions
|
59
|
+
|
60
|
+
__contracts = __contracts_for(name.to_s, @__contracts)
|
61
|
+
@__contracts = Contracts::List.new
|
62
|
+
|
63
|
+
if !__contracts.empty?
|
64
|
+
@__skip_other_contracts_definitions = true
|
65
|
+
original_method_name = "#{name}_without_contracts"
|
66
|
+
define_method(original_method_name, instance_method(name))
|
67
|
+
|
68
|
+
method = <<-EOM
|
69
|
+
def #{name}(*args, &block)
|
70
|
+
__args = block.nil? ? args : args + [block]
|
71
|
+
self.class.__eval_before_contracts("#{name}", self, __args)
|
72
|
+
result = #{original_method_name}(*args, &block)
|
73
|
+
self.class.__eval_after_contracts("#{name}", self, __args, result)
|
74
|
+
return result
|
75
|
+
end
|
76
|
+
EOM
|
77
|
+
|
78
|
+
class_eval method
|
79
|
+
|
80
|
+
@__skip_other_contracts_definitions = false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def __eval_before_contracts(name, context, arguments)
|
85
|
+
last_failure_msg = nil
|
86
|
+
__contracts_for(name).each do |contracts|
|
87
|
+
next if contracts.empty?
|
88
|
+
success = true
|
89
|
+
contracts.before_contracts.each do |contract|
|
90
|
+
begin
|
91
|
+
unless satisfied = contract.satisfied?(context, arguments)
|
92
|
+
last_failure_msg = contract.message
|
93
|
+
success = false
|
94
|
+
break
|
95
|
+
end
|
96
|
+
rescue
|
97
|
+
last_failure_msg = "Contract evaluation failed with #{$!} for #{contract.class} #{contract.message}"
|
98
|
+
success = false
|
99
|
+
break
|
100
|
+
end
|
101
|
+
end
|
102
|
+
return if success
|
103
|
+
end
|
104
|
+
|
105
|
+
__contract_failure!(name, last_failure_msg, nil, arguments) if last_failure_msg
|
106
|
+
end
|
107
|
+
|
108
|
+
def __eval_after_contracts(name, context, arguments, result)
|
109
|
+
__contracts_for(name).each do |contracts|
|
110
|
+
next if contracts.empty?
|
111
|
+
contracts.after_contracts.each do |contract|
|
112
|
+
begin
|
113
|
+
unless satisfied = contract.satisfied?(context, arguments, result)
|
114
|
+
__contract_failure!(name, contract.message, result, arguments)
|
115
|
+
break
|
116
|
+
end
|
117
|
+
rescue
|
118
|
+
__contract_failure!(name, "Contract evaluation failed with #{$!} for #{contract.class} #{contract.message}", result, arguments)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
module EmptyClassMethods
|
126
|
+
def type(*args) ; end
|
127
|
+
def pre(*args) ; end
|
128
|
+
def post(*args) ; end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Contracts
|
2
|
+
class InputType < Precondition
|
3
|
+
attr_reader :message
|
4
|
+
|
5
|
+
def initialize(expected_classes)
|
6
|
+
@expected_classes = expected_classes
|
7
|
+
@expected_argument_count = expected_classes.size
|
8
|
+
end
|
9
|
+
|
10
|
+
def satisfied?(context, arguments, result=nil)
|
11
|
+
if !match_size?(arguments)
|
12
|
+
@message = "the method expect #{@expected_argument_count} arguments when #{arguments.size} was given"
|
13
|
+
false
|
14
|
+
elsif unmatched = unmatched_type_from(arguments)
|
15
|
+
i, arg, expected_klass = unmatched
|
16
|
+
@message = "the method expect a kind of #{expected_klass} for argument ##{i} when a kind of #{arg.class} was given"
|
17
|
+
false
|
18
|
+
else
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def match_size?(arguments)
|
26
|
+
arguments.size == @expected_argument_count
|
27
|
+
end
|
28
|
+
|
29
|
+
def unmatched_type_from(arguments)
|
30
|
+
unmatched = nil
|
31
|
+
arguments.zip(@expected_classes).each_with_index do |(arg, klass), i|
|
32
|
+
unless arg.kind_of?(klass)
|
33
|
+
unmatched = [i, arg, klass]
|
34
|
+
break
|
35
|
+
end
|
36
|
+
end
|
37
|
+
unmatched
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Contracts
|
2
|
+
class OutputType < Postcondition
|
3
|
+
def initialize(expected_class)
|
4
|
+
@expected_class = expected_class
|
5
|
+
end
|
6
|
+
|
7
|
+
def message
|
8
|
+
"the result must be an kind of #{@expected_class}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def satisfied?(context, arguments, result=nil)
|
12
|
+
result.kind_of? @expected_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Contracts
|
2
|
+
class Postcondition < Contract
|
3
|
+
attr_reader :message
|
4
|
+
|
5
|
+
def initialize(message, block)
|
6
|
+
@message = "invalid postcondition: #{message || 'no message given'}"
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def satisfied?(context, arguments, result=nil)
|
11
|
+
context.instance_exec(result, *arguments, &@block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def before?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def after?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Contracts
|
2
|
+
class Precondition < Contract
|
3
|
+
attr_reader :message
|
4
|
+
|
5
|
+
def initialize(message, block)
|
6
|
+
@message = "invalid precondition: #{message || 'no message given'}"
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def satisfied?(context, arguments, result=nil)
|
11
|
+
context.instance_exec(*arguments, &@block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def before?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def after?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/ruby_contracts.rb
CHANGED
@@ -3,163 +3,11 @@ require "ruby_contracts/version"
|
|
3
3
|
module Contracts
|
4
4
|
class Error < Exception ; end
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def self.empty_contracts
|
14
|
-
{:before => [], :after => []}
|
15
|
-
end
|
16
|
-
|
17
|
-
module DSL
|
18
|
-
def self.included(base)
|
19
|
-
base.extend Contracts::DSL::ClassMethods
|
20
|
-
base.__contracts_initialize
|
21
|
-
end
|
22
|
-
|
23
|
-
module ClassMethods
|
24
|
-
def inherited(subclass)
|
25
|
-
super
|
26
|
-
subclass.__contracts_initialize
|
27
|
-
end
|
28
|
-
|
29
|
-
def __contracts_initialize
|
30
|
-
@__contracts = Contracts.empty_contracts
|
31
|
-
@__contracts_for = {}
|
32
|
-
end
|
33
|
-
|
34
|
-
def __contracts_for(name, current_contracts=nil)
|
35
|
-
inherited_contracts = ancestors[1..-1].reduce(Contracts.empty_contracts) do |c, klass|
|
36
|
-
ancestor_hash = klass.instance_variable_get('@__contracts_for') || {}
|
37
|
-
c[:before] += ancestor_hash[name][:before] if ancestor_hash.has_key?(name)
|
38
|
-
c[:after] += ancestor_hash[name][:after] if ancestor_hash.has_key?(name)
|
39
|
-
c
|
40
|
-
end
|
41
|
-
|
42
|
-
if current_contracts
|
43
|
-
inherited_contracts[:before] << current_contracts[:before] unless current_contracts[:before].empty?
|
44
|
-
inherited_contracts[:after] += current_contracts[:after] unless current_contracts[:after].empty?
|
45
|
-
end
|
46
|
-
|
47
|
-
inherited_contracts
|
48
|
-
end
|
49
|
-
|
50
|
-
def __contract_failure!(name, message, result, *args)
|
51
|
-
args.pop if args.last.kind_of?(Proc)
|
52
|
-
raise Contracts::Error.new("#{self}##{name}(#{args.join ', '}) => #{result || "?"} ; #{message}.")
|
53
|
-
end
|
54
|
-
|
55
|
-
def type(options)
|
56
|
-
@__contracts[:before] << [:type, options[:in]] if ENV['ENABLE_ASSERTION'] && options.has_key?(:in)
|
57
|
-
@__contracts[:after] << [:type, options[:out]] if ENV['ENABLE_ASSERTION'] && options.has_key?(:out)
|
58
|
-
end
|
59
|
-
|
60
|
-
def pre(message=nil, &block)
|
61
|
-
@__contracts[:before] << [:params, message, block] if ENV['ENABLE_ASSERTION']
|
62
|
-
end
|
63
|
-
|
64
|
-
def post(message=nil, &block)
|
65
|
-
@__contracts[:after] << [:result, message, block] if ENV['ENABLE_ASSERTION']
|
66
|
-
end
|
67
|
-
|
68
|
-
def method_added(name)
|
69
|
-
super
|
70
|
-
|
71
|
-
return unless ENV['ENABLE_ASSERTION']
|
72
|
-
return if @__skip_other_contracts_definitions
|
73
|
-
return if @__contracts_for.has_key?(name)
|
74
|
-
|
75
|
-
__contracts = @__contracts_for[name] ||= __contracts_for(name, @__contracts)
|
76
|
-
@__contracts = Contracts.empty_contracts
|
77
|
-
|
78
|
-
if !__contracts[:before].empty? || !__contracts[:after].empty?
|
79
|
-
@__skip_other_contracts_definitions = true
|
80
|
-
original_method_name = "#{name}__with_contracts"
|
81
|
-
define_method(original_method_name, instance_method(name))
|
82
|
-
|
83
|
-
count = 0
|
84
|
-
before_contracts = __contracts[:before].reduce("__before_contracts_disjunction = []\n") do |code, contracts_disjunction|
|
85
|
-
contracts_conjunction = contracts_disjunction.reduce("__before_contracts_conjunction = []\n") do |code, contract|
|
86
|
-
type, *args = contract
|
87
|
-
case type
|
88
|
-
when :type
|
89
|
-
classes = args[0]
|
90
|
-
code << "if __before_contracts_conjunction.empty? then\n"
|
91
|
-
code << " if __args.size < #{classes.size} then\n"
|
92
|
-
code << " __before_contracts_conjunction << ['#{name}', \"need at least #{classes.size} arguments (%i given)\" % [__args.size], nil, *args]\n"
|
93
|
-
code << " else\n"
|
94
|
-
conditions = []
|
95
|
-
classes.each_with_index{ |klass, i| conditions << "__args[#{i}].kind_of?(#{klass})" }
|
96
|
-
code << " if !(#{conditions.join(' && ')}) then\n"
|
97
|
-
code << " __before_contracts_conjunction << ['#{name}', 'input type error', nil, *__args]\n"
|
98
|
-
code << " end\n"
|
99
|
-
code << " end\n"
|
100
|
-
code << "end\n"
|
101
|
-
code
|
102
|
-
|
103
|
-
when :params
|
104
|
-
# Define a method that verify the assertion
|
105
|
-
contract_method_name = "__verify_contract_#{name}_in_#{count = count + 1}"
|
106
|
-
define_method(contract_method_name) { |*params| self.instance_exec(*params, &args[1]) }
|
107
|
-
|
108
|
-
code << "if __before_contracts_conjunction.empty? then\n"
|
109
|
-
code << " if !#{contract_method_name}(*__args) then\n"
|
110
|
-
code << " __before_contracts_conjunction << ['#{name}', \"invalid precondition: #{args[0]}\", nil, *__args]\n"
|
111
|
-
code << " end\n"
|
112
|
-
code << "end\n"
|
113
|
-
code
|
114
|
-
else
|
115
|
-
code
|
116
|
-
end
|
117
|
-
end
|
118
|
-
code << contracts_conjunction
|
119
|
-
code << "__before_contracts_disjunction << __before_contracts_conjunction\n"
|
120
|
-
code
|
121
|
-
end
|
122
|
-
before_contracts << "unless __before_contracts_disjunction.any?{|conj| conj.empty?} then\n"
|
123
|
-
before_contracts << " self.class.__contract_failure!(*__before_contracts_disjunction.find{|conj| !conj.empty?}.first)\n"
|
124
|
-
before_contracts << "end\n"
|
125
|
-
|
126
|
-
after_contracts = __contracts[:after].reduce("") do |code, contract|
|
127
|
-
type, *args = contract
|
128
|
-
case type
|
129
|
-
when :type
|
130
|
-
code << "if !result.kind_of?(#{args[0]}) then\n"
|
131
|
-
code << " self.class.__contract_failure!(name, \"result must be a kind of '#{args[0]}' not '%s'\" % [result.class.to_s], result, *__args)\n"
|
132
|
-
code << "end\n"
|
133
|
-
code
|
134
|
-
when :result
|
135
|
-
# Define a method that verify the assertion
|
136
|
-
contract_method_name = "__verify_contract_#{name}_out_#{count = count + 1}"
|
137
|
-
define_method(contract_method_name) { |*params| self.instance_exec(*params, &args[1]) }
|
138
|
-
|
139
|
-
code << "if !#{contract_method_name}(result, *__args) then\n"
|
140
|
-
code << " self.class.__contract_failure!('#{name}', \"invalid postcondition: #{args[0]}\", result, *__args)\n"
|
141
|
-
code << "end\n"
|
142
|
-
code
|
143
|
-
else
|
144
|
-
code
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
method = <<-EOM
|
149
|
-
def #{name}(*args, &block)
|
150
|
-
__args = block.nil? ? args : args + [block]
|
151
|
-
#{before_contracts}
|
152
|
-
result = #{original_method_name}(*args, &block)
|
153
|
-
#{after_contracts}
|
154
|
-
return result
|
155
|
-
end
|
156
|
-
EOM
|
157
|
-
|
158
|
-
class_eval method
|
159
|
-
|
160
|
-
@__skip_other_contracts_definitions = false
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
6
|
+
autoload :List, 'ruby_contracts/list'
|
7
|
+
autoload :Contract, 'ruby_contracts/contract'
|
8
|
+
autoload :Precondition, 'ruby_contracts/precondition'
|
9
|
+
autoload :Postcondition, 'ruby_contracts/postcondition'
|
10
|
+
autoload :InputType, 'ruby_contracts/input_type'
|
11
|
+
autoload :OutputType, 'ruby_contracts/output_type'
|
12
|
+
autoload :DSL, 'ruby_contracts/dsl'
|
165
13
|
end
|
data/test/inheritance_test.rb
CHANGED
@@ -35,6 +35,15 @@ describe 'the contracts behavior in an inheritance context' do
|
|
35
35
|
child.increment(3)
|
36
36
|
proc { child.increment(2) }.must_raise Contracts::Error
|
37
37
|
end
|
38
|
+
|
39
|
+
it 'fails with the deepest failing precondition message' do
|
40
|
+
begin
|
41
|
+
child.increment(2)
|
42
|
+
true.must_equal false # Force a failure
|
43
|
+
rescue Contracts::Error
|
44
|
+
$!.message.must_include 'n >= minimum_incr'
|
45
|
+
end
|
46
|
+
end
|
38
47
|
end
|
39
48
|
end
|
40
49
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-06-
|
12
|
+
date: 2013-06-16 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Micro DSL to add pre & post condition to methods. It tries to bring some
|
15
15
|
design by contract in the Ruby world.
|
@@ -25,6 +25,13 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- Rakefile
|
27
27
|
- lib/ruby_contracts.rb
|
28
|
+
- lib/ruby_contracts/contract.rb
|
29
|
+
- lib/ruby_contracts/dsl.rb
|
30
|
+
- lib/ruby_contracts/input_type.rb
|
31
|
+
- lib/ruby_contracts/list.rb
|
32
|
+
- lib/ruby_contracts/output_type.rb
|
33
|
+
- lib/ruby_contracts/postcondition.rb
|
34
|
+
- lib/ruby_contracts/precondition.rb
|
28
35
|
- lib/ruby_contracts/version.rb
|
29
36
|
- ruby_contracts.gemspec
|
30
37
|
- test/fixtures/inheritance_classes.rb
|