Module: Contracts::MethodDecorators

Defined in:
lib/contracts/decorators.rb

Defined Under Namespace

Modules: EigenclassWithOwner

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extended(klass) ⇒ Object



3
4
5
6
7
8
9
# File 'lib/contracts/decorators.rb', line 3

def self.extended(klass)
  return if klass.respond_to?(:decorated_methods=)

  class << klass
    attr_accessor :decorated_methods
  end
end

Instance Method Details

#common_method_added(name, is_class_method) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/contracts/decorators.rb', line 48

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

#decorate(klass, *args) ⇒ Object



198
199
200
201
202
203
204
205
# File 'lib/contracts/decorators.rb', line 198

def decorate(klass, *args)
  if Support.eigenclass? self
    return EigenclassWithOwner.lift(self).owner_class.decorate(klass, *args)
  end

  @decorators ||= []
  @decorators << [klass, args]
end

#fetch_decoratorsObject



44
45
46
# File 'lib/contracts/decorators.rb', line 44

def fetch_decorators
  pop_decorators + Eigenclass.lift(self).pop_decorators
end

#method_added(name) ⇒ Object

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.



30
31
32
33
# File 'lib/contracts/decorators.rb', line 30

def method_added(name)
  common_method_added name, false
  super
end

#pop_decoratorsObject



40
41
42
# File 'lib/contracts/decorators.rb', line 40

def pop_decorators
  Array(@decorators).tap { @decorators = nil }
end

#singleton_method_added(name) ⇒ Object



35
36
37
38
# File 'lib/contracts/decorators.rb', line 35

def singleton_method_added(name)
  common_method_added name, true
  super
end