with_model 2.1.7 → 2.3.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +22 -6
- data/.github/workflows/ci.yml +67 -21
- data/.rspec +1 -1
- data/CHANGELOG.md +29 -0
- data/CONTRIBUTING.md +83 -0
- data/Gemfile +16 -23
- data/LICENSE +1 -1
- data/README.md +116 -22
- data/Rakefile +8 -17
- data/lib/with_model/constant_stubber.rb +1 -1
- data/lib/with_model/descendants_tracker.rb +87 -0
- 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 +97 -21
- data/lib/with_model/null_table.rb +98 -0
- data/lib/with_model/table.rb +18 -6
- data/lib/with_model/version.rb +1 -1
- data/lib/with_model.rb +45 -7
- data/spec/active_record_behaviors_spec.rb +31 -28
- data/spec/constant_stubber_spec.rb +5 -5
- data/spec/descendants_tracking_spec.rb +49 -0
- data/spec/readme_spec.rb +82 -33
- data/spec/spec_helper.rb +8 -18
- data/spec/table_false_spec.rb +516 -0
- data/spec/with_model_spec.rb +138 -124
- 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 +9 -18
- data/test/with_model_test.rb +9 -9
- data/with_model.gemspec +15 -15
- metadata +16 -12
- data/.rubocop.yml +0 -33
- data/.rubocop_todo.yml +0 -78
- data/spec/.rubocop.yml +0 -5
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
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
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class HasCustomInheritanceColumn < ActiveRecord::Base
|
|
17
|
+
self.inheritance_column = "kind"
|
|
18
|
+
end
|
|
19
|
+
|
|
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
|
+
end
|
|
25
|
+
|
|
26
|
+
class HasDefaultScope < ActiveRecord::Base
|
|
27
|
+
default_scope { where(archived: false) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class HasNoTypeColumn < ActiveRecord::Base
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class HasNilInheritanceColumn < ActiveRecord::Base
|
|
34
|
+
self.inheritance_column = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class IsAbstract < ActiveRecord::Base
|
|
38
|
+
self.abstract_class = true
|
|
39
|
+
end
|
|
40
|
+
|
|
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
|
|
46
|
+
|
|
47
|
+
# Its table is never created.
|
|
48
|
+
class HasMissingTable < ApplicationRecord
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Its table is created partway through an example.
|
|
52
|
+
class HasLateTable < ActiveRecord::Base
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
RSpec.describe "table(false)" do
|
|
56
|
+
describe "with a concrete superclass" do
|
|
57
|
+
with_table(HasTypeColumn.table_name) do |t|
|
|
58
|
+
t.string :type
|
|
59
|
+
t.string :name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
before { HasTypeColumn.reset_column_information }
|
|
63
|
+
|
|
64
|
+
with_model :Truck, superclass: HasTypeColumn do
|
|
65
|
+
table(false)
|
|
66
|
+
model do
|
|
67
|
+
def honk = "beep"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "inherits the superclass table rather than creating its own" do
|
|
72
|
+
expect(Truck.table_name).to eq HasTypeColumn.table_name
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "creates no table of its own" do
|
|
76
|
+
expect(ActiveRecord::Base.connection.tables.grep(/with_model_truck/)).to be_empty
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "reports the superclass as the STI base class" do
|
|
80
|
+
expect(Truck.base_class).to eq HasTypeColumn
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "still evaluates the model block" do
|
|
84
|
+
expect(Truck.new.honk).to eq "beep"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "writes its own name to the inheritance column" do
|
|
88
|
+
Truck.create!(name: "big")
|
|
89
|
+
expect(HasTypeColumn.first.type).to eq "Truck"
|
|
90
|
+
end
|
|
91
|
+
|
|
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
|
|
96
|
+
|
|
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
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe "teardown" do
|
|
107
|
+
with_table(HasTypeColumn.table_name) do |t|
|
|
108
|
+
t.string :type
|
|
109
|
+
t.string :name
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
before { HasTypeColumn.reset_column_information }
|
|
113
|
+
|
|
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
|
|
118
|
+
end
|
|
119
|
+
|
|
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")
|
|
125
|
+
|
|
126
|
+
model.destroy
|
|
127
|
+
|
|
128
|
+
expect(HasTypeColumn.where(type: "Truck").count).to eq 0
|
|
129
|
+
expect(HasTypeColumn.where(type: nil).count).to eq 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "leaves the superclass queryable" do
|
|
133
|
+
model = build(:Truck, superclass: HasTypeColumn) { table(false) }
|
|
134
|
+
model.create
|
|
135
|
+
Truck.create!(name: "child row")
|
|
136
|
+
|
|
137
|
+
model.destroy
|
|
138
|
+
|
|
139
|
+
expect { HasTypeColumn.first }.not_to raise_error
|
|
140
|
+
end
|
|
141
|
+
|
|
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) }
|
|
145
|
+
|
|
146
|
+
expect { model.create }.to raise_error(WithModel::InvalidSuperclass)
|
|
147
|
+
expect { model.destroy }.not_to raise_error
|
|
148
|
+
|
|
149
|
+
expect(HasTypeColumn.count).to eq 1
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe "with a superclass that has a default_scope" do
|
|
154
|
+
with_table(HasDefaultScope.table_name) do |t|
|
|
155
|
+
t.string :type
|
|
156
|
+
t.boolean :archived, default: false
|
|
157
|
+
end
|
|
158
|
+
|
|
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)
|
|
167
|
+
|
|
168
|
+
model.destroy
|
|
169
|
+
|
|
170
|
+
expect(HasDefaultScope.unscoped.where(type: "ScopedChild").count).to eq 0
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe "with a non-standard inheritance_column" do
|
|
175
|
+
with_table(HasCustomInheritanceColumn.table_name) do |t|
|
|
176
|
+
t.string :kind
|
|
177
|
+
t.string :name
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
before { HasCustomInheritanceColumn.reset_column_information }
|
|
181
|
+
|
|
182
|
+
with_model :KindedChild, superclass: HasCustomInheritanceColumn do
|
|
183
|
+
table(false)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "writes to the custom column" do
|
|
187
|
+
KindedChild.create!(name: "x")
|
|
188
|
+
expect(HasCustomInheritanceColumn.first.kind).to eq "KindedChild"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "accepts a Symbol inheritance_column" do
|
|
192
|
+
expect(HasCustomInheritanceColumn.inheritance_column).to eq "kind"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
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|
|
|
200
|
+
t.string :kind
|
|
201
|
+
t.string :type
|
|
202
|
+
t.string :name
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
before { HasCustomInheritanceColumnAndTypeColumn.reset_column_information }
|
|
206
|
+
|
|
207
|
+
with_model :KindedTypeChild, superclass: HasCustomInheritanceColumnAndTypeColumn do
|
|
208
|
+
table(false)
|
|
209
|
+
end
|
|
210
|
+
|
|
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
|
|
217
|
+
end
|
|
218
|
+
|
|
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
|
|
226
|
+
|
|
227
|
+
with_model :StringChild, superclass: "Parent" do
|
|
228
|
+
table(false)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
with_model :SymbolChild, superclass: :Parent do
|
|
232
|
+
table(false)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
with_model :CallableChild, superclass: -> { Parent } do
|
|
236
|
+
table(false)
|
|
237
|
+
end
|
|
238
|
+
|
|
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
|
|
243
|
+
|
|
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
|
|
249
|
+
|
|
250
|
+
it "resolves a callable superclass" do
|
|
251
|
+
expect(CallableChild.superclass).to eq Parent
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "round-trips through the parent" do
|
|
255
|
+
StringChild.create!(name: "x")
|
|
256
|
+
expect(Parent.first).to be_a StringChild
|
|
257
|
+
end
|
|
258
|
+
|
|
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
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
describe "namespaced constants" do
|
|
268
|
+
with_table(HasTypeColumn.table_name) do |t|
|
|
269
|
+
t.string :type
|
|
270
|
+
t.string :name
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
before { HasTypeColumn.reset_column_information }
|
|
274
|
+
|
|
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
|
|
281
|
+
|
|
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
|
|
287
|
+
end
|
|
288
|
+
|
|
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)
|
|
295
|
+
end
|
|
296
|
+
|
|
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
|
|
303
|
+
|
|
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
|
|
308
|
+
|
|
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
|
|
314
|
+
end
|
|
315
|
+
|
|
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
|
|
320
|
+
|
|
321
|
+
after do
|
|
322
|
+
ActiveRecord::Base.connection.drop_table HasLateTable.table_name, if_exists: true
|
|
323
|
+
end
|
|
324
|
+
|
|
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
|
|
331
|
+
|
|
332
|
+
LateChild.create!(name: "late")
|
|
333
|
+
|
|
334
|
+
expect(HasLateTable.count).to eq 1
|
|
335
|
+
expect(HasLateTable.first).to be_a LateChild
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
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
|
|
344
|
+
end
|
|
345
|
+
|
|
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/)
|
|
349
|
+
end
|
|
350
|
+
|
|
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/)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
it "refuses an abstract superclass" do
|
|
357
|
+
expect { create_model(superclass: IsAbstract) }
|
|
358
|
+
.to raise_error(WithModel::InvalidSuperclass, /IsAbstract has none to inherit/)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it "refuses an abstract superclass that a Rails app would call ApplicationRecord" do
|
|
362
|
+
expect { create_model(superclass: ApplicationRecord) }
|
|
363
|
+
.to raise_error(WithModel::InvalidSuperclass, /ApplicationRecord has none to inherit/)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
context "when the superclass table lacks the inheritance column" do
|
|
367
|
+
with_table(HasNoTypeColumn.table_name) do |t|
|
|
368
|
+
t.string :name
|
|
369
|
+
end
|
|
370
|
+
|
|
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
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
context "when the superclass disables inheritance" do
|
|
380
|
+
with_table(HasNilInheritanceColumn.table_name) do |t|
|
|
381
|
+
t.string :name
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "refuses" do
|
|
385
|
+
expect { create_model(superclass: HasNilInheritanceColumn) }
|
|
386
|
+
.to raise_error(WithModel::InvalidSuperclass)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
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/)
|
|
394
|
+
end
|
|
395
|
+
|
|
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/)
|
|
412
|
+
end
|
|
413
|
+
|
|
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
|
|
422
|
+
end
|
|
423
|
+
|
|
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
|
|
427
|
+
end
|
|
428
|
+
|
|
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/)
|
|
433
|
+
end
|
|
434
|
+
|
|
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/)
|
|
439
|
+
end
|
|
440
|
+
|
|
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/)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
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
|
|
456
|
+
end
|
|
457
|
+
|
|
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)
|
|
462
|
+
end
|
|
463
|
+
|
|
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/)
|
|
469
|
+
end
|
|
470
|
+
|
|
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")
|
|
476
|
+
end
|
|
477
|
+
|
|
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
|
|
485
|
+
|
|
486
|
+
expect(messages.join).to include("#{__FILE__}:#{__LINE__ - 3}")
|
|
487
|
+
expect(messages.join).not_to include("lib/with_model.rb")
|
|
488
|
+
end
|
|
489
|
+
|
|
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
|
|
495
|
+
end
|
|
496
|
+
|
|
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
|
|
505
|
+
end
|
|
506
|
+
expect(messages).to be_empty
|
|
507
|
+
end
|
|
508
|
+
|
|
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 } }
|
|
512
|
+
end
|
|
513
|
+
expect(messages).to be_empty
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|