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.
@@ -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
@@ -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
- with_model :BlogPost
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
- with_model(:BlogPost) {}
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
- with_model :BlogPost
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