forked from rails/rails
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rails#30920 from lugray/attributes_to_am
Start bringing attributes API to AM
- Loading branch information
Showing
3 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# frozen_string_literal: true | ||
|
||
require "active_model/type" | ||
|
||
module ActiveModel | ||
module Attributes #:nodoc: | ||
extend ActiveSupport::Concern | ||
include ActiveModel::AttributeMethods | ||
include ActiveModel::Dirty | ||
|
||
included do | ||
attribute_method_suffix "=" | ||
class_attribute :attribute_types, :_default_attributes, instance_accessor: false | ||
self.attribute_types = {} | ||
self._default_attributes = {} | ||
end | ||
|
||
module ClassMethods | ||
def attribute(name, cast_type = Type::Value.new, **options) | ||
self.attribute_types = attribute_types.merge(name.to_s => cast_type) | ||
self._default_attributes = _default_attributes.merge(name.to_s => options[:default]) | ||
define_attribute_methods(name) | ||
end | ||
|
||
private | ||
|
||
def define_method_attribute=(name) | ||
safe_name = name.unpack("h*".freeze).first | ||
ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name | ||
|
||
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 | ||
def __temp__#{safe_name}=(value) | ||
name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name} | ||
write_attribute(name, value) | ||
end | ||
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= | ||
undef_method :__temp__#{safe_name}= | ||
STR | ||
end | ||
end | ||
|
||
def initialize(*) | ||
super | ||
clear_changes_information | ||
end | ||
|
||
private | ||
|
||
def write_attribute(attr_name, value) | ||
name = if self.class.attribute_alias?(attr_name) | ||
self.class.attribute_alias(attr_name).to_s | ||
else | ||
attr_name.to_s | ||
end | ||
|
||
cast_type = self.class.attribute_types[name] | ||
|
||
deserialized_value = ActiveModel::Type.lookup(cast_type).cast(value) | ||
attribute_will_change!(name) unless deserialized_value == attribute(name) | ||
instance_variable_set("@#{name}", deserialized_value) | ||
deserialized_value | ||
end | ||
|
||
def attribute(name) | ||
if instance_variable_defined?("@#{name}") | ||
instance_variable_get("@#{name}") | ||
else | ||
default = self.class._default_attributes[name] | ||
default.respond_to?(:call) ? default.call : default | ||
end | ||
end | ||
|
||
# Handle *= for method_missing. | ||
def attribute=(attribute_name, value) | ||
write_attribute(attribute_name, value) | ||
end | ||
end | ||
|
||
module AttributeMethods #:nodoc: | ||
AttrNames = Module.new { | ||
def self.set_name_cache(name, value) | ||
const_name = "ATTR_#{name}" | ||
unless const_defined? const_name | ||
const_set const_name, value.dup.freeze | ||
end | ||
end | ||
} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
# frozen_string_literal: true | ||
|
||
require "cases/helper" | ||
require "active_model/attributes" | ||
|
||
class AttributesDirtyTest < ActiveModel::TestCase | ||
class DirtyModel | ||
include ActiveModel::Model | ||
include ActiveModel::Attributes | ||
attribute :name, :string | ||
attribute :color, :string | ||
attribute :size, :integer | ||
|
||
def save | ||
changes_applied | ||
end | ||
|
||
def reload | ||
clear_changes_information | ||
end | ||
end | ||
|
||
setup do | ||
@model = DirtyModel.new | ||
end | ||
|
||
test "setting attribute will result in change" do | ||
assert !@model.changed? | ||
assert !@model.name_changed? | ||
@model.name = "Ringo" | ||
assert @model.changed? | ||
assert @model.name_changed? | ||
end | ||
|
||
test "list of changed attribute keys" do | ||
assert_equal [], @model.changed | ||
@model.name = "Paul" | ||
assert_equal ["name"], @model.changed | ||
end | ||
|
||
test "changes to attribute values" do | ||
assert !@model.changes["name"] | ||
@model.name = "John" | ||
assert_equal [nil, "John"], @model.changes["name"] | ||
end | ||
|
||
test "checking if an attribute has changed to a particular value" do | ||
@model.name = "Ringo" | ||
assert @model.name_changed?(from: nil, to: "Ringo") | ||
assert_not @model.name_changed?(from: "Pete", to: "Ringo") | ||
assert @model.name_changed?(to: "Ringo") | ||
assert_not @model.name_changed?(to: "Pete") | ||
assert @model.name_changed?(from: nil) | ||
assert_not @model.name_changed?(from: "Pete") | ||
end | ||
|
||
test "changes accessible through both strings and symbols" do | ||
@model.name = "David" | ||
assert_not_nil @model.changes[:name] | ||
assert_not_nil @model.changes["name"] | ||
end | ||
|
||
test "be consistent with symbols arguments after the changes are applied" do | ||
@model.name = "David" | ||
assert @model.attribute_changed?(:name) | ||
@model.save | ||
@model.name = "Rafael" | ||
assert @model.attribute_changed?(:name) | ||
end | ||
|
||
test "attribute mutation" do | ||
@model.instance_variable_set("@name", "Yam".dup) | ||
assert !@model.name_changed? | ||
@model.name.replace("Hadad") | ||
assert !@model.name_changed? | ||
@model.name_will_change! | ||
@model.name.replace("Baal") | ||
assert @model.name_changed? | ||
end | ||
|
||
test "resetting attribute" do | ||
@model.name = "Bob" | ||
@model.restore_name! | ||
assert_nil @model.name | ||
assert !@model.name_changed? | ||
end | ||
|
||
test "setting color to same value should not result in change being recorded" do | ||
@model.color = "red" | ||
assert @model.color_changed? | ||
@model.save | ||
assert !@model.color_changed? | ||
assert !@model.changed? | ||
@model.color = "red" | ||
assert !@model.color_changed? | ||
assert !@model.changed? | ||
end | ||
|
||
test "saving should reset model's changed status" do | ||
@model.name = "Alf" | ||
assert @model.changed? | ||
@model.save | ||
assert !@model.changed? | ||
assert !@model.name_changed? | ||
end | ||
|
||
test "saving should preserve previous changes" do | ||
@model.name = "Jericho Cane" | ||
@model.save | ||
assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"] | ||
end | ||
|
||
test "setting new attributes should not affect previous changes" do | ||
@model.name = "Jericho Cane" | ||
@model.save | ||
@model.name = "DudeFella ManGuy" | ||
assert_equal [nil, "Jericho Cane"], @model.name_previous_change | ||
end | ||
|
||
test "saving should preserve model's previous changed status" do | ||
@model.name = "Jericho Cane" | ||
@model.save | ||
assert @model.name_previously_changed? | ||
end | ||
|
||
test "previous value is preserved when changed after save" do | ||
assert_equal({}, @model.changed_attributes) | ||
@model.name = "Paul" | ||
assert_equal({ "name" => nil }, @model.changed_attributes) | ||
|
||
@model.save | ||
|
||
@model.name = "John" | ||
assert_equal({ "name" => "Paul" }, @model.changed_attributes) | ||
end | ||
|
||
test "changing the same attribute multiple times retains the correct original value" do | ||
@model.name = "Otto" | ||
@model.save | ||
@model.name = "DudeFella ManGuy" | ||
@model.name = "Mr. Manfredgensonton" | ||
assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change | ||
assert_equal @model.name_was, "Otto" | ||
end | ||
|
||
test "using attribute_will_change! with a symbol" do | ||
@model.size = 1 | ||
assert @model.size_changed? | ||
end | ||
|
||
test "reload should reset all changes" do | ||
@model.name = "Dmitry" | ||
@model.name_changed? | ||
@model.save | ||
@model.name = "Bob" | ||
|
||
assert_equal [nil, "Dmitry"], @model.previous_changes["name"] | ||
assert_equal "Dmitry", @model.changed_attributes["name"] | ||
|
||
@model.reload | ||
|
||
assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes | ||
assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes | ||
end | ||
|
||
test "restore_attributes should restore all previous data" do | ||
@model.name = "Dmitry" | ||
@model.color = "Red" | ||
@model.save | ||
@model.name = "Bob" | ||
@model.color = "White" | ||
|
||
@model.restore_attributes | ||
|
||
assert_not @model.changed? | ||
assert_equal "Dmitry", @model.name | ||
assert_equal "Red", @model.color | ||
end | ||
|
||
test "restore_attributes can restore only some attributes" do | ||
@model.name = "Dmitry" | ||
@model.color = "Red" | ||
@model.save | ||
@model.name = "Bob" | ||
@model.color = "White" | ||
|
||
@model.restore_attributes(["name"]) | ||
|
||
assert @model.changed? | ||
assert_equal "Dmitry", @model.name | ||
assert_equal "White", @model.color | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# frozen_string_literal: true | ||
|
||
require "cases/helper" | ||
require "active_model/attributes" | ||
|
||
module ActiveModel | ||
class AttributesTest < ActiveModel::TestCase | ||
class ModelForAttributesTest | ||
include ActiveModel::Model | ||
include ActiveModel::Attributes | ||
|
||
attribute :integer_field, :integer | ||
attribute :string_field, :string | ||
attribute :decimal_field, :decimal | ||
attribute :string_with_default, :string, default: "default string" | ||
attribute :date_field, :string, default: -> { Date.new(2016, 1, 1) } | ||
attribute :boolean_field, :boolean | ||
end | ||
|
||
class ChildModelForAttributesTest < ModelForAttributesTest | ||
end | ||
|
||
class GrandchildModelForAttributesTest < ChildModelForAttributesTest | ||
attribute :integer_field, :string | ||
end | ||
|
||
test "properties assignment" do | ||
data = ModelForAttributesTest.new( | ||
integer_field: "2.3", | ||
string_field: "Rails FTW", | ||
decimal_field: "12.3", | ||
boolean_field: "0" | ||
) | ||
|
||
assert_equal 2, data.integer_field | ||
assert_equal "Rails FTW", data.string_field | ||
assert_equal BigDecimal.new("12.3"), data.decimal_field | ||
assert_equal "default string", data.string_with_default | ||
assert_equal Date.new(2016, 1, 1), data.date_field | ||
assert_equal false, data.boolean_field | ||
|
||
data.integer_field = 10 | ||
data.string_with_default = nil | ||
data.boolean_field = "1" | ||
|
||
assert_equal 10, data.integer_field | ||
assert_nil data.string_with_default | ||
assert_equal true, data.boolean_field | ||
end | ||
|
||
test "dirty" do | ||
data = ModelForAttributesTest.new( | ||
integer_field: "2.3", | ||
string_field: "Rails FTW", | ||
decimal_field: "12.3", | ||
boolean_field: "0" | ||
) | ||
|
||
assert_equal false, data.changed? | ||
|
||
data.integer_field = "2.1" | ||
|
||
assert_equal false, data.changed? | ||
|
||
data.string_with_default = "default string" | ||
|
||
assert_equal false, data.changed? | ||
|
||
data.integer_field = "5.1" | ||
|
||
assert_equal true, data.changed? | ||
assert_equal true, data.integer_field_changed? | ||
assert_equal({ "integer_field" => [2, 5] }, data.changes) | ||
end | ||
|
||
test "nonexistent attribute" do | ||
assert_raise ActiveModel::UnknownAttributeError do | ||
ModelForAttributesTest.new(nonexistent: "nonexistent") | ||
end | ||
end | ||
|
||
test "children inherit attributes" do | ||
data = ChildModelForAttributesTest.new(integer_field: "4.4") | ||
|
||
assert_equal 4, data.integer_field | ||
end | ||
|
||
test "children can override parents" do | ||
data = GrandchildModelForAttributesTest.new(integer_field: "4.4") | ||
|
||
assert_equal "4.4", data.integer_field | ||
end | ||
end | ||
end |