Created
July 27, 2009 01:18
-
-
Save chikamichi/155990 to your computer and use it in GitHub Desktop.
Marshalization plugin: how to override an instance private method from ActiveRecord::Dirty
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
# rails/init.rb | |
require 'marshalize' | |
ActiveRecord::Base.send(:include, Marshalization::Base) | |
ActiveRecord::Base.send(:include, Marshalization::AttributeMethods) | |
ActiveRecord::Base.send(:include, Marshalization::Dirty) |
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
# 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 |
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
# 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 |
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
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!] |
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
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