Skip to content

Commit

Permalink
Merge pull request rails#19333 from palkan/dirty-store
Browse files Browse the repository at this point in the history
Add dirty methods for store accessors
  • Loading branch information
kaspth authored Mar 31, 2019
2 parents 563bf57 + b574d28 commit ba4e74e
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 0 deletions.
48 changes: 48 additions & 0 deletions activerecord/lib/active_record/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ module ActiveRecord
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
# already built around just accessing attributes on the model.
#
# Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
# methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
# +key_before_last_save+).
#
# NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
#
# Make sure that you declare the database column used for the serialized store as a text, so there's
# plenty of room.
#
Expand Down Expand Up @@ -49,6 +55,12 @@ module ActiveRecord
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
#
# # Dirty tracking
# u.color = 'green'
# u.color_changed? # => true
# u.color_was # => 'black'
# u.color_change # => ['black', 'red']
#
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
Expand Down Expand Up @@ -127,6 +139,42 @@ def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
define_method(accessor_key) do
read_store_attribute(store_attribute, key)
end

define_method("#{accessor_key}_changed?") do
return false unless attribute_changed?(store_attribute)
prev_store, new_store = changes[store_attribute]
prev_store&.dig(key) != new_store&.dig(key)
end

define_method("#{accessor_key}_change") do
return unless attribute_changed?(store_attribute)
prev_store, new_store = changes[store_attribute]
[prev_store&.dig(key), new_store&.dig(key)]
end

define_method("#{accessor_key}_was") do
return unless attribute_changed?(store_attribute)
prev_store, _new_store = changes[store_attribute]
prev_store&.dig(key)
end

define_method("saved_change_to_#{accessor_key}?") do
return false unless saved_change_to_attribute?(store_attribute)
prev_store, new_store = saved_change_to_attribute(store_attribute)
prev_store&.dig(key) != new_store&.dig(key)
end

define_method("saved_change_to_#{accessor_key}") do
return unless saved_change_to_attribute?(store_attribute)
prev_store, new_store = saved_change_to_attribute(store_attribute)
[prev_store&.dig(key), new_store&.dig(key)]
end

define_method("#{accessor_key}_before_last_save") do
return unless saved_change_to_attribute?(store_attribute)
prev_store, _new_store = saved_change_to_attribute(store_attribute)
prev_store&.dig(key)
end
end
end

Expand Down
16 changes: 16 additions & 0 deletions activerecord/test/cases/adapters/postgresql/hstore_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ def test_yaml_round_trip_with_store_accessors
assert_equal "GMT", y.timezone
end

def test_changes_with_store_accessors
x = Hstore.new(language: "de")
assert x.language_changed?
assert_nil x.language_was
assert_equal [nil, "de"], x.language_change
x.save!

assert_not x.language_changed?
x.reload

x.settings = nil
assert x.language_changed?
assert_equal "de", x.language_was
assert_equal ["de", nil], x.language_change
end

def test_changes_in_place
hstore = Hstore.create!(settings: { "one" => "two" })
hstore.settings["three"] = "four"
Expand Down
68 changes: 68 additions & 0 deletions activerecord/test/cases/store_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,74 @@ class StoreTest < ActiveRecord::TestCase
assert_not_predicate @john, :settings_changed?
end

test "updating the store will mark accessor as changed" do
@john.color = "red"
assert @john.color_changed?
end

test "new record and no accessors changes" do
user = Admin::User.new
assert_not user.color_changed?
assert_nil user.color_was
assert_nil user.color_change

user.color = "red"
assert user.color_changed?
assert_nil user.color_was
assert_equal "red", user.color_change[1]
end

test "updating the store won't mark accessor as changed if the whole store was updated" do
@john.settings = { color: @john.color, some: "thing" }
assert @john.settings_changed?
assert_not @john.color_changed?
end

test "updating the store populates the accessor changed array correctly" do
@john.color = "red"
assert_equal "black", @john.color_was
assert_equal "black", @john.color_change[0]
assert_equal "red", @john.color_change[1]
end

test "updating the store won't mark accessor as changed if the value isn't changed" do
@john.color = @john.color
assert_not @john.color_changed?
end

test "nullifying the store mark accessor as changed" do
color = @john.color
@john.settings = nil
assert @john.color_changed?
assert_equal color, @john.color_was
assert_equal [color, nil], @john.color_change
end

test "dirty methods for suffixed accessors" do
@john.configs[:two_factor_auth] = true
assert @john.two_factor_auth_configs_changed?
assert_nil @john.two_factor_auth_configs_was
assert_equal [nil, true], @john.two_factor_auth_configs_change
end

test "dirty methods for prefixed accessors" do
@john.spouse[:name] = "Lena"
assert @john.partner_name_changed?
assert_equal "Dallas", @john.partner_name_was
assert_equal ["Dallas", "Lena"], @john.partner_name_change
end

test "saved changes tracking for accessors" do
@john.spouse[:name] = "Lena"
assert @john.partner_name_changed?

@john.save!
assert_not @john.partner_name_change
assert @john.saved_change_to_partner_name?
assert_equal ["Dallas", "Lena"], @john.saved_change_to_partner_name
assert_equal "Dallas", @john.partner_name_before_last_save
end

test "object initialization with not nullable column" do
assert_equal true, @john.remember_login
end
Expand Down

0 comments on commit ba4e74e

Please sign in to comment.