diff --git a/lib/active_resource.rb b/lib/active_resource.rb index 42b074aa53..f588e7c877 100644 --- a/lib/active_resource.rb +++ b/lib/active_resource.rb @@ -41,6 +41,7 @@ module ActiveResource autoload :Coder autoload :Connection autoload :CustomMethods + autoload :Dirty autoload :Formats autoload :HttpMock autoload :Rescuable diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb index 10312bb6f5..f5b9755295 100644 --- a/lib/active_resource/base.rb +++ b/lib/active_resource/base.rb @@ -129,6 +129,34 @@ module ActiveResource # Person.format.decode(person.encode) # # => {"first_name"=>"First", "last_name"=>"Last"} # + # === Attribute change tracking + # + # Active Resources track local attribute changes by integrating with + # ActiveModel::Dirty. + # + # Changes are discard when Active Resource successfuly saves the resource + # to the remote server or reloads data from the remote server: + # + # person = Person.find(1) + # person.name # => "Matz" + # person.name = "Changed" + # + # person.name_changed? # => true + # person.name_changes # => ["Matz", "Changed"] + # person.save + # + # person.name_changed? # => false + # person.name_changes # => [] + # + # person.name = "Matz" + # person.name_changed? # => true + # person.name_changes # => ["Changed", "Matz"] + # + # person.reload + # person.name # => "Changed" + # person.name_changed? # => false + # person.name_changes # => [] + # # === Custom REST methods # # Since simple CRUD/life cycle methods can't accomplish every task, Active Resource also supports @@ -463,6 +491,7 @@ def schema(&block) @schema[k] = v @known_attributes << k end + define_attribute_methods @known_attributes @schema else @@ -492,6 +521,7 @@ def schema=(the_schema) # purposefully nulling out the schema @schema = nil @known_attributes = [] + undefine_attribute_methods return end @@ -1735,13 +1765,17 @@ def read_attribute(attr_name) name = self.class.primary_key if name == "id" && self.class.primary_key @attributes[name] end + alias_method :attribute, :read_attribute def write_attribute(attr_name, value) name = attr_name.to_s name = self.class.primary_key if name == "id" && self.class.primary_key + singleton_class.define_attribute_methods(name) unless known_attributes.include?(name) + attribute_will_change!(name) if @attributes[name] != value @attributes[name] = value end + alias_method :attribute=, :write_attribute protected def connection(refresh = false) @@ -1904,6 +1938,7 @@ class Base extend ActiveResource::Associations include Callbacks, CustomMethods, Validations, Serialization + include Dirty include ActiveModel::Conversion include ActiveModel::ForbiddenAttributesProtection include ActiveModel::Serializers::JSON diff --git a/lib/active_resource/dirty.rb b/lib/active_resource/dirty.rb new file mode 100644 index 0000000000..2b277662e0 --- /dev/null +++ b/lib/active_resource/dirty.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActiveResource + module Dirty # :nodoc: + extend ActiveSupport::Concern + + included do + include ActiveModel::Dirty + + after_save :changes_applied + after_reload :clear_changes_information + + private + + def mutations_from_database + @mutations_from_database ||= ActiveModel::ForcedMutationTracker.new(self) + end + + def forget_attribute_assignments + # no-op + end + end + end +end diff --git a/test/cases/dirty_test.rb b/test/cases/dirty_test.rb new file mode 100644 index 0000000000..5d761c438d --- /dev/null +++ b/test/cases/dirty_test.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "fixtures/person" + +class DirtyTest < ActiveSupport::TestCase + setup do + setup_response + @previous_schema = Person.schema + end + + teardown do + Person.schema = @previous_schema + end + + test "is clean when built" do + resource = Person.new + + assert_empty resource.changes + end + + test "is clean when reloaded" do + Person.schema do + attribute :name, :string + end + + resource = Person.find(1) + resource.name = "changed" + + assert_changes -> { resource.name_changed? }, from: true, to: false do + resource.reload + end + end + + test "is clean after create" do + Person.schema do + attribute :name, :string + end + + resource = Person.new name: "changed" + ActiveResource::HttpMock.respond_to.post "/people.json", {}, { id: 1, name: "changed" }.to_json + + assert_changes -> { resource.name_changed? }, from: true, to: false do + resource.save + end + assert_empty resource.changes + end + + test "is clean after update" do + Person.schema do + attribute :name, :string + end + + resource = Person.find(1) + ActiveResource::HttpMock.respond_to.put "/people/1.json", {}, { id: 1, name: "changed" }.to_json + + assert_changes -> { resource.name_changed? }, from: true, to: false do + resource.update(name: "changed") + end + assert_empty resource.changes + end + + test "is dirty when known attribute changes are unsaved" do + Person.schema do + attribute :name, :string + end + expected_changes = { + "name" => [ nil, "known" ] + } + + resource = Person.new name: "known" + + assert_predicate resource, :name_changed? + assert_equal expected_changes, resource.changes + end + + test "is dirty when unknown attribute changes are unsaved" do + expected_changes = { + "name" => [ nil, "unknown" ] + } + + resource = Person.new name: "unknown" + + assert_predicate resource, :name_changed? + assert_equal expected_changes, resource.changes + end +end