Skip to content

Commit

Permalink
Initial import of shadow.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jordan Fowler committed May 2, 2008
0 parents commit 26dea2f
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2008 Jordan Fowler

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13 changes: 13 additions & 0 deletions README
@@ -0,0 +1,13 @@
Shadow
======

Introduction goes here.


Example
=======

Example goes here.


Copyright (c) 2008 [name of plugin creator], released under the MIT license
22 changes: 22 additions & 0 deletions Rakefile
@@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the shadow plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

desc 'Generate documentation for the shadow plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Shadow'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
2 changes: 2 additions & 0 deletions init.rb
@@ -0,0 +1,2 @@
require 'activerecord/shadow'
ActiveRecord::Base.send :include, ActiveRecord::Acts::Shadow
259 changes: 259 additions & 0 deletions lib/activerecord/shadow.rb
@@ -0,0 +1,259 @@
# Copyright (c) 2008 Jordan Fowler
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

module ActiveRecord
class ShadowAttachmentError < ActiveRecordError
def initialize(owner_class_name, attachment)
super("Expected attached #{attachment} on #{owner_class_name}.")
end
end

module Acts
module Shadow
def self.included(base)
base.extend BaseClassMethods
end

module BaseClassMethods
def shadow(options = {})
return if self.included_modules.include?(ActiveRecord::Acts::Shadow::BaseClassMethods::ShadowInstanceMethods)
self.send :cattr_accessor, *[
:skip_attributes, :shadowed_attributes, :skip_associations, :shadowed_associations, :shadowed_attachments
]

self.send :include, ShadowInstanceMethods
self.send :extend, ShadowClassMethods

self.skip_attributes = [
[options[:skip_attributes]].flatten.compact.collect(&:to_s), ['created_at', 'updated_at', 'id', 'version']
].flatten.compact

self.skip_associations = [
[options[:skip_associations], :versions].flatten.compact.collect(&:to_sym)
].flatten.compact

self.shadowed_attributes = case options[:attributes]
when :none then []
when Array then options[:attributes].flatten.collect(&:to_s) - self.skip_attributes
else self.columns.collect(&:name) - self.skip_attributes
end

self.shadowed_associations = case options[:associations]
when :none then []
when Array then options[:associations].flatten.collect(&:to_sym) - self.skip_associations
else self.reflect_on_all_associations.collect(&:name) - self.skip_associations
end

self.shadowed_attachments = case options[:attach]
when Symbol, Array then [options[:attach]].flatten.compact.collect(&:to_sym)
else []
end

class_eval do
attr_accessor :updated_attributes
attr_accessor :updated_associations

build_instance_attachments
build_association_attachments
build_association_callbacks

has_many(:attribute_updates, {
:class_name => "#{self.to_s}::AttributeShadow",
:foreign_key => self.to_s.foreign_key
})
has_many(:association_updates, {
:class_name => "#{self.to_s}::AssociationShadow",
:foreign_key => self.to_s.foreign_key
})

before_save :determine_updated_attributes
after_save :store_updated_attributes

const_set('AttributeShadow', Class.new(ActiveRecord::Base)).class_eval do
serialize :updated_attributes, Array

before_save do |shadow|
shadow.updated_attributes ||= []
end
end

const_set('AssociationShadow', Class.new(ActiveRecord::Base)).class_eval do
before_save do |shadow|
shadow.association = shadow.association.to_s
shadow.action = shadow.action.to_s
end

def record
@record ||= self.association.to_s.classify.constantize.find(self.record_id)
end
end
end

self.shadowed_attachments.each do |attachment|
[self::AttributeShadow, self::AssociationShadow].each do |klass|
klass.class_eval <<-END
belongs_to :#{attachment}, :class_name => '::#{attachment.to_s.classify}'
END
end
end

[self::AttributeShadow, self::AssociationShadow].each do |klass|
klass.class_eval <<-END
belongs_to :#{self.to_s.underscore}, :class_name => '::#{self.to_s}'
END
end

table_name_prefixes = [table_name_prefix,base_class.name.demodulize.underscore].join
self::AttributeShadow.set_table_name([table_name_prefixes, '_attribute_shadows', table_name_suffix].join)
self::AssociationShadow.set_table_name([table_name_prefixes, '_association_shadows', table_name_suffix].join)
end

module ShadowInstanceMethods
def store_updated_association(updated_association)
self.class::AssociationShadow.create! updated_association
end

def store_updated_attributes
attribute_shadow = {
self.class.to_s.foreign_key => self.id,
:updated_attributes => @updated_attributes,
:version => self.version
}

self.class.shadowed_attachments.each do |attachment|
if (attached_object = self.send(attachment)).nil? or attached_object.new_record?
raise ShadowAttachmentError.new(self.class.to_s, attachment)
else
attribute_shadow.update("#{attachment}_id".to_sym => attached_object.id)
end
end

self.class::AttributeShadow.create! attribute_shadow
end

protected
def determine_updated_attributes
@attributes_before_save = self.new_record? ? {} : self.class.find(self.id, {
:select => self.class.shadowed_attributes.join(',')
}).attributes.dup

@updated_attributes = @attributes_before_save.keys.select do |name|
(@attributes_before_save[name].to_s != self[name].to_s)
end
end
end

module ShadowClassMethods
def create_shadowed_attributes_table
# this should generate the (table_name)_shadowed_attributes table

# [
# table_name_prefix,
# base_class.name.demodulize.underscore,
# '_shadowed_attributes',
# table_name_suffix
# ].join
end

def create_shadowed_associations_table
# this should generate the (table_name)_shadowed_associations table

# [
# table_name_prefix,
# base_class.name.demodulize.underscore,
# '_shadowed_associations',
# table_name_suffix
# ].join
end

def create_shadow_tables
create_shadow_attributes_table
create_shadow_associations_table
end

protected
def build_instance_attachments
self.shadowed_attachments.each do |attachment|
unless [attachment.to_s, "#{attachment}="].include?(self.instance_methods)
self.class_eval do
attr_accessor attachment
end
end
end
end

def build_association_attachments
self.shadowed_associations.each do |name|
reflection = self.reflect_on_association name

reflection_class = reflection.options.key?(:class_name) ?
reflection.options[:class_name].to_s.constantize : reflection.name.to_s.classify.constantize

self.shadowed_attachments.each do |attachment|
unless [attachment.to_s, "#{attachment}="].include?(reflection_class.instance_methods)
reflection_class.class_eval do
attr_accessor attachment
end
end
end
end
end

def build_association_callbacks
self.shadowed_associations.each do |name|
add_association_callbacks(name, {:after_add => :added, :after_remove => :removed}.inject({}) do |hsh,pair|
hsh.update({
pair.first => Proc.new do |owner,target|
unless target.new_record?
updated_association = {
:association => name,
owner.class.to_s.foreign_key => owner.id,
:record_id => target.id,
:action => pair.last
}

owner.class.shadowed_attachments.each do |attachment|
if (attached_object = target.send(attachment)).nil? or attached_object.new_record?
raise ShadowAttachmentError.new(target.class.to_s, attachment)
else
updated_association.update(attachment.to_s.classify.foreign_key.to_sym => attached_object.id)
end
end

if target.class.respond_to?(:version_column)
updated_association[:record_version] = target.send(target.class.send(:version_column))
end

owner.updated_associations = [owner.updated_associations].flatten.compact
owner.updated_associations << updated_association

owner.store_updated_association updated_association
end
end
})
end)
end
end
end
end
end
end
end
8 changes: 8 additions & 0 deletions test/shadow_test.rb
@@ -0,0 +1,8 @@
require 'test/unit'

class ShadowTest < Test::Unit::TestCase
# Replace this with your real tests.
def test_this_plugin
flunk
end
end

1 comment on commit 26dea2f

@cdb
Copy link

@cdb cdb commented on 26dea2f Sep 16, 2008

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Jordan, saw your presentation on SD.rb podcast and am curious about shadow….looking at the code (not running it yet), it seems that you could be using dirty objects to determine what has changed, instead of doing a separate find and compare as you’re doing now in the determine_updated_attributes method.

http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects

I’d be curious to try this out…what’s especially interesting is that you can do some_object.changes and get not only which attributes changed, but the before and after versions of them, which could effectively eliminate any ties you have with acts as versioned, and all this data could theoretically be stored in the tables you generate. Just an idea, but kind of interesting.

(First github comment here, hope it finds it way to you)

Cameron

Please sign in to comment.