Skip to content

Commit da13531

Browse files
committed
Merge pull request #141 from waterlink/non_intrusive_contracts_v2
Refactor to contracts engine
2 parents bc4dcd6 + ea19e3b commit da13531

File tree

13 files changed

+525
-303
lines changed

13 files changed

+525
-303
lines changed

TUTORIAL.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,12 +510,11 @@ This is because the first contract eliminated the possibility of `age` being les
510510

511511
## Contracts in modules
512512

513-
To use contracts on module you need to include both `Contracts` and `Contracts::Modules` into it:
513+
Usage is the same as contracts in classes:
514514

515515
```ruby
516516
module M
517517
include Contracts
518-
include Contracts::Modules
519518

520519
Contract String => String
521520
def self.parse

lib/contracts.rb

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
require "contracts/builtin_contracts"
22
require "contracts/decorators"
3-
require "contracts/eigenclass"
43
require "contracts/errors"
54
require "contracts/formatters"
65
require "contracts/invariants"
76
require "contracts/method_reference"
8-
require "contracts/modules"
97
require "contracts/support"
8+
require "contracts/engine"
9+
require "contracts/method_handler"
1010

1111
module Contracts
1212
def self.included(base)
@@ -18,15 +18,13 @@ def self.extended(base)
1818
end
1919

2020
def self.common(base)
21-
Eigenclass.lift(base)
22-
2321
return if base.respond_to?(:Contract)
2422

2523
base.extend(MethodDecorators)
2624

2725
base.instance_eval do
2826
def functype(funcname)
29-
contracts = decorated_methods[:class_methods][funcname]
27+
contracts = Engine.fetch_from(self).decorated_methods_for(:class_methods, funcname)
3028
if contracts.nil?
3129
"No contract for #{self}.#{funcname}"
3230
else
@@ -36,22 +34,14 @@ def functype(funcname)
3634
end
3735

3836
base.class_eval do
39-
unless base.instance_of?(Module)
40-
def Contract(*args)
41-
return if ENV["NO_CONTRACTS"]
42-
if self.class == Module
43-
puts %{
44-
Warning: You have added a Contract on a module function
45-
without including Contracts::Modules. Your Contract will
46-
just be ignored. Please include Contracts::Modules into
47-
your module.}
48-
end
49-
self.class.Contract(*args)
50-
end
37+
# TODO: deprecate
38+
# Required when contracts are included in global scope
39+
def Contract(*args)
40+
self.class.Contract(*args)
5141
end
5242

5343
def functype(funcname)
54-
contracts = self.class.decorated_methods[:instance_methods][funcname]
44+
contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname)
5545
if contracts.nil?
5646
"No contract for #{self.class}.#{funcname}"
5747
else

lib/contracts/decorators.rb

Lines changed: 4 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,18 @@
11
module Contracts
22
module MethodDecorators
33
def self.extended(klass)
4-
return if klass.respond_to?(:decorated_methods=)
5-
6-
class << klass
7-
attr_accessor :decorated_methods
8-
end
9-
end
10-
11-
module EigenclassWithOwner
12-
def self.lift(eigenclass)
13-
fail Contracts::ContractsNotIncluded unless with_owner?(eigenclass)
14-
15-
eigenclass
16-
end
17-
18-
private
19-
20-
def self.with_owner?(eigenclass)
21-
eigenclass.respond_to?(:owner_class) && eigenclass.owner_class
22-
end
4+
Engine.apply(klass)
235
end
246

25-
# first, when you write a contract, the decorate method gets called which
26-
# sets the @decorators variable. Then when the next method after the contract
27-
# is defined, method_added is called and we look at the @decorators variable
28-
# to find the decorator for that method. This is how we associate decorators
29-
# with methods.
307
def method_added(name)
31-
common_method_added name, false
8+
MethodHandler.new(name, false, self).handle
329
super
3310
end
3411

3512
def singleton_method_added(name)
36-
common_method_added name, true
13+
MethodHandler.new(name, true, self).handle
3714
super
3815
end
39-
40-
def pop_decorators
41-
Array(@decorators).tap { @decorators = nil }
42-
end
43-
44-
def fetch_decorators
45-
pop_decorators + Eigenclass.lift(self).pop_decorators
46-
end
47-
48-
def common_method_added(name, is_class_method)
49-
decorators = fetch_decorators
50-
return if decorators.empty?
51-
52-
@decorated_methods ||= { :class_methods => {}, :instance_methods => {} }
53-
54-
if is_class_method
55-
method_reference = SingletonMethodReference.new(name, method(name))
56-
method_type = :class_methods
57-
else
58-
method_reference = MethodReference.new(name, instance_method(name))
59-
method_type = :instance_methods
60-
end
61-
62-
@decorated_methods[method_type][name] ||= []
63-
64-
unless decorators.size == 1
65-
fail %{
66-
Oops, it looks like method '#{name}' has multiple contracts:
67-
#{decorators.map { |x| x[1][0].inspect }.join("\n")}
68-
69-
Did you accidentally put more than one contract on a single function, like so?
70-
71-
Contract String => String
72-
Contract Num => String
73-
def foo x
74-
end
75-
76-
If you did NOT, then you have probably discovered a bug in this library.
77-
Please file it along with the relevant code at:
78-
https://github.com/egonSchiele/contracts.ruby/issues
79-
}
80-
end
81-
82-
pattern_matching = false
83-
decorators.each do |klass, args|
84-
# a reference to the method gets passed into the contract here. This is good because
85-
# we are going to redefine this method with a new name below...so this reference is
86-
# now the *only* reference to the old method that exists.
87-
# We assume here that the decorator (klass) responds to .new
88-
decorator = klass.new(self, method_reference, *args)
89-
new_args_contract = decorator.args_contracts
90-
matched = @decorated_methods[method_type][name].select do |contract|
91-
contract.args_contracts == new_args_contract
92-
end
93-
unless matched.empty?
94-
fail ContractError.new(%{
95-
It looks like you are trying to use pattern-matching, but
96-
multiple definitions for function '#{name}' have the same
97-
contract for input parameters:
98-
99-
#{(matched + [decorator]).map(&:to_s).join("\n")}
100-
101-
Each definition needs to have a different contract for the parameters.
102-
}, {})
103-
end
104-
@decorated_methods[method_type][name] << decorator
105-
pattern_matching ||= decorator.pattern_match?
106-
end
107-
108-
if @decorated_methods[method_type][name].any? { |x| x.method != method_reference }
109-
@decorated_methods[method_type][name].each(&:pattern_match!)
110-
111-
pattern_matching = true
112-
end
113-
114-
method_reference.make_alias(self)
115-
116-
return if ENV["NO_CONTRACTS"] && !pattern_matching
117-
118-
# in place of this method, we are going to define our own method. This method
119-
# just calls the decorator passing in all args that were to be passed into the method.
120-
# The decorator in turn has a reference to the actual method, so it can call it
121-
# on its own, after doing it's decorating of course.
122-
123-
# Very important: THe line `current = #{self}` in the start is crucial.
124-
# Not having it means that any method that used contracts could NOT use `super`
125-
# (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27).
126-
# Here's why: Suppose you have this code:
127-
#
128-
# class Foo
129-
# Contract String
130-
# def to_s
131-
# "Foo"
132-
# end
133-
# end
134-
#
135-
# class Bar < Foo
136-
# Contract String
137-
# def to_s
138-
# super + "Bar"
139-
# end
140-
# end
141-
#
142-
# b = Bar.new
143-
# p b.to_s
144-
#
145-
# `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However,
146-
# we have overwritten the function (that's what this next defn is). So it gets a
147-
# reference to the function to call by looking at `decorated_methods`.
148-
#
149-
# Now, this line used to read something like:
150-
#
151-
# current = self#{is_class_method ? "" : ".class"}
152-
#
153-
# In that case, `self` would always be `Bar`, regardless of whether you were calling
154-
# Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which
155-
# means you would always call Bar's to_s...infinite recursion! Instead, you want to
156-
# call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
157-
158-
current = self
159-
method_reference.make_definition(self) do |*args, &blk|
160-
ancestors = current.ancestors
161-
ancestors.shift # first one is just the class itself
162-
while current && !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
163-
current = ancestors.shift
164-
end
165-
if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
166-
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."
167-
end
168-
methods = current.decorated_methods[method_type][name]
169-
170-
# this adds support for overloading methods. Here we go through each method and call it with the arguments.
171-
# If we get a ContractError, we move to the next function. Otherwise we return the result.
172-
# If we run out of functions, we raise the last ContractError.
173-
success = false
174-
i = 0
175-
result = nil
176-
expected_error = methods[0].failure_exception
177-
until success
178-
method = methods[i]
179-
i += 1
180-
begin
181-
success = true
182-
result = method.call_with(self, *args, &blk)
183-
rescue expected_error => error
184-
success = false
185-
unless methods[i]
186-
begin
187-
::Contract.failure_callback(error.data, false)
188-
rescue expected_error => final_error
189-
raise final_error.to_contract_error
190-
end
191-
end
192-
end
193-
end
194-
result
195-
end
196-
end
197-
198-
def decorate(klass, *args)
199-
if Support.eigenclass? self
200-
return EigenclassWithOwner.lift(self).owner_class.decorate(klass, *args)
201-
end
202-
203-
@decorators ||= []
204-
@decorators << [klass, args]
205-
end
20616
end
20717

20818
class Decorator
@@ -220,7 +30,7 @@ def self.inherited(klass)
22030
# inside, `decorate` is called with those params.
22131
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
22232
def #{klass}(*args, &blk)
223-
decorate(#{klass}, *args, &blk)
33+
::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk)
22434
end
22535
ruby_eval
22636
end

lib/contracts/eigenclass.rb

Lines changed: 0 additions & 38 deletions
This file was deleted.

lib/contracts/engine.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require "contracts/engine/base"
2+
require "contracts/engine/target"
3+
require "contracts/engine/eigenclass"
4+
5+
require "forwardable"
6+
7+
module Contracts
8+
# Engine facade, normally you shouldn't refer internals of Engine
9+
# module directly.
10+
module Engine
11+
class << self
12+
extend Forwardable
13+
14+
# .apply(klass) - enables contracts engine on klass
15+
# .applied?(klass) - returns true if klass has contracts engine
16+
# .fetch_from(klass) - returns contracts engine for klass
17+
def_delegators :base_engine, :apply, :applied?, :fetch_from
18+
19+
private
20+
21+
def base_engine
22+
Base
23+
end
24+
end
25+
end
26+
end

0 commit comments

Comments
 (0)