From d1d6b6bce38c6d15d0d37585b9e4659b08266d47 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Tue, 9 Jan 2024 12:01:54 -0500 Subject: [PATCH] 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. --- activesupport/CHANGELOG.md | 10 +++++++ .../lib/active_support/current_attributes.rb | 30 +++++++++++++++++-- activesupport/test/current_attributes_test.rb | 26 ++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index a6a7dab122c..31033d86bde 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -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 diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 9c592a7e2df..fcc73898e65 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -102,7 +102,14 @@ module ActiveSupport end # Declares one or more attributes that will be given both class and instance accessor methods. - def attribute(*names) + # + # ==== Options + # + # * :default - 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(", ")}" @@ -126,6 +133,8 @@ module ActiveSupport 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. @@ -177,10 +186,12 @@ module ActiveSupport 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. @@ -200,8 +211,21 @@ module ActiveSupport # 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) + defaults.each_with_object(attributes) do |(name, default), values| + value = + case default + when Proc then default.call + else default.dup + end + + values[name] = value + end + end end end diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb index 69a4fdd328a..abd74b47b39 100644 --- a/activesupport/test/current_attributes_test.rb +++ b/activesupport/test/current_attributes_test.rb @@ -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 @@ -86,6 +88,30 @@ class CurrentAttributesTest < ActiveSupport::TestCase 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