1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
|
# frozen_string_literal: true
module ActiveSupport
# Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
# before and after each request. This allows you to keep all the per-request attributes easily
# available to the whole system.
#
# The following full app-like example demonstrates how to use a Current class to
# facilitate easy access to the global, per-request attributes without passing them deeply
# around everywhere:
#
# # app/models/current.rb
# class Current < ActiveSupport::CurrentAttributes
# attribute :account, :user
# attribute :request_id, :user_agent, :ip_address
#
# resets { Time.zone = nil }
#
# def user=(user)
# super
# self.account = user.account
# Time.zone = user.time_zone
# end
# end
#
# # app/controllers/concerns/authentication.rb
# module Authentication
# extend ActiveSupport::Concern
#
# included do
# before_action :authenticate
# end
#
# private
# def authenticate
# if authenticated_user = User.find_by(id: cookies.encrypted[:user_id])
# Current.user = authenticated_user
# else
# redirect_to new_session_url
# end
# end
# end
#
# # app/controllers/concerns/set_current_request_details.rb
# module SetCurrentRequestDetails
# extend ActiveSupport::Concern
#
# included do
# before_action do
# Current.request_id = request.uuid
# Current.user_agent = request.user_agent
# Current.ip_address = request.ip
# end
# end
# end
#
# class ApplicationController < ActionController::Base
# include Authentication
# include SetCurrentRequestDetails
# end
#
# class MessagesController < ApplicationController
# def create
# Current.account.messages.create(message_params)
# end
# end
#
# class Message < ApplicationRecord
# belongs_to :creator, default: -> { Current.user }
# after_create { |message| Event.create(record: message) }
# end
#
# class Event < ApplicationRecord
# before_create do
# self.request_id = Current.request_id
# self.user_agent = Current.user_agent
# self.ip_address = Current.ip_address
# end
# end
#
# A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
# Current should only be used for a few, top-level globals, like account, user, and request details.
# The attributes stuck in Current should be used by more or less all actions on all requests. If you start
# sticking controller-specific attributes in there, you're going to create a mess.
class CurrentAttributes
include ActiveSupport::Callbacks
define_callbacks :reset
class << self
# Returns singleton instance for this class in this thread. If none exists, one is created.
def instance
current_instances[name] ||= new
end
# Declares one or more attributes that will be given both class and instance accessor methods.
def attribute(*names)
generated_attribute_methods.module_eval do
names.each do |name|
define_method(name) do
attributes[name.to_sym]
end
define_method("#{name}=") do |attribute|
attributes[name.to_sym] = attribute
end
end
end
names.each do |name|
define_singleton_method(name) do
instance.public_send(name)
end
define_singleton_method("#{name}=") do |attribute|
instance.public_send("#{name}=", attribute)
end
end
end
# Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
def resets(&block)
set_callback :reset, :after, &block
end
delegate :set, :reset, to: :instance
def reset_all # :nodoc:
current_instances.each_value(&:reset)
end
def clear_all # :nodoc:
reset_all
current_instances.clear
end
private
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
def current_instances
Thread.current[:current_attributes_instances] ||= {}
end
def method_missing(name, *args, &block)
# Caches the method definition as a singleton method of the receiver.
#
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
singleton_class.delegate name, to: :instance
send(name, *args, &block)
end
end
attr_accessor :attributes
def initialize
@attributes = {}
end
# Expose one or more attributes within a block. Old values are returned after the block concludes.
# Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
#
# class Chat::PublicationJob < ApplicationJob
# def perform(attributes, room_number, creator)
# Current.set(person: creator) do
# Chat::Publisher.publish(attributes: attributes, room_number: room_number)
# end
# end
# end
def set(set_attributes)
old_attributes = compute_attributes(set_attributes.keys)
assign_attributes(set_attributes)
yield
ensure
assign_attributes(old_attributes)
end
# Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
def reset
run_callbacks :reset do
self.attributes = {}
end
end
private
def assign_attributes(new_attributes)
new_attributes.each { |key, value| public_send("#{key}=", value) }
end
def compute_attributes(keys)
keys.collect { |key| [ key, public_send(key) ] }.to_h
end
end
end
|