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.