acts_as_relation 1.0.0 → 1.1.2

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.
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