with_model 2.3.0 → 2.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bac7bd2d2e529887f1e9bd82117cc809f23b336a8acaa2b90e4fb9305c96fe2
4
- data.tar.gz: 1867b36f42957d744858fcefe70f53c58b0c5f39b951a4749a969c39c46b42f0
3
+ metadata.gz: e161c7fe750e7eb3b78e178a14b2e23ef88b0fa4612abfb22e3920be9eda614e
4
+ data.tar.gz: fb6d9310626f6a006463782d179406e1a2af10b6672899fb57c2bf26f6df61a0
5
5
  SHA512:
6
- metadata.gz: eec34fe045c932273600775ccf7b2719dad1d10d6631baf43b9b7d54eec50bc4592d24cc1da1e9af7a844e7fbc73f707a0027d9f58c891afd18fb934e1f796d9
7
- data.tar.gz: 5720ad24f60c91dee5b1eac354a0ca3d3f00d6c65e5d2f45fc76a310d6330564f43ec14924f168edc064121e9cc70b4ca7483c08a33587ea90f20031fff3fba2
6
+ metadata.gz: 7d80585ba48b4b87593f9b7f18b8c4674391e51458b5e273ee121e4962bf4b479000a178a83db14cfa38a1eb185c8f09ac01d727ce6f79afe39ce5e45686ca70
7
+ data.tar.gz: 3e241e4d17f7b5d889e665e5cb17e5d66de1ca7fdd3b3cbd4e73615421561f735aa3d3f40176b27c1be32cfc9e1db7059d642662ad35fc786fc45620b8a8146f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ### Unreleased
2
+
3
+ ### 2.3.1
4
+
5
+ - Honor Active Record destruction callbacks and dependent associations when
6
+ cleaning up rows for a `table(false)` model.
7
+
1
8
  ### 2.3.0
2
9
 
3
10
  - Add `table(false)` to create no table, so that a model whose superclass is a
@@ -13,17 +13,17 @@ module WithModel
13
13
  class NullTable
14
14
  # @param [Class] superclass The resolved superclass whose table will be
15
15
  # inherited.
16
- # @param [Symbol, String] name The model's name, so that a refusal can say
16
+ # @param [Symbol, String] name The model's name, so that an error can say
17
17
  # which model it is talking about.
18
18
  def initialize(superclass, name)
19
19
  @superclass = superclass
20
20
  @name = name
21
21
  end
22
22
 
23
- # Creates nothing, but refuses a superclass that cannot support single
24
- # table inheritance.
23
+ # Creates nothing, but raises {WithModel::InvalidSuperclass} when the
24
+ # superclass cannot support single table inheritance.
25
25
  #
26
- # Refusals describe the model's situation rather than the call that produced
26
+ # Errors describe the model's situation rather than the call that produced
27
27
  # it. Any expression that evaluates to false selects a NullTable, so there
28
28
  # is no call spelling to quote - and in with_model 3.0, omitting `table`
29
29
  # will arrive here too.
@@ -31,20 +31,20 @@ module WithModel
31
31
  # A superclass whose table does not exist *yet* is deliberately allowed: the
32
32
  # table may be created later in the example, and a test may legitimately
33
33
  # want a model whose table is missing. Active Record raises a clear
34
- # `StatementInvalid` naming the table if it never appears, so refusing here
34
+ # `StatementInvalid` naming the table if it never appears, so raising here
35
35
  # would only forbid working setups.
36
36
  #
37
- # What is left is {WithModel::InvalidSuperclass}: both refusals are permanent
37
+ # What is left is {WithModel::InvalidSuperclass}: both errors are permanent
38
38
  # facts about the superclass passed in, not about when it was looked at, so no
39
39
  # amount of waiting makes them work.
40
40
  def create
41
- refuse "#{@superclass} has none to inherit" unless table_name?
41
+ raise_invalid_superclass "#{@superclass} has none to inherit" unless table_name?
42
42
 
43
43
  # Nothing more can be checked until there is a table to look at.
44
44
  return unless table_exists?
45
45
  return if inheritance_column?
46
46
 
47
- refuse "#{@superclass}'s table #{@superclass.table_name.inspect} has no " \
47
+ raise_invalid_superclass "#{@superclass}'s table #{@superclass.table_name.inspect} has no " \
48
48
  "#{inheritance_column.inspect} column, so Active Record cannot tell its rows " \
49
49
  "apart from a subclass's"
50
50
  end
@@ -59,7 +59,7 @@ module WithModel
59
59
  # (`ActiveRecord::SubclassNotFound`).
60
60
  #
61
61
  # `unscoped` is required because a `default_scope` on the superclass would
62
- # otherwise hide rows from the delete; the inheritance-column condition is
62
+ # otherwise hide rows from cleanup; the inheritance-column condition is
63
63
  # then reapplied explicitly, since `unscoped` also discards the type
64
64
  # condition that keeps this from touching the superclass's own rows.
65
65
  #
@@ -68,7 +68,7 @@ module WithModel
68
68
  def teardown(klass)
69
69
  return unless klass.table_exists?
70
70
 
71
- klass.unscoped.where(klass.inheritance_column => klass.sti_name).delete_all
71
+ klass.unscoped.where(klass.inheritance_column => klass.sti_name).each(&:destroy!)
72
72
  end
73
73
 
74
74
  # Drops nothing; there is no table of our own to drop.
@@ -90,7 +90,7 @@ module WithModel
90
90
  inheritance_column.present? && @superclass.columns_hash.key?(inheritance_column)
91
91
  end
92
92
 
93
- def refuse(problem)
93
+ def raise_invalid_superclass(problem)
94
94
  raise InvalidSuperclass,
95
95
  "with_model #{@name.inspect} has no table of its own, but #{problem}"
96
96
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithModel
4
- VERSION = "2.3.0"
4
+ VERSION = "2.3.1"
5
5
  end
@@ -2,515 +2,671 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- # Wrapping the file in a module keeps these parents out of the global namespace
6
- # while still defining them at file scope, where Standard's
7
- # Lint/ConstantDefinitionInBlock does not fire. Blocks resolve constants from
8
- # where they are written, so the examples below name them without qualifying.
9
- # Their tables are supplied per-example by `with_table`, so no schema persists
10
- # between spec files.
11
- module TableFalseSpec
12
- # The ordinary shape single table inheritance expects.
13
- class HasTypeColumn < ActiveRecord::Base
5
+ RSpec.describe "table(false)" do
6
+ # Defines a named Active Record class for one example, equivalent to:
7
+ #
8
+ # class HasLateTable < ActiveRecord::Base
9
+ # end
10
+ #
11
+ # RSpec removes the constant afterward; the matching `after` hook below also
12
+ # clears Active Record's descendant trackers so the class object cannot leak.
13
+ def stub_active_record_class(name, superclass: ActiveRecord::Base)
14
+ klass = Class.new(superclass)
15
+ stub_const(name.to_s, klass)
16
+ (@stubbed_active_record_classes ||= []) << klass
17
+ klass
14
18
  end
15
19
 
16
- class HasCustomInheritanceColumn < ActiveRecord::Base
17
- self.inheritance_column = "kind"
18
- end
20
+ after do
21
+ classes = @stubbed_active_record_classes
22
+ next unless classes
19
23
 
20
- # Both a custom inheritance column and a `type` column, so a test can tell
21
- # which one with_model actually reads.
22
- class HasCustomInheritanceColumnAndTypeColumn < ActiveRecord::Base
23
- self.inheritance_column = "kind"
24
+ ActiveSupport::DescendantsTracker.clear(classes) \
25
+ unless ActiveSupport::DescendantsTracker.clear_disabled
26
+ ActiveSupport::Dependencies::Reference.clear! \
27
+ if defined?(ActiveSupport::Dependencies::Reference)
28
+ WithModel::DescendantsTracker.clear(classes)
24
29
  end
25
30
 
26
- class HasDefaultScope < ActiveRecord::Base
27
- default_scope { where(archived: false) }
28
- end
31
+ describe "with a concrete superclass" do
32
+ with_model :HasTypeColumn do
33
+ table do |t|
34
+ t.string :type
35
+ t.string :name
36
+ end
37
+ end
29
38
 
30
- class HasNoTypeColumn < ActiveRecord::Base
31
- end
39
+ with_model :Truck, superclass: :HasTypeColumn do
40
+ table(false)
41
+ model do
42
+ def honk = "beep"
43
+ end
44
+ end
32
45
 
33
- class HasNilInheritanceColumn < ActiveRecord::Base
34
- self.inheritance_column = nil
35
- end
46
+ it "inherits the superclass table rather than creating its own" do
47
+ expect(Truck.table_name).to eq HasTypeColumn.table_name
48
+ end
36
49
 
37
- class IsAbstract < ActiveRecord::Base
38
- self.abstract_class = true
39
- end
50
+ it "creates no table of its own" do
51
+ expect(ActiveRecord::Base.connection.tables.grep(/with_model_truck/)).to be_empty
52
+ end
40
53
 
41
- # The shape a Rails app's ApplicationRecord takes: abstract, and inherited from
42
- # rather than ActiveRecord::Base directly.
43
- class ApplicationRecord < ActiveRecord::Base
44
- self.abstract_class = true
45
- end
54
+ it "reports the superclass as the STI base class" do
55
+ expect(Truck.base_class).to eq HasTypeColumn
56
+ end
46
57
 
47
- # Its table is never created.
48
- class HasMissingTable < ApplicationRecord
49
- end
58
+ it "still evaluates the model block" do
59
+ expect(Truck.new.honk).to eq "beep"
60
+ end
61
+
62
+ it "writes its own name to the inheritance column" do
63
+ Truck.create!(name: "big")
64
+ expect(HasTypeColumn.first.type).to eq "Truck"
65
+ end
50
66
 
51
- # Its table is created partway through an example.
52
- class HasLateTable < ActiveRecord::Base
67
+ it "is found through the superclass as an instance of itself" do
68
+ Truck.create!(name: "big")
69
+ expect(HasTypeColumn.first).to be_a Truck
70
+ end
71
+
72
+ # Deliberately a second example asserting the same thing: both models are
73
+ # rebuilt for every example, so a superclass resolved once and cached on the
74
+ # Model instance would still point to the first example's dead parent.
75
+ it "works in a second example too" do
76
+ Truck.create!(name: "also big")
77
+ expect(HasTypeColumn.first).to be_a Truck
78
+ end
53
79
  end
54
80
 
55
- RSpec.describe "table(false)" do
56
- describe "with a concrete superclass" do
57
- with_table(HasTypeColumn.table_name) do |t|
81
+ describe "teardown" do
82
+ with_model :HasTypeColumn do
83
+ table do |t|
58
84
  t.string :type
59
85
  t.string :name
60
86
  end
87
+ end
61
88
 
62
- before { HasTypeColumn.reset_column_information }
89
+ context "when child and superclass rows exist" do
90
+ after do
91
+ expect(HasTypeColumn.where(type: "Truck").count).to eq 0
92
+ expect(HasTypeColumn.where(type: nil).count).to eq 1
93
+ end
63
94
 
64
- with_model :Truck, superclass: HasTypeColumn do
95
+ with_model :Truck, superclass: :HasTypeColumn do
65
96
  table(false)
66
- model do
67
- def honk = "beep"
68
- end
69
97
  end
70
98
 
71
- it "inherits the superclass table rather than creating its own" do
72
- expect(Truck.table_name).to eq HasTypeColumn.table_name
99
+ it "deletes its own rows and leaves the superclass's rows alone" do
100
+ Truck.create!(name: "child row")
101
+ HasTypeColumn.create!(name: "parent row")
73
102
  end
103
+ end
74
104
 
75
- it "creates no table of its own" do
76
- expect(ActiveRecord::Base.connection.tables.grep(/with_model_truck/)).to be_empty
105
+ context "when a child row exists" do
106
+ after do
107
+ expect { HasTypeColumn.first }.not_to raise_error
77
108
  end
78
109
 
79
- it "reports the superclass as the STI base class" do
80
- expect(Truck.base_class).to eq HasTypeColumn
110
+ with_model :Truck, superclass: :HasTypeColumn do
111
+ table(false)
81
112
  end
82
113
 
83
- it "still evaluates the model block" do
84
- expect(Truck.new.honk).to eq "beep"
114
+ it "leaves the superclass queryable" do
115
+ Truck.create!(name: "child row")
85
116
  end
117
+ end
86
118
 
87
- it "writes its own name to the inheritance column" do
88
- Truck.create!(name: "big")
89
- expect(HasTypeColumn.first.type).to eq "Truck"
119
+ context "with an abstract superclass" do
120
+ with_model :TablelessSuperclass do
121
+ table
122
+ model do
123
+ self.abstract_class = true
124
+ self.table_name = nil
125
+ end
90
126
  end
91
127
 
92
- it "is found through the superclass as an instance of itself" do
93
- Truck.create!(name: "big")
94
- expect(HasTypeColumn.first).to be_a Truck
95
- end
128
+ it "does not delete rows when create raises WithModel::InvalidSuperclass" do
129
+ HasTypeColumn.create!(name: "parent row")
130
+ tableless_subclass = WithModel::Model.new(
131
+ :TablelessSubclass,
132
+ superclass: TablelessSuperclass
133
+ )
134
+ WithModel::Model::DSL.new(tableless_subclass).table(false)
135
+
136
+ expect { tableless_subclass.create }.to raise_error(WithModel::InvalidSuperclass)
137
+ expect { tableless_subclass.destroy }.not_to raise_error
96
138
 
97
- # Deliberately a second example asserting the same thing: a superclass
98
- # resolved once and cached on the Model instance would go stale here,
99
- # because the parent is the same class object but the child is rebuilt.
100
- it "works in a second example too" do
101
- Truck.create!(name: "also big")
102
- expect(HasTypeColumn.first).to be_a Truck
139
+ expect(HasTypeColumn.count).to eq 1
103
140
  end
104
141
  end
105
142
 
106
- describe "teardown" do
107
- with_table(HasTypeColumn.table_name) do |t|
108
- t.string :type
109
- t.string :name
143
+ context "when destruction of its row is aborted" do
144
+ with_model :AbortsDestroy do
145
+ table do |t|
146
+ t.string :type
147
+ end
148
+ model do
149
+ before_destroy { throw :abort }
150
+ end
110
151
  end
111
152
 
112
- before { HasTypeColumn.reset_column_information }
153
+ subject(:null_table) do
154
+ WithModel::NullTable.new(AbortsDestroy, :TemporaryAbortedDestroy)
155
+ end
113
156
 
114
- def build(name, superclass:, &dsl)
115
- WithModel::Model.new(name, superclass: superclass).tap do |model|
116
- WithModel::Model::DSL.new(model).instance_eval(&dsl)
117
- end
157
+ after do
158
+ expect(Object.const_defined?(:TemporaryAbortedDestroy, false)).to be false
118
159
  end
119
160
 
120
- it "deletes its own rows and leaves the superclass's rows alone" do
121
- model = build(:Truck, superclass: HasTypeColumn) { table(false) }
122
- model.create
123
- Truck.create!(name: "child row")
124
- HasTypeColumn.create!(name: "parent row")
161
+ with_model :TemporaryAbortedDestroy, superclass: :AbortsDestroy do
162
+ table(false)
163
+ end
125
164
 
126
- model.destroy
165
+ it "fails loudly" do
166
+ TemporaryAbortedDestroy.create!
127
167
 
128
- expect(HasTypeColumn.where(type: "Truck").count).to eq 0
129
- expect(HasTypeColumn.where(type: nil).count).to eq 1
168
+ expect do
169
+ null_table.teardown(TemporaryAbortedDestroy)
170
+ end.to raise_error(ActiveRecord::RecordNotDestroyed)
171
+
172
+ AbortsDestroy.where(type: TemporaryAbortedDestroy.sti_name).delete_all
130
173
  end
174
+ end
131
175
 
132
- it "leaves the superclass queryable" do
133
- model = build(:Truck, superclass: HasTypeColumn) { table(false) }
134
- model.create
135
- Truck.create!(name: "child row")
176
+ context "when its rows have dependent records protected by a foreign key" do
177
+ with_model :HasDependentChildren do
178
+ table do |t|
179
+ t.string :type
180
+ end
181
+ model do
182
+ has_many :dependent_children,
183
+ class_name: "DependentChild",
184
+ foreign_key: :parent_id,
185
+ inverse_of: :parent,
186
+ dependent: :destroy
187
+ end
188
+ end
136
189
 
137
- model.destroy
190
+ with_model :DependentChild do
191
+ table do |t|
192
+ t.references :parent,
193
+ null: false,
194
+ foreign_key: {to_table: HasDependentChildren.table_name}
195
+ end
196
+ model do
197
+ belongs_to :parent,
198
+ class_name: "HasDependentChildren",
199
+ inverse_of: :dependent_children
200
+ end
201
+ end
138
202
 
139
- expect { HasTypeColumn.first }.not_to raise_error
203
+ with_model :OtherDependentParent, superclass: :HasDependentChildren do
204
+ table(false)
140
205
  end
141
206
 
142
- it "does not delete rows when create refused the superclass" do
143
- HasTypeColumn.create!(name: "parent row")
144
- model = build(:Truck, superclass: IsAbstract) { table(false) }
207
+ after do
208
+ expect(HasDependentChildren.unscoped.ids).to contain_exactly(@superclass.id, @sibling.id)
209
+ expect(DependentChild.pluck(:parent_id)).to contain_exactly(@superclass.id, @sibling.id)
210
+ end
145
211
 
146
- expect { model.create }.to raise_error(WithModel::InvalidSuperclass)
147
- expect { model.destroy }.not_to raise_error
212
+ with_model :TemporaryDependentParent, superclass: :HasDependentChildren do
213
+ table(false)
214
+ end
148
215
 
149
- expect(HasTypeColumn.count).to eq 1
216
+ it "destroys its dependents without removing superclass or sibling rows" do
217
+ temporary = TemporaryDependentParent.create!
218
+ @superclass = HasDependentChildren.create!
219
+ @sibling = OtherDependentParent.create!
220
+ [temporary, @superclass, @sibling].each do |parent|
221
+ parent.dependent_children.create!
222
+ end
150
223
  end
151
224
  end
225
+ end
152
226
 
153
- describe "with a superclass that has a default_scope" do
154
- with_table(HasDefaultScope.table_name) do |t|
227
+ describe "with a superclass that has a default_scope" do
228
+ with_model :HasDefaultScope do
229
+ table do |t|
155
230
  t.string :type
156
231
  t.boolean :archived, default: false
157
232
  end
233
+ model do
234
+ default_scope { where(archived: false) }
235
+ end
236
+ end
158
237
 
159
- before { HasDefaultScope.reset_column_information }
160
-
161
- it "deletes rows the default scope would have hidden" do
162
- model = WithModel::Model.new(:ScopedChild, superclass: HasDefaultScope)
163
- WithModel::Model::DSL.new(model).table(false)
164
- model.create
165
- ScopedChild.create!(archived: false)
166
- ScopedChild.create!(archived: true)
238
+ after do
239
+ expect(HasDefaultScope.unscoped.where(type: "ScopedChild").count).to eq 0
240
+ end
167
241
 
168
- model.destroy
242
+ with_model :ScopedChild, superclass: :HasDefaultScope do
243
+ table(false)
244
+ end
169
245
 
170
- expect(HasDefaultScope.unscoped.where(type: "ScopedChild").count).to eq 0
171
- end
246
+ it "deletes rows the default scope would have hidden" do
247
+ ScopedChild.create!(archived: false)
248
+ ScopedChild.create!(archived: true)
172
249
  end
250
+ end
173
251
 
174
- describe "with a non-standard inheritance_column" do
175
- with_table(HasCustomInheritanceColumn.table_name) do |t|
252
+ describe "with a non-standard inheritance_column" do
253
+ with_model :HasCustomInheritanceColumn do
254
+ table do |t|
176
255
  t.string :kind
177
256
  t.string :name
178
257
  end
179
-
180
- before { HasCustomInheritanceColumn.reset_column_information }
181
-
182
- with_model :KindedChild, superclass: HasCustomInheritanceColumn do
183
- table(false)
258
+ model do
259
+ self.inheritance_column = "kind"
184
260
  end
261
+ end
185
262
 
186
- it "writes to the custom column" do
187
- KindedChild.create!(name: "x")
188
- expect(HasCustomInheritanceColumn.first.kind).to eq "KindedChild"
189
- end
263
+ with_model :KindedChild, superclass: :HasCustomInheritanceColumn do
264
+ table(false)
265
+ end
190
266
 
191
- it "accepts a Symbol inheritance_column" do
192
- expect(HasCustomInheritanceColumn.inheritance_column).to eq "kind"
193
- end
267
+ it "writes to the custom column" do
268
+ KindedChild.create!(name: "x")
269
+ expect(HasCustomInheritanceColumn.first.kind).to eq "KindedChild"
194
270
  end
195
271
 
196
- # The anchor case: a validation or teardown that hard-codes "type" passes
197
- # every other example here and still scopes on the wrong column.
198
- describe "with both a type column and a custom inheritance_column" do
199
- with_table(HasCustomInheritanceColumnAndTypeColumn.table_name) do |t|
272
+ it "accepts a Symbol inheritance_column" do
273
+ expect(HasCustomInheritanceColumn.inheritance_column).to eq "kind"
274
+ end
275
+ end
276
+
277
+ # The anchor case: a validation or teardown that hard-codes "type" passes
278
+ # every other example here and still scopes on the wrong column.
279
+ describe "with both a type column and a custom inheritance_column" do
280
+ with_model :CustomInheritanceWithType do
281
+ table do |t|
200
282
  t.string :kind
201
283
  t.string :type
202
284
  t.string :name
203
285
  end
204
-
205
- before { HasCustomInheritanceColumnAndTypeColumn.reset_column_information }
206
-
207
- with_model :KindedTypeChild, superclass: HasCustomInheritanceColumnAndTypeColumn do
208
- table(false)
286
+ model do
287
+ self.inheritance_column = "kind"
209
288
  end
289
+ end
210
290
 
211
- it "writes the custom column and leaves type null" do
212
- KindedTypeChild.create!(name: "x")
213
- row = HasCustomInheritanceColumnAndTypeColumn.first
214
- expect(row.kind).to eq "KindedTypeChild"
215
- expect(row.type).to be_nil
216
- end
291
+ with_model :KindedTypeChild, superclass: :CustomInheritanceWithType do
292
+ table(false)
217
293
  end
218
294
 
219
- describe "with a with_model superclass" do
220
- with_model :Parent do
221
- table do |t|
222
- t.string :type
223
- t.string :name
224
- end
225
- end
295
+ it "writes the custom column and leaves type null" do
296
+ KindedTypeChild.create!(name: "x")
297
+ row = CustomInheritanceWithType.first
298
+ expect(row.kind).to eq "KindedTypeChild"
299
+ expect(row.type).to be_nil
300
+ end
301
+ end
226
302
 
227
- with_model :StringChild, superclass: "Parent" do
228
- table(false)
303
+ describe "with a with_model superclass" do
304
+ with_model :Parent do
305
+ table do |t|
306
+ t.string :type
307
+ t.string :name
229
308
  end
309
+ end
230
310
 
231
- with_model :SymbolChild, superclass: :Parent do
232
- table(false)
233
- end
311
+ with_model :StringChild, superclass: "Parent" do
312
+ table(false)
313
+ end
234
314
 
235
- with_model :CallableChild, superclass: -> { Parent } do
236
- table(false)
237
- end
315
+ with_model :SymbolChild, superclass: :Parent do
316
+ table(false)
317
+ end
238
318
 
239
- it "resolves a String superclass" do
240
- expect(StringChild.superclass).to eq Parent
241
- expect(StringChild.table_name).to eq Parent.table_name
242
- end
319
+ with_model :CallableChild, superclass: -> { Parent } do
320
+ table(false)
321
+ end
243
322
 
244
- # The same spelling with_model names its own models with.
245
- it "resolves a Symbol superclass" do
246
- expect(SymbolChild.superclass).to eq Parent
247
- expect(SymbolChild.table_name).to eq Parent.table_name
248
- end
323
+ it "resolves a String superclass" do
324
+ expect(StringChild.superclass).to eq Parent
325
+ expect(StringChild.table_name).to eq Parent.table_name
326
+ end
249
327
 
250
- it "resolves a callable superclass" do
251
- expect(CallableChild.superclass).to eq Parent
252
- end
328
+ # The same spelling with_model names its own models with.
329
+ it "resolves a Symbol superclass" do
330
+ expect(SymbolChild.superclass).to eq Parent
331
+ expect(SymbolChild.table_name).to eq Parent.table_name
332
+ end
253
333
 
254
- it "round-trips through the parent" do
255
- StringChild.create!(name: "x")
256
- expect(Parent.first).to be_a StringChild
257
- end
334
+ it "resolves a callable superclass" do
335
+ expect(CallableChild.superclass).to eq Parent
336
+ end
258
337
 
259
- # Second example: the parent class object is rebuilt every example, so a
260
- # superclass memoized on the first resolution refers to a dead class here.
261
- it "resolves against the current parent in a later example" do
262
- expect(StringChild.superclass).to eq Parent
263
- expect(StringChild.superclass).to be Parent
264
- end
338
+ it "round-trips through the parent" do
339
+ StringChild.create!(name: "x")
340
+ expect(Parent.first).to be_a StringChild
265
341
  end
266
342
 
267
- describe "namespaced constants" do
268
- with_table(HasTypeColumn.table_name) do |t|
343
+ # Second example: the parent class object is rebuilt every example, so a
344
+ # superclass memoized on the first resolution refers to a dead class here.
345
+ it "resolves against the current parent in a later example" do
346
+ expect(StringChild.superclass).to eq Parent
347
+ expect(StringChild.superclass).to be Parent
348
+ end
349
+ end
350
+
351
+ describe "namespaced constants" do
352
+ with_model :HasTypeColumn do
353
+ table do |t|
269
354
  t.string :type
270
355
  t.string :name
271
356
  end
357
+ end
272
358
 
273
- before { HasTypeColumn.reset_column_information }
359
+ # WithModel::ConstantStubber resolves a namespaced name with `const_get` and
360
+ # stubs only the last segment, so the namespace has to exist already.
361
+ before { stub_const("TableFalseSpec", Module.new) }
274
362
 
275
- # WithModel::ConstantStubber resolves a namespaced name with `const_get` and
276
- # stubs only the last segment, so the namespace has to exist already. This
277
- # file's own module supplies one.
278
- with_model "TableFalseSpec::Step", superclass: HasTypeColumn do
279
- table(false)
280
- end
363
+ with_model "TableFalseSpec::Step", superclass: :HasTypeColumn do
364
+ table(false)
365
+ end
281
366
 
282
- it "stores and round-trips the fully qualified name" do
283
- Step.create!(name: "x")
284
- expect(HasTypeColumn.first.type).to eq "TableFalseSpec::Step"
285
- expect(HasTypeColumn.first).to be_a Step
286
- end
367
+ it "stores and round-trips the fully qualified name" do
368
+ TableFalseSpec::Step.create!(name: "x")
369
+ expect(HasTypeColumn.first.type).to eq "TableFalseSpec::Step"
370
+ expect(HasTypeColumn.first).to be_a TableFalseSpec::Step
287
371
  end
372
+ end
288
373
 
289
- # A missing table is a fact about the schema at that moment, not about the
290
- # model, so it is allowed: the table may arrive later in the example, or its
291
- # absence may be the very thing under test.
292
- describe "when the superclass's table does not exist" do
293
- with_model :Orphan, superclass: HasMissingTable do
294
- table(false)
374
+ # A missing table is a fact about the schema at that moment, not about the
375
+ # model, so it is allowed: the table may arrive later in the example, or its
376
+ # absence may be the very thing under test.
377
+ describe "when the superclass's table does not exist" do
378
+ # The shape a Rails app's ApplicationRecord takes: abstract, and inherited
379
+ # from rather than ActiveRecord::Base directly.
380
+ with_model :ApplicationRecord do
381
+ table
382
+ model do
383
+ self.abstract_class = true
384
+ self.table_name = nil
295
385
  end
386
+ end
296
387
 
297
- it "still defines the model, which inherits the missing table's name" do
298
- # Not `new`: building an instance reads column information, which is
299
- # itself a use of the absent table.
300
- expect(Orphan).to be < HasMissingTable
301
- expect(Orphan.table_name).to eq HasMissingTable.table_name
302
- end
388
+ before do
389
+ stub_active_record_class(:HasMissingTable, superclass: ApplicationRecord)
390
+ end
303
391
 
304
- it "lets Active Record raise its own error, naming the table" do
305
- expect { Orphan.create!(name: "x") }
306
- .to raise_error(ActiveRecord::StatementInvalid, /#{HasMissingTable.table_name}/)
307
- end
392
+ with_model :Orphan, superclass: -> { HasMissingTable } do
393
+ table(false)
394
+ end
308
395
 
309
- it "tears down without failing an example that otherwise passed" do
310
- # Reaching the end of this example at all is the assertion: teardown has
311
- # no rows to delete and must not go looking for them.
312
- expect(Orphan.name).to eq "Orphan"
313
- end
396
+ it "still defines the model, which inherits the missing table's name" do
397
+ # Not `new`: building an instance reads column information, which is
398
+ # itself a use of the absent table.
399
+ expect(Orphan).to be < HasMissingTable
400
+ expect(Orphan.table_name).to eq HasMissingTable.table_name
314
401
  end
315
402
 
316
- describe "when the superclass's table is created during the example" do
317
- with_model :LateChild, superclass: HasLateTable do
318
- table(false)
319
- end
403
+ it "lets Active Record raise its own error, naming the table" do
404
+ expect { Orphan.create!(name: "x") }
405
+ .to raise_error(ActiveRecord::StatementInvalid, /#{HasMissingTable.table_name}/)
406
+ end
320
407
 
321
- after do
322
- ActiveRecord::Base.connection.drop_table HasLateTable.table_name, if_exists: true
323
- end
408
+ it "tears down without failing an example that otherwise passed" do
409
+ # Reaching the end of this example at all is the assertion: teardown has
410
+ # no rows to delete and must not go looking for them.
411
+ expect(Orphan.name).to eq "Orphan"
412
+ end
413
+ end
324
414
 
325
- it "inherits the table once it exists" do
326
- ActiveRecord::Base.connection.create_table HasLateTable.table_name, force: true do |t|
327
- t.string :type
328
- t.string :name
329
- end
330
- HasLateTable.reset_column_information
415
+ describe "when the superclass's table is created during the example" do
416
+ before { stub_active_record_class(:HasLateTable) }
331
417
 
332
- LateChild.create!(name: "late")
418
+ with_model :LateChild, superclass: -> { HasLateTable } do
419
+ table(false)
420
+ end
333
421
 
334
- expect(HasLateTable.count).to eq 1
335
- expect(HasLateTable.first).to be_a LateChild
336
- end
422
+ after do
423
+ ActiveRecord::Base.connection.drop_table HasLateTable.table_name, if_exists: true
337
424
  end
338
425
 
339
- describe "refusals" do
340
- def create_model(superclass:, &dsl)
341
- model = WithModel::Model.new(:Refused, superclass: superclass)
342
- WithModel::Model::DSL.new(model).instance_eval(&dsl || proc { table(false) })
343
- model.create
426
+ it "inherits the table once it exists" do
427
+ ActiveRecord::Base.connection.create_table HasLateTable.table_name, force: true do |t|
428
+ t.string :type
429
+ t.string :name
344
430
  end
431
+ HasLateTable.reset_column_information
432
+
433
+ LateChild.create!(name: "late")
434
+
435
+ expect(HasLateTable.count).to eq 1
436
+ expect(HasLateTable.first).to be_a LateChild
437
+ end
438
+ end
439
+
440
+ describe "invalid configurations" do
441
+ # Creates immediately the same tableless subclass normally generated by:
442
+ #
443
+ # with_model :TablelessSubclass, superclass: superclass do
444
+ # table(false)
445
+ # end
446
+ #
447
+ # Bypassing `with_model`'s setup hook lets these examples assert failures
448
+ # raised while the superclass is resolved or the table configuration checked.
449
+ def create_tableless_subclass(superclass:, &dsl)
450
+ model = WithModel::Model.new(:TablelessSubclass, superclass: superclass)
451
+ WithModel::Model::DSL.new(model).instance_eval(&dsl || proc { table(false) })
452
+ model.create
453
+ end
454
+
455
+ it "names the model, which the superclass's name does not identify" do
456
+ expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
457
+ .to raise_error(
458
+ WithModel::InvalidSuperclass,
459
+ /with_model :TablelessSubclass has no table of its own/
460
+ )
461
+ end
462
+
463
+ it "raises WithModel::InvalidSuperclass for ActiveRecord::Base" do
464
+ expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
465
+ .to raise_error(WithModel::InvalidSuperclass, /ActiveRecord::Base has none to inherit/)
466
+ end
345
467
 
346
- it "names the model, which the superclass's name does not identify" do
347
- expect { create_model(superclass: ActiveRecord::Base) }
348
- .to raise_error(WithModel::InvalidSuperclass, /with_model :Refused has no table of its own/)
468
+ context "with an abstract superclass" do
469
+ with_model :TablelessSuperclass do
470
+ table
471
+ model do
472
+ self.abstract_class = true
473
+ self.table_name = nil
474
+ end
349
475
  end
350
476
 
351
- it "refuses ActiveRecord::Base as the superclass" do
352
- expect { create_model(superclass: ActiveRecord::Base) }
353
- .to raise_error(WithModel::InvalidSuperclass, /ActiveRecord::Base has none to inherit/)
477
+ it "raises WithModel::InvalidSuperclass" do
478
+ expect { create_tableless_subclass(superclass: TablelessSuperclass) }
479
+ .to raise_error(WithModel::InvalidSuperclass, /TablelessSuperclass has none to inherit/)
354
480
  end
481
+ end
355
482
 
356
- it "refuses an abstract superclass" do
357
- expect { create_model(superclass: IsAbstract) }
358
- .to raise_error(WithModel::InvalidSuperclass, /IsAbstract has none to inherit/)
483
+ context "with an abstract superclass a Rails app would call ApplicationRecord" do
484
+ with_model :ApplicationRecord do
485
+ table
486
+ model do
487
+ self.abstract_class = true
488
+ self.table_name = nil
489
+ end
359
490
  end
360
491
 
361
- it "refuses an abstract superclass that a Rails app would call ApplicationRecord" do
362
- expect { create_model(superclass: ApplicationRecord) }
492
+ it "raises WithModel::InvalidSuperclass" do
493
+ expect { create_tableless_subclass(superclass: ApplicationRecord) }
363
494
  .to raise_error(WithModel::InvalidSuperclass, /ApplicationRecord has none to inherit/)
364
495
  end
496
+ end
365
497
 
366
- context "when the superclass table lacks the inheritance column" do
367
- with_table(HasNoTypeColumn.table_name) do |t|
498
+ context "when the superclass table lacks the inheritance column" do
499
+ with_model :HasNoTypeColumn do
500
+ table do |t|
368
501
  t.string :name
369
502
  end
503
+ end
370
504
 
371
- before { HasNoTypeColumn.reset_column_information }
372
-
373
- it "refuses and names the column it looked for" do
374
- expect { create_model(superclass: HasNoTypeColumn) }
375
- .to raise_error(WithModel::InvalidSuperclass, /"type" column/)
376
- end
505
+ it "raises WithModel::InvalidSuperclass and names the missing column" do
506
+ expect { create_tableless_subclass(superclass: HasNoTypeColumn) }
507
+ .to raise_error(WithModel::InvalidSuperclass, /"type" column/)
377
508
  end
509
+ end
378
510
 
379
- context "when the superclass disables inheritance" do
380
- with_table(HasNilInheritanceColumn.table_name) do |t|
511
+ context "when the superclass disables inheritance" do
512
+ with_model :HasNilInheritanceColumn do
513
+ table do |t|
381
514
  t.string :name
382
515
  end
383
-
384
- it "refuses" do
385
- expect { create_model(superclass: HasNilInheritanceColumn) }
386
- .to raise_error(WithModel::InvalidSuperclass)
516
+ model do
517
+ self.inheritance_column = nil
387
518
  end
388
519
  end
389
520
 
390
- it "refuses a table(false) call that also passes a block" do
391
- expect do
392
- create_model(superclass: HasTypeColumn) { table(false) { |t| t.string :nope } }
393
- end.to raise_error(ArgumentError, /table does not take a block when its first argument is falsy/)
521
+ it "raises WithModel::InvalidSuperclass" do
522
+ expect { create_tableless_subclass(superclass: HasNilInheritanceColumn) }
523
+ .to raise_error(WithModel::InvalidSuperclass)
394
524
  end
525
+ end
395
526
 
396
- describe "a superclass that cannot be found" do
397
- it "quotes the reason it could not be resolved" do
398
- expect { create_model(superclass: "NoSuchParent") }
399
- .to raise_error(WithModel::MissingSuperclass,
400
- /superclass "NoSuchParent" could not be resolved: uninitialized constant NoSuchParent/)
401
- end
402
-
403
- it "names the segment of a namespaced superclass that is missing" do
404
- expect { create_model(superclass: "NoSuchNamespace::Deep::Parent") }
405
- .to raise_error(WithModel::MissingSuperclass, /uninitialized constant NoSuchNamespace\./)
406
- end
407
-
408
- it "reports an unresolvable Symbol as the Symbol that was passed" do
409
- expect { create_model(superclass: :NoSuchParent) }
410
- .to raise_error(WithModel::MissingSuperclass,
411
- /superclass :NoSuchParent could not be resolved: uninitialized constant NoSuchParent/)
527
+ context "when table(false) is passed a block" do
528
+ with_model :HasTypeColumn do
529
+ table do |t|
530
+ t.string :type
531
+ t.string :name
412
532
  end
533
+ end
413
534
 
414
- it "keeps the NameError as its cause" do
415
- # Ruby sets this for a raise inside a rescue, so the original backtrace
416
- # stays reachable.
417
- expect { create_model(superclass: "NoSuchParent") }
418
- .to raise_error(WithModel::MissingSuperclass) { |error|
419
- expect(error.cause).to be_a NameError
420
- }
421
- end
535
+ it "raises ArgumentError" do
536
+ expect do
537
+ create_tableless_subclass(superclass: HasTypeColumn) { table(false) { |t| t.string :nope } }
538
+ end.to raise_error(ArgumentError, /table does not take a block when its first argument is falsy/)
422
539
  end
540
+ end
423
541
 
424
- it "tells a missing superclass apart from an unusable one, but catches both" do
425
- expect(WithModel::MissingSuperclass).to be < WithModel::InvalidSuperclass
426
- expect(WithModel::InvalidSuperclass).to be < ArgumentError
542
+ describe "a superclass that cannot be found" do
543
+ it "quotes the reason it could not be resolved" do
544
+ expect { create_tableless_subclass(superclass: "NoSuchParent") }
545
+ .to raise_error(WithModel::MissingSuperclass,
546
+ /superclass "NoSuchParent" could not be resolved: uninitialized constant NoSuchParent/)
427
547
  end
428
548
 
429
- it "says how to name a class that is not defined yet" do
430
- expect { create_model(superclass: 42) }
431
- .to raise_error(WithModel::InvalidSuperclass,
432
- /name it with a String or a Symbol, or pass a callable returning it/)
549
+ it "names the segment of a namespaced superclass that is missing" do
550
+ expect { create_tableless_subclass(superclass: "NoSuchNamespace::Deep::Parent") }
551
+ .to raise_error(WithModel::MissingSuperclass, /uninitialized constant NoSuchNamespace\./)
433
552
  end
434
553
 
435
- it "refuses a value that only has a to_s" do
436
- # Every object has one, so honoring it would look up the constant "42".
437
- expect { create_model(superclass: 42) }
438
- .to raise_error(WithModel::InvalidSuperclass, /but was 42/)
554
+ it "reports an unresolvable Symbol as the Symbol that was passed" do
555
+ expect { create_tableless_subclass(superclass: :NoSuchParent) }
556
+ .to raise_error(WithModel::MissingSuperclass,
557
+ /superclass :NoSuchParent could not be resolved: uninitialized constant NoSuchParent/)
439
558
  end
440
559
 
441
- it "refuses a callable returning a non-ActiveRecord value" do
442
- expect { create_model(superclass: -> { 42 }) }
443
- .to raise_error(WithModel::InvalidSuperclass, /but was 42/)
560
+ it "keeps the NameError as its cause" do
561
+ # Ruby sets this for a raise inside a rescue, so the original backtrace
562
+ # stays reachable.
563
+ expect { create_tableless_subclass(superclass: "NoSuchParent") }
564
+ .to raise_error(WithModel::MissingSuperclass) { |error|
565
+ expect(error.cause).to be_a NameError
566
+ }
444
567
  end
445
568
  end
446
- end
447
569
 
448
- RSpec.describe "omitting table" do
449
- def capture_deprecations
450
- collected = []
451
- WithModel.deprecator.behavior = ->(message, *) { collected << message }
452
- yield
453
- collected
454
- ensure
455
- WithModel.deprecator.behavior = :raise
570
+ it "tells a missing superclass apart from an unusable one, but catches both" do
571
+ expect(WithModel::MissingSuperclass).to be < WithModel::InvalidSuperclass
572
+ expect(WithModel::InvalidSuperclass).to be < ArgumentError
456
573
  end
457
574
 
458
- # Definition-time warning, so the assertion drives a throwaway example group
459
- # rather than relying on this file's own load order.
460
- def define_group(&body)
461
- RSpec::Core::ExampleGroup.describe("throwaway", &body)
575
+ it "says how to name a class that is not defined yet" do
576
+ expect { create_tableless_subclass(superclass: 42) }
577
+ .to raise_error(WithModel::InvalidSuperclass,
578
+ /name it with a String or a Symbol, or pass a callable returning it/)
462
579
  end
463
580
 
464
- it "warns when no table is specified" do
465
- messages = capture_deprecations do
466
- define_group { with_model(:Warned) }
467
- end
468
- expect(messages.join).to match(/table/)
581
+ it "raises WithModel::InvalidSuperclass for a value that only has a to_s" do
582
+ # Every object has one, so honoring it would look up the constant "42".
583
+ expect { create_tableless_subclass(superclass: 42) }
584
+ .to raise_error(WithModel::InvalidSuperclass, /but was 42/)
469
585
  end
470
586
 
471
- it "names the 3.0 horizon in the warning" do
472
- messages = capture_deprecations do
473
- define_group { with_model(:Warned) }
474
- end
475
- expect(messages.join).to include("3.0")
587
+ it "raises WithModel::InvalidSuperclass for a callable returning a non-ActiveRecord value" do
588
+ expect { create_tableless_subclass(superclass: -> { 42 }) }
589
+ .to raise_error(WithModel::InvalidSuperclass, /but was 42/)
476
590
  end
591
+ end
592
+ end
477
593
 
478
- # Left to itself ActiveSupport::Deprecation blames the first frame it does not
479
- # recognize as Rails or the standard library, which is with_model's own source
480
- # - the same line for every omission in a suite.
481
- it "blames the with_model call rather than with_model's own source" do
482
- messages = capture_deprecations do
483
- define_group { with_model(:Warned) }
484
- end
594
+ RSpec.describe "omitting table" do
595
+ before do
596
+ allow(WithModel.deprecator).to receive(:warn)
597
+ end
485
598
 
486
- expect(messages.join).to include("#{__FILE__}:#{__LINE__ - 3}")
487
- expect(messages.join).not_to include("lib/with_model.rb")
599
+ # Defines the supplied block as if it had appeared in an isolated group:
600
+ #
601
+ # RSpec.describe "throwaway" do
602
+ # # supplied definitions
603
+ # end
604
+ #
605
+ # The warning happens at definition time, so the assertion cannot rely on this
606
+ # file's own example-group load order.
607
+ def define_group(&body)
608
+ RSpec::Core::ExampleGroup.describe("throwaway", &body)
609
+ end
610
+
611
+ it "warns when no table is specified" do
612
+ define_group { with_model(:Warned) }
613
+
614
+ expect(WithModel.deprecator).to have_received(:warn) do |message, _callstack|
615
+ expect(message).to match(/table/)
488
616
  end
617
+ end
489
618
 
490
- it "does not warn for table(false)" do
491
- messages = capture_deprecations do
492
- define_group { with_model(:Quiet, superclass: HasTypeColumn) { table(false) } }
493
- end
494
- expect(messages).to be_empty
619
+ it "names the 3.0 horizon in the warning" do
620
+ define_group { with_model(:Warned) }
621
+
622
+ expect(WithModel.deprecator).to have_received(:warn) do |message, _callstack|
623
+ expect(message).to include("3.0")
495
624
  end
625
+ end
496
626
 
497
- it "does not warn for an empty table block" do
498
- messages = capture_deprecations do
499
- define_group do
500
- with_model(:Quiet) do
501
- table do
502
- end
503
- end
504
- end
627
+ # Left to itself ActiveSupport::Deprecation blames the first frame it does not
628
+ # recognize as Rails or the standard library, which is with_model's own source
629
+ # - the same line for every omission in a suite.
630
+ it "blames the with_model call rather than with_model's own source" do
631
+ expected_source_line = __LINE__ + 1
632
+ define_group { with_model(:Warned) }
633
+
634
+ expect(WithModel.deprecator).to have_received(:warn) do |_message, callstack|
635
+ expect(callstack.first.path).to eq(__FILE__)
636
+ expect(callstack.first.lineno).to eq(expected_source_line)
637
+ expect(callstack.first.path).not_to include("lib/with_model.rb")
638
+ end
639
+ end
640
+
641
+ context "when table(false) is used" do
642
+ with_model :HasTypeColumn do
643
+ table do |t|
644
+ t.string :type
645
+ t.string :name
505
646
  end
506
- expect(messages).to be_empty
507
647
  end
508
648
 
509
- it "does not warn for a `table` call with no arguments" do
510
- messages = capture_deprecations do
511
- define_group { with_model(:Quiet) { table } }
649
+ it "does not warn" do
650
+ define_group { with_model(:Quiet, superclass: HasTypeColumn) { table(false) } }
651
+
652
+ expect(WithModel.deprecator).not_to have_received(:warn)
653
+ end
654
+ end
655
+
656
+ it "does not warn for an empty table block" do
657
+ define_group do
658
+ with_model(:Quiet) do
659
+ table do
660
+ end
512
661
  end
513
- expect(messages).to be_empty
514
662
  end
663
+
664
+ expect(WithModel.deprecator).not_to have_received(:warn)
665
+ end
666
+
667
+ it "does not warn for a `table` call with no arguments" do
668
+ define_group { with_model(:Quiet) { table } }
669
+
670
+ expect(WithModel.deprecator).not_to have_received(:warn)
515
671
  end
516
672
  end
@@ -124,7 +124,7 @@ describe "a temporary ActiveRecord model created with with_model" do
124
124
 
125
125
  it "does not singularize the constant name" do
126
126
  expect(BlogPosts).to be
127
- expect(-> { BlogPost }).to raise_error(NameError)
127
+ expect { BlogPost }.to raise_error(NameError)
128
128
  end
129
129
  end
130
130
 
@@ -198,7 +198,7 @@ describe "a temporary ActiveRecord model created with with_model" do
198
198
  end
199
199
 
200
200
  it "has the mixin" do
201
- expect(-> { WithAMixin.new.foo }).not_to raise_error
201
+ expect { WithAMixin.new.foo }.not_to raise_error
202
202
  expect(WithAMixin.include?(AMixin)).to be true
203
203
  end
204
204
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Case Commons, LLC
@@ -90,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
90
  - !ruby/object:Gem::Version
91
91
  version: '0'
92
92
  requirements: []
93
- rubygems_version: 4.0.16
93
+ rubygems_version: 4.0.18
94
94
  specification_version: 4
95
95
  summary: Dynamically build a model within an RSpec context
96
96
  test_files: []