acts_as_relation 0.0.5 → 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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