acts_as_relation 1.0.0 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +91 -62
  6. data/README.rdoc +28 -5
  7. data/Rakefile +1 -1
  8. data/acts_as_relation.gemspec +10 -10
  9. data/lib/active_record/acts_as_relation.rb +45 -126
  10. data/lib/active_record/acts_as_relation/access_methods.rb +63 -0
  11. data/lib/active_record/acts_as_relation/acts_as.rb +34 -0
  12. data/lib/active_record/acts_as_relation/acts_as_modules.rb +109 -0
  13. data/lib/active_record/acts_as_relation/superclass_migration.rb +32 -0
  14. data/lib/acts_as_relation.rb +4 -1
  15. data/lib/version.rb +1 -1
  16. data/spec/acts_as_relation_spec.rb +136 -59
  17. data/spec/acts_as_superclass_spec.rb +7 -7
  18. data/spec/dummy/app/models/product.rb +4 -0
  19. data/spec/dummy/app/models/store.rb +1 -0
  20. data/spec/dummy/config/application.rb +2 -49
  21. data/spec/dummy/config/boot.rb +1 -1
  22. data/spec/dummy/config/environments/development.rb +0 -3
  23. data/spec/dummy/config/environments/test.rb +1 -6
  24. data/spec/dummy/db/schema.rb +4 -0
  25. data/spec/spec_helper.rb +10 -7
  26. metadata +33 -73
  27. data/lib/active_record/acts_as_relation_superclass_migration.rb +0 -30
  28. data/spec/dummy/.rspec +0 -1
  29. data/spec/dummy/README.rdoc +0 -261
  30. data/spec/dummy/app/assets/javascripts/application.js +0 -15
  31. data/spec/dummy/app/assets/stylesheets/application.css +0 -13
  32. data/spec/dummy/app/controllers/application_controller.rb +0 -3
  33. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  34. data/spec/dummy/app/mailers/.gitkeep +0 -0
  35. data/spec/dummy/app/views/layouts/application.html.erb +0 -14
  36. data/spec/dummy/config/environments/production.rb +0 -65
  37. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  38. data/spec/dummy/config/initializers/inflections.rb +0 -15
  39. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  40. data/spec/dummy/config/initializers/secret_token.rb +0 -7
  41. data/spec/dummy/config/initializers/session_store.rb +0 -8
  42. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  43. data/spec/dummy/config/locales/en.yml +0 -5
  44. data/spec/dummy/config/routes.rb +0 -58
  45. data/spec/dummy/lib/assets/.gitkeep +0 -0
  46. data/spec/dummy/public/404.html +0 -26
  47. data/spec/dummy/public/422.html +0 -26
  48. data/spec/dummy/public/500.html +0 -25
  49. data/spec/dummy/public/favicon.ico +0 -0
@@ -0,0 +1,63 @@
1
+ module ActiveRecord
2
+ module ActsAsRelation
3
+ module AccessMethods
4
+ def define_acts_as_accessors(model_name)
5
+ # The weird order of the if-else branches is so that we query ourselves
6
+ # before we query our superclass.
7
+ class_eval <<-EndCode, __FILE__, __LINE__ + 1
8
+ def read_attribute(attr_name, *args, &proc)
9
+ if attribute_method?(attr_name)
10
+ super(attr_name, *args)
11
+ else
12
+ #{model_name}.read_attribute(attr_name, *args, &proc)
13
+ end
14
+ end
15
+
16
+ def attributes
17
+ if #{model_name}.changed? || changed?
18
+ @attributes = #{model_name}.attributes.merge(super)
19
+ else
20
+ @attributes ||= #{model_name}.attributes.merge(super)
21
+ end
22
+ end
23
+
24
+ def attributes=(new_attributes)
25
+ sub = new_attributes.select { |k,v| attribute_method?(k) }
26
+ sup = new_attributes.select { |k,v| !attribute_method?(k) }
27
+ super(sub)
28
+ #{model_name}.attributes = sup
29
+ end
30
+
31
+ def touch(name = nil, *args, &proc)
32
+ if attribute_method?(name.to_s)
33
+ super(name, *args, &proc)
34
+ else
35
+ super(nil, *args, &proc)
36
+ #{model_name}.touch(name, *args, &proc)
37
+ end
38
+ end
39
+
40
+ def save(*args)
41
+ super(*args) && #{model_name}.save(*args)
42
+ end
43
+
44
+ def save!(*args)
45
+ super(*args) && #{model_name}.save!(*args)
46
+ end
47
+
48
+ private
49
+
50
+ def write_attribute(attr_name, *args, &proc)
51
+ if attribute_method?(attr_name.to_s)
52
+ super(attr_name, *args)
53
+ else
54
+ #{model_name}.send(:write_attribute, attr_name, *args, &proc)
55
+ end
56
+ end
57
+ EndCode
58
+ end
59
+
60
+ protected :define_acts_as_accessors
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveRecord
2
+ module ActsAsRelation
3
+ class ActsAs
4
+ attr_accessor :module
5
+ attr_reader :name, :class_name, :model, :association_name, :module_name, :module_name, :scope, :options
6
+
7
+ def initialize(model_name, scope = nil, options = {})
8
+ @model_name = model_name
9
+ @options, @scope = scope.is_a?(Hash) ? [scope, nil] : [options, scope]
10
+
11
+ @name = @model_name.to_s.underscore.singularize.to_sym
12
+ @class_name = @options[:class_name] || @name.to_s.camelcase
13
+ @model = @class_name.constantize
14
+ @association_name = @options[:as] || @model.acts_as_association_name
15
+ @module_name = "ActsAs#{name.to_s.camelcase}"
16
+ end
17
+
18
+ def parent_relations
19
+ @parent_relations ||= (model.reflect_on_all_associations.map(&:name) - [association_name]).map { |a| a.to_s + '_id' }
20
+ end
21
+
22
+ def has_one_options
23
+ @has_one_options ||= {
24
+ as: association_name,
25
+ class_name: class_name,
26
+ inverse_of: association_name.to_sym,
27
+ autosave: true,
28
+ validate: false,
29
+ dependent: options.fetch(:dependent, :destroy)
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,109 @@
1
+ module ActiveRecord
2
+ module ActsAsRelation
3
+ module ActsAsModules
4
+ class << self
5
+ def [](acts_as)
6
+ if const_defined? acts_as.module_name
7
+ const_get acts_as.module_name
8
+ else
9
+ acts_as_module = Module.new
10
+ const_set acts_as.module_name, acts_as_module
11
+ acts_as.module = acts_as_module
12
+ create_assocication(acts_as)
13
+ autobuild_superclass(acts_as)
14
+ validate_superclass(acts_as)
15
+ make_superclass_methods_accessible(acts_as)
16
+ make_superclass_attributes_accessible(acts_as)
17
+ fix_is_a(acts_as)
18
+ end
19
+ end
20
+
21
+ def create_assocication(acts_as)
22
+ acts_as.module.extend ActiveSupport::Concern
23
+ acts_as.module.included do
24
+ has_one acts_as.name, acts_as.scope, acts_as.has_one_options
25
+ alias_method_chain acts_as.name, :autobuild
26
+
27
+ validate "#{acts_as.name}_must_be_valid".to_sym
28
+
29
+ extend ActiveRecord::ActsAsRelation::AccessMethods
30
+ define_acts_as_accessors(acts_as.name)
31
+
32
+ if defined?(::ProtectedAttributes)
33
+ attr_accessible.update(acts_as.model.attr_accessible)
34
+ end
35
+ end
36
+ end
37
+
38
+ def autobuild_superclass(acts_as)
39
+ acts_as.module.module_eval do
40
+ define_method "#{acts_as.name}_with_autobuild" do
41
+ send("#{acts_as.name}_without_autobuild") || send("build_#{acts_as.name}")
42
+ end
43
+ end
44
+ end
45
+
46
+ def make_superclass_methods_accessible(acts_as)
47
+ acts_as.module.module_eval do
48
+ define_method :method_missing do |method, *arg, &block|
49
+ if (method.to_s == 'id' || method.to_s == acts_as.name) || !send(acts_as.name).respond_to?(method)
50
+ super(method, *arg, &block)
51
+ else
52
+ class_eval do
53
+ delegate method, to: acts_as.name
54
+ end
55
+ send(acts_as.name).send(method, *arg, &block)
56
+ end
57
+ end
58
+
59
+ define_method :respond_to? do |method, include_private_methods = false|
60
+ super(method, include_private_methods) || send(acts_as.name).respond_to?(method, include_private_methods)
61
+ end
62
+ end
63
+ end
64
+
65
+ def fix_is_a(acts_as)
66
+ acts_as.module.module_eval do
67
+ define_method :is_a? do |klass|
68
+ klass.name == acts_as.class_name ? true : super(klass)
69
+ end
70
+ alias_method :acts_as?, :is_a?
71
+ end
72
+ end
73
+
74
+ def make_superclass_attributes_accessible(acts_as)
75
+ acts_as.module.module_eval do
76
+ define_method :[] do |key|
77
+ if acts_as.parent_relations.include?(key.to_s)
78
+ send(acts_as.name)[key]
79
+ else
80
+ super(key)
81
+ end
82
+ end
83
+
84
+ define_method :[]= do |key, value|
85
+ if acts_as.parent_relations.include?(key.to_s)
86
+ send(acts_as.name)[key] = value
87
+ else
88
+ super(key, value)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def validate_superclass(acts_as)
95
+ acts_as.module.module_eval do
96
+ define_method "#{acts_as.name}_must_be_valid" do
97
+ unless send(acts_as.name).valid?
98
+ send(acts_as.name).errors.each do |att, message|
99
+ errors.add(att, message)
100
+ end
101
+ end
102
+ end
103
+ protected "#{acts_as.name}_must_be_valid".to_sym
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveRecord
2
+ module ActsAsRelation
3
+ module SuperclassMigration
4
+ def self.included(base)
5
+ base.class_eval do
6
+ alias_method_chain :create_table, :as_relation_superclass
7
+ end
8
+ end
9
+
10
+ def create_table_with_as_relation_superclass(table_name, options = {})
11
+ create_table_without_as_relation_superclass(table_name, options) do |t|
12
+ if options.key? :as_relation_superclass
13
+ name = options[:as_relation_superclass]
14
+ if name == true
15
+ name = ActiveRecord::Base.acts_as_association_name table_name
16
+ end
17
+
18
+ t.integer "#{name}_id"
19
+ t.string "#{name}_type"
20
+ t.index ["#{name}_id", "#{name}_type"], name: "#{table_name}_#{name}_index"
21
+ end
22
+
23
+ yield t if block_given?
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ module ConnectionAdapters::SchemaStatements
30
+ include ActsAsRelation::SuperclassMigration
31
+ end
32
+ end
@@ -1,2 +1,5 @@
1
1
  require 'active_record/acts_as_relation'
2
- require 'active_record/acts_as_relation_superclass_migration'
2
+ require 'active_record/acts_as_relation/acts_as'
3
+ require 'active_record/acts_as_relation/access_methods'
4
+ require 'active_record/acts_as_relation/acts_as_modules'
5
+ require 'active_record/acts_as_relation/superclass_migration'
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module ActsAsRelation
3
- VERSION = "1.0.0"
3
+ VERSION = '1.1.2'
4
4
  end
5
5
  end
@@ -1,43 +1,62 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "Submodel" do
4
- it "inherits Supermodel attributes" do
3
+ describe 'Submodel' do
4
+ it 'respond to supermodel attributes' do
5
5
  pen = Pen.new
6
- ['name', 'name=', 'name_changed?', 'name_was',
7
- 'price', 'price=', 'price_changed?', 'price_was'].each do |m|
8
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
9
- pen.should respond_to(m)
6
+ %w(name name= name_changed? name_was price price= price_changed? price_was).each do |at|
7
+ expect(pen).to respond_to(at)
10
8
  end
9
+ end
10
+
11
+ it 'returns supermodel attributes with #attributes' do
12
+ pen = Pen.new
13
+ expect(pen.attributes.keys).to include(*%w(name price))
14
+ end
15
+
16
+ it 'makes supermodel attributes assingeable through #attributes=' do
17
+ pen = Pen.new
18
+ pen.attributes = {name: 'test', price: 10, color: 'black'}
19
+ expect(pen.name).to eq('test')
20
+ expect(pen.price).to eq(10)
21
+ expect(pen.color).to eq('black')
22
+ end
11
23
 
12
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
13
- pen.name.should == 'RedPen'
14
- pen.price.should == 0.8
15
- pen.color.should == 'red'
24
+ it 'accept supermodel attributes on create' do
25
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
26
+ expect(pen.name).to eq('RedPen')
27
+ expect(pen.price).to eq(0.8)
28
+ expect(pen.color).to eq('red')
16
29
 
17
30
  pen.price = 0.9
18
- pen.price_changed?.should be_true
19
- pen.price_was.should == 0.8
31
+ expect(pen.price_changed?).to be true
32
+ expect(pen.price_was).to eq(0.8)
20
33
  end
21
34
 
22
- it "inherits Supermodel associations" do
23
- store = Store.create :name => 'Big Store'
24
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
35
+ it 'inherits Supermodel associations' do
36
+ store = Store.create name: 'Big Store'
37
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
25
38
  pen.store = store
26
39
  pen.save
27
- Pen.find(pen.id).store.should == store
28
- Pen.find(pen.id).product.store.should == store
40
+ expect(Pen.find(pen.id).store).to eq(store)
41
+ expect(Pen.find(pen.id).product.store).to eq(store)
29
42
  end
30
43
 
31
- it "inherits Supermodel validations" do
44
+ it 'inherits Supermodel validations' do
32
45
  pen = Pen.new
33
- pen.should be_invalid
34
- pen.errors.keys.should include(:name, :price, :color)
46
+ expect(pen).to be_invalid
47
+ expect(pen.errors.keys).to include(:name, :price, :color)
48
+ end
49
+
50
+ it 'inherits Supermodel methods' do
51
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
52
+ expect(pen).to respond_to('parent_method')
53
+ expect(pen.parent_method).to eq('RedPen - 0.8')
35
54
  end
36
55
 
37
- it "inherits Supermodel methods" do
38
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
39
- pen.should respond_to('parent_method')
40
- pen.parent_method.should == "RedPen - 0.8"
56
+ it 'raise NoMethodError correctly for Supermodel methods' do
57
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
58
+ expect(pen).to respond_to('dummy_raise_method')
59
+ expect { pen.dummy_raise_method(nil) }.to raise_error(NoMethodError, /undefined method `dummy' for nil:NilClass/)
41
60
  end
42
61
 
43
62
  # it "inherits Supermodel dynamic finders" do
@@ -47,72 +66,130 @@ describe "Submodel" do
47
66
  # Product.find_by_name('SomeProduct').should == product
48
67
  # end
49
68
 
50
- it "should raise NoMethodEror on unexisting method calls" do
51
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
52
- lambda { pen.unexisted_method }.should raise_error(NoMethodError)
69
+ it 'should raise NoMethodEror on unexisting method calls' do
70
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
71
+ expect { pen.unexisted_method }.to raise_error(NoMethodError)
53
72
  end
54
73
 
55
- it "destroies Supermodel on destroy" do
56
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
74
+ it 'destroies Supermodel on destroy' do
75
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
57
76
  product_id = pen.product.id
58
77
  pen.destroy
59
- lambda { Product.find product_id }.should raise_error(ActiveRecord::RecordNotFound)
78
+ expect { Product.find product_id }.to raise_error(ActiveRecord::RecordNotFound)
79
+ end
80
+
81
+ describe '#acts_as_other_model?' do
82
+ it 'return true on models wich acts_as other ones' do
83
+ expect(Pen.acts_as_other_model?).to be true
84
+ end
60
85
  end
61
86
 
62
- describe "#acts_as_other_model?" do
63
- it "return true on models wich acts_as other ones" do
64
- Pen.acts_as_other_model?.should be_true
87
+ describe '#acts_as_model_name' do
88
+ it 'returns name of model wich it acts as' do
89
+ expect(Pen.acts_as_model_name).to eq(:product)
65
90
  end
66
91
  end
67
92
 
68
- describe "#acts_as_model_name" do
69
- it "returns name of model wich it acts as" do
70
- Pen.acts_as_model_name.should == :product
93
+ describe '#is_a?' do
94
+ it 'should return true when the supermodel is passed' do
95
+ product = Product.new
96
+ expect(product.is_a?(Product)).to be true
97
+ expect(product.instance_of?(Product)).to be true
98
+ expect(product.is_a?(Product)).to be true
99
+ expect(product.acts_as?(Product)).to be true
100
+ expect(product.acts_as?(String)).to be false
101
+
102
+ pen = Pen.new
103
+ expect(pen.is_a?(Product)).to be true
104
+ expect(pen.instance_of?(Product)).to be true
105
+ expect(pen.is_a?(Product)).to be true
106
+ expect(pen.acts_as?(Product)).to be true
107
+ expect(pen.acts_as?(Store)).to be false
108
+ end
109
+
110
+ it 'should return true when the supermodel is passed to model' do
111
+ expect(Pen.acts_as? Product).to be true
112
+ expect(Pen.acts_as? String).to be false
113
+ end
114
+ end
115
+
116
+ context 'in a has_many relation' do
117
+ it 'should be appendable using << operator' do
118
+ store = Store.create(name: 'Big Store')
119
+ pen = Pen.create(name: 'RedPen', price: 0.8, color: 'red')
120
+ store.products << pen
121
+ expect(pen.store).to eq(store)
122
+ end
123
+
124
+ it 'should access child attributes' do
125
+ store = Store.create(name: 'Big Store')
126
+ pen = Pen.create(name: 'RedPen', price: 0.8, color: 'red')
127
+ store.products << pen
128
+ store.reload
129
+ expect(store.products.first.is_a?(Pen)).to be true
130
+ expect(store.products.first).to eq(pen)
131
+ end
132
+
133
+ it 'associate relation on saving superclass object' do
134
+ store = Store.new(name: 'Big Store')
135
+ pen = Pen.new(name: 'RedPen', price: 0.8, color: 'red')
136
+ store.products << pen
137
+ store.save
138
+ store.reload
139
+ expect(store.products.first).to eq(pen)
71
140
  end
72
141
  end
73
142
 
74
- it "have supermodel attr_accessibles as attr_accessibles" do
143
+ it 'have supermodel attr_accessibles as attr_accessibles' do
75
144
  if defined?(::ProtectedAttributes)
76
145
  Pen.attr_accessible[:default].each do |a|
77
- Pencil.attr_accessible[:default].should include(a)
146
+ expect(Pencil.attr_accessible[:default]).to include(a)
78
147
  end
79
148
  end
80
149
  end
81
150
 
82
- it "should be findable" do
83
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
151
+ it 'should be findable' do
152
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
84
153
  pen = Pen.find(pen.id)
85
- pen.should be_valid
154
+ expect(pen).to be_valid
86
155
  end
87
156
 
88
- it "should be saveable" do
89
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
157
+ it 'should be saveable' do
158
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
90
159
  pen = Pen.find(pen.id)
91
- lambda { pen.save }.should_not raise_error
160
+ expect { pen.save }.not_to raise_error
92
161
  end
93
162
 
94
- describe "Query Interface" do
95
- describe "auto_join" do
96
- it "automaticaly joins Supermodel on Submodel queries" do
97
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
98
- Pen.create :name => 'RedPen2', :price => 1.2, :color => 'red'
99
- Pen.create :name => 'BluePen', :price => 1.2, :color => 'blue'
100
- lambda { Pen.where("price > 1").to_a }.should_not raise_error(ActiveRecord::StatementInvalid)
101
- Pen.where("name = ?", "RedPen").should include(pen)
163
+ describe 'Query Interface' do
164
+ describe 'auto_join' do
165
+ it 'automaticaly joins Supermodel on Submodel queries' do
166
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
167
+ Pen.create name: 'RedPen2', price: 1.2, color: 'red'
168
+ Pen.create name: 'BluePen', price: 1.2, color: 'blue'
169
+ expect { Pen.where('price > 1').to_a }.not_to raise_error
170
+ expect(Pen.where('name = ?', 'RedPen')).to include(pen)
102
171
  end
103
172
 
104
- it "can be disabled by setting auto_join option to false" do
105
- lambda { Pencil.where("name = 1").to_a }.should raise_error(ActiveRecord::StatementInvalid)
173
+ it 'can be disabled by setting auto_join option to false' do
174
+ expect { Pencil.where('name = 1').to_a }.to raise_error(ActiveRecord::StatementInvalid)
106
175
  end
107
176
  end
108
177
  end
109
178
  end
110
179
 
111
- describe "Supermodel" do
112
- describe "#specific" do
113
- it "returns the specific subclass object" do
114
- pen = Pen.create :name => 'RedPen', :price => 0.8, :color => 'red'
115
- pen.product.specific.should == pen
180
+ describe 'Supermodel' do
181
+ describe '#specific' do
182
+ it 'returns the specific subclass object' do
183
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
184
+ expect(pen.product.specific).to eq(pen)
185
+ end
186
+ end
187
+
188
+ context 'on destroy' do
189
+ it 'deletes specific subclass' do
190
+ pen = Pen.create name: 'RedPen', price: 0.8, color: 'red'
191
+ pen.product.destroy
192
+ expect { pen.reload }.to raise_error(ActiveRecord::RecordNotFound)
116
193
  end
117
194
  end
118
195
  end