src/derails/passive_resistance/core_ext/module/attribute_accessors_per_thread.rb
# frozen_string_literal: true

# == Attribute Accessors per Thread
#
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes, but does so on a per-thread basis.
#
# So the values are scoped within the Thread.current space under the class name
# of the module.
#
# Note that it can also be scoped per-fiber if +Rails.application.config.passive_resistance.isolation_level+
# is set to +:fiber+.
class Module
  # Defines a per-thread class attribute and creates class and instance reader methods.
  # The underlying per-thread class variable is set to +nil+, if it is not previously defined.
  #
  #   module Current
  #     thread_mattr_reader :user
  #   end
  #
  #   Current.user = "DHH"
  #   Current.user # => "DHH"
  #   Thread.new { Current.user }.value # => nil
  #
  # The attribute name must be a valid method name in Ruby.
  #
  #   module Foo
  #     thread_mattr_reader :"1_Badname"
  #   end
  #   # => NameError: invalid attribute name: 1_Badname
  #
  # To omit the instance reader method, pass
  # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
  #
  #   class Current
  #     thread_mattr_reader :user, instance_reader: false
  #   end
  #
  #   Current.new.user # => NoMethodError
  def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
    syms.each do |sym|
      raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)

      # The following generated method concatenates `object_id` because we want
      # subclasses to maintain independent values.
      if default.nil?
        class_eval(<<-EOS, __FILE__, __LINE__ + 1)
          def self.#{sym}
            @__thread_mattr_#{sym} ||= "attr_#{sym}_\#{object_id}"
            ::PassiveResistance::IsolatedExecutionState[@__thread_mattr_#{sym}]
          end
        EOS
      else
        default = default.dup.freeze unless default.frozen?
        singleton_class.define_method("#{sym}_default_value") { default }

        class_eval(<<-EOS, __FILE__, __LINE__ + 1)
          def self.#{sym}
            @__thread_mattr_#{sym} ||= "attr_#{sym}_\#{object_id}"
            value = ::PassiveResistance::IsolatedExecutionState[@__thread_mattr_#{sym}]

            if value.nil? && !::PassiveResistance::IsolatedExecutionState.key?(@__thread_mattr_#{sym})
              ::PassiveResistance::IsolatedExecutionState[@__thread_mattr_#{sym}] = #{sym}_default_value
            else
              value
            end
          end
        EOS
      end

      if instance_reader && instance_accessor
        class_eval(<<-EOS, __FILE__, __LINE__ + 1)
          def #{sym}
            self.class.#{sym}
          end
        EOS
      end
    end
  end
  alias :thread_cattr_reader :thread_mattr_reader

  # Defines a per-thread class attribute and creates a class and instance writer methods to
  # allow assignment to the attribute.
  #
  #   module Current
  #     thread_mattr_writer :user
  #   end
  #
  #   Current.user = "DHH"
  #   Thread.current[:attr_Current_user] # => "DHH"
  #
  # To omit the instance writer method, pass
  # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
  #
  #   class Current
  #     thread_mattr_writer :user, instance_writer: false
  #   end
  #
  #   Current.new.user = "DHH" # => NoMethodError
  def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc:
    syms.each do |sym|
      raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)

      # The following generated method concatenates `object_id` because we want
      # subclasses to maintain independent values.
      class_eval(<<-EOS, __FILE__, __LINE__ + 1)
        def self.#{sym}=(obj)
          @__thread_mattr_#{sym} ||= "attr_#{sym}_\#{object_id}"
          ::PassiveResistance::IsolatedExecutionState[@__thread_mattr_#{sym}] = obj
        end
      EOS

      if instance_writer && instance_accessor
        class_eval(<<-EOS, __FILE__, __LINE__ + 1)
          def #{sym}=(obj)
            self.class.#{sym} = obj
          end
        EOS
      end
    end
  end
  alias :thread_cattr_writer :thread_mattr_writer

  # Defines both class and instance accessors for class attributes.
  #
  #   class Account
  #     thread_mattr_accessor :user
  #   end
  #
  #   Account.user = "DHH"
  #   Account.user     # => "DHH"
  #   Account.new.user # => "DHH"
  #
  # Unlike +mattr_accessor+, values are *not* shared with subclasses or parent classes.
  # If a subclass changes the value, the parent class' value is not changed.
  # If the parent class changes the value, the value of subclasses is not changed.
  #
  #   class Customer < Account
  #   end
  #
  #   Account.user   # => "DHH"
  #   Customer.user  # => nil
  #   Customer.user  = "Rafael"
  #   Customer.user  # => "Rafael"
  #   Account.user   # => "DHH"
  #
  # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
  # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
  #
  #   class Current
  #     thread_mattr_accessor :user, instance_writer: false, instance_reader: false
  #   end
  #
  #   Current.new.user = "DHH"  # => NoMethodError
  #   Current.new.user          # => NoMethodError
  #
  # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
  #
  #   class Current
  #     thread_mattr_accessor :user, instance_accessor: false
  #   end
  #
  #   Current.new.user = "DHH"  # => NoMethodError
  #   Current.new.user          # => NoMethodError
  #
  # A default value may be specified using the +:default+ option. Because
  # multiple threads can access the default value, non-frozen default values
  # will be <tt>dup</tt>ed and frozen.
  def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
    thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
    thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
  end
  alias :thread_cattr_accessor :thread_mattr_accessor
end
