Last active
July 9, 2018 22:55
-
-
Save zspencer/1e79bc40dda6f0df11b8442a21323221 to your computer and use it in GitHub Desktop.
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
# I frequently use ActiveModel::Type to define complex objects in my database. When doing | |
# so, there is always a moment when I run into a bug that is caused by me forgetting to | |
# support ActiveRecord knowing that the complex type is dirty! | |
# So here's a quick example that I can reference when I go to make a complex type again | |
# that plays nicely *with* ActiveModel::Dirty! | |
# frozen_string_literal: true | |
begin | |
require "bundler/inline" | |
rescue LoadError => e | |
$stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" | |
raise e | |
end | |
gemfile(true) do | |
source "https://rubygems.org" | |
git_source(:github) { |repo| "https://github.com/#{repo}.git" } | |
gem "rails", github: "rails/rails" | |
gem "sqlite3" | |
end | |
require "active_record" | |
require "minitest/autorun" | |
require "logger" | |
# This connection will do for database-independent bug reports. | |
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") | |
ActiveRecord::Base.logger = Logger.new(STDOUT) | |
ActiveRecord::Schema.define do | |
create_table :posts, force: true do |t| | |
t.text :metadata, null: false, default: "{}" | |
end | |
create_table :comments, force: true do |t| | |
t.integer :post_id | |
end | |
end | |
class Metadata | |
include ActiveModel::Dirty | |
include ActiveModel::Model | |
define_attribute_methods :likes | |
attr_reader :likes | |
def initialize(attributes) | |
@likes = attributes[:likes] | |
end | |
def likes=(num) | |
likes_will_change! unless num == @likes | |
@likes = num | |
end | |
def to_h | |
{ likes: likes } | |
end | |
class Type < ActiveModel::Type::Value | |
def cast(value) | |
return value if value.is_a?(Metadata) | |
return Metadata.new(value) if value.is_a?(Hash) | |
deserialize(value) | |
end | |
def serialize(value) | |
return JSON.dump(value.to_h) if value.respond_to?(:to_h) | |
raise "#{value} of type #{value.class} cannot be serialized back into a metadata field" | |
end | |
def deserialize(value) | |
return Metadata.new(JSON.parse(value, symbolize_names: true)) if value.is_a?(String) | |
raise "#{value} of type #{value.class} cannot be cast to a Metadata" | |
end | |
def changed_in_place?(raw_old_value, new_value) | |
serialize(new_value) != raw_old_value | |
end | |
def changed?(old_value, new_value, _new_value_before_type_cast) | |
p super | |
end | |
end | |
end | |
class Post < ActiveRecord::Base | |
has_many :comments | |
attribute :metadata, Metadata::Type.new | |
end | |
class Comment < ActiveRecord::Base | |
belongs_to :post | |
end | |
class DirtyComplexAttributesTest < Minitest::Test | |
def test_dirty_complex_objects | |
post = Post.create! | |
post.metadata.likes = 10 | |
assert post.metadata_changed? | |
post.save | |
refute post.metadata_changed? | |
loaded_post = Post.find_by(id: post.id) | |
assert_equal 10, loaded_post.metadata.likes | |
refute loaded_post.metadata_changed? | |
# Changing the data makes it dirty | |
loaded_post.metadata.likes = 5 | |
assert loaded_post.metadata_changed? | |
# Reloading clears the dirty state | |
loaded_post.reload | |
refute post.metadata_changed? | |
assert loaded_post.metadata.likes = 10 | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment