ruby_contracts 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ module Contracts
2
+ class List < Array
3
+ def before_contracts
4
+ self.class.new(select{ |contract| contract.before? })
5
+ end
6
+
7
+ def after_contracts
8
+ self.class.new(select{ |contract| contract.after? })
9
+ end
10
+ end
11
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module RubyContracts
2
- VERSION = "0.2.5"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -3,163 +3,11 @@ require "ruby_contracts/version"
3
3
  module Contracts
4
4
  class Error < Exception ; end
5
5
 
6
- # When it is used with @__contracts_for:
7
- # before key must contain a disjunction of conjunction with preconditions
8
- # after key must contain a conjunction with postconditions
9
- #
10
- # When it is used with @__contracts:
11
- # before key must contain a conjunction
12
- # after key must contain a conjunction
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
@@ -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.2.5
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-15 00:00:00.000000000 Z
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