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
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.