with_model 2.2.0 → 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 +64 -29
- data/CHANGELOG.md +19 -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 +516 -0
- data/spec/with_model_spec.rb +31 -8
- 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,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
|
data/spec/with_model_spec.rb
CHANGED
|
@@ -96,7 +96,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
96
96
|
shadowing_example_ran = false
|
|
97
97
|
|
|
98
98
|
context "with the with_model block" do
|
|
99
|
-
with_model :MyConst
|
|
99
|
+
with_model :MyConst do
|
|
100
|
+
table
|
|
101
|
+
end
|
|
100
102
|
|
|
101
103
|
after do
|
|
102
104
|
shadowing_example_ran = true
|
|
@@ -116,7 +118,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
describe "with a plural name" do
|
|
119
|
-
with_model :BlogPosts
|
|
121
|
+
with_model :BlogPosts do
|
|
122
|
+
table
|
|
123
|
+
end
|
|
120
124
|
|
|
121
125
|
it "does not singularize the constant name" do
|
|
122
126
|
expect(BlogPosts).to be
|
|
@@ -125,7 +129,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
125
129
|
end
|
|
126
130
|
|
|
127
131
|
describe "with a name containing capital letters" do
|
|
128
|
-
with_model :BlogPost
|
|
132
|
+
with_model :BlogPost do
|
|
133
|
+
table
|
|
134
|
+
end
|
|
129
135
|
|
|
130
136
|
it "tableizes the table name" do
|
|
131
137
|
expect(BlogPost.table_name).to match(/_blog_posts_/)
|
|
@@ -134,7 +140,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
134
140
|
end
|
|
135
141
|
|
|
136
142
|
describe "with a name with underscores" do
|
|
137
|
-
with_model :blog_post
|
|
143
|
+
with_model :blog_post do
|
|
144
|
+
table
|
|
145
|
+
end
|
|
138
146
|
|
|
139
147
|
it "constantizes the name" do
|
|
140
148
|
expect(BlogPost).to be
|
|
@@ -149,7 +157,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
149
157
|
describe "with a name which is namespaced" do
|
|
150
158
|
before { stub_const("Stuff", Module.new) }
|
|
151
159
|
|
|
152
|
-
with_model :"Stuff::BlogPost"
|
|
160
|
+
with_model :"Stuff::BlogPost" do
|
|
161
|
+
table
|
|
162
|
+
end
|
|
153
163
|
|
|
154
164
|
it "creates the model in the namespace" do
|
|
155
165
|
expect(defined?(BlogPost)).to be_falsey
|
|
@@ -159,6 +169,7 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
159
169
|
|
|
160
170
|
describe "using the constant in the model block" do
|
|
161
171
|
with_model :BlogPost do
|
|
172
|
+
table
|
|
162
173
|
model do
|
|
163
174
|
raise "I am not myself!" unless self == BlogPost
|
|
164
175
|
end
|
|
@@ -180,6 +191,7 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
180
191
|
before { stub_const("AMixin", mixin) }
|
|
181
192
|
|
|
182
193
|
with_model :WithAMixin do
|
|
194
|
+
table
|
|
183
195
|
model do
|
|
184
196
|
include AMixin
|
|
185
197
|
end
|
|
@@ -207,8 +219,10 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
207
219
|
before { stub_const("AMixin", mixin) }
|
|
208
220
|
|
|
209
221
|
with_model :WithAClassEval do
|
|
222
|
+
table
|
|
210
223
|
model do
|
|
211
224
|
include AMixin
|
|
225
|
+
|
|
212
226
|
def my_method
|
|
213
227
|
end
|
|
214
228
|
end
|
|
@@ -238,8 +252,13 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
238
252
|
end
|
|
239
253
|
end
|
|
240
254
|
|
|
255
|
+
# Omitting `table` is deprecated, but still supported for the rest of 2.x.
|
|
256
|
+
# These three contexts are its only coverage, so they keep omitting it and
|
|
257
|
+
# silence the warning rather than adopting the replacement spelling.
|
|
241
258
|
context "without a block" do
|
|
242
|
-
|
|
259
|
+
WithModel.deprecator.silence do
|
|
260
|
+
with_model :BlogPost
|
|
261
|
+
end
|
|
243
262
|
|
|
244
263
|
it "acts like a normal ActiveRecord model" do
|
|
245
264
|
record = BlogPost.create!
|
|
@@ -256,7 +275,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
256
275
|
end
|
|
257
276
|
|
|
258
277
|
context "with an empty block" do
|
|
259
|
-
|
|
278
|
+
WithModel.deprecator.silence do
|
|
279
|
+
with_model(:BlogPost) {}
|
|
280
|
+
end
|
|
260
281
|
|
|
261
282
|
it "acts like a normal ActiveRecord model" do
|
|
262
283
|
record = BlogPost.create!
|
|
@@ -303,7 +324,9 @@ describe "a temporary ActiveRecord model created with with_model" do
|
|
|
303
324
|
end
|
|
304
325
|
|
|
305
326
|
context "without a table or model block" do
|
|
306
|
-
|
|
327
|
+
WithModel.deprecator.silence do
|
|
328
|
+
with_model :BlogPost
|
|
329
|
+
end
|
|
307
330
|
|
|
308
331
|
it "acts like a normal ActiveRecord model" do
|
|
309
332
|
expect(BlogPost.columns.map(&:name)).to eq ["id"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
# Models are created in declaration order and destroyed in reverse, so each one
|
|
6
|
+
# can depend on the ones above it for as long as it exists. Both halves of that
|
|
7
|
+
# are tested here.
|
|
8
|
+
#
|
|
9
|
+
# The model block is evaluated when the model is created, so a model that
|
|
10
|
+
# refers to another one by constant can only work if the model it refers to
|
|
11
|
+
# was created first.
|
|
12
|
+
class DeclarationOrderTest < Minitest::Test
|
|
13
|
+
with_model :Author do
|
|
14
|
+
table do |t|
|
|
15
|
+
t.string "name"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
with_model :Book do
|
|
20
|
+
table do |t|
|
|
21
|
+
t.integer "author_id"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
model do
|
|
25
|
+
belongs_to :author, class_name: Author.name
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
with_model :Shelf do
|
|
30
|
+
table do |t|
|
|
31
|
+
t.string "name"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# `to_table` because `foreign_key: true` would infer a table name from the
|
|
36
|
+
# column rather than asking the model, and with_model's table names are
|
|
37
|
+
# generated.
|
|
38
|
+
with_model :Jar do
|
|
39
|
+
table do |t|
|
|
40
|
+
t.references "shelf", foreign_key: {to_table: Shelf.table_name}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_a_model_can_refer_to_one_declared_above_it
|
|
45
|
+
author = Author.create!(name: "Ursula K. Le Guin")
|
|
46
|
+
book = Book.create!(author: author)
|
|
47
|
+
|
|
48
|
+
assert_equal author, book.reload.author
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The assertion for teardown order is that this test finishes cleanly. A
|
|
52
|
+
# referenced table cannot be dropped while a row still points at it, so tearing
|
|
53
|
+
# these down in declaration order raises ActiveRecord::InvalidForeignKey after
|
|
54
|
+
# the body has already passed.
|
|
55
|
+
def test_a_table_can_point_at_one_declared_above_it
|
|
56
|
+
shelf = Shelf.create!(name: "pantry")
|
|
57
|
+
jar = Jar.create!(shelf_id: shelf.id)
|
|
58
|
+
|
|
59
|
+
assert_equal shelf.id, jar.reload.shelf_id
|
|
60
|
+
end
|
|
61
|
+
end
|