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.
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
BaseService we can include
ActiveSupport::Callbacks, define callbacks and use it inside
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,
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
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
class Example include ActiveSupport::Configurable end Example.configure do |config| config.some_option = 'value' end Example.config.some_option => "value"
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.
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.
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
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
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.
The last utility I want to talk about is
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"
MessageVerifier it accepts
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.
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.