Useful Active Support features you may not have heard of

Posted on 2021-05-10 by Karol Bąk Comments
ruby
rails
activesupport

Active Support is a really powerful library that is a part of Rails framework. It contains many useful core extensions that empower standard ruby objects with additional features. For example it adds hours method to Integer class and allows us to create simple and readable code like created_at < 2.hours.ago instead of created_at < Time.current - 2*60*60. Today we’ll dive beyond that and focus on many utilities it includes, which can be useful in your next Rails project or any other ruby app.

Callbacks

We are all familiar with callbacks. before_action in the controllers and after_save in the models are commonly seen in Rails applications. I’m not going to arbitrate if they are good or bad - there are many different opinions around. Personally, I try to avoid them if possible but it’s up to you to decide. I’ll just focus on how we can easily add them to any ruby object.

Imagine a simple service that handles some work:

class ExampleService < BaseService
  def call
    # do_some_work
  end
end

In our BaseService we can include ActiveSupport::Callbacks, define callbacks and use it inside call method.

class BaseService
  include ActiveSupport::Callbacks
  define_callbacks :call

  def call
    run_callbacks :call do
      process
    end
  end
end

Now we have to replace call method with process in our service:

class ExampleService < BaseService
  def process
    # do_some_work
  end
end

Now our services supports callbacks before, after and around call method. What can we use it for? There are many possibilities - we can validate attributes before performing some operation, we can save information if the data has been processed and skip operation next time the service is called and much more. All that outside process method which can now focus only on the job it needs to perform.

class ExampleService < BaseService
  set_callback :call, :before, :check_already_processed
  set_callback :call, :before, :check_attributes
  set_callback :call, :after, :log_processed

  def process
    # do_some_work
  end

  private

  def log_processed
    #...
  end

  def check_attributes
    #...
  end

  def check_already_processed
    #...
  end
end

You can also add an if argument to set_callback method to execute callback only under some conditions, use skip_callback method to skip already defined callbacks or add terminator argument to define_callbacks method to halt the execution in some cases (for example when before filter returns false). Please refer to docs if you would like to learn more - https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html

Configurable

The next utility I want to focus on is ActiveSupport::Configurable which adds a config method to the class which can store some configuration.

class Example
  include ActiveSupport::Configurable
end

Example.config.some_option = 'value'

Example.config.some_option
=> "value"

We can use it with a block using configure method:

class Example
  include ActiveSupport::Configurable
end

Example.configure do |config|
  config.some_option = 'value'
end

Example.config.some_option
=> "value"

It provides config_accessor method which exposes configuration as a class method and allows to set up default value:

class Example
  include ActiveSupport::Configurable

  config_accessor :some_option do
    'value'
  end
end

Example.config.some_option
=> "value"
Example.some_option
=> "value"

It is really useful if you need to add configuration to your gem or any other class in your app.

CurrentAttributes

ActiveSupport::CurrentAttributes allows to store some objects and access them anywhere in the code. It’s thread-safe and resets automatically on each request. It can be used to store request information such as current user, action, IP address and use them in models, services and any other place without passing them directly. Some real word example - imagine a case when you need to log all model changes with information who changed it. Passing this data everywhere can be really painful. We can use CurrentAttributes to store them and use them in after_save callback on the tracked model.

class Current < ActiveSupport::CurrentAttributes
  attribute :user, :ip, :controller, :action
end

class SampleController < ApplicationController
  before_action :set_context

  def set_context
    Current.user = current_user
    Current.ip = request.remote_ip
    Current.controller = params[:controller]
    Current.action = params[:action]
  end
end

class MyModel < ApplicationRecord
  has_many :object_changes

  after_save :save_object_changes

  private

  def save_object_changes
    object_changes.create!(
      user_id: Current.user.id,
      ip: Current.ip,
      controller: Current.controller,
      action: Current.action,
      changes: previous_changes
    )
  end
end

There is a more complex example in Active Support docs: https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html

Since having such global variables accessible from anywhere is considered a bad pattern, I would advise using it only when it’s necessary.

MessageVerifier

If you follow Rails release notes you’ve probably noticed that “Signed ids” were added in Rails 6.1. They are built on the top of MessageVerifier. Using this utility we are able to generate signed messages and verify them.

key = 'my_secret_key' # use env variable or generate via ActiveSupport::KeyGenerator
verifier = ActiveSupport::MessageVerifier.new(key)
verifier.generate('my message')
=> "BAhJIg9teSBtZXNzYWdlBjoGRVQ=--078c6389020294311bb45f099ab56450d9127d44" 

It’s also possible to set purpose and expiration time using expires_in or expires_at:

verifier.generate('my message', purpose: :login, expires_in: 2.hours)
=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZzl0ZVNCdFpYTnpZV2RsQmpvR1JWUT0iLCJleHAiOiIyMDIxLTA1LTA4VDIyOjU1OjA1LjMxM1oiLCJwdXIiOiJsb2dpbiJ9fQ==--dabf60a144f587f267b6bef2bb7e6e4a831f3534" 

To verify a message we can use verified method (which returns nil if a message is invalid) or verify (which will raise an error):

verifier.verified(message)
=> "my message" 
verifier.verified('invalid message')
=> nil 

It can be used to create reset password links without storing any tokens in the database. Important note - message content can be easily determined. Signature only guarantees that it has not been changed. If you want to keep the content safe take a look below.

MessageEncryptor

The last utility I want to talk about is ActiveSupport::MessageEncryptor. It’s pretty similar to MessageVerifier but it keeps the content of the message safe and impossible to read without the key.

key = SecureRandom.random_bytes(32) 
crypt = ActiveSupport::MessageEncryptor.new(key) 

message = crypt.encrypt_and_sign('my message')

crypt.decrypt_and_verify(message)
=> "my message" 

Like MessageVerifier it accepts purpose, expires_in and expires_at attributes.

It can be used to send sensitive information between services on the client-side (for example in url) or to encrypt data before storing it in the database.

Conclusion

As you can see ActiveSupport is much more than just additional features for ruby core classes. It provides many useful utilities that can be used even in non-rails applications. I haven’t covered all of them so if you are curious just take a look at the docs and see what else you can find.