/
attribute_accessors_per_thread.rb
175 lines (166 loc) · 6.09 KB
/
attribute_accessors_per_thread.rb
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
# 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.active_support.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}"
::ActiveSupport::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 = ::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}]
if value.nil? && !::ActiveSupport::IsolatedExecutionState.key?(@__thread_mattr_#{sym})
::ActiveSupport::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}"
::ActiveSupport::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