Skip to content

Commit c6d0b75

Browse files
committed
Integrate with ActiveModel::Dirty
Introduce the `ActiveResource::Dirty` module to extend the [ActiveModel::Dirty][] module with a variety of internal overrides. Like Active Record, Active Resource has two mechanisms to affect dirty state: * persistence (via `#save` and `#update`) * reloading (via `#reload`) Custom methods and other instances of direct interaction with the underlying HTTP connection will require ad hoc invocations of [ActiveModel::Dirty][] methods like `*_will_change!` and `clear_changes_information`. Currently, only attributes declared by a resource's schema are dirty-tracked. [ActiveModel::Dirty]: https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
1 parent 4fffccc commit c6d0b75

File tree

4 files changed

+147
-0
lines changed

4 files changed

+147
-0
lines changed

lib/active_resource.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module ActiveResource
4141
autoload :Coder
4242
autoload :Connection
4343
autoload :CustomMethods
44+
autoload :Dirty
4445
autoload :Formats
4546
autoload :HttpMock
4647
autoload :Rescuable

lib/active_resource/base.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,34 @@ module ActiveResource
129129
# Person.format.decode(person.encode)
130130
# # => {"first_name"=>"First", "last_name"=>"Last"}
131131
#
132+
# === Attribute change tracking
133+
#
134+
# Active Resources track local attribute changes by integrating with
135+
# ActiveModel::Dirty.
136+
#
137+
# Changes are discard when Active Resource successfuly saves the resource
138+
# to the remote server or reloads data from the remote server:
139+
#
140+
# person = Person.find(1)
141+
# person.name # => "Matz"
142+
# person.name = "Changed"
143+
#
144+
# person.name_changed? # => true
145+
# person.name_changes # => ["Matz", "Changed"]
146+
# person.save
147+
#
148+
# person.name_changed? # => false
149+
# person.name_changes # => []
150+
#
151+
# person.name = "Matz"
152+
# person.name_changed? # => true
153+
# person.name_changes # => ["Changed", "Matz"]
154+
#
155+
# person.reload
156+
# person.name # => "Changed"
157+
# person.name_changed? # => false
158+
# person.name_changes # => []
159+
#
132160
# === Custom REST methods
133161
#
134162
# Since simple CRUD/life cycle methods can't accomplish every task, Active Resource also supports
@@ -463,6 +491,7 @@ def schema(&block)
463491
@schema[k] = v
464492
@known_attributes << k
465493
end
494+
define_attribute_methods @known_attributes
466495

467496
@schema
468497
else
@@ -492,6 +521,7 @@ def schema=(the_schema)
492521
# purposefully nulling out the schema
493522
@schema = nil
494523
@known_attributes = []
524+
undefine_attribute_methods
495525
return
496526
end
497527

@@ -1735,13 +1765,17 @@ def read_attribute(attr_name)
17351765
name = self.class.primary_key if name == "id" && self.class.primary_key
17361766
@attributes[name]
17371767
end
1768+
alias_method :attribute, :read_attribute
17381769

17391770
def write_attribute(attr_name, value)
17401771
name = attr_name.to_s
17411772

17421773
name = self.class.primary_key if name == "id" && self.class.primary_key
1774+
singleton_class.define_attribute_methods(name) unless known_attributes.include?(name)
1775+
attribute_will_change!(name) if @attributes[name] != value
17431776
@attributes[name] = value
17441777
end
1778+
alias_method :attribute=, :write_attribute
17451779

17461780
protected
17471781
def connection(refresh = false)
@@ -1904,6 +1938,7 @@ class Base
19041938
extend ActiveResource::Associations
19051939

19061940
include Callbacks, CustomMethods, Validations, Serialization
1941+
include Dirty
19071942
include ActiveModel::Conversion
19081943
include ActiveModel::ForbiddenAttributesProtection
19091944
include ActiveModel::Serializers::JSON

lib/active_resource/dirty.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveResource
4+
module Dirty # :nodoc:
5+
extend ActiveSupport::Concern
6+
7+
included do
8+
include ActiveModel::Dirty
9+
10+
after_save :changes_applied
11+
after_reload :clear_changes_information
12+
13+
private
14+
15+
def mutations_from_database
16+
@mutations_from_database ||= ActiveModel::ForcedMutationTracker.new(self)
17+
end
18+
19+
def forget_attribute_assignments
20+
# no-op
21+
end
22+
end
23+
end
24+
end

test/cases/dirty_test.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
require "fixtures/person"
5+
6+
class DirtyTest < ActiveSupport::TestCase
7+
setup do
8+
setup_response
9+
@previous_schema = Person.schema
10+
end
11+
12+
teardown do
13+
Person.schema = @previous_schema
14+
end
15+
16+
test "is clean when built" do
17+
resource = Person.new
18+
19+
assert_empty resource.changes
20+
end
21+
22+
test "is clean when reloaded" do
23+
Person.schema do
24+
attribute :name, :string
25+
end
26+
27+
resource = Person.find(1)
28+
resource.name = "changed"
29+
30+
assert_changes -> { resource.name_changed? }, from: true, to: false do
31+
resource.reload
32+
end
33+
end
34+
35+
test "is clean after create" do
36+
Person.schema do
37+
attribute :name, :string
38+
end
39+
40+
resource = Person.new name: "changed"
41+
ActiveResource::HttpMock.respond_to.post "/people.json", {}, { id: 1, name: "changed" }.to_json
42+
43+
assert_changes -> { resource.name_changed? }, from: true, to: false do
44+
resource.save
45+
end
46+
assert_empty resource.changes
47+
end
48+
49+
test "is clean after update" do
50+
Person.schema do
51+
attribute :name, :string
52+
end
53+
54+
resource = Person.find(1)
55+
ActiveResource::HttpMock.respond_to.put "/people/1.json", {}, { id: 1, name: "changed" }.to_json
56+
57+
assert_changes -> { resource.name_changed? }, from: true, to: false do
58+
resource.update(name: "changed")
59+
end
60+
assert_empty resource.changes
61+
end
62+
63+
test "is dirty when known attribute changes are unsaved" do
64+
Person.schema do
65+
attribute :name, :string
66+
end
67+
expected_changes = {
68+
"name" => [nil, "known"]
69+
}
70+
71+
resource = Person.new name: "known"
72+
73+
assert_predicate resource, :name_changed?
74+
assert_equal expected_changes, resource.changes
75+
end
76+
77+
test "is dirty when unknown attribute changes are unsaved" do
78+
expected_changes = {
79+
"name" => [nil, "unknown"]
80+
}
81+
82+
resource = Person.new name: "unknown"
83+
84+
assert_predicate resource, :name_changed?
85+
assert_equal expected_changes, resource.changes
86+
end
87+
end

0 commit comments

Comments
 (0)