From eed57a9354dc5fd7104e5ce4427c41372328037d Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Tue, 28 Apr 2015 20:48:07 +0200 Subject: [PATCH 01/13] Moved #decorate, #fetch_decorators, #pop_decorators and #decorated_methods to Engine --- lib/contracts.rb | 7 +- lib/contracts/decorators.rb | 67 +++++------------ lib/contracts/engine.rb | 142 ++++++++++++++++++++++++++++++++++++ lib/contracts/modules.rb | 4 +- 4 files changed, 166 insertions(+), 54 deletions(-) create mode 100644 lib/contracts/engine.rb diff --git a/lib/contracts.rb b/lib/contracts.rb index f50087d..4fb0a36 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -7,6 +7,7 @@ require "contracts/method_reference" require "contracts/modules" require "contracts/support" +require "contracts/engine" module Contracts def self.included(base) @@ -18,7 +19,7 @@ def self.extended(base) end def self.common(base) - Eigenclass.lift(base) + #Eigenclass.lift(base) return if base.respond_to?(:Contract) @@ -26,7 +27,7 @@ def self.common(base) base.instance_eval do def functype(funcname) - contracts = decorated_methods[:class_methods][funcname] + contracts = Engine.fetch_from(self).decorated_methods[:class_methods][funcname] if contracts.nil? "No contract for #{self}.#{funcname}" else @@ -51,7 +52,7 @@ def Contract(*args) end def functype(funcname) - contracts = self.class.decorated_methods[:instance_methods][funcname] + contracts = Engine.fetch_from(self.class).decorated_methods[:instance_methods][funcname] if contracts.nil? "No contract for #{self.class}.#{funcname}" else diff --git a/lib/contracts/decorators.rb b/lib/contracts/decorators.rb index ab0290c..41b0f33 100644 --- a/lib/contracts/decorators.rb +++ b/lib/contracts/decorators.rb @@ -1,25 +1,7 @@ module Contracts module MethodDecorators def self.extended(klass) - return if klass.respond_to?(:decorated_methods=) - - class << klass - attr_accessor :decorated_methods - end - end - - module EigenclassWithOwner - def self.lift(eigenclass) - fail Contracts::ContractsNotIncluded unless with_owner?(eigenclass) - - eigenclass - end - - private - - def self.with_owner?(eigenclass) - eigenclass.respond_to?(:owner_class) && eigenclass.owner_class - end + Engine.apply(klass) end # first, when you write a contract, the decorate method gets called which @@ -37,19 +19,12 @@ def singleton_method_added(name) super end - def pop_decorators - Array(@decorators).tap { @decorators = nil } - end - - def fetch_decorators - pop_decorators + Eigenclass.lift(self).pop_decorators - end - def common_method_added(name, is_class_method) - decorators = fetch_decorators - return if decorators.empty? + return unless Engine.applied?(self) + engine = Engine.fetch_from(self) - @decorated_methods ||= { :class_methods => {}, :instance_methods => {} } + decorators = engine.all_decorators + return if decorators.empty? if is_class_method method_reference = SingletonMethodReference.new(name, method(name)) @@ -59,8 +34,6 @@ def common_method_added(name, is_class_method) method_type = :instance_methods end - @decorated_methods[method_type][name] ||= [] - unless decorators.size == 1 fail %{ Oops, it looks like method '#{name}' has multiple contracts: @@ -79,6 +52,8 @@ def foo x } end + engine.decorated_methods[method_type][name] ||= [] + pattern_matching = false decorators.each do |klass, args| # a reference to the method gets passed into the contract here. This is good because @@ -87,7 +62,7 @@ def foo x # We assume here that the decorator (klass) responds to .new decorator = klass.new(self, method_reference, *args) new_args_contract = decorator.args_contracts - matched = @decorated_methods[method_type][name].select do |contract| + matched = engine.decorated_methods[method_type][name].select do |contract| contract.args_contracts == new_args_contract end unless matched.empty? @@ -101,12 +76,12 @@ def foo x Each definition needs to have a different contract for the parameters. }, {}) end - @decorated_methods[method_type][name] << decorator + engine.add_method_decorator(method_type, name, decorator) pattern_matching ||= decorator.pattern_match? end - if @decorated_methods[method_type][name].any? { |x| x.method != method_reference } - @decorated_methods[method_type][name].each(&:pattern_match!) + if engine.decorated_methods[method_type][name].any? { |x| x.method != method_reference } + engine.decorated_methods[method_type][name].each(&:pattern_match!) pattern_matching = true end @@ -156,16 +131,19 @@ def foo x # call Foo's version of decorated_methods. So the line needs to be `current = #{self}`. current = self + current_engine = engine method_reference.make_definition(self) do |*args, &blk| ancestors = current.ancestors ancestors.shift # first one is just the class itself - while current && !current.respond_to?(:decorated_methods) || current.decorated_methods.nil? + while current && current_engine && !current_engine.has_decorated_methods? current = ancestors.shift + current_engine = Engine.fetch_from(current) end - if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil? + + unless current_engine && current_engine.has_decorated_methods? fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case." end - methods = current.decorated_methods[method_type][name] + methods = current_engine.decorated_methods[method_type][name] # this adds support for overloading methods. Here we go through each method and call it with the arguments. # If we get a ContractError, we move to the next function. Otherwise we return the result. @@ -194,15 +172,6 @@ def foo x result end end - - def decorate(klass, *args) - if Support.eigenclass? self - return EigenclassWithOwner.lift(self).owner_class.decorate(klass, *args) - end - - @decorators ||= [] - @decorators << [klass, args] - end end class Decorator @@ -220,7 +189,7 @@ def self.inherited(klass) # inside, `decorate` is called with those params. MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1 def #{klass}(*args, &blk) - decorate(#{klass}, *args, &blk) + ::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk) end ruby_eval end diff --git a/lib/contracts/engine.rb b/lib/contracts/engine.rb new file mode 100644 index 0000000..faa0d5c --- /dev/null +++ b/lib/contracts/engine.rb @@ -0,0 +1,142 @@ +module Contracts + class EngineTarget + def initialize(target) + @target = target + end + + def apply(engine_class = Engine) + return if applied? + + apply_to_eigenclass + + target.class_eval do + define_singleton_method(:__contracts_engine) do + @__contracts_engine ||= engine_class.new(self) + end + end + + engine.set_eigenclass_owner + end + + def applied? + target.respond_to?(:__contracts_engine) + end + + def engine + applied? && target.__contracts_engine + end + + private + attr_reader :target + + def apply_to_eigenclass + return unless has_meaningless_eigenclass? + + EngineTarget.new(eigenclass).apply(EigenclassEngine) + eigenclass.extend(MethodDecorators) + eigenclass.send(:include, Contracts) + end + + def eigenclass + Support.eigenclass_of(target) + end + + def has_meaningless_eigenclass? + return true if target.class == Module + return false if target < Module + !Support.eigenclass?(target) + end + end + + class Engine + def self.apply(klass) + EngineTarget.new(klass).apply + end + + def self.applied?(klass) + EngineTarget.new(klass).applied? + end + + def self.fetch_from(klass) + EngineTarget.new(klass).engine + end + + def initialize(target) + @target = target + end + + def decorate(klass, *args) + validate! + decorators << [klass, args] + end + + def validate! + end + + def set_eigenclass_owner + Engine.fetch_from(eigenclass).owner_class = target + end + + def all_decorators + pop_decorators + eigenclass_engine.all_decorators + end + + def pop_decorators + decorators.tap { clear_decorators } + end + + def decorated_methods + @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} } + end + + def has_decorated_methods? + !decorated_methods[:class_methods].empty? || + !decorated_methods[:instance_methods].empty? + end + + def add_method_decorator(type, name, decorator) + decorated_methods[type][name] ||= [] + decorated_methods[type][name] << decorator + end + + private + attr_reader :target + + def eigenclass + Support.eigenclass_of(target) + end + + def eigenclass_engine + Engine.fetch_from(eigenclass) + end + + def decorators + @_decorators ||= [] + end + + def clear_decorators + @_decorators = [] + end + end + + class EigenclassEngine < Engine + attr_accessor :owner_class + + def validate! + fail Contracts::ContractsNotIncluded unless has_owner? + end + + def set_eigenclass_owner + end + + def all_decorators + pop_decorators + end + + private + + def has_owner? + !!owner_class + end + end +end diff --git a/lib/contracts/modules.rb b/lib/contracts/modules.rb index 1e2a72c..65b5edb 100644 --- a/lib/contracts/modules.rb +++ b/lib/contracts/modules.rb @@ -10,8 +10,8 @@ def self.extended(base) def self.common(base) return unless base.instance_of?(Module) - base.extend(MethodDecorators) - Eigenclass.lift(base) + #base.extend(MethodDecorators) + #Eigenclass.lift(base) end end end From f8266d39fdd82c04a8e6b5ab54357cc0aca6ab1c Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Tue, 28 Apr 2015 20:55:29 +0200 Subject: [PATCH 02/13] Move out #common_method_added to MethodHandler object --- lib/contracts.rb | 1 + lib/contracts/decorators.rb | 158 +----------------------------- lib/contracts/method_handler.rb | 168 ++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 156 deletions(-) create mode 100644 lib/contracts/method_handler.rb diff --git a/lib/contracts.rb b/lib/contracts.rb index 4fb0a36..819a69c 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -8,6 +8,7 @@ require "contracts/modules" require "contracts/support" require "contracts/engine" +require "contracts/method_handler" module Contracts def self.included(base) diff --git a/lib/contracts/decorators.rb b/lib/contracts/decorators.rb index 41b0f33..c7a283f 100644 --- a/lib/contracts/decorators.rb +++ b/lib/contracts/decorators.rb @@ -10,168 +10,14 @@ def self.extended(klass) # to find the decorator for that method. This is how we associate decorators # with methods. def method_added(name) - common_method_added name, false + MethodHandler.new(name, false).handle(self) super end def singleton_method_added(name) - common_method_added name, true + MethodHandler.new(name, true).handle(self) super end - - def common_method_added(name, is_class_method) - return unless Engine.applied?(self) - engine = Engine.fetch_from(self) - - decorators = engine.all_decorators - return if decorators.empty? - - if is_class_method - method_reference = SingletonMethodReference.new(name, method(name)) - method_type = :class_methods - else - method_reference = MethodReference.new(name, instance_method(name)) - method_type = :instance_methods - end - - unless decorators.size == 1 - fail %{ -Oops, it looks like method '#{name}' has multiple contracts: -#{decorators.map { |x| x[1][0].inspect }.join("\n")} - -Did you accidentally put more than one contract on a single function, like so? - -Contract String => String -Contract Num => String -def foo x -end - -If you did NOT, then you have probably discovered a bug in this library. -Please file it along with the relevant code at: -https://github.com/egonSchiele/contracts.ruby/issues - } - end - - engine.decorated_methods[method_type][name] ||= [] - - pattern_matching = false - decorators.each do |klass, args| - # a reference to the method gets passed into the contract here. This is good because - # we are going to redefine this method with a new name below...so this reference is - # now the *only* reference to the old method that exists. - # We assume here that the decorator (klass) responds to .new - decorator = klass.new(self, method_reference, *args) - new_args_contract = decorator.args_contracts - matched = engine.decorated_methods[method_type][name].select do |contract| - contract.args_contracts == new_args_contract - end - unless matched.empty? - fail ContractError.new(%{ -It looks like you are trying to use pattern-matching, but -multiple definitions for function '#{name}' have the same -contract for input parameters: - -#{(matched + [decorator]).map(&:to_s).join("\n")} - -Each definition needs to have a different contract for the parameters. - }, {}) - end - engine.add_method_decorator(method_type, name, decorator) - pattern_matching ||= decorator.pattern_match? - end - - if engine.decorated_methods[method_type][name].any? { |x| x.method != method_reference } - engine.decorated_methods[method_type][name].each(&:pattern_match!) - - pattern_matching = true - end - - method_reference.make_alias(self) - - return if ENV["NO_CONTRACTS"] && !pattern_matching - - # in place of this method, we are going to define our own method. This method - # just calls the decorator passing in all args that were to be passed into the method. - # The decorator in turn has a reference to the actual method, so it can call it - # on its own, after doing it's decorating of course. - - # Very important: THe line `current = #{self}` in the start is crucial. - # Not having it means that any method that used contracts could NOT use `super` - # (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27). - # Here's why: Suppose you have this code: - # - # class Foo - # Contract String - # def to_s - # "Foo" - # end - # end - # - # class Bar < Foo - # Contract String - # def to_s - # super + "Bar" - # end - # end - # - # b = Bar.new - # p b.to_s - # - # `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However, - # we have overwritten the function (that's what this next defn is). So it gets a - # reference to the function to call by looking at `decorated_methods`. - # - # Now, this line used to read something like: - # - # current = self#{is_class_method ? "" : ".class"} - # - # In that case, `self` would always be `Bar`, regardless of whether you were calling - # Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which - # means you would always call Bar's to_s...infinite recursion! Instead, you want to - # call Foo's version of decorated_methods. So the line needs to be `current = #{self}`. - - current = self - current_engine = engine - method_reference.make_definition(self) do |*args, &blk| - ancestors = current.ancestors - ancestors.shift # first one is just the class itself - while current && current_engine && !current_engine.has_decorated_methods? - current = ancestors.shift - current_engine = Engine.fetch_from(current) - end - - unless current_engine && current_engine.has_decorated_methods? - fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case." - end - methods = current_engine.decorated_methods[method_type][name] - - # this adds support for overloading methods. Here we go through each method and call it with the arguments. - # If we get a ContractError, we move to the next function. Otherwise we return the result. - # If we run out of functions, we raise the last ContractError. - success = false - i = 0 - result = nil - expected_error = methods[0].failure_exception - until success - method = methods[i] - i += 1 - begin - success = true - result = method.call_with(self, *args, &blk) - rescue expected_error => error - success = false - unless methods[i] - begin - ::Contract.failure_callback(error.data, false) - rescue expected_error => final_error - raise final_error.to_contract_error - end - end - end - end - result - end - end end class Decorator diff --git a/lib/contracts/method_handler.rb b/lib/contracts/method_handler.rb new file mode 100644 index 0000000..6963472 --- /dev/null +++ b/lib/contracts/method_handler.rb @@ -0,0 +1,168 @@ +module Contracts + class MethodHandler + def initialize(method_name, is_class_method) + @method_name = method_name + @is_class_method = is_class_method + end + + # TODO: refactor it to private methods + def handle(target) + name = method_name + + return unless Engine.applied?(target) + engine = Engine.fetch_from(target) + + decorators = engine.all_decorators + return if decorators.empty? + + if is_class_method + method_reference = SingletonMethodReference.new(name, target.method(name)) + method_type = :class_methods + else + method_reference = MethodReference.new(name, target.instance_method(name)) + method_type = :instance_methods + end + + unless decorators.size == 1 + fail %{ +Oops, it looks like method '#{name}' has multiple contracts: +#{decorators.map { |x| x[1][0].inspect }.join("\n")} + +Did you accidentally put more than one contract on a single function, like so? + +Contract String => String +Contract Num => String +def foo x +end + +If you did NOT, then you have probably discovered a bug in this library. +Please file it along with the relevant code at: +https://github.com/egonSchiele/contracts.ruby/issues + } + end + + engine.decorated_methods[method_type][name] ||= [] + + pattern_matching = false + decorators.each do |klass, args| + # a reference to the method gets passed into the contract here. This is good because + # we are going to redefine this method with a new name below...so this reference is + # now the *only* reference to the old method that exists. + # We assume here that the decorator (klass) responds to .new + decorator = klass.new(target, method_reference, *args) + new_args_contract = decorator.args_contracts + matched = engine.decorated_methods[method_type][name].select do |contract| + contract.args_contracts == new_args_contract + end + unless matched.empty? + fail ContractError.new(%{ +It looks like you are trying to use pattern-matching, but +multiple definitions for function '#{name}' have the same +contract for input parameters: + +#{(matched + [decorator]).map(&:to_s).join("\n")} + +Each definition needs to have a different contract for the parameters. + }, {}) + end + engine.add_method_decorator(method_type, name, decorator) + pattern_matching ||= decorator.pattern_match? + end + + if engine.decorated_methods[method_type][name].any? { |x| x.method != method_reference } + engine.decorated_methods[method_type][name].each(&:pattern_match!) + + pattern_matching = true + end + + method_reference.make_alias(target) + + return if ENV["NO_CONTRACTS"] && !pattern_matching + + # in place of this method, we are going to define our own method. This method + # just calls the decorator passing in all args that were to be passed into the method. + # The decorator in turn has a reference to the actual method, so it can call it + # on its own, after doing it's decorating of course. + + # Very important: THe line `current = #{target}` in the start is crucial. + # Not having it means that any method that used contracts could NOT use `super` + # (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27). + # Here's why: Suppose you have this code: + # + # class Foo + # Contract String + # def to_s + # "Foo" + # end + # end + # + # class Bar < Foo + # Contract String + # def to_s + # super + "Bar" + # end + # end + # + # b = Bar.new + # p b.to_s + # + # `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However, + # we have overwritten the function (that's what this next defn is). So it gets a + # reference to the function to call by looking at `decorated_methods`. + # + # Now, this line used to read something like: + # + # current = target#{is_class_method ? "" : ".class"} + # + # In that case, `target` would always be `Bar`, regardless of whether you were calling + # Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which + # means you would always call Bar's to_s...infinite recursion! Instead, you want to + # call Foo's version of decorated_methods. So the line needs to be `current = #{target}`. + + current = target + current_engine = engine + method_reference.make_definition(target) do |*args, &blk| + ancestors = current.ancestors + ancestors.shift # first one is just the class itself + while current && current_engine && !current_engine.has_decorated_methods? + current = ancestors.shift + current_engine = Engine.fetch_from(current) + end + + unless current_engine && current_engine.has_decorated_methods? + fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case." + end + methods = current_engine.decorated_methods[method_type][name] + + # this adds support for overloading methods. Here we go through each method and call it with the arguments. + # If we get a ContractError, we move to the next function. Otherwise we return the result. + # If we run out of functions, we raise the last ContractError. + success = false + i = 0 + result = nil + expected_error = methods[0].failure_exception + until success + method = methods[i] + i += 1 + begin + success = true + result = method.call_with(self, *args, &blk) + rescue expected_error => error + success = false + unless methods[i] + begin + ::Contract.failure_callback(error.data, false) + rescue expected_error => final_error + raise final_error.to_contract_error + end + end + end + end + result + end + end + + private + attr_reader :method_name, :is_class_method + end +end From df923a1532ecf548c777a4a6f1c047187495f8cc Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 11:56:27 +0200 Subject: [PATCH 03/13] Remove: Eigenclass, Modules --- lib/contracts.rb | 2 -- lib/contracts/eigenclass.rb | 38 ------------------------------------- lib/contracts/modules.rb | 17 ----------------- spec/contracts_spec.rb | 1 - spec/fixtures/fixtures.rb | 3 --- 5 files changed, 61 deletions(-) delete mode 100644 lib/contracts/eigenclass.rb delete mode 100644 lib/contracts/modules.rb diff --git a/lib/contracts.rb b/lib/contracts.rb index 819a69c..8bbd9a0 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -1,11 +1,9 @@ require "contracts/builtin_contracts" require "contracts/decorators" -require "contracts/eigenclass" require "contracts/errors" require "contracts/formatters" require "contracts/invariants" require "contracts/method_reference" -require "contracts/modules" require "contracts/support" require "contracts/engine" require "contracts/method_handler" diff --git a/lib/contracts/eigenclass.rb b/lib/contracts/eigenclass.rb deleted file mode 100644 index e95ece4..0000000 --- a/lib/contracts/eigenclass.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Contracts - module Eigenclass - def self.extended(eigenclass) - return if eigenclass.respond_to?(:owner_class=) - - class << eigenclass - attr_accessor :owner_class - end - end - - def self.lift(base) - return NullEigenclass if Support.eigenclass? base - - eigenclass = Support.eigenclass_of base - - eigenclass.extend(Eigenclass) unless eigenclass.respond_to?(:owner_class=) - - unless eigenclass.respond_to?(:pop_decorators) - eigenclass.extend(MethodDecorators) - eigenclass.send(:include, Contracts) - end - - eigenclass.owner_class = base - - eigenclass - end - - module NullEigenclass - def self.owner_class - self - end - - def self.pop_decorators - [] - end - end - end -end diff --git a/lib/contracts/modules.rb b/lib/contracts/modules.rb deleted file mode 100644 index 65b5edb..0000000 --- a/lib/contracts/modules.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Contracts - module Modules - def self.included(base) - common(base) - end - - def self.extended(base) - common(base) - end - - def self.common(base) - return unless base.instance_of?(Module) - #base.extend(MethodDecorators) - #Eigenclass.lift(base) - end - end -end diff --git a/spec/contracts_spec.rb b/spec/contracts_spec.rb index 3c63c30..af4359c 100644 --- a/spec/contracts_spec.rb +++ b/spec/contracts_spec.rb @@ -250,7 +250,6 @@ def greeting(name) let(:mod) do Module.new do include Contracts - include Contracts::Modules Contract String => String def greeting(name) diff --git a/spec/fixtures/fixtures.rb b/spec/fixtures/fixtures.rb index ad44fbe..018a309 100644 --- a/spec/fixtures/fixtures.rb +++ b/spec/fixtures/fixtures.rb @@ -528,10 +528,7 @@ def on_response(status, body) end module ModuleExample - # This inclusion is required to actually override `method_added` - # hooks for module. include Contracts - include Contracts::Modules Contract Num, Num => Num def plus(a, b) From fbdb85a9adcab6f4ad612452280fc9459ebfbdd4 Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 11:59:56 +0200 Subject: [PATCH 04/13] Engine :: small cleanup --- lib/contracts/engine.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/contracts/engine.rb b/lib/contracts/engine.rb index faa0d5c..b4aef25 100644 --- a/lib/contracts/engine.rb +++ b/lib/contracts/engine.rb @@ -70,11 +70,8 @@ def decorate(klass, *args) decorators << [klass, args] end - def validate! - end - def set_eigenclass_owner - Engine.fetch_from(eigenclass).owner_class = target + eigenclass_engine.owner_class = target end def all_decorators @@ -102,6 +99,9 @@ def add_method_decorator(type, name, decorator) private attr_reader :target + def validate! + end + def eigenclass Support.eigenclass_of(target) end @@ -122,10 +122,6 @@ def clear_decorators class EigenclassEngine < Engine attr_accessor :owner_class - def validate! - fail Contracts::ContractsNotIncluded unless has_owner? - end - def set_eigenclass_owner end @@ -135,6 +131,10 @@ def all_decorators private + def validate! + fail Contracts::ContractsNotIncluded unless has_owner? + end + def has_owner? !!owner_class end From ba9e52071aeb53ade45ec7948e63129d49de03c6 Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 13:31:54 +0200 Subject: [PATCH 05/13] Remove Eigenclass reference --- lib/contracts.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/contracts.rb b/lib/contracts.rb index 8bbd9a0..afdd496 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -18,8 +18,6 @@ def self.extended(base) end def self.common(base) - #Eigenclass.lift(base) - return if base.respond_to?(:Contract) base.extend(MethodDecorators) From 030f57caa9fac52484145f2459c9bb21d8db6eca Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 13:46:18 +0200 Subject: [PATCH 06/13] split engine module into multiple files and provide a facade --- lib/contracts/engine.rb | 152 ++++------------------------- lib/contracts/engine/base.rb | 74 ++++++++++++++ lib/contracts/engine/eigenclass.rb | 24 +++++ lib/contracts/engine/target.rb | 52 ++++++++++ 4 files changed, 168 insertions(+), 134 deletions(-) create mode 100644 lib/contracts/engine/base.rb create mode 100644 lib/contracts/engine/eigenclass.rb create mode 100644 lib/contracts/engine/target.rb diff --git a/lib/contracts/engine.rb b/lib/contracts/engine.rb index b4aef25..ddcbae1 100644 --- a/lib/contracts/engine.rb +++ b/lib/contracts/engine.rb @@ -1,142 +1,26 @@ -module Contracts - class EngineTarget - def initialize(target) - @target = target - end - - def apply(engine_class = Engine) - return if applied? - - apply_to_eigenclass - - target.class_eval do - define_singleton_method(:__contracts_engine) do - @__contracts_engine ||= engine_class.new(self) - end - end - - engine.set_eigenclass_owner - end - - def applied? - target.respond_to?(:__contracts_engine) - end - - def engine - applied? && target.__contracts_engine - end - - private - attr_reader :target - - def apply_to_eigenclass - return unless has_meaningless_eigenclass? - - EngineTarget.new(eigenclass).apply(EigenclassEngine) - eigenclass.extend(MethodDecorators) - eigenclass.send(:include, Contracts) - end - - def eigenclass - Support.eigenclass_of(target) - end - - def has_meaningless_eigenclass? - return true if target.class == Module - return false if target < Module - !Support.eigenclass?(target) - end - end - - class Engine - def self.apply(klass) - EngineTarget.new(klass).apply - end - - def self.applied?(klass) - EngineTarget.new(klass).applied? - end - - def self.fetch_from(klass) - EngineTarget.new(klass).engine - end - - def initialize(target) - @target = target - end - - def decorate(klass, *args) - validate! - decorators << [klass, args] - end +require "contracts/engine/base" +require "contracts/engine/target" +require "contracts/engine/eigenclass" - def set_eigenclass_owner - eigenclass_engine.owner_class = target - end - - def all_decorators - pop_decorators + eigenclass_engine.all_decorators - end - - def pop_decorators - decorators.tap { clear_decorators } - end - - def decorated_methods - @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} } - end - - def has_decorated_methods? - !decorated_methods[:class_methods].empty? || - !decorated_methods[:instance_methods].empty? - end - - def add_method_decorator(type, name, decorator) - decorated_methods[type][name] ||= [] - decorated_methods[type][name] << decorator - end - - private - attr_reader :target - - def validate! - end - - def eigenclass - Support.eigenclass_of(target) - end - - def eigenclass_engine - Engine.fetch_from(eigenclass) - end - - def decorators - @_decorators ||= [] - end - - def clear_decorators - @_decorators = [] - end - end +require "forwardable" - class EigenclassEngine < Engine - attr_accessor :owner_class - - def set_eigenclass_owner - end - - def all_decorators - pop_decorators - end +module Contracts + # Engine facade, normally you shouldn't refer internals of Engine + # module directly. + module Engine + class << self + extend Forwardable - private + # .apply(klass) - enables contracts engine on klass + # .applied?(klass) - returns true if klass has contracts engine + # .fetch_from(klass) - returns contracts engine for klass + delegate [:apply, :applied?, :fetch_from] => :base_engine - def validate! - fail Contracts::ContractsNotIncluded unless has_owner? - end + private - def has_owner? - !!owner_class + def base_engine + Base + end end end end diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb new file mode 100644 index 0000000..0fe182b --- /dev/null +++ b/lib/contracts/engine/base.rb @@ -0,0 +1,74 @@ +module Contracts + module Engine + class Base + def self.apply(klass) + Engine::Target.new(klass).apply + end + + def self.applied?(klass) + Engine::Target.new(klass).applied? + end + + def self.fetch_from(klass) + Engine::Target.new(klass).engine + end + + def initialize(target) + @target = target + end + + def decorate(klass, *args) + validate! + decorators << [klass, args] + end + + def set_eigenclass_owner + eigenclass_engine.owner_class = target + end + + def all_decorators + pop_decorators + eigenclass_engine.all_decorators + end + + def pop_decorators + decorators.tap { clear_decorators } + end + + def decorated_methods + @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} } + end + + def has_decorated_methods? + !decorated_methods[:class_methods].empty? || + !decorated_methods[:instance_methods].empty? + end + + def add_method_decorator(type, name, decorator) + decorated_methods[type][name] ||= [] + decorated_methods[type][name] << decorator + end + + private + attr_reader :target + + def validate! + end + + def eigenclass + Support.eigenclass_of(target) + end + + def eigenclass_engine + Engine.fetch_from(eigenclass) + end + + def decorators + @_decorators ||= [] + end + + def clear_decorators + @_decorators = [] + end + end + end +end diff --git a/lib/contracts/engine/eigenclass.rb b/lib/contracts/engine/eigenclass.rb new file mode 100644 index 0000000..e8b642d --- /dev/null +++ b/lib/contracts/engine/eigenclass.rb @@ -0,0 +1,24 @@ +module Contracts + module Engine + class Eigenclass < Base + attr_accessor :owner_class + + def set_eigenclass_owner + end + + def all_decorators + pop_decorators + end + + private + + def validate! + fail ContractsNotIncluded unless has_owner? + end + + def has_owner? + !!owner_class + end + end + end +end diff --git a/lib/contracts/engine/target.rb b/lib/contracts/engine/target.rb new file mode 100644 index 0000000..59422fb --- /dev/null +++ b/lib/contracts/engine/target.rb @@ -0,0 +1,52 @@ +module Contracts + module Engine + class Target + def initialize(target) + @target = target + end + + def apply(engine_class = Base) + return if applied? + + apply_to_eigenclass + + target.class_eval do + define_singleton_method(:__contracts_engine) do + @__contracts_engine ||= engine_class.new(self) + end + end + + engine.set_eigenclass_owner + end + + def applied? + target.respond_to?(:__contracts_engine) + end + + def engine + applied? && target.__contracts_engine + end + + private + attr_reader :target + + def apply_to_eigenclass + return unless has_meaningless_eigenclass? + + self.class.new(eigenclass).apply(Eigenclass) + eigenclass.extend(MethodDecorators) + eigenclass.send(:include, Contracts) + end + + def eigenclass + Support.eigenclass_of(target) + end + + def has_meaningless_eigenclass? + return true if target.class == Module + return false if target < Module + !Support.eigenclass?(target) + end + end + end +end From ea8fd8bd9e9f2b11b445cdfb2e40c483ac8e1e0b Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 14:46:10 +0200 Subject: [PATCH 07/13] Provide useful documentation for engine module --- lib/contracts/engine/base.rb | 63 +++++++++++++++++++++----- lib/contracts/engine/eigenclass.rb | 7 +++ lib/contracts/engine/target.rb | 21 +++++++-- lib/contracts/support.rb | 71 +++++++++++++++++------------- 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb index 0fe182b..5b52024 100644 --- a/lib/contracts/engine/base.rb +++ b/lib/contracts/engine/base.rb @@ -1,61 +1,102 @@ module Contracts module Engine + # Contracts engine class Base + # Enable contracts engine for klass + # + # @param [Class] klass - target class def self.apply(klass) Engine::Target.new(klass).apply end + # Returns true if klass has contracts engine + # + # @param [Class] klass - target class + # @return [Bool] def self.applied?(klass) Engine::Target.new(klass).applied? end + # Fetches contracts engine out of klass + # + # @param [Class] klass - target class + # @return [Engine::Base or Engine::Eigenclass] def self.fetch_from(klass) Engine::Target.new(klass).engine end - def initialize(target) - @target = target + # Creates new instance of contracts engine + # + # @param [Class] klass - class that owns this engine + def initialize(klass) + @klass = klass end - def decorate(klass, *args) + # Adds provided decorator to the engine + # It validates that decorator can be added to this engine at the + # moment + # + # @param [Decorator:Class] decorator_class + # @param args - arguments for decorator + def decorate(decorator_class, *args) validate! - decorators << [klass, args] + decorators << [decorator_class, args] end + # Sets eigenclass' owner to klass def set_eigenclass_owner - eigenclass_engine.owner_class = target + eigenclass_engine.owner_class = klass end + # Fetches all accumulated decorators (both this engine and + # corresponding eigenclass' engine) + # It clears all accumulated decorators + # + # @return [ArrayOf[Decorator]] def all_decorators pop_decorators + eigenclass_engine.all_decorators end - def pop_decorators - decorators.tap { clear_decorators } - end - + # Returns decorated methods hash (contains both class and + # instance methods) + # + # @return [{ :class_methods => HashOf[Symbol => Decorator], + # :instance_methods => HashOf[Symbol => Decorator] }] def decorated_methods @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} } end + # Returns true if there are any decorated methods + # + # @return [Bool] def has_decorated_methods? !decorated_methods[:class_methods].empty? || !decorated_methods[:instance_methods].empty? end + # Adds method decorator + # + # @param [Or[:class_methods, :instance_methods]] type - method type + # @param [Symbol] name - method name + # @param [Decorator] decorator - method decorator def add_method_decorator(type, name, decorator) decorated_methods[type][name] ||= [] decorated_methods[type][name] << decorator end private - attr_reader :target + attr_reader :klass + # No-op because it is safe to add decorators to normal classes def validate! end + def pop_decorators + decorators.tap { clear_decorators } + end + def eigenclass - Support.eigenclass_of(target) + Support.eigenclass_of(klass) end def eigenclass_engine diff --git a/lib/contracts/engine/eigenclass.rb b/lib/contracts/engine/eigenclass.rb index e8b642d..73246ae 100644 --- a/lib/contracts/engine/eigenclass.rb +++ b/lib/contracts/engine/eigenclass.rb @@ -1,17 +1,24 @@ module Contracts module Engine + # Special case of contracts engine for eigenclasses + # We don't care about eigenclass of eigenclass at this point class Eigenclass < Base + # Class that owns this eigenclass attr_accessor :owner_class + + # No-op for eigenclasses def set_eigenclass_owner end + # Fetches just eigenclasses decorators def all_decorators pop_decorators end private + # Fails when contracts are not included in owner class def validate! fail ContractsNotIncluded unless has_owner? end diff --git a/lib/contracts/engine/target.rb b/lib/contracts/engine/target.rb index 59422fb..5ee9173 100644 --- a/lib/contracts/engine/target.rb +++ b/lib/contracts/engine/target.rb @@ -1,10 +1,21 @@ module Contracts module Engine + # Represents class in question class Target + # Creates new instance of Target + # + # @param [Class] target - class in question def initialize(target) @target = target end + # Enable contracts engine for target + # - it is no-op if contracts engine is already enabled + # - it automatically enables contracts engine for its eigenclass + # - it sets owner class to target for its eigenclass + # + # @param [Engine::Base:Class] engine_class - type of engine to + # enable (Base or Eigenclass) def apply(engine_class = Base) return if applied? @@ -19,10 +30,16 @@ def apply(engine_class = Base) engine.set_eigenclass_owner end + # Returns true if target has contracts engine already + # + # @return [Bool] def applied? target.respond_to?(:__contracts_engine) end + # Returns contracts engine of target + # + # @return [Engine::Base or Engine::Eigenclass] def engine applied? && target.__contracts_engine end @@ -32,7 +49,7 @@ def engine def apply_to_eigenclass return unless has_meaningless_eigenclass? - + self.class.new(eigenclass).apply(Eigenclass) eigenclass.extend(MethodDecorators) eigenclass.send(:include, Contracts) @@ -43,8 +60,6 @@ def eigenclass end def has_meaningless_eigenclass? - return true if target.class == Module - return false if target < Module !Support.eigenclass?(target) end end diff --git a/lib/contracts/support.rb b/lib/contracts/support.rb index 671ad9f..718d5c3 100644 --- a/lib/contracts/support.rb +++ b/lib/contracts/support.rb @@ -1,44 +1,55 @@ module Contracts module Support - def self.method_position(method) - return method.method_position if method.is_a?(MethodReference) + class << self + def method_position(method) + return method.method_position if method.is_a?(MethodReference) - if RUBY_VERSION =~ /^1\.8/ - if method.respond_to?(:__file__) - method.__file__ + ":" + method.__line__.to_s + if RUBY_VERSION =~ /^1\.8/ + if method.respond_to?(:__file__) + method.__file__ + ":" + method.__line__.to_s + else + method.inspect + end else - method.inspect + file, line = method.source_location + file + ":" + line.to_s end - else - file, line = method.source_location - file + ":" + line.to_s end - end - def self.method_name(method) - method.is_a?(Proc) ? "Proc" : method.name - end + def method_name(method) + method.is_a?(Proc) ? "Proc" : method.name + end - # Generates unique id, which can be used as a part of identifier - # - # Example: - # Contracts::Support.unique_id # => "i53u6tiw5hbo" - def self.unique_id - # Consider using SecureRandom.hex here, and benchmark which one is better - (Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36) - end + # Generates unique id, which can be used as a part of identifier + # + # Example: + # Contracts::Support.unique_id # => "i53u6tiw5hbo" + def unique_id + # Consider using SecureRandom.hex here, and benchmark which one is better + (Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36) + end - def self.eigenclass_hierarchy_supported? - return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0 - RUBY_VERSION.to_f > 1.8 - end + def eigenclass_hierarchy_supported? + return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0 + RUBY_VERSION.to_f > 1.8 + end - def self.eigenclass_of(target) - class << target; self; end - end + def eigenclass_of(target) + class << target; self; end + end + + def eigenclass?(target) + module_eigenclass?(target) || + target <= eigenclass_of(Object) + end + + private - def self.eigenclass?(target) - target <= eigenclass_of(Object) + # Module eigenclass can be detected by its ancestor chain + # containing a Module + def module_eigenclass?(target) + target < Module + end end end end From 2ff320e8d2655b6791ffb01393eb3d51ca7197aa Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 14:47:13 +0200 Subject: [PATCH 08/13] Update tutorial to match the fact that inclusion of Contracts::Modules is no longer needed (and at all possible) --- TUTORIAL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 56ad581..c833c51 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -510,12 +510,11 @@ This is because the first contract eliminated the possibility of `age` being les ## Contracts in modules -To use contracts on module you need to include both `Contracts` and `Contracts::Modules` into it: +Usage is the same as contracts in classes: ```ruby module M include Contracts - include Contracts::Modules Contract String => String def self.parse From 0b2c2fdeeaa4a555e795df19009de26b8062748b Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 16:00:58 +0200 Subject: [PATCH 09/13] Refactor MethodHandler --- lib/contracts.rb | 18 +-- lib/contracts/decorators.rb | 9 +- lib/contracts/engine/base.rb | 29 +++- lib/contracts/method_handler.rb | 264 ++++++++++++++++++-------------- 4 files changed, 171 insertions(+), 149 deletions(-) diff --git a/lib/contracts.rb b/lib/contracts.rb index afdd496..eb97d3b 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -24,7 +24,7 @@ def self.common(base) base.instance_eval do def functype(funcname) - contracts = Engine.fetch_from(self).decorated_methods[:class_methods][funcname] + contracts = Engine.fetch_from(self).decorated_methods_for(:class_methods, funcname) if contracts.nil? "No contract for #{self}.#{funcname}" else @@ -34,22 +34,8 @@ def functype(funcname) end base.class_eval do - unless base.instance_of?(Module) - def Contract(*args) - return if ENV["NO_CONTRACTS"] - if self.class == Module - puts %{ -Warning: You have added a Contract on a module function -without including Contracts::Modules. Your Contract will -just be ignored. Please include Contracts::Modules into -your module.} - end - self.class.Contract(*args) - end - end - def functype(funcname) - contracts = Engine.fetch_from(self.class).decorated_methods[:instance_methods][funcname] + contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname) if contracts.nil? "No contract for #{self.class}.#{funcname}" else diff --git a/lib/contracts/decorators.rb b/lib/contracts/decorators.rb index c7a283f..278c26c 100644 --- a/lib/contracts/decorators.rb +++ b/lib/contracts/decorators.rb @@ -4,18 +4,13 @@ def self.extended(klass) Engine.apply(klass) end - # first, when you write a contract, the decorate method gets called which - # sets the @decorators variable. Then when the next method after the contract - # is defined, method_added is called and we look at the @decorators variable - # to find the decorator for that method. This is how we associate decorators - # with methods. def method_added(name) - MethodHandler.new(name, false).handle(self) + MethodHandler.new(name, false, self).handle super end def singleton_method_added(name) - MethodHandler.new(name, true).handle(self) + MethodHandler.new(name, true, self).handle super end end diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb index 5b52024..1fa15ab 100644 --- a/lib/contracts/engine/base.rb +++ b/lib/contracts/engine/base.rb @@ -57,13 +57,8 @@ def all_decorators pop_decorators + eigenclass_engine.all_decorators end - # Returns decorated methods hash (contains both class and - # instance methods) - # - # @return [{ :class_methods => HashOf[Symbol => Decorator], - # :instance_methods => HashOf[Symbol => Decorator] }] - def decorated_methods - @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} } + def decorated_methods_for(type, name) + Array(decorated_methods[type][name]) end # Returns true if there are any decorated methods @@ -84,9 +79,29 @@ def add_method_decorator(type, name, decorator) decorated_methods[type][name] << decorator end + # Returns nearest ancestor's engine that has decorated methods + # + # @return [Engine::Base or Engine::Eigenclass] + def nearest_decorated_ancestor + current = klass + current_engine = self + ancestors = current.ancestors[1..-1] + + while current && current_engine && !current_engine.has_decorated_methods? + current = ancestors.shift + current_engine = Engine.fetch_from(current) + end + + current_engine + end + private attr_reader :klass + def decorated_methods + @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} } + end + # No-op because it is safe to add decorators to normal classes def validate! end diff --git a/lib/contracts/method_handler.rb b/lib/contracts/method_handler.rb index 6963472..e5d259a 100644 --- a/lib/contracts/method_handler.rb +++ b/lib/contracts/method_handler.rb @@ -1,155 +1,143 @@ module Contracts + # Handles class and instance methods addition + # Represents single such method class MethodHandler - def initialize(method_name, is_class_method) + METHOD_REFERENCE_FACTORY = { + :class_methods => SingletonMethodReference, + :instance_methods => MethodReference, + } + + RAW_METHOD_STRATEGY = { + :class_methods => lambda { |target, name| target.method(name) }, + :instance_methods => lambda { |target, name| target.instance_method(name) }, + } + + # Creates new instance of MethodHandler + # + # @param [Symbol] method_name + # @param [Bool] is_class_method + # @param [Class] target - class that method got added to + def initialize(method_name, is_class_method, target) @method_name = method_name @is_class_method = is_class_method + @target = target end - # TODO: refactor it to private methods - def handle(target) - name = method_name + # Handles method addition + def handle + return unless engine? + return if decorators.empty? - return unless Engine.applied?(target) - engine = Engine.fetch_from(target) + validate_decorators! + validate_pattern_matching! - decorators = engine.all_decorators - return if decorators.empty? + engine.add_method_decorator(method_type, method_name, decorator) + mark_pattern_matching_decorators + method_reference.make_alias(target) + redefine_method + end - if is_class_method - method_reference = SingletonMethodReference.new(name, target.method(name)) - method_type = :class_methods - else - method_reference = MethodReference.new(name, target.instance_method(name)) - method_type = :instance_methods - end + private + attr_reader :method_name, :is_class_method, :target - unless decorators.size == 1 - fail %{ -Oops, it looks like method '#{name}' has multiple contracts: -#{decorators.map { |x| x[1][0].inspect }.join("\n")} + def engine? + Engine.applied?(target) + end -Did you accidentally put more than one contract on a single function, like so? + def engine + Engine.fetch_from(target) + end -Contract String => String -Contract Num => String -def foo x -end + def decorators + @_decorators ||= engine.all_decorators + end -If you did NOT, then you have probably discovered a bug in this library. -Please file it along with the relevant code at: -https://github.com/egonSchiele/contracts.ruby/issues - } - end + def method_type + @_method_type ||= is_class_method ? :class_methods : :instance_methods + end + # _method_type is required for assigning it to local variable with + # the same name. See: #redefine_method + alias_method :_method_type, :method_type - engine.decorated_methods[method_type][name] ||= [] - - pattern_matching = false - decorators.each do |klass, args| - # a reference to the method gets passed into the contract here. This is good because - # we are going to redefine this method with a new name below...so this reference is - # now the *only* reference to the old method that exists. - # We assume here that the decorator (klass) responds to .new - decorator = klass.new(target, method_reference, *args) - new_args_contract = decorator.args_contracts - matched = engine.decorated_methods[method_type][name].select do |contract| - contract.args_contracts == new_args_contract - end - unless matched.empty? - fail ContractError.new(%{ -It looks like you are trying to use pattern-matching, but -multiple definitions for function '#{name}' have the same -contract for input parameters: + def method_reference + @_method_reference ||= METHOD_REFERENCE_FACTORY[method_type].new(method_name, raw_method) + end -#{(matched + [decorator]).map(&:to_s).join("\n")} + def raw_method + RAW_METHOD_STRATEGY[method_type].call(target, method_name) + end -Each definition needs to have a different contract for the parameters. - }, {}) - end - engine.add_method_decorator(method_type, name, decorator) - pattern_matching ||= decorator.pattern_match? - end + def ignore_decorators? + ENV["NO_CONTRACTS"] && !pattern_matching? + end - if engine.decorated_methods[method_type][name].any? { |x| x.method != method_reference } - engine.decorated_methods[method_type][name].each(&:pattern_match!) + def decorated_methods + @_decorated_methods ||= engine.decorated_methods_for(method_type, method_name) + end - pattern_matching = true - end + def pattern_matching? + return @_pattern_matching if defined?(@_pattern_matching) + @_pattern_matching = decorated_methods.any? { |x| x.method != method_reference } + end - method_reference.make_alias(target) + def mark_pattern_matching_decorators + return unless pattern_matching? + decorated_methods.each(&:pattern_match!) + end + + def decorator + @_decorator ||= decorator_class.new(target, method_reference, *decorator_args) + end - return if ENV["NO_CONTRACTS"] && !pattern_matching - - # in place of this method, we are going to define our own method. This method - # just calls the decorator passing in all args that were to be passed into the method. - # The decorator in turn has a reference to the actual method, so it can call it - # on its own, after doing it's decorating of course. - - # Very important: THe line `current = #{target}` in the start is crucial. - # Not having it means that any method that used contracts could NOT use `super` - # (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27). - # Here's why: Suppose you have this code: - # - # class Foo - # Contract String - # def to_s - # "Foo" - # end - # end - # - # class Bar < Foo - # Contract String - # def to_s - # super + "Bar" - # end - # end - # - # b = Bar.new - # p b.to_s - # - # `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However, - # we have overwritten the function (that's what this next defn is). So it gets a - # reference to the function to call by looking at `decorated_methods`. - # - # Now, this line used to read something like: - # - # current = target#{is_class_method ? "" : ".class"} - # - # In that case, `target` would always be `Bar`, regardless of whether you were calling - # Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which - # means you would always call Bar's to_s...infinite recursion! Instead, you want to - # call Foo's version of decorated_methods. So the line needs to be `current = #{target}`. - - current = target + def decorator_class + decorators.first[0] + end + + def decorator_args + decorators.first[1] + end + + def redefine_method + return if ignore_decorators? + + # Those are required for instance_eval to be able to refer them + name = method_name + method_type = _method_type current_engine = engine + + # We are gonna redefine original method here method_reference.make_definition(target) do |*args, &blk| - ancestors = current.ancestors - ancestors.shift # first one is just the class itself - while current && current_engine && !current_engine.has_decorated_methods? - current = ancestors.shift - current_engine = Engine.fetch_from(current) - end + engine = current_engine.nearest_decorated_ancestor - unless current_engine && current_engine.has_decorated_methods? + # If we weren't able to find any ancestor that has decorated methods + # FIXME : this looks like untested code (commenting it out doesn't make specs red) + unless engine fail "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case." end - methods = current_engine.decorated_methods[method_type][name] - # this adds support for overloading methods. Here we go through each method and call it with the arguments. - # If we get a ContractError, we move to the next function. Otherwise we return the result. - # If we run out of functions, we raise the last ContractError. + # Fetch decorated methods out of the contracts engine + decorated_methods = engine.decorated_methods_for(method_type, name) + + # This adds support for overloading methods. Here we go + # through each method and call it with the arguments. + # If we get a failure_exception, we move to the next + # function. Otherwise we return the result. + # If we run out of functions, we raise the last error, but + # convert it to_contract_error. success = false i = 0 result = nil - expected_error = methods[0].failure_exception + expected_error = decorated_methods[0].failure_exception + until success - method = methods[i] + decorated_method = decorated_methods[i] i += 1 begin success = true - result = method.call_with(self, *args, &blk) + result = decorated_method.call_with(self, *args, &blk) rescue expected_error => error success = false - unless methods[i] + unless decorated_methods[i] begin ::Contract.failure_callback(error.data, false) rescue expected_error => final_error @@ -158,11 +146,49 @@ def foo x end end end + + # Return the result of successfully called method result end end - private - attr_reader :method_name, :is_class_method + def validate_decorators! + unless decorators.size == 1 + fail %{ +Oops, it looks like method '#{name}' has multiple contracts: +#{decorators.map { |x| x[1][0].inspect }.join("\n")} + +Did you accidentally put more than one contract on a single function, like so? + +Contract String => String +Contract Num => String +def foo x +end + +If you did NOT, then you have probably discovered a bug in this library. +Please file it along with the relevant code at: +https://github.com/egonSchiele/contracts.ruby/issues + } + end + end + + def validate_pattern_matching! + new_args_contract = decorator.args_contracts + matched = decorated_methods.select do |contract| + contract.args_contracts == new_args_contract + end + + unless matched.empty? + fail ContractError.new(%{ +It looks like you are trying to use pattern-matching, but +multiple definitions for function '#{method_name}' have the same +contract for input parameters: + +#{(matched + [decorator]).map(&:to_s).join("\n")} + +Each definition needs to have a different contract for the parameters. + }, {}) + end + end end end From aaabb0f6993d11a921b5da96e65e0dacfb5395c3 Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 17:18:56 +0200 Subject: [PATCH 10/13] Return Contract method definition for global scope --- lib/contracts.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/contracts.rb b/lib/contracts.rb index eb97d3b..909d5de 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -34,6 +34,12 @@ def functype(funcname) end base.class_eval do + # TODO: deprecate + # Required when contracts are included in global scope + def Contract(*args) + self.class.Contract(*args) + end + def functype(funcname) contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname) if contracts.nil? From c8d12f368a663b5ef1e6a1e4fe226918bbd1c80f Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 17:35:26 +0200 Subject: [PATCH 11/13] Satisfy rubocop --- lib/contracts/engine/base.rb | 5 +++-- lib/contracts/engine/eigenclass.rb | 5 ++--- lib/contracts/engine/target.rb | 5 +++-- lib/contracts/method_handler.rb | 21 +++++++++++---------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb index 1fa15ab..f642c00 100644 --- a/lib/contracts/engine/base.rb +++ b/lib/contracts/engine/base.rb @@ -64,7 +64,7 @@ def decorated_methods_for(type, name) # Returns true if there are any decorated methods # # @return [Bool] - def has_decorated_methods? + def decorated_methods? !decorated_methods[:class_methods].empty? || !decorated_methods[:instance_methods].empty? end @@ -87,7 +87,7 @@ def nearest_decorated_ancestor current_engine = self ancestors = current.ancestors[1..-1] - while current && current_engine && !current_engine.has_decorated_methods? + while current && current_engine && !current_engine.decorated_methods? current = ancestors.shift current_engine = Engine.fetch_from(current) end @@ -96,6 +96,7 @@ def nearest_decorated_ancestor end private + attr_reader :klass def decorated_methods diff --git a/lib/contracts/engine/eigenclass.rb b/lib/contracts/engine/eigenclass.rb index 73246ae..9ca1126 100644 --- a/lib/contracts/engine/eigenclass.rb +++ b/lib/contracts/engine/eigenclass.rb @@ -6,7 +6,6 @@ class Eigenclass < Base # Class that owns this eigenclass attr_accessor :owner_class - # No-op for eigenclasses def set_eigenclass_owner end @@ -20,10 +19,10 @@ def all_decorators # Fails when contracts are not included in owner class def validate! - fail ContractsNotIncluded unless has_owner? + fail ContractsNotIncluded unless owner? end - def has_owner? + def owner? !!owner_class end end diff --git a/lib/contracts/engine/target.rb b/lib/contracts/engine/target.rb index 5ee9173..c76875c 100644 --- a/lib/contracts/engine/target.rb +++ b/lib/contracts/engine/target.rb @@ -45,10 +45,11 @@ def engine end private + attr_reader :target def apply_to_eigenclass - return unless has_meaningless_eigenclass? + return unless meaningless_eigenclass? self.class.new(eigenclass).apply(Eigenclass) eigenclass.extend(MethodDecorators) @@ -59,7 +60,7 @@ def eigenclass Support.eigenclass_of(target) end - def has_meaningless_eigenclass? + def meaningless_eigenclass? !Support.eigenclass?(target) end end diff --git a/lib/contracts/method_handler.rb b/lib/contracts/method_handler.rb index e5d259a..ee16b6b 100644 --- a/lib/contracts/method_handler.rb +++ b/lib/contracts/method_handler.rb @@ -4,12 +4,12 @@ module Contracts class MethodHandler METHOD_REFERENCE_FACTORY = { :class_methods => SingletonMethodReference, - :instance_methods => MethodReference, + :instance_methods => MethodReference } RAW_METHOD_STRATEGY = { :class_methods => lambda { |target, name| target.method(name) }, - :instance_methods => lambda { |target, name| target.instance_method(name) }, + :instance_methods => lambda { |target, name| target.instance_method(name) } } # Creates new instance of MethodHandler @@ -38,6 +38,7 @@ def handle end private + attr_reader :method_name, :is_class_method, :target def engine? @@ -153,8 +154,9 @@ def redefine_method end def validate_decorators! - unless decorators.size == 1 - fail %{ + return if decorators.size == 1 + + fail %{ Oops, it looks like method '#{name}' has multiple contracts: #{decorators.map { |x| x[1][0].inspect }.join("\n")} @@ -168,8 +170,7 @@ def foo x If you did NOT, then you have probably discovered a bug in this library. Please file it along with the relevant code at: https://github.com/egonSchiele/contracts.ruby/issues - } - end + } end def validate_pattern_matching! @@ -178,8 +179,9 @@ def validate_pattern_matching! contract.args_contracts == new_args_contract end - unless matched.empty? - fail ContractError.new(%{ + return if matched.empty? + + fail ContractError.new(%{ It looks like you are trying to use pattern-matching, but multiple definitions for function '#{method_name}' have the same contract for input parameters: @@ -187,8 +189,7 @@ def validate_pattern_matching! #{(matched + [decorator]).map(&:to_s).join("\n")} Each definition needs to have a different contract for the parameters. - }, {}) - end + }, {}) end end end From 054aff92dce0ccb870dbdf1b0846abe0b32d4b29 Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 17:46:41 +0200 Subject: [PATCH 12/13] Add documentation for Engine::Base#decorated_methods_for --- lib/contracts/engine/base.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb index f642c00..d14ed96 100644 --- a/lib/contracts/engine/base.rb +++ b/lib/contracts/engine/base.rb @@ -57,6 +57,11 @@ def all_decorators pop_decorators + eigenclass_engine.all_decorators end + # Fetches decorators of specified type for method with name + # + # @param [Or[:class_methods, :instance_methods]] type - method type + # @param [Symbol] name - method name + # @return [ArrayOf[Decorator]] def decorated_methods_for(type, name) Array(decorated_methods[type][name]) end From ea19e3bfb67ffadce05cea7cc9b2a1629f261ed2 Mon Sep 17 00:00:00 2001 From: Alexey Fedorov Date: Mon, 4 May 2015 18:00:39 +0200 Subject: [PATCH 13/13] Support for different rubies --- lib/contracts/engine.rb | 2 +- lib/contracts/engine/base.rb | 2 +- lib/contracts/engine/eigenclass.rb | 16 ++++++++++++++++ lib/contracts/engine/target.rb | 4 ++-- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/contracts/engine.rb b/lib/contracts/engine.rb index ddcbae1..063a397 100644 --- a/lib/contracts/engine.rb +++ b/lib/contracts/engine.rb @@ -14,7 +14,7 @@ class << self # .apply(klass) - enables contracts engine on klass # .applied?(klass) - returns true if klass has contracts engine # .fetch_from(klass) - returns contracts engine for klass - delegate [:apply, :applied?, :fetch_from] => :base_engine + def_delegators :base_engine, :apply, :applied?, :fetch_from private diff --git a/lib/contracts/engine/base.rb b/lib/contracts/engine/base.rb index d14ed96..cd86630 100644 --- a/lib/contracts/engine/base.rb +++ b/lib/contracts/engine/base.rb @@ -121,7 +121,7 @@ def eigenclass end def eigenclass_engine - Engine.fetch_from(eigenclass) + Eigenclass.lift(eigenclass, klass) end def decorators diff --git a/lib/contracts/engine/eigenclass.rb b/lib/contracts/engine/eigenclass.rb index 9ca1126..be6f66c 100644 --- a/lib/contracts/engine/eigenclass.rb +++ b/lib/contracts/engine/eigenclass.rb @@ -6,6 +6,22 @@ class Eigenclass < Base # Class that owns this eigenclass attr_accessor :owner_class + # Automatically enables eigenclass engine if it is not + # Returns its engine + # NOTE: Required by jruby in 1.9 mode. Otherwise inherited + # eigenclasses don't have their engines + # + # @param [Class] eigenclass - class in question + # @param [Class] owner - owner of eigenclass + # @return [Engine::Eigenclass] + def self.lift(eigenclass, owner) + return Engine.fetch_from(eigenclass) if Engine.applied?(eigenclass) + + Target.new(eigenclass).apply(Eigenclass) + Engine.fetch_from(owner).set_eigenclass_owner + Engine.fetch_from(eigenclass) + end + # No-op for eigenclasses def set_eigenclass_owner end diff --git a/lib/contracts/engine/target.rb b/lib/contracts/engine/target.rb index c76875c..f8553f6 100644 --- a/lib/contracts/engine/target.rb +++ b/lib/contracts/engine/target.rb @@ -21,8 +21,8 @@ def apply(engine_class = Base) apply_to_eigenclass - target.class_eval do - define_singleton_method(:__contracts_engine) do + eigenclass.class_eval do + define_method(:__contracts_engine) do @__contracts_engine ||= engine_class.new(self) end end