Skip to content

Instantly share code, notes, and snippets.

@chikamichi
Created July 27, 2009 01:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chikamichi/155990 to your computer and use it in GitHub Desktop.
Save chikamichi/155990 to your computer and use it in GitHub Desktop.
Marshalization plugin: how to override an instance private method from ActiveRecord::Dirty
# rails/init.rb
require 'marshalize'
ActiveRecord::Base.send(:include, Marshalization::Base)
ActiveRecord::Base.send(:include, Marshalization::AttributeMethods)
ActiveRecord::Base.send(:include, Marshalization::Dirty)
# lib/marshalize.rb
require 'activesupport'
module Marshalization #:nodoc:
VERSION = 0.1
module Dirty #:nodoc:
private
def update_with_marshalization
puts "> dans update_with_dirty marshalized"
if partial_updates?
# Serialized *and* marshalized attributes should always be written in case they've been
# changed in place.
update_without_dirty(self.class.marshalized_attributes.keys)
else
update_without_dirty
end
update_with_dirty # an implicit call to super, which would fail as it is a private method
# update_with_dirty is not
end
# historical purpose
def private_test
puts "------------- I'm (dirty) private !"
end
def self.included(receiver)
receiver.alias_method_chain :update, :marshalization
end
end
#module Timestamp #:nodoc:
# private
# def update_with_timestamps(*args) #:nodoc:
# if record_timestamps && (!partial_updates? || changed?)
# t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
# write_attribute('updated_at', t) if respond_to?(:updated_at)
# write_attribute('updated_on', t) if respond_to?(:updated_on)
# end
# update_without_timestamps()
# end
#end
module Base # :nodoc:
marshalized_attributes ||= Hash.new
module ClassMethods #:nodoc:
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
# then specify the name of that attribute using this method and it will be handled automatically.
# The serialization is done through Marshal. If +class_name+ is specified, the serialized object must be of that
# class on retrieval or SerializationTypeMismatch will be raised.
#
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
# * +class_name+ - Optional, class name that the object type should be equal to.
#
# ==== Example
# # Serialize a preferences attribute with Marshal
# class User
# marshalize :preferences
# end
def marshalize(attr_name, class_name = Object)
attr_name = attr_name.to_s
marshalized_attributes[attr_name] = class_name
end
# Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
def marshalized_attributes
read_inheritable_attribute(:attr_marshalized) or write_inheritable_attribute(:attr_marshalized, {})
end
end # class: Marshalization::Base::ClassMethods
module InstanceMethods
# Encode an attribute with ActiveSupport::Base64.encode64
def encode_attribute(attribute)
ActiveSupport::Base64.encode64(attribute)
end
# Decode an attribute with ActiveSupport::Base64.decode64
def decode_attribute(attribute)
ActiveSupport::Base64.decode64(attribute)
end
# Returns a copy of the attributes hash where all the values have been safely quoted for use in
# an SQL statement.
def attributes_with_quotes_with_marshalization(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
quoted = {}
connection = self.class.connection
attribute_names.each do |name|
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
if self.class.marshalized_attributes.has_key?(name)
# we don't want to quote Marshal-dumped values
quoted[name] = "'#{@attributes[name].to_s}'"
else
value = read_attribute(name)
# We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
value = value.to_yaml
end
quoted[name] = connection.quote(value, column)
end
end
end
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
end
end # module: Marshalization::Base::InstanceMethods
def self.included(receiver)
receiver.send :include, InstanceMethods
receiver.send :extend, ClassMethods
unless receiver.respond_to?(:attributes_with_quotes_without_marshalization)
receiver.instance_eval do
alias_method_chain :attributes_with_quotes, :marshalization
end
end
end
end # module: Marshalization::Base
module AttributeMethods
module ClassMethods
# There is no way to efficiently use alias_method_chain here,
# for deeply AR-nested checkings are performed along the way.
# That's why I went for a redundant rewriting of the main method.
# Hopefully, the core API won't break soon.
def define_attribute_methods
return if generated_methods?
columns_hash.each do |name, column|
if instance_method_already_implemented?(name)
else
if self.serialized_attributes[name]
define_read_method_for_serialized_attribute(name)
elsif self.marshalized_attributes[name]
define_read_method_for_marshalized_attribute(name)
elsif create_time_zone_conversion_attribute?(name, column)
define_read_method_for_time_zone_conversion(name)
else
define_read_method(name.to_sym, name, column)
end
end
unless instance_method_already_implemented?("#{name}=")
if create_time_zone_conversion_attribute?(name, column)
define_write_method_for_time_zone_conversion(name)
elsif self.marshalized_attributes[name]
define_write_method_for_marshalized_attribute(name.to_sym)
else
define_write_method(name.to_sym)
end
end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end
end
# Define read method for marshalized attribute.
def define_read_method_for_marshalized_attribute(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}; unmarshalize_attribute('#{attr_name.to_sym}'); end"
end
def define_write_method_for_marshalized_attribute(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value); marshalize_attribute('#{attr_name.to_sym}', new_value); end", "#{attr_name}="
end
end # class: Marshalization::AttributeMethods::ClassMethods
# Initialize an empty marshalized attribute.
# Prevents initialization error (marshal: data too short)
def initialize_marshalized_attribute(name, class_name)
@attributes[name] = encode_attribute(Marshal.dump(class_name.new))
end
def read_attribute(attr_name)
attr_name = attr_name.to_s
if !(value = @attributes[attr_name]).nil?
if column = column_for_attribute(attr_name)
if unmarshalizable_attribute?(attr_name, column)
unmarshalize_attribute(attr_name.to_sym)
else
super(attr_name.to_sym)
end
end
else
end
end
def marshalize_attribute(attr_name, value)
attr_name = attr_name.to_s
@attributes_cache.delete(attr_name)
if self.class.marshalized_attributes[attr_name]
dumped = encode_attribute(Marshal.dump(value))
@attributes[attr_name] = dumped
end
end
# Returns the unmarshalized object of the attribute.
def unmarshalize_attribute(attr_name)
attr_name = attr_name.to_s
if @attributes[attr_name].nil? || @attributes[attr_name] == ''
initialize_marshalized_attribute(attr_name, self.class.marshalized_attributes[attr_name])
end
unmarshalized_object = Marshal.load(decode_attribute(@attributes[attr_name]))
if unmarshalized_object.is_a?(self.class.marshalized_attributes[attr_name]) || unmarshalized_object.nil?
return unmarshalized_object
else
raise ActiveRecord::SerializationTypeMismatch,
"#{attr_name} was supposed to be a #{self.class.marshalized_attributes[attr_name]}, but was a #{unmarshalized_object.class.to_s}"
end
end
# Returns true if the attribute is of a text column and marked for marshalization.
def unmarshalizable_attribute?(attr_name, column)
column.text? && self.class.marshalized_attributes[attr_name]
end
def self.included(receiver)
receiver.send :extend, ClassMethods
end
end # module: Marshalization::AttributeMethods
end # module: Marshalization
# test/schema.rb
ActiveRecord::Schema.define(:version => 0) do
create_table :birds, :force => true do |t|
t.string :name
t.text :locations
t.text :songs
t.timestamps
#t.text :songs, :default => '\004\b\"\000'
#t.text :songs, :default => Marshal.dump ""
end
end
require 'rubygems'
require 'activerecord'
# "Test" module mimics "ActiveRecord"
module Test
module Dirty
def self.included(base)
base.alias_method_chain :update, :dirty
end
private
def update_with_dirty
puts "> in update_with_dirty() [aliased Dirty]"
end
end
end
module Test
class Base
def update
puts "> in update() class method [original Base]"
end
end
Base.class_eval do
include Dirty
end
end
testy = Test::Base.new
testy.update
module Marshalization
module Dirty
private
def update_with_marshalization
puts "> in update_with_dirty() [Marshalized!]"
end
def self.included(receiver)
puts "hey"
receiver.alias_method_chain :update, :marshalization
end
end
end
Test::Base.send :include, Marshalization::Dirty
testy2 = Test::Base.new
testy2.update
# behavior:
# $ ruby test-overriding.rb
# > in update_with_dirty() [aliased Dirty]
# hey
# > in update_with_dirty() [Marshalized!]
test/test_marshalize.rb
require File.dirname(__FILE__) + '/test_helper.rb'
class ExtendActiveRecordTest < Test::Unit::TestCase
class Bird < ActiveRecord::Base
marshalize :songs
marshalize :locations, Array
end
def test_marshalize_the_songs_attribute
load_schema
test_songs = {'Il fait beau' => 'texte de la chanson...', 'Perche sur une branche' => 'piou piou piou!'}
piou = Bird.new
piou.name = "Coco"
piou.songs = test_songs
piou.save!
retrieved = Bird.find_by_id piou.id
assert_equal test_songs, retrieved.songs
#piou.locations_will_change!
piou.name = "Bibi"
piou.songs = 23
test_locations = ["Milan", "Paris"]
piou.locations = test_locations
piou.save!
retrieved = Bird.find_by_id piou.id
assert_kind_of(Array, retrieved.locations, "Locations are stored in an array.")
# It fails here on the assert_kind, for it retrieves an empty array.
# This is related to update_with_dirty() which must be edited so that marshalized_attributes keys
# skip the dirty updating (this is done for serialized_attributes as well).
# Tests run fine if this is implemented directly in my activerecord lib files, but I'dd like to
# override it from my plugin, btw.
assert_equal test_locations, retrieved.locations
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment