Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save zspencer/1e79bc40dda6f0df11b8442a21323221 to your computer and use it in GitHub Desktop.
Save zspencer/1e79bc40dda6f0df11b8442a21323221 to your computer and use it in GitHub Desktop.
# 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