with_model 2.2.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 +4 -4
- data/.github/dependabot.yml +22 -6
- data/.github/workflows/ci.yml +64 -29
- data/CHANGELOG.md +26 -0
- data/CONTRIBUTING.md +83 -0
- data/Gemfile +2 -7
- data/LICENSE +1 -1
- data/README.md +116 -22
- data/lib/with_model/invalid_superclass.rb +15 -0
- data/lib/with_model/missing_superclass.rb +14 -0
- data/lib/with_model/model/dsl.rb +5 -2
- data/lib/with_model/model.rb +86 -6
- data/lib/with_model/null_table.rb +98 -0
- data/lib/with_model/table.rb +12 -0
- data/lib/with_model/version.rb +1 -1
- data/lib/with_model.rb +39 -1
- data/spec/active_record_behaviors_spec.rb +4 -1
- data/spec/descendants_tracking_spec.rb +1 -0
- data/spec/readme_spec.rb +61 -12
- data/spec/spec_helper.rb +2 -0
- data/spec/table_false_spec.rb +672 -0
- data/spec/with_model_spec.rb +33 -10
- data/test/declaration_order_test.rb +61 -0
- data/test/spec_dsl_test.rb +48 -0
- data/test/sti_test.rb +68 -0
- data/test/test_helper.rb +2 -0
- data/with_model.gemspec +2 -2
- metadata +14 -6
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
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
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
after do
|
|
21
|
+
classes = @stubbed_active_record_classes
|
|
22
|
+
next unless classes
|
|
23
|
+
|
|
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)
|
|
29
|
+
end
|
|
30
|
+
|
|
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
|
|
38
|
+
|
|
39
|
+
with_model :Truck, superclass: :HasTypeColumn do
|
|
40
|
+
table(false)
|
|
41
|
+
model do
|
|
42
|
+
def honk = "beep"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "inherits the superclass table rather than creating its own" do
|
|
47
|
+
expect(Truck.table_name).to eq HasTypeColumn.table_name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "creates no table of its own" do
|
|
51
|
+
expect(ActiveRecord::Base.connection.tables.grep(/with_model_truck/)).to be_empty
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "reports the superclass as the STI base class" do
|
|
55
|
+
expect(Truck.base_class).to eq HasTypeColumn
|
|
56
|
+
end
|
|
57
|
+
|
|
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
|
|
66
|
+
|
|
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
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "teardown" do
|
|
82
|
+
with_model :HasTypeColumn do
|
|
83
|
+
table do |t|
|
|
84
|
+
t.string :type
|
|
85
|
+
t.string :name
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
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
|
|
94
|
+
|
|
95
|
+
with_model :Truck, superclass: :HasTypeColumn do
|
|
96
|
+
table(false)
|
|
97
|
+
end
|
|
98
|
+
|
|
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")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context "when a child row exists" do
|
|
106
|
+
after do
|
|
107
|
+
expect { HasTypeColumn.first }.not_to raise_error
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
with_model :Truck, superclass: :HasTypeColumn do
|
|
111
|
+
table(false)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "leaves the superclass queryable" do
|
|
115
|
+
Truck.create!(name: "child row")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
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
|
|
126
|
+
end
|
|
127
|
+
|
|
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
|
|
138
|
+
|
|
139
|
+
expect(HasTypeColumn.count).to eq 1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
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
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
subject(:null_table) do
|
|
154
|
+
WithModel::NullTable.new(AbortsDestroy, :TemporaryAbortedDestroy)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
after do
|
|
158
|
+
expect(Object.const_defined?(:TemporaryAbortedDestroy, false)).to be false
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
with_model :TemporaryAbortedDestroy, superclass: :AbortsDestroy do
|
|
162
|
+
table(false)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "fails loudly" do
|
|
166
|
+
TemporaryAbortedDestroy.create!
|
|
167
|
+
|
|
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
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
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
|
|
189
|
+
|
|
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
|
|
202
|
+
|
|
203
|
+
with_model :OtherDependentParent, superclass: :HasDependentChildren do
|
|
204
|
+
table(false)
|
|
205
|
+
end
|
|
206
|
+
|
|
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
|
|
211
|
+
|
|
212
|
+
with_model :TemporaryDependentParent, superclass: :HasDependentChildren do
|
|
213
|
+
table(false)
|
|
214
|
+
end
|
|
215
|
+
|
|
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
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
describe "with a superclass that has a default_scope" do
|
|
228
|
+
with_model :HasDefaultScope do
|
|
229
|
+
table do |t|
|
|
230
|
+
t.string :type
|
|
231
|
+
t.boolean :archived, default: false
|
|
232
|
+
end
|
|
233
|
+
model do
|
|
234
|
+
default_scope { where(archived: false) }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
after do
|
|
239
|
+
expect(HasDefaultScope.unscoped.where(type: "ScopedChild").count).to eq 0
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
with_model :ScopedChild, superclass: :HasDefaultScope do
|
|
243
|
+
table(false)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it "deletes rows the default scope would have hidden" do
|
|
247
|
+
ScopedChild.create!(archived: false)
|
|
248
|
+
ScopedChild.create!(archived: true)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
describe "with a non-standard inheritance_column" do
|
|
253
|
+
with_model :HasCustomInheritanceColumn do
|
|
254
|
+
table do |t|
|
|
255
|
+
t.string :kind
|
|
256
|
+
t.string :name
|
|
257
|
+
end
|
|
258
|
+
model do
|
|
259
|
+
self.inheritance_column = "kind"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
with_model :KindedChild, superclass: :HasCustomInheritanceColumn do
|
|
264
|
+
table(false)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it "writes to the custom column" do
|
|
268
|
+
KindedChild.create!(name: "x")
|
|
269
|
+
expect(HasCustomInheritanceColumn.first.kind).to eq "KindedChild"
|
|
270
|
+
end
|
|
271
|
+
|
|
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|
|
|
282
|
+
t.string :kind
|
|
283
|
+
t.string :type
|
|
284
|
+
t.string :name
|
|
285
|
+
end
|
|
286
|
+
model do
|
|
287
|
+
self.inheritance_column = "kind"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
with_model :KindedTypeChild, superclass: :CustomInheritanceWithType do
|
|
292
|
+
table(false)
|
|
293
|
+
end
|
|
294
|
+
|
|
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
|
|
302
|
+
|
|
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
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
with_model :StringChild, superclass: "Parent" do
|
|
312
|
+
table(false)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
with_model :SymbolChild, superclass: :Parent do
|
|
316
|
+
table(false)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
with_model :CallableChild, superclass: -> { Parent } do
|
|
320
|
+
table(false)
|
|
321
|
+
end
|
|
322
|
+
|
|
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
|
|
327
|
+
|
|
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
|
|
333
|
+
|
|
334
|
+
it "resolves a callable superclass" do
|
|
335
|
+
expect(CallableChild.superclass).to eq Parent
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "round-trips through the parent" do
|
|
339
|
+
StringChild.create!(name: "x")
|
|
340
|
+
expect(Parent.first).to be_a StringChild
|
|
341
|
+
end
|
|
342
|
+
|
|
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|
|
|
354
|
+
t.string :type
|
|
355
|
+
t.string :name
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
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) }
|
|
362
|
+
|
|
363
|
+
with_model "TableFalseSpec::Step", superclass: :HasTypeColumn do
|
|
364
|
+
table(false)
|
|
365
|
+
end
|
|
366
|
+
|
|
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
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
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
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
before do
|
|
389
|
+
stub_active_record_class(:HasMissingTable, superclass: ApplicationRecord)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
with_model :Orphan, superclass: -> { HasMissingTable } do
|
|
393
|
+
table(false)
|
|
394
|
+
end
|
|
395
|
+
|
|
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
|
|
401
|
+
end
|
|
402
|
+
|
|
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
|
|
407
|
+
|
|
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
|
|
414
|
+
|
|
415
|
+
describe "when the superclass's table is created during the example" do
|
|
416
|
+
before { stub_active_record_class(:HasLateTable) }
|
|
417
|
+
|
|
418
|
+
with_model :LateChild, superclass: -> { HasLateTable } do
|
|
419
|
+
table(false)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
after do
|
|
423
|
+
ActiveRecord::Base.connection.drop_table HasLateTable.table_name, if_exists: true
|
|
424
|
+
end
|
|
425
|
+
|
|
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
|
|
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
|
|
467
|
+
|
|
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
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
it "raises WithModel::InvalidSuperclass" do
|
|
478
|
+
expect { create_tableless_subclass(superclass: TablelessSuperclass) }
|
|
479
|
+
.to raise_error(WithModel::InvalidSuperclass, /TablelessSuperclass has none to inherit/)
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
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
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
it "raises WithModel::InvalidSuperclass" do
|
|
493
|
+
expect { create_tableless_subclass(superclass: ApplicationRecord) }
|
|
494
|
+
.to raise_error(WithModel::InvalidSuperclass, /ApplicationRecord has none to inherit/)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
context "when the superclass table lacks the inheritance column" do
|
|
499
|
+
with_model :HasNoTypeColumn do
|
|
500
|
+
table do |t|
|
|
501
|
+
t.string :name
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
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/)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
context "when the superclass disables inheritance" do
|
|
512
|
+
with_model :HasNilInheritanceColumn do
|
|
513
|
+
table do |t|
|
|
514
|
+
t.string :name
|
|
515
|
+
end
|
|
516
|
+
model do
|
|
517
|
+
self.inheritance_column = nil
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
it "raises WithModel::InvalidSuperclass" do
|
|
522
|
+
expect { create_tableless_subclass(superclass: HasNilInheritanceColumn) }
|
|
523
|
+
.to raise_error(WithModel::InvalidSuperclass)
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
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
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
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/)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
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/)
|
|
547
|
+
end
|
|
548
|
+
|
|
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\./)
|
|
552
|
+
end
|
|
553
|
+
|
|
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/)
|
|
558
|
+
end
|
|
559
|
+
|
|
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
|
+
}
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
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
|
|
573
|
+
end
|
|
574
|
+
|
|
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/)
|
|
579
|
+
end
|
|
580
|
+
|
|
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/)
|
|
585
|
+
end
|
|
586
|
+
|
|
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/)
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
RSpec.describe "omitting table" do
|
|
595
|
+
before do
|
|
596
|
+
allow(WithModel.deprecator).to receive(:warn)
|
|
597
|
+
end
|
|
598
|
+
|
|
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/)
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
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")
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
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
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
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
|
|
661
|
+
end
|
|
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)
|
|
671
|
+
end
|
|
672
|
+
end
|