with_model 2.3.0 → 2.4.0

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