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.
@@ -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