Vinícius Manjabosco
Posted on November 26, 2020
I've recently found myself passing a lot of parameters down from controllers to service objects and then to jobs, etc.
This was one of the problems that were solved by the context pattern in React so I tried to do the same in the Rails app that I've been working on.
I had seen something a bit similar to in the I18n.with_locale
function.
So I wrote this:
# frozen_string_literal: true
require "concurrent-ruby"
class RequestValueContext
class << self
# For the multi threaded environment
@@request_value = Concurrent::ThreadLocalVar.new
def with(request_value)
if get.present?
raise ContextAlreadyDefinedError,
"Context already defined!"
end
begin
@@request_value.value = request_value
yield
ensure
@@request_value.value = nil
end
end
def get
@@request_value.value
end
end
ContextAlreadyDefinedError = Class.new(StandardError)
end
And in the ApplicationController
I've added this:
class ApplicationController < ActionController::Base
around_action :with_context
def with_context
RequestValueContext.with("foo") do
yield
end
end
end
Then I can access the value using RequestValueContext.get
from any method that is called "within the controller stack".
A nice feature of this pattern is that the current context can be captured when the using ActiveJob::Callbacks.before_enqueue
and then provided by ActiveJob::Callbacks.around_perform
like so:
# frozen_string_literal: true
module WithContexts
extend ActiveSupport::Concern
REQUEST_VALUE_KEY = "request_value"
included do
attr_reader :request_value, :deserialize_called
before_enqueue :capture_context
around_perform :provide_context
end
def serialize
super.merge(REQUEST_VALUE_KEY => request_value)
end
def deserialize(job_data)
# "detects" when a job is called by *perform_now*
@deserialize_called = true
super
@doorkeeper_application = request_value
end
def capture_context
@doorkeeper_application = RequestValueContext.get
end
def provide_context
if job_called_by_perform_now?
# if the job is called by *perform_now* it will be executed inline
# with the current context
yield
else
RequestValueContext.with_application(request_value) do
yield
end
end
end
def job_called_by_perform_now?
!deserialize_called
end
end
I believe something similar could be done for Proc/Block/Lambda.
I started writing Ruby less than a year ago and I found it to be quite a tricky language so if you have any feedback please let me know.
Posted on November 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.