Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default: support for ActiveSupport::CurrentAttributes.attribute #50677

Merged
merged 1 commit into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions activesupport/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
* Add `default:` support for `ActiveSupport::CurrentAttributes.attribute`

```ruby
class Current < ActiveSupport::CurrentAttributes
attribute :counter, default: 0
end
```

*Sean Doyle*

* Yield instance to `Object#with` block

```ruby
Expand Down
30 changes: 27 additions & 3 deletions activesupport/lib/active_support/current_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ def instance
end

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

singleton_class.delegate(*names.flat_map { |name| [name, "#{name}="] }, to: :instance, as: self)

defaults.merge! names.index_with { default }
end

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

class_attribute :defaults, instance_writer: false, default: {}

attr_accessor :attributes

def initialize
@attributes = {}
@attributes = merge_defaults!({})
end

# Expose one or more attributes within a block. Old values are returned after the block concludes.
Expand All @@ -200,8 +211,21 @@ def set(attributes, &block)
# 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 = {}
self.attributes = merge_defaults!({})
end
end

private
def merge_defaults!(attributes)
rafaelfranca marked this conversation as resolved.
Show resolved Hide resolved
defaults.each_with_object(attributes) do |(name, default), values|
value =
case default
when Proc then default.call
else default.dup
end
Comment on lines +221 to +225
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To compare to prior art, this is the code that Active Model uses when determining the default:

def value_before_type_cast
if user_provided_value.is_a?(Proc)
@memoized_value_before_type_cast ||= user_provided_value.call
else
@user_provided_value
end
end


values[name] = value
end
end
end
end
26 changes: 26 additions & 0 deletions activesupport/test/current_attributes_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class CurrentAttributesTest < ActiveSupport::TestCase
Person = Struct.new(:id, :name, :time_zone)

class Current < ActiveSupport::CurrentAttributes
attribute :counter_integer, default: 0
attribute :counter_callable, default: -> { 0 }
attribute :world, :account, :person, :request
delegate :time_zone, to: :person

Expand Down Expand Up @@ -86,6 +88,30 @@ def after_teardown
assert_equal "world/1", Current.world
end

test "read and write attribute with default value" do
assert_equal 0, Current.counter_integer

Current.counter_integer += 1

assert_equal 1, Current.counter_integer

Current.reset

assert_equal 0, Current.counter_integer
end

test "read attribute with default callable" do
assert_equal 0, Current.counter_callable

Current.counter_callable += 1

assert_equal 1, Current.counter_callable

Current.reset

assert_equal 0, Current.counter_callable
end

test "read overwritten attribute method" do
Current.request = "request/1"
assert_equal "request/1 something", Current.request
Expand Down