Skip to content

Commit d1d6b6b

Browse files
committed
Add default: support for ActiveSupport::CurrentAttributes.attribute
Extend the `.attribute` class method to accept a `:default` option for its list of attributes: ```ruby class Current < ActiveSupport::CurrentAttributes attribute :counter, default: 0 end ``` Internally, `ActiveSupport::CurrentAttributes` will maintain a `.defaults` class attribute to determine default values during instance initialization.
1 parent cdfca90 commit d1d6b6b

File tree

3 files changed

+63
-3
lines changed

3 files changed

+63
-3
lines changed

activesupport/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Add `default:` support for `ActiveSupport::CurrentAttributes.attribute`
2+
3+
```ruby
4+
class Current < ActiveSupport::CurrentAttributes
5+
attribute :counter, default: 0
6+
end
7+
```
8+
9+
*Sean Doyle*
10+
111
* Yield instance to `Object#with` block
212

313
```ruby

activesupport/lib/active_support/current_attributes.rb

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,14 @@ def instance
102102
end
103103

104104
# Declares one or more attributes that will be given both class and instance accessor methods.
105-
def attribute(*names)
105+
#
106+
# ==== Options
107+
#
108+
# * <tt>:default</tt> - The default value for the attributes. If the value
109+
# is a proc or lambda, it will be called whenever an instance is
110+
# constructed. Otherwise, the value will be duplicated with +#dup+.
111+
# Default values are re-assigned when the attributes are reset.
112+
def attribute(*names, default: nil)
106113
invalid_attribute_names = names.map(&:to_sym) & INVALID_ATTRIBUTE_NAMES
107114
if invalid_attribute_names.any?
108115
raise ArgumentError, "Restricted attribute names: #{invalid_attribute_names.join(", ")}"
@@ -126,6 +133,8 @@ def attribute(*names)
126133
end
127134

128135
singleton_class.delegate(*names.flat_map { |name| [name, "#{name}="] }, to: :instance, as: self)
136+
137+
defaults.merge! names.index_with { default }
129138
end
130139

131140
# Calls this callback before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
@@ -177,10 +186,12 @@ def respond_to_missing?(name, _)
177186
end
178187
end
179188

189+
class_attribute :defaults, instance_writer: false, default: {}
190+
180191
attr_accessor :attributes
181192

182193
def initialize
183-
@attributes = {}
194+
@attributes = merge_defaults!({})
184195
end
185196

186197
# Expose one or more attributes within a block. Old values are returned after the block concludes.
@@ -200,8 +211,21 @@ def set(attributes, &block)
200211
# Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
201212
def reset
202213
run_callbacks :reset do
203-
self.attributes = {}
214+
self.attributes = merge_defaults!({})
204215
end
205216
end
217+
218+
private
219+
def merge_defaults!(attributes)
220+
defaults.each_with_object(attributes) do |(name, default), values|
221+
value =
222+
case default
223+
when Proc then default.call
224+
else default.dup
225+
end
226+
227+
values[name] = value
228+
end
229+
end
206230
end
207231
end

activesupport/test/current_attributes_test.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class CurrentAttributesTest < ActiveSupport::TestCase
1111
Person = Struct.new(:id, :name, :time_zone)
1212

1313
class Current < ActiveSupport::CurrentAttributes
14+
attribute :counter_integer, default: 0
15+
attribute :counter_callable, default: -> { 0 }
1416
attribute :world, :account, :person, :request
1517
delegate :time_zone, to: :person
1618

@@ -86,6 +88,30 @@ def after_teardown
8688
assert_equal "world/1", Current.world
8789
end
8890

91+
test "read and write attribute with default value" do
92+
assert_equal 0, Current.counter_integer
93+
94+
Current.counter_integer += 1
95+
96+
assert_equal 1, Current.counter_integer
97+
98+
Current.reset
99+
100+
assert_equal 0, Current.counter_integer
101+
end
102+
103+
test "read attribute with default callable" do
104+
assert_equal 0, Current.counter_callable
105+
106+
Current.counter_callable += 1
107+
108+
assert_equal 1, Current.counter_callable
109+
110+
Current.reset
111+
112+
assert_equal 0, Current.counter_callable
113+
end
114+
89115
test "read overwritten attribute method" do
90116
Current.request = "request/1"
91117
assert_equal "request/1 something", Current.request

0 commit comments

Comments
 (0)