aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
blob: a6e87aeb689c3f3928aa873efd831d93494e5d04 (plain) (blame)
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
# frozen_string_literal: true

# 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.
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 # => nil
  #   Thread.current[:attr_Current_user] = "DHH"
  #   Current.user # => "DHH"
  #
  # 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) # :nodoc:
    syms.each do |sym|
      raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)

      # The following generated method concatenates `name` because we want it
      # to work with inheritance via polymorphism.
      class_eval(<<-EOS, __FILE__, __LINE__ + 1)
        def self.#{sym}
          Thread.current["attr_" + name + "_#{sym}"]
        end
      EOS

      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 `name` because we want it
      # to work with inheritance via polymorphism.
      class_eval(<<-EOS, __FILE__, __LINE__ + 1)
        def self.#{sym}=(obj)
          Thread.current["attr_" + name + "_#{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"
  #
  # If a subclass changes the value, the parent class' value is not changed.
  # Similarly, if the parent class changes the value, the value of subclasses
  # is not changed.
  #
  #   class Customer < Account
  #   end
  #
  #   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
  def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true)
    thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor)
    thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
  end
  alias :thread_cattr_accessor :thread_mattr_accessor
end