Module: Contracts::MethodDecorators
- Defined in:
- lib/contracts/decorators.rb
Defined Under Namespace
Modules: EigenclassWithOwner
Class Method Summary collapse
Instance Method Summary collapse
- #common_method_added(name, is_class_method) ⇒ Object
- #decorate(klass, *args) ⇒ Object
- #fetch_decorators ⇒ Object
-
#method_added(name) ⇒ Object
first, when you write a contract, the decorate method gets called which sets the @decorators variable.
- #pop_decorators ⇒ Object
- #singleton_method_added(name) ⇒ Object
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_decorators ⇒ Object
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_decorators ⇒ Object
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 |