acts_as_relation 0.0.5 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/.rspec +2 -0
- data/README.rdoc +98 -0
- data/Rakefile +4 -9
- data/acts_as_relation.gemspec +6 -5
- data/lib/active_record/acts_as_relation.rb +137 -0
- data/lib/active_record/acts_as_relation_superclass_migration.rb +30 -0
- data/lib/acts_as_relation.rb +2 -2
- data/lib/version.rb +2 -4
- data/spec/acts_as_migration/acts_as_migratoin_spec.rb +24 -0
- data/spec/acts_as_migration/models.rb +7 -0
- data/spec/acts_as_migration/schema.rb +33 -0
- data/spec/acts_as_relation/acts_as_relation_spec.rb +94 -0
- data/spec/acts_as_relation/models.rb +25 -0
- data/spec/acts_as_relation/schema.rb +28 -0
- data/spec/spec_helper.rb +10 -0
- metadata +42 -23
- data/README.markdown +0 -85
- data/init.rb +0 -1
- data/lib/active_record/acts/as_relation.rb +0 -114
- data/lib/active_record/acts/as_relation_superclass_migration.rb +0 -27
- data/test/acts_as_relation_test.rb +0 -121
- data/test/schema.rb +0 -52
- data/test/test_helper.rb +0 -4
data/.gitignore
CHANGED
data/.rspec
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
= ActsAsRelation
|
2
|
+
|
3
|
+
A +acts_as+ relationship sets up a one-to-one connection with another model,
|
4
|
+
such that declaring model inherits from other model (with separate database
|
5
|
+
tabels). For example in a shop all products have common attributes (+name+,
|
6
|
+
+price+, +image+ ...), while each type of them has their specific attributes,
|
7
|
+
+pen+ has +color+, +book+ has +author+ and +publisher+ and so on.
|
8
|
+
|
9
|
+
ActiveRecord only supports singletable inheritance, but with single table inheritance
|
10
|
+
number of attributes on parent model (product in this example) grow exponentially,
|
11
|
+
and must of them will always stay +NULL+.
|
12
|
+
|
13
|
+
+acts_as+ use a polymorphic +has_one+
|
14
|
+
association to simulate a multi-table inheritance. For the shop example you'd
|
15
|
+
declare the product as a +supermodel+ and all types of it as +acts_as+ +:product+
|
16
|
+
(if you prefer you can use their aliases +is_a+ and +is_a_superclass+)
|
17
|
+
|
18
|
+
class Product < ActiveRecord::Base
|
19
|
+
acts_as_superclass
|
20
|
+
end
|
21
|
+
|
22
|
+
class Pen < ActiveRecord::Base
|
23
|
+
acts_as :product
|
24
|
+
end
|
25
|
+
|
26
|
+
class Book < ActiveRecord::Base
|
27
|
+
acts_as :product
|
28
|
+
end
|
29
|
+
|
30
|
+
To make this work, you need to declare both a foreign key column and a type column
|
31
|
+
in the model that declares superclass. To do this you can set +:as_relation_superclass+
|
32
|
+
option to +true+ on +products+ +create_table+ (or pass it name of the association):
|
33
|
+
|
34
|
+
create_table :products, :as_relation_superclass => true do |t|
|
35
|
+
# ...
|
36
|
+
end
|
37
|
+
|
38
|
+
Or declare them as you do on a +polymorphic+ +belongs_to+ association, it this case
|
39
|
+
you must pass name to +acts_as+ in +:as+ option:
|
40
|
+
|
41
|
+
change_table :products do |t|
|
42
|
+
t.integer :producible_id
|
43
|
+
t.string :producible_type
|
44
|
+
end
|
45
|
+
|
46
|
+
class Pen < ActiveRecord::Base
|
47
|
+
acts_as :product, :as => :producible
|
48
|
+
end
|
49
|
+
|
50
|
+
Now +Pen+ and +Book+ *act* *as* +Product+. This means that they inherit +Product+
|
51
|
+
_attributes_, _associations_, _validations_ and _methods_.
|
52
|
+
|
53
|
+
To see its functionality lets add some stuff to product:
|
54
|
+
|
55
|
+
class Product
|
56
|
+
validates_presence_of :name, :price
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
"#{name} $#{price}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
now we can to things like this:
|
64
|
+
|
65
|
+
Pen.create :name => "Nice Pen", :price => 1.3, :color => "Red"
|
66
|
+
Pen.where "name = ?", "Some Pen"
|
67
|
+
pen = Pen.new
|
68
|
+
pen.valid? # => false
|
69
|
+
pen.errors.keys # => [:name, :price]
|
70
|
+
Pen.first.to_s # => "Nice Pen $1.3"
|
71
|
+
|
72
|
+
When you declare an +acts_as+ relation, the declaring class automatically gains parent
|
73
|
+
methods (includeing accessors) so you can access them directly.
|
74
|
+
|
75
|
+
On the other hand you can always access a specific object from its parent by calling +specific+ method on it:
|
76
|
+
|
77
|
+
Product.first.specific # will return a specific product, a pen for example
|
78
|
+
|
79
|
+
== Options
|
80
|
+
The +acts_as+ relation support these options:
|
81
|
+
|
82
|
+
* +:as+
|
83
|
+
* +:auto_join+
|
84
|
+
* +:class_name+
|
85
|
+
* +:conditions+
|
86
|
+
* +:dependent+
|
87
|
+
* +:include+
|
88
|
+
|
89
|
+
when +:auto_join+ option set to +true+ (wich is by default), every query on child
|
90
|
+
will automatically joins with paren. For example:
|
91
|
+
|
92
|
+
Pen.where("name = ?", "somename")
|
93
|
+
|
94
|
+
will result in the following SQL:
|
95
|
+
|
96
|
+
SELECT "pens".* FROM "pens" INNER JOIN "products" ON "products"."as_product_id" = "pens"."id" AND "products"."as_product_type" = 'Pen' WHERE (name = 'somename')
|
97
|
+
|
98
|
+
All other options are same as +has_one+ options.
|
data/Rakefile
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
require 'rake'
|
2
|
-
require '
|
2
|
+
require 'rspec/core/rake_task'
|
3
3
|
|
4
|
-
task :default => :
|
4
|
+
task :default => :spec
|
5
5
|
|
6
|
-
desc '
|
7
|
-
|
8
|
-
t.libs << 'lib'
|
9
|
-
t.libs << 'test'
|
10
|
-
t.pattern = 'test/**/*_test.rb'
|
11
|
-
t.verbose = true
|
12
|
-
end
|
6
|
+
desc 'Run specs'
|
7
|
+
RSpec::Core::RakeTask.new
|
data/acts_as_relation.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
|
|
6
6
|
|
7
7
|
# Description Meta...
|
8
8
|
s.name = 'acts_as_relation'
|
9
|
-
s.version = ActiveRecord::
|
9
|
+
s.version = ActiveRecord::ActsAsRelation::VERSION
|
10
10
|
s.platform = Gem::Platform::RUBY
|
11
11
|
s.author = 'Hassan Zamani'
|
12
12
|
s.email = 'hsn.zamani@gmail.com'
|
@@ -23,8 +23,9 @@ Gem::Specification.new do |s|
|
|
23
23
|
|
24
24
|
|
25
25
|
# Dependencies (installed via 'bundle install')...
|
26
|
-
s.add_development_dependency
|
27
|
-
s.add_development_dependency
|
28
|
-
s.add_development_dependency
|
29
|
-
s.add_development_dependency
|
26
|
+
s.add_development_dependency "rake"
|
27
|
+
s.add_development_dependency "bundler"
|
28
|
+
s.add_development_dependency "sqlite3"
|
29
|
+
s.add_development_dependency "rspec"
|
30
|
+
s.add_dependency "activerecord"
|
30
31
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
module ActsAsModules end
|
4
|
+
|
5
|
+
module ActsAsRelation
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module AccessMethods
|
9
|
+
protected
|
10
|
+
def define_acts_as_accessors(attribs, model_name)
|
11
|
+
attribs.each do |attrib|
|
12
|
+
class_eval <<-EndEval
|
13
|
+
def #{attrib}
|
14
|
+
#{model_name}.#{attrib}
|
15
|
+
end
|
16
|
+
|
17
|
+
def #{attrib}=(value)
|
18
|
+
#{model_name}.#{attrib} = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def #{attrib}?
|
22
|
+
#{model_name}.#{attrib}?
|
23
|
+
end
|
24
|
+
EndEval
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def acts_as(model_name, options={})
|
31
|
+
name = model_name.to_s.underscore.singularize
|
32
|
+
class_name = options[:class_name] || name.camelcase
|
33
|
+
association_name = options[:as] || acts_as_association_name(name)
|
34
|
+
module_name = "ActsAs#{name.camelcase}"
|
35
|
+
|
36
|
+
unless ActiveRecord::ActsAsModules.const_defined? module_name
|
37
|
+
# Create ActsAsModel module
|
38
|
+
acts_as_model = Module.new
|
39
|
+
ActiveRecord::ActsAsModules.const_set(module_name, acts_as_model)
|
40
|
+
|
41
|
+
has_one_options = {
|
42
|
+
:as => association_name,
|
43
|
+
:class_name => class_name,
|
44
|
+
:autosave => true,
|
45
|
+
:validate => false,
|
46
|
+
:dependent => options.fetch(:dependent, :destroy),
|
47
|
+
:include => options[:include],
|
48
|
+
:conditions => options[:conditions]
|
49
|
+
}
|
50
|
+
|
51
|
+
acts_as_model.module_eval <<-EndEval
|
52
|
+
def self.included(base)
|
53
|
+
base.has_one :#{name}, #{has_one_options}
|
54
|
+
base.validate :#{name}_must_be_valid
|
55
|
+
base.alias_method_chain :#{name}, :autobuild
|
56
|
+
|
57
|
+
base.extend ActiveRecord::ActsAsRelation::AccessMethods
|
58
|
+
all_attributes = #{class_name}.content_columns.map(&:name)
|
59
|
+
ignored_attributes = ["created_at", "updated_at", "#{association_name}_id", "#{association_name}_type"]
|
60
|
+
associations = #{class_name}.reflect_on_all_associations.map! { |assoc| assoc.name } - ["#{association_name}"]
|
61
|
+
attributes_to_delegate = all_attributes - ignored_attributes + associations
|
62
|
+
base.send :define_acts_as_accessors, attributes_to_delegate, "#{name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def #{name}_with_autobuild
|
66
|
+
#{name}_without_autobuild || build_#{name}
|
67
|
+
end
|
68
|
+
|
69
|
+
def method_missing method, *arg, &block
|
70
|
+
raise NoMethodError if method.to_s == 'id' || method.to_s == '#{name}'
|
71
|
+
|
72
|
+
#{name}.send(method, *arg, &block)
|
73
|
+
rescue NoMethodError
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def respond_to?(method, include_private_methods = false)
|
78
|
+
super || #{name}.respond_to?(method, include_private_methods)
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def #{name}_must_be_valid
|
84
|
+
unless #{name}.valid?
|
85
|
+
#{name}.errors.each do |att, message|
|
86
|
+
errors.add(att, message)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
EndEval
|
91
|
+
end
|
92
|
+
|
93
|
+
class_eval do
|
94
|
+
include "ActiveRecord::ActsAsModules::#{module_name}".constantize
|
95
|
+
end
|
96
|
+
|
97
|
+
if options.fetch :auto_join, true
|
98
|
+
class_eval "default_scope joins(:#{name})"
|
99
|
+
end
|
100
|
+
|
101
|
+
instance_eval <<-EndEval
|
102
|
+
def acts_as_other_model?
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def acts_as_model_name
|
107
|
+
:#{name}
|
108
|
+
end
|
109
|
+
EndEval
|
110
|
+
end
|
111
|
+
alias :is_a :acts_as
|
112
|
+
|
113
|
+
def acts_as_superclass
|
114
|
+
association_name = acts_as_association_name
|
115
|
+
|
116
|
+
class_eval <<-EndEval
|
117
|
+
belongs_to :#{association_name}, :polymorphic => true
|
118
|
+
|
119
|
+
def specific
|
120
|
+
self.#{association_name}
|
121
|
+
end
|
122
|
+
alias :specific_class :specific
|
123
|
+
EndEval
|
124
|
+
end
|
125
|
+
alias :is_a_superclass :acts_as_superclass
|
126
|
+
|
127
|
+
def acts_as_association_name model_name=nil
|
128
|
+
model_name ||= self.name
|
129
|
+
"as_#{model_name.to_s.demodulize.singularize.underscore}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class ActiveRecord::Base
|
136
|
+
include ActiveRecord::ActsAsRelation
|
137
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ActsAsRelationSuperclassMigration
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
alias_method_chain :create_table, :as_relation_superclass
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_table_with_as_relation_superclass(table_name, options = {})
|
10
|
+
create_table_without_as_relation_superclass(table_name, options) do |td|
|
11
|
+
if superclass = options[:as_relation_superclass]
|
12
|
+
association_name = if superclass.is_a? Symbol or superclass.is_a? String then
|
13
|
+
superclass
|
14
|
+
else
|
15
|
+
ActiveRecord::Base.acts_as_association_name table_name
|
16
|
+
end
|
17
|
+
|
18
|
+
td.integer "#{association_name}_id"
|
19
|
+
td.string "#{association_name}_type"
|
20
|
+
end
|
21
|
+
|
22
|
+
yield td if block_given?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ActiveRecord::ConnectionAdapters::SchemaStatements
|
29
|
+
include ActiveRecord::ActsAsRelationSuperclassMigration
|
30
|
+
end
|
data/lib/acts_as_relation.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require 'active_record/
|
2
|
-
require 'active_record/
|
1
|
+
require 'active_record/acts_as_relation'
|
2
|
+
require 'active_record/acts_as_relation_superclass_migration'
|
data/lib/version.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "create_table acts_as_superclass" do
|
4
|
+
before :all do
|
5
|
+
ActsAsSuperclassSchema.migrate
|
6
|
+
end
|
7
|
+
|
8
|
+
it "creates foreign key and type columns on" do
|
9
|
+
name = Product.acts_as_association_name
|
10
|
+
Product.attribute_names.should include("#{name}_id")
|
11
|
+
Product.attribute_names.should include("#{name}_type")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "create_table acts_as_superclass option withname" do
|
16
|
+
before :all do
|
17
|
+
ActsAsSuperclassWithNameSchema.migrate
|
18
|
+
end
|
19
|
+
|
20
|
+
it "creates foreign key and type columns on" do
|
21
|
+
OtherProduct.attribute_names.should include("producible_id")
|
22
|
+
OtherProduct.attribute_names.should include("producible_type")
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActsAsSuperclassSchema
|
2
|
+
def self.migrate
|
3
|
+
ActiveRecord::Base.establish_connection(
|
4
|
+
:adapter => "sqlite3",
|
5
|
+
:database => ":memory:"
|
6
|
+
)
|
7
|
+
ActiveRecord::Schema.define do
|
8
|
+
suppress_messages do
|
9
|
+
create_table :products, :as_relation_superclass => true do |t|
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require Pathname(__FILE__).parent.join("models.rb")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ActsAsSuperclassWithNameSchema
|
19
|
+
def self.migrate
|
20
|
+
ActiveRecord::Base.establish_connection(
|
21
|
+
:adapter => "sqlite3",
|
22
|
+
:database => ":memory:"
|
23
|
+
)
|
24
|
+
ActiveRecord::Schema.define do
|
25
|
+
suppress_messages do
|
26
|
+
create_table :other_products, :as_relation_superclass => :producible do |t|
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require Pathname(__FILE__).parent.join("models.rb")
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Submodel" do
|
4
|
+
before :all do
|
5
|
+
ActsAsRelationSchema.migrate
|
6
|
+
end
|
7
|
+
|
8
|
+
it "inherits Supermodel attributes" do
|
9
|
+
pen = Pen.new
|
10
|
+
['name', 'name=', 'name_changed?', 'name_was',
|
11
|
+
'price', 'price=', 'price_changed?', 'price_was'].each do |m|
|
12
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
13
|
+
pen.should respond_to(m)
|
14
|
+
end
|
15
|
+
|
16
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
17
|
+
pen.name.should == 'RedPen'
|
18
|
+
pen.price.should == 0.8
|
19
|
+
pen.color.should == 'red'
|
20
|
+
|
21
|
+
pen.price = 0.9
|
22
|
+
pen.price_changed?.should be_true
|
23
|
+
pen.price_was.should == 0.8
|
24
|
+
end
|
25
|
+
|
26
|
+
it "inherits Supermodel associations" do
|
27
|
+
store = Store.new
|
28
|
+
pen = Pen.new
|
29
|
+
pen.store = store
|
30
|
+
pen.store.should == store
|
31
|
+
pen.product.store.should == store
|
32
|
+
end
|
33
|
+
|
34
|
+
it "inherits Supermodel validations" do
|
35
|
+
pen = Pen.new
|
36
|
+
pen.should be_invalid
|
37
|
+
pen.errors.keys.should include(:name, :price, :color)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "inherits Supermodel methods" do
|
41
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
42
|
+
pen.should respond_to('parent_method')
|
43
|
+
pen.parent_method.should == "RedPen - 0.8"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should raise NoMethodEror on unexisting method calls" do
|
47
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
48
|
+
lambda { pen.unexisted_method }.should raise_error(NoMethodError)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "destroies Supermodel on destroy" do
|
52
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
53
|
+
product_id = pen.product.id
|
54
|
+
pen.destroy
|
55
|
+
lambda { Product.find product_id }.should raise_error(ActiveRecord::RecordNotFound)
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#acts_as_other_model?" do
|
59
|
+
it "return true on models wich acts_as other ones" do
|
60
|
+
Pen.acts_as_other_model?.should be_true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#acts_as_model_name" do
|
65
|
+
it "returns name of model wich it acts as" do
|
66
|
+
Pen.acts_as_model_name.should == :product
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "Query Interface" do
|
71
|
+
describe "auto_join" do
|
72
|
+
it "automaticaly joins Supermodel on Submodel queries" do
|
73
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
74
|
+
Pen.create :name => 'RedPen2', :price => 1.2, :color => 'red'
|
75
|
+
Pen.create :name => 'BluePen', :price => 1.2, :color => 'blue'
|
76
|
+
lambda { Pen.where("price > 1").to_a }.should_not raise_error(ActiveRecord::StatementInvalid)
|
77
|
+
Pen.where("name = ?", "RedPen").should include(pen)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "can be disabled by setting auto_join option to false" do
|
81
|
+
lambda { Pencil.where("name = 1").to_a }.should raise_error(ActiveRecord::StatementInvalid)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "Supermodel" do
|
88
|
+
describe "#specific" do
|
89
|
+
it "returns the specific subclass object" do
|
90
|
+
pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
|
91
|
+
pen.product.specific.should == pen
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Store < ActiveRecord::Base
|
2
|
+
has_many :products
|
3
|
+
end
|
4
|
+
|
5
|
+
class Product < ActiveRecord::Base
|
6
|
+
acts_as_superclass
|
7
|
+
|
8
|
+
belongs_to :store
|
9
|
+
validates_presence_of :name, :price
|
10
|
+
|
11
|
+
def parent_method
|
12
|
+
"#{name} - #{price}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Pen < ActiveRecord::Base
|
17
|
+
acts_as_superclass
|
18
|
+
|
19
|
+
acts_as :product
|
20
|
+
validates_presence_of :color
|
21
|
+
end
|
22
|
+
|
23
|
+
class Pencil < ActiveRecord::Base
|
24
|
+
acts_as :pen
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActsAsRelationSchema
|
2
|
+
def self.migrate
|
3
|
+
ActiveRecord::Base.establish_connection(
|
4
|
+
:adapter => "sqlite3",
|
5
|
+
:database => ":memory:"
|
6
|
+
)
|
7
|
+
ActiveRecord::Schema.define do
|
8
|
+
suppress_messages do
|
9
|
+
create_table :stores do |t|
|
10
|
+
t.string :store_name
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :products, :as_relation_superclass => true do |t|
|
14
|
+
t.string :name
|
15
|
+
t.float :price
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table :pens, :as_relation_superclass => true do |t|
|
19
|
+
t.string :color
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :pencils
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require Pathname(__FILE__).parent.join("models.rb")
|
27
|
+
end
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'acts_as_relation'
|
3
|
+
|
4
|
+
Dir[Pathname(__FILE__).parent.join "*/*schema.rb"].each {|f| require f}
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
8
|
+
config.run_all_when_everything_filtered = true
|
9
|
+
config.filter_run :focus
|
10
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_relation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.1'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-02-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
16
|
-
requirement: &
|
15
|
+
name: rake
|
16
|
+
requirement: &17449100 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *17449100
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
|
-
name:
|
27
|
-
requirement: &
|
26
|
+
name: bundler
|
27
|
+
requirement: &17448680 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *17448680
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: sqlite3
|
38
|
-
requirement: &
|
38
|
+
requirement: &17448260 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *17448260
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
49
|
-
requirement: &
|
48
|
+
name: rspec
|
49
|
+
requirement: &17447840 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,18 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *17447840
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: activerecord
|
60
|
+
requirement: &17447420 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *17447420
|
58
69
|
description: This 'acts_as' extension provides multi-table inheritance for rails models.
|
59
70
|
email: hsn.zamani@gmail.com
|
60
71
|
executables: []
|
@@ -62,18 +73,22 @@ extensions: []
|
|
62
73
|
extra_rdoc_files: []
|
63
74
|
files:
|
64
75
|
- .gitignore
|
76
|
+
- .rspec
|
65
77
|
- Gemfile
|
66
|
-
- README.
|
78
|
+
- README.rdoc
|
67
79
|
- Rakefile
|
68
80
|
- acts_as_relation.gemspec
|
69
|
-
-
|
70
|
-
- lib/active_record/
|
71
|
-
- lib/active_record/acts/as_relation_superclass_migration.rb
|
81
|
+
- lib/active_record/acts_as_relation.rb
|
82
|
+
- lib/active_record/acts_as_relation_superclass_migration.rb
|
72
83
|
- lib/acts_as_relation.rb
|
73
84
|
- lib/version.rb
|
74
|
-
-
|
75
|
-
-
|
76
|
-
-
|
85
|
+
- spec/acts_as_migration/acts_as_migratoin_spec.rb
|
86
|
+
- spec/acts_as_migration/models.rb
|
87
|
+
- spec/acts_as_migration/schema.rb
|
88
|
+
- spec/acts_as_relation/acts_as_relation_spec.rb
|
89
|
+
- spec/acts_as_relation/models.rb
|
90
|
+
- spec/acts_as_relation/schema.rb
|
91
|
+
- spec/spec_helper.rb
|
77
92
|
homepage: http://github.com/hzamani/acts_as_relation
|
78
93
|
licenses: []
|
79
94
|
post_install_message:
|
@@ -99,6 +114,10 @@ signing_key:
|
|
99
114
|
specification_version: 3
|
100
115
|
summary: Easy multi-table inheritance for rails
|
101
116
|
test_files:
|
102
|
-
-
|
103
|
-
-
|
104
|
-
-
|
117
|
+
- spec/acts_as_migration/acts_as_migratoin_spec.rb
|
118
|
+
- spec/acts_as_migration/models.rb
|
119
|
+
- spec/acts_as_migration/schema.rb
|
120
|
+
- spec/acts_as_relation/acts_as_relation_spec.rb
|
121
|
+
- spec/acts_as_relation/models.rb
|
122
|
+
- spec/acts_as_relation/schema.rb
|
123
|
+
- spec/spec_helper.rb
|
data/README.markdown
DELETED
@@ -1,85 +0,0 @@
|
|
1
|
-
Acts As Realation
|
2
|
-
=================
|
3
|
-
|
4
|
-
Easy multi-table inheritance for rails.
|
5
|
-
With `acts_as_relation` models inherit parent model:
|
6
|
-
|
7
|
-
* columns
|
8
|
-
* validations
|
9
|
-
* methods
|
10
|
-
|
11
|
-
Multi-table inheritance
|
12
|
-
-----------------------
|
13
|
-
|
14
|
-
Multi-table inheritance happens when each model in the hierarchy is a model all by itself
|
15
|
-
that corresponds to its own database table and can be queried and created individually.
|
16
|
-
The inheritance relationship introduces links between the child model and each of its
|
17
|
-
parents (via an automatically-created `has_one` associations).
|
18
|
-
|
19
|
-
Example
|
20
|
-
-------
|
21
|
-
|
22
|
-
generate models
|
23
|
-
|
24
|
-
$ rails g model product name:string price:float
|
25
|
-
$ rails g model pen color:string
|
26
|
-
|
27
|
-
Add next option to Product Migration:
|
28
|
-
|
29
|
-
create_table :products, :as_relation_superclass => true
|
30
|
-
|
31
|
-
add some validations and instance methods
|
32
|
-
|
33
|
-
class Product < ActiveRecord::Base
|
34
|
-
validates_presence_of :name, :price
|
35
|
-
|
36
|
-
def hello
|
37
|
-
puts "Hello, My name is '#{name}', my price is $#{price}."
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
product is superclass for all kind of products:
|
42
|
-
|
43
|
-
class Product < ActiveRecord::Base
|
44
|
-
acts_as_superclass
|
45
|
-
end
|
46
|
-
|
47
|
-
pen inherits from product
|
48
|
-
|
49
|
-
class Pen < ActiveRecord::Base
|
50
|
-
acts_as :product
|
51
|
-
end
|
52
|
-
|
53
|
-
to get some specific class(as example: specific product - Pen) from superclass can be used method specific_class:
|
54
|
-
|
55
|
-
Pen.create :name => 'Pen A', :color=> 'black', :price => 0.42
|
56
|
-
product = Product.first
|
57
|
-
product.specific_class # will be instance of Pen class
|
58
|
-
|
59
|
-
to get name of association used between superclass and children can be used method acts_as_association_name:
|
60
|
-
|
61
|
-
Product.acts_as_association_name # 'Productable'
|
62
|
-
|
63
|
-
after deleting specific object will be removed linked superobject:
|
64
|
-
|
65
|
-
Pen.first.destroy # delete as Pen row as linked Product row
|
66
|
-
|
67
|
-
pen inherits products validations and columns
|
68
|
-
|
69
|
-
p = Pen.new
|
70
|
-
p.valid? => false
|
71
|
-
p.errors => {:name=>["can't be blank"], :price=>["can't be blank"]}
|
72
|
-
|
73
|
-
pen inherits product methods
|
74
|
-
|
75
|
-
pen = Pen.new(:name=>"Red Pen", :color=>:red, :price=>0.99)
|
76
|
-
pen.hello => Hello, My name is 'Red Pen', my price is $0.99.
|
77
|
-
|
78
|
-
we can make queries on both models
|
79
|
-
|
80
|
-
Product.where("price <= 1")
|
81
|
-
Pen.where("color = ?", color)
|
82
|
-
|
83
|
-
---
|
84
|
-
|
85
|
-
Copyright (c) 2011 Hassan Zamani, released under the MIT license.
|
data/init.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require 'acts_as_relation'
|
@@ -1,114 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module Acts
|
3
|
-
module AsRelation
|
4
|
-
def self.included(base)
|
5
|
-
base.extend(ClassMethods)
|
6
|
-
end
|
7
|
-
|
8
|
-
module AccessMethods
|
9
|
-
def define_acts_as_accessors(attribs, model_name)
|
10
|
-
attribs.each do |attrib|
|
11
|
-
class_eval <<-EndClass
|
12
|
-
def #{attrib}
|
13
|
-
#{model_name}.#{attrib}
|
14
|
-
end
|
15
|
-
|
16
|
-
def #{attrib}=(value)
|
17
|
-
self.#{model_name}.#{attrib} = value
|
18
|
-
end
|
19
|
-
|
20
|
-
def #{attrib}?
|
21
|
-
self.#{model_name}.#{attrib}?
|
22
|
-
end
|
23
|
-
EndClass
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
module ClassMethods
|
29
|
-
def acts_as_association_name model_name = nil
|
30
|
-
suffix = 'able'
|
31
|
-
model_name = self.name unless model_name
|
32
|
-
|
33
|
-
name = model_name.to_s.demodulize.singularize
|
34
|
-
if name[-1].chr =~ /[^aeiou]/ || name[-2..-1] =~ /ge|ce/
|
35
|
-
name = name + suffix
|
36
|
-
elsif name[-7..-1] == 'ability'
|
37
|
-
name = name[0..-8] + suffix
|
38
|
-
else
|
39
|
-
name = name[0..-2] + suffix
|
40
|
-
end
|
41
|
-
|
42
|
-
name.underscore
|
43
|
-
end
|
44
|
-
|
45
|
-
def acts_as(model_name)
|
46
|
-
name = model_name.to_s.underscore.singularize
|
47
|
-
association_name = acts_as_association_name name
|
48
|
-
|
49
|
-
# Create A AsModel module
|
50
|
-
as_model = Module.new
|
51
|
-
Object.const_set("As#{name.camelcase}", as_model)
|
52
|
-
|
53
|
-
as_model.module_eval <<-EndModule
|
54
|
-
def self.included(base)
|
55
|
-
base.has_one :#{name}, :as => :#{association_name}, :autosave => true, :validate => false, :dependent => :destroy
|
56
|
-
base.validate :#{name}_must_be_valid
|
57
|
-
base.alias_method_chain :#{name}, :autobuild
|
58
|
-
|
59
|
-
base.extend ActiveRecord::Acts::AsRelation::AccessMethods
|
60
|
-
all_attributes = #{name.camelcase.constantize}.content_columns.map(&:name)
|
61
|
-
ignored_attributes = ["created_at", "updated_at", "#{association_name}_type"]
|
62
|
-
associations = #{name.camelcase.constantize}.reflect_on_all_associations(:belongs_to).map! { |assoc| assoc.name }
|
63
|
-
attributes_to_delegate = all_attributes - ignored_attributes + associations
|
64
|
-
base.define_acts_as_accessors(attributes_to_delegate, "#{name}")
|
65
|
-
end
|
66
|
-
|
67
|
-
def #{name}_with_autobuild
|
68
|
-
#{name}_without_autobuild || build_#{name}
|
69
|
-
end
|
70
|
-
|
71
|
-
def method_missing method, *arg, &block
|
72
|
-
#{name}.send method, *arg, &block
|
73
|
-
rescue NoMethodError
|
74
|
-
super
|
75
|
-
end
|
76
|
-
|
77
|
-
def respond_to?(method, include_private_methods = false)
|
78
|
-
super || self.#{name}.respond_to?(method, include_private_methods)
|
79
|
-
end
|
80
|
-
|
81
|
-
protected
|
82
|
-
|
83
|
-
def #{name}_must_be_valid
|
84
|
-
unless #{name}.valid?
|
85
|
-
#{name}.errors.each do |att, message|
|
86
|
-
errors.add(att, message)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
EndModule
|
91
|
-
|
92
|
-
class_eval do
|
93
|
-
include "As#{name.camelcase}".constantize
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def acts_as_superclass
|
98
|
-
association_name = acts_as_association_name
|
99
|
-
|
100
|
-
class_eval <<-CLASS
|
101
|
-
belongs_to :#{association_name}, :polymorphic => true
|
102
|
-
|
103
|
-
def specific_class
|
104
|
-
self.#{association_name}
|
105
|
-
end
|
106
|
-
CLASS
|
107
|
-
end
|
108
|
-
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
ActiveRecord::Base.send :include, ActiveRecord::Acts::AsRelation
|
@@ -1,27 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module Acts
|
3
|
-
module AsRelationSuperclassMigration
|
4
|
-
|
5
|
-
def self.included(base)
|
6
|
-
base.class_eval do
|
7
|
-
alias_method_chain :create_table, :as_relation_superclass
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
def create_table_with_as_relation_superclass(table_name, options = {})
|
12
|
-
association_name = ActiveRecord::Base.acts_as_association_name table_name
|
13
|
-
|
14
|
-
create_table_without_as_relation_superclass(table_name, options) do |td|
|
15
|
-
if options[:as_relation_superclass]
|
16
|
-
td.integer "#{association_name}_id"
|
17
|
-
td.string "#{association_name}_type"
|
18
|
-
end
|
19
|
-
|
20
|
-
yield td if block_given?
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
ActiveRecord::ConnectionAdapters::SchemaStatements.send :include, ActiveRecord::Acts::AsRelationSuperclassMigration
|
@@ -1,121 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
class ActsAsRelationTest < ActiveSupport::TestCase
|
4
|
-
|
5
|
-
test "acts as validation" do
|
6
|
-
pen = Pen.new
|
7
|
-
assert !pen.valid?
|
8
|
-
assert_equal pen.errors.keys, [:name, :price, :color]
|
9
|
-
|
10
|
-
pen.name = "TestPen"
|
11
|
-
assert !pen.valid?
|
12
|
-
assert_equal pen.errors.keys, [:price, :color]
|
13
|
-
|
14
|
-
pencil = Pencil.new
|
15
|
-
assert !pencil.valid?
|
16
|
-
assert_equal pencil.errors.keys, [:name, :price, :color]
|
17
|
-
|
18
|
-
pencil.color = "red"
|
19
|
-
assert !pencil.valid?
|
20
|
-
assert_equal pencil.errors.keys, [:name, :price]
|
21
|
-
end
|
22
|
-
|
23
|
-
test "save model" do
|
24
|
-
assert Pen.new(:name=>"FOO", :color=>"black", :price=>0.89).save
|
25
|
-
pen = Pen.new
|
26
|
-
pen.name = "BAR"
|
27
|
-
pen.color = "red"
|
28
|
-
pen.price = 0.99
|
29
|
-
assert pen.save
|
30
|
-
end
|
31
|
-
|
32
|
-
test "access methods" do
|
33
|
-
assert_nothing_raised(ActiveRecord::UnknownAttributeError) do
|
34
|
-
Pen.new(:name=>"RedPen", :price=>0.59, :color=>"red")
|
35
|
-
Pencil.new(:name=>"RedPencil", :price=>0.59, :color=>"red")
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
test "acts as method missing" do
|
40
|
-
assert_nothing_raised(NoMethodError) do
|
41
|
-
pen = Pen.new
|
42
|
-
pen.name_changed?
|
43
|
-
pen.price_changed?
|
44
|
-
pen.name_was
|
45
|
-
|
46
|
-
pencil = Pencil.new
|
47
|
-
pencil.name_changed?
|
48
|
-
pencil.price_changed?
|
49
|
-
pencil.name_was
|
50
|
-
pencil.color_was
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
test "acts as respond to?" do
|
55
|
-
pen = Pen.new
|
56
|
-
assert(pen.respond_to? :name_changed?)
|
57
|
-
assert(pen.respond_to? :name_was)
|
58
|
-
assert(pen.respond_to? :price_will_change!)
|
59
|
-
|
60
|
-
pencil = Pencil.new
|
61
|
-
assert(pencil.respond_to? :name_changed?)
|
62
|
-
assert(pencil.respond_to? :name_was)
|
63
|
-
assert(pencil.respond_to? :price_will_change!)
|
64
|
-
assert(pencil.respond_to? :color_changed?)
|
65
|
-
assert(pencil.respond_to? :color_was)
|
66
|
-
end
|
67
|
-
|
68
|
-
test "association reflections" do
|
69
|
-
store = Store.new
|
70
|
-
pen = Pen.new
|
71
|
-
pen.store = store
|
72
|
-
|
73
|
-
assert_equal store, pen.product.store
|
74
|
-
assert_equal store, pen.store
|
75
|
-
end
|
76
|
-
|
77
|
-
test "call parent methods" do
|
78
|
-
pen = Pen.new(:name=>"RedPen", :price=>0.59, :color=>"red")
|
79
|
-
assert_equal pen.parent_method, "RedPen - 0.59"
|
80
|
-
end
|
81
|
-
|
82
|
-
test "call unexisted method" do
|
83
|
-
assert_raise NoMethodError do
|
84
|
-
pen = Pen.new
|
85
|
-
pen.unexisted_method
|
86
|
-
end
|
87
|
-
|
88
|
-
end
|
89
|
-
|
90
|
-
test "acts as association name" do
|
91
|
-
assert_equal Pencil.acts_as_association_name, 'pencilable'
|
92
|
-
assert_equal Pencil.acts_as_association_name( Pen ), 'penable'
|
93
|
-
end
|
94
|
-
|
95
|
-
test "acts as superclass" do
|
96
|
-
pen = Pen.create(:name => "RedPen", :price => 0.59, :color => "red")
|
97
|
-
product = pen.product
|
98
|
-
|
99
|
-
assert_equal product.specific_class.class, Pen
|
100
|
-
end
|
101
|
-
|
102
|
-
test "destroy action" do
|
103
|
-
pen = Pen.create(:name => "RedPen", :price => 0.59, :color => "red")
|
104
|
-
product = pen.product
|
105
|
-
|
106
|
-
pen.destroy
|
107
|
-
|
108
|
-
assert_raise ActiveRecord::RecordNotFound do
|
109
|
-
Product.find product.id
|
110
|
-
end
|
111
|
-
assert_raise ActiveRecord::RecordNotFound do
|
112
|
-
Pen.find pen.id
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
end
|
117
|
-
|
118
|
-
#ActiveRecord::Base.connection.tables.each do |table|
|
119
|
-
# ActiveRecord::Base.connection.drop_table(table)
|
120
|
-
#end
|
121
|
-
|
data/test/schema.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'active_record'
|
3
|
-
require 'acts_as_relation'
|
4
|
-
|
5
|
-
ActiveRecord::Base.establish_connection(
|
6
|
-
:adapter => "sqlite3",
|
7
|
-
:database => ":memory:"
|
8
|
-
)
|
9
|
-
|
10
|
-
ActiveRecord::Schema.define(:version => 1) do
|
11
|
-
|
12
|
-
create_table :stores do |t|
|
13
|
-
t.string :store_name
|
14
|
-
end
|
15
|
-
|
16
|
-
create_table :products, :as_relation_superclass => true do |t|
|
17
|
-
t.string :name
|
18
|
-
t.float :price
|
19
|
-
end
|
20
|
-
|
21
|
-
create_table :pens, :as_relation_superclass => true do |t|
|
22
|
-
t.string :color
|
23
|
-
end
|
24
|
-
|
25
|
-
create_table :pencils
|
26
|
-
end
|
27
|
-
|
28
|
-
class Store < ActiveRecord::Base
|
29
|
-
has_many :products
|
30
|
-
end
|
31
|
-
|
32
|
-
class Product < ActiveRecord::Base
|
33
|
-
acts_as_superclass
|
34
|
-
|
35
|
-
belongs_to :store
|
36
|
-
validates_presence_of :name, :price
|
37
|
-
|
38
|
-
def parent_method
|
39
|
-
"#{name} - #{price}"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
class Pen < ActiveRecord::Base
|
44
|
-
acts_as_superclass
|
45
|
-
|
46
|
-
acts_as :product
|
47
|
-
validates_presence_of :color
|
48
|
-
end
|
49
|
-
|
50
|
-
class Pencil < ActiveRecord::Base
|
51
|
-
acts_as :pen
|
52
|
-
end
|
data/test/test_helper.rb
DELETED