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("[email protected]", "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("[email protected]", "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("[email protected]", "password") end self.class.get('/comments', @options) end end end klass = Espresso::Client.new klass.get_posts klass.get_comments 
10
  • 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. Commented Mar 12, 2018 at 17:01
  • 1
    To avoid XY Problems it might make sense to make an example that better represents your intent than something that returns toy strings. Commented Mar 12, 2018 at 17:04
  • 1
    Possible duplicate of Executing code for every method call in a Ruby module Commented Mar 12, 2018 at 17:14
  • 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? Commented Mar 12, 2018 at 17:40
  • 1
    @DogEatDog it is worth a try. Maybe you can show your previous attempt Commented Mar 12, 2018 at 19:59

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.