Module: Decoratable

Included in:
Countable, Debuggable, Deprecatable, Hintable, Memoizable, Pryable, Retryable, Synchronizable
Defined in:
lib/decoratable.rb,
lib/decoratable/classic.rb

Overview

Public: provide an easy way to define decorations that add common behaviour to a method.

More info on decorations as implemented in Python: en.wikipedia.org/wiki/Python_syntax_and_semantics#Decorators

Examples

module Decorations
  extend Decoratable

  def retryable(tries = 1, options = { on: [RuntimeError] })
    attempts = 0

    begin
      yield
    rescue *options[:on]
      attempts += 1
      attempts > tries ? raise : retry
    end
  end

  def measurable(logger = STDOUT)
    start = Time.now
    yield
  ensure
    original_method = __decorated_method__
    method_location, line = original_method.source_location
    marker = "#{original_method.owner}##{original_method.name}[#{method_location}:#{line}]"
    duration = (Time.now - start).round(2)

    logger.puts "#{marker} took #{duration}s to run."
  end

  def debuggable
    begin
      yield
    rescue => e
      puts "Caught #{e}!!!"
      require "debug"
    end
  end

  def memoizable
    key = :"@#{__decorated_method__.name}_cache"

    instance_variable_set(key, {}) unless defined?(key)
    cache = instance_variable_get(key)

    if cache.key?(__args__)
      cache[__args__]
    else
      cache[__args__] = yield
    end
  end
end

class Client
  extend Decorations

  # Let's keep track of how long #get takes to run,
  # and memoize the return value
  def get
    

Constant Summary collapse

@@lock =
Mutex.new

Class Method Summary collapse

Class Method Details

.extended(klass) ⇒ Object



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
# File 'lib/decoratable.rb', line 80

def self.extended(klass)
  # This #method_added affects all methods defined in the module
  # that extends Decoratable.
  def klass.method_added(decoration_name)
    return unless @@lock.try_lock

    decoration_method = instance_method(decoration_name)

    define_method(decoration_name) do |*decorator_args|
      # Wrap method_added to decorate the next method definition.
      self.singleton_class.instance_eval do
        alias_method "method_added_without_#{decoration_name}", :method_added
      end

      unless method_defined?(:__original_caller__)
        define_method(:__original_caller__) { @__original_caller__ }
      end

      unless method_defined?(:__decorated_method__)
        define_method(:__decorated_method__) { @__decorated_method__ }
      end

      unless method_defined?(:__args__)
        define_method(:__args__) { @__args__ }
      end

      unless method_defined?(:__block__)
        define_method(:__block__) { @__block__ }
      end

      # This method_added will affect the next decorated method.
      define_singleton_method(:method_added) do |method_name|

        original_method = instance_method(method_name)

        decoration = Module.new do
          self.singleton_class.instance_eval do
            define_method(:name) do
              "Decoratable::#{decoration_name}(#{method_name})"
            end

            alias_method :inspect, :name
            alias_method :to_s, :name
          end

          define_method(method_name) do |*args, &block|
            begin
              # The decoration should have access to the original
              # method it's modifying, along with the method call's
              # arguments.
              @__decorated_method__ = original_method
              @__args__ = args
              @__block__ = block
              @__original_caller__ ||= caller

              decoration_method.bind(self).call(*decorator_args) do
                super(*args, &block)
              end
            ensure
              @__decorated_method__ = nil
              @__args__ = nil
              @__block__ = nil
              @__original_caller__ = nil
            end
          end
        end

        # Call aspect before "real" method.
        prepend decoration

        # Call next method_added link in the chain.
        __send__("method_added_without_#{decoration_name}", method_name)

        # Remove ourselves from method_added chain.
        self.singleton_class.instance_eval do
          alias_method :method_added, "method_added_without_#{decoration_name}"
          remove_method  "method_added_without_#{decoration_name}"
        end
      end
    end
  ensure
    @@lock.unlock if @@lock.locked?
  end
end