Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/contracts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
require "contracts/method_reference"
require "contracts/modules"
require "contracts/support"
require "contracts/engine"
require "contracts/method_handler"

module Contracts
def self.included(base)
Expand All @@ -18,15 +20,15 @@ def self.extended(base)
end

def self.common(base)
Eigenclass.lift(base)
#Eigenclass.lift(base)

return if base.respond_to?(:Contract)

base.extend(MethodDecorators)

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
Expand All @@ -51,7 +53,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
Expand Down
193 changes: 4 additions & 189 deletions lib/contracts/decorators.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -28,181 +10,14 @@ def self.with_owner?(eigenclass)
# 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 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?

@decorated_methods ||= { :class_methods => {}, :instance_methods => {} }

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

@decorated_methods[method_type][name] ||= []

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

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 = @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
@decorated_methods[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!)

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
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?
current = ancestors.shift
end
if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
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]

# 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

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
Expand All @@ -220,7 +35,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
Expand Down
142 changes: 142 additions & 0 deletions lib/contracts/engine.rb
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just eigenclass_engine here

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
Loading