3

I have a plain ruby class Espresso::MyExampleClass.

module Espresso
  class MyExampleClass
    def my_first_function(value)
      puts "my_first_function"
    end

    def my_function_to_run_before
      puts "Running before"
    end
  end
end

With some of the methods in the class, I want to perform a before or after callback similar to ActiveSupport callbacks before_action or before_filter. I'd like to put something like this in my class, which will run my_function_to_run_before before my_first_function:

before_method :my_function_to_run_before, only: :my_first_function

The result should be something like:

klass = Espresso::MyExampleClass.new
klass.my_first_function("yes")

> "Running before"
> "my_first_function"

How do I use call backs in a plain ruby class like in Rails to run a method before each specified method?

Edit2:

Thanks @tadman for recommending XY problem. The real issue we have is with an API client that has a token expiration. Before each call to the API, we need to check to see if the token is expired. If we have a ton of function for the API, it would be cumbersome to check if the token was expired each time.

Here is the example class:

require "rubygems"
require "bundler/setup"
require 'active_support/all'
require 'httparty'
require 'json'

module Espresso

  class Client
    include HTTParty
    include ActiveSupport::Callbacks

    def initialize
      login("admin@example.com", "password")
    end

    def login(username, password)
      puts "logging in"
      uri = URI.parse("localhost:3000" + '/login')
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
      request = Net::HTTP::Post.new(uri.request_uri)
      request.set_form_data(username: username, password: password)
      response = http.request(request)
      body = JSON.parse(response.body)
      @access_token = body['access_token']
      @expires_in = body['expires_in']
      @expires = @expires_in.seconds.from_now
      @options = {
          headers: {
              Authorization: "Bearer #{@access_token}"
          }
      }
    end

    def is_token_expired?
      #if Time.now > @expires.
      if 1.hour.ago > @expires
        puts "Going to expire"
      else
        puts "not going to expire"
      end

      1.hour.ago > @expires ? false : true
    end

    # Gets posts
    def get_posts
      #Check if the token is expired, if is login again and get a new token
      if is_token_expired?
        login("admin@example.com", "password")
      end
      self.class.get('/posts', @options)
    end

    # Gets comments
    def get_comments
      #Check if the token is expired, if is login again and get a new token
      if is_token_expired?
        login("admin@example.com", "password")
      end
      self.class.get('/comments', @options)
    end
  end
end

klass = Espresso::Client.new
klass.get_posts
klass.get_comments
DogEatDog
  • 2,899
  • 2
  • 36
  • 65
  • Before *what* though? – tadman Mar 12 '18 at 16:49
  • When I call `my_first_function`, the function `my_function_to_run_before` should run right before it – DogEatDog Mar 12 '18 at 16:54
  • 1
    This method chaining gets pretty ugly in terms of implementations since you have to redefine `x` to wrap around `x`. ActiveRecord doesn't wrap methods because it has a better internal dispatch system. – tadman Mar 12 '18 at 17:01
  • I have a use case where before specific methods, I want to check if a timeout has expired in an instance variable. so, before each specified method is run in the class, I want to run a method to check the expiration. The easy way would be to just put that method call at the top of each method, but that would be a lot of bloat. – DogEatDog Mar 12 '18 at 17:03
  • 1
    To avoid [XY Problems](http://xyproblem.info) it might make sense to make an example that better represents your intent than something that returns toy strings. – tadman Mar 12 '18 at 17:04
  • 1
    Possible duplicate of [Executing code for every method call in a Ruby module](https://stackoverflow.com/questions/5513558/executing-code-for-every-method-call-in-a-ruby-module) – eyevan Mar 12 '18 at 17:14
  • @Tagman I added clarification for the specific question. Thank you for recommending the XY problems. – DogEatDog Mar 12 '18 at 17:16
  • 1
    @DogEatDog you're incuding `ActiveSupport::Callbacks`, but don't seem to be using it as far as I can tell. Have you tried implementing something analogous to [their example](http://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html)? – max pleaner Mar 12 '18 at 17:40
  • That can be ommitted. It was one of the attempts I had made earlier. – DogEatDog Mar 12 '18 at 17:59
  • 1
    @DogEatDog it is worth a try. Maybe you can show your previous attempt – max pleaner Mar 12 '18 at 19:59

1 Answers1

3

A naive implementation would be;

module Callbacks

  def self.extended(base)
    base.send(:include, InstanceMethods)
  end

  def overridden_methods
    @overridden_methods ||= []
  end

  def callbacks
    @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
  end

  def method_added(method_name)
    return if should_override?(method_name)

    overridden_methods << method_name
    original_method_name = "original_#{method_name}"
    alias_method(original_method_name, method_name)

    define_method(method_name) do |*args|
      run_callbacks_for(method_name)
      send(original_method_name, *args)
    end
  end

  def should_override?(method_name)
    overridden_methods.include?(method_name) || method_name =~ /original_/
  end

  def before_run(method_name, callback)
    callbacks[method_name] << callback
  end

  module InstanceMethods
    def run_callbacks_for(method_name)
      self.class.callbacks[method_name].to_a.each do |callback|
        send(callback)
      end
    end
  end
end

class Foo
  extend Callbacks

  before_run :bar, :zoo

  def bar
    puts 'bar'
  end

  def zoo
    puts 'This runs everytime you call `bar`'
  end

end

Foo.new.bar #=> This runs everytime you call `bar`
            #=> bar

The tricky point in this implementation is, method_added. Whenever a method gets bind, method_added method gets called by ruby with the name of the method. Inside of this method, what I am doing is just name mangling and overriding the original method with the new one which first runs the callbacks then calls the original method.

Note that, this implementation neither supports block callbacks nor callbacks for super class methods. Both of them could be implemented easily though.

Foo Bar Zoo
  • 206
  • 3
  • 10