Enable users to build up on your Rails Engines and Gems without monkey patching, inheritence or overriding methods

This short tutorial is intended for authors of Gems and Engines.

When writing gems or Rails engines they can be difficult for the user to extend.

For example Devise, the authentication Rails engine, has a Wiki page showing how to send an email after a user changes their password.

The Wiki suggests monkey patching your model overriding the update_with_password method.

def update_with_password(params, *options)
  if super
    UserMailer.password_changed(self.id).deliver
  end
end

Monkey patching is dangerous particually if the method is not part of the public API.

Even patch version updates of Devise could potentially break your code.

In addition the method is not very readable, it seems super returns a boolean, but what is it actually doing? Can we be sure it will always return a boolean, or have the same method signature…

An alternative would be for Devise to broadcast events which the host application can listen for and react to. This can be achived with the Wisper gem.

In the Devise implementation of update_with_password they could broadcast an event:

module DatabaseAuthenticatable
  extend ActiveSupport::Concern
  
  included do
    include Wisper.publisher # <-- LINE ADDED HERE
  end
  
  def update_with_password(params, *options)
    current_password = params.delete(:current_password)

    if params[:password].blank?
      params.delete(:password)
      params.delete(:password_confirmation) if params[:password_confirmation].blank?
    end

    result = if valid_password?(current_password)
      update_attributes(params, *options)
      broadcast(:password_updated, self) # <-- LINE ADDED HERE
    else
      self.assign_attributes(params, *options)
      self.valid?
      self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
      false
    end

    clean_up_passwords
    result
  end
end

Within our Rails application we could then subscribe a listener for this event in an initializer:

User.subscribe(UserListener.new, prefix: 'on')

In the case of sending an email after the email address has changed the listener would look like this:

class UserListener
  def on_password_changed(user_id)
    UserMailer.password_changed(user_id).deliver
  end
end

The nice thing about this pattern is the Gem/Engine author can broadcast many events, with a single line of code for each and does not need to consider the method name and signature since it no longer needs to be overridden in the Rails app.

On the flip side the Rails application does not need to know about the Gem internals and can be confident changes to the API will not break it.

  • Easy for Engine/Gem author to impliment, simply add Wisper as a runtime dependency and use the broadcast method
  • Easy for the Engine/Gem user, simply subscribe a listener, no need to monkey patch internals.

On top of this the additional options built in to Wisper are avalible including support for async events, note that the Gem/Engine author does not need to make any changes or add any additional dependencies to allow the Rails app to respond to its events async.

Other places Devise could broadcast events would be signin / signout / change password etc. A listener could subscribe to these events to, for example, record an audit of all logins.

In the above example I subscribed the listener to all instances of User, but you could also subscribe to indavidual instances:

@user.subscribe(SpecialListener.new, prefix: 'on')

Any gem or engine can use this technique to provide optional, decoupled callbacks. I hope you consider it for your own Gem or Engine.

Happy hacking.