module Decoratable

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
  measurable
  memoizable
  def get
    
  end

  # Rescue and retry any Timeout::Errors, up to 5 times
  retryable 5, on: [Timeout::Error]
  def post
    
  end
end

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
    
  end
  measurable :get
  memoizable :get

  # Rescue and retry any Timeout::Errors, up to 5 times
  def post
    
  end
  retryable :post, 5, on: [Timeout::Error]
end

Public Class Methods

extended(klass) click to toggle source
# 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