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.
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_model'
4
- require 'spec_helper'
3
+ require "active_model"
4
+ require "spec_helper"
5
5
 
6
- shared_examples_for 'ActiveModel' do
7
- require 'active_model/lint'
6
+ shared_examples_for "ActiveModel" do
7
+ require "active_model/lint"
8
8
  include SpecHelper::RailsTestCompatibility
9
9
  include ActiveModel::Lint::Tests
10
10
 
@@ -12,26 +12,24 @@ shared_examples_for 'ActiveModel' do
12
12
  active_model_lint_tests = active_model_methods.map(&:to_s).grep(/^test/)
13
13
 
14
14
  active_model_lint_tests.each do |method_name|
15
- friendly_name = method_name.tr('_', ' ')
15
+ friendly_name = method_name.tr("_", " ")
16
16
 
17
- # rubocop:disable RSpec/NoExpectationExample
18
17
  example friendly_name do
19
18
  public_send method_name.to_sym
20
19
  end
21
- # rubocop:enable RSpec/NoExpectationExample
22
20
  end
23
21
 
24
22
  before { @model = subject }
25
23
  end
26
24
 
27
- describe 'a temporary ActiveRecord model created with with_model' do
25
+ describe "a temporary ActiveRecord model created with with_model" do
28
26
  non_shadowing_example_ran = false
29
27
 
30
28
  describe "which doesn't shadow an existing class" do
31
29
  with_model :BlogPost do
32
30
  table do |t|
33
- t.string 'title'
34
- t.text 'content'
31
+ t.string "title"
32
+ t.text "content"
35
33
  t.timestamps null: false
36
34
  end
37
35
 
@@ -46,13 +44,13 @@ describe 'a temporary ActiveRecord model created with with_model' do
46
44
  non_shadowing_example_ran = true
47
45
  end
48
46
 
49
- it 'acts like a normal ActiveRecord model' do
50
- record = BlogPost.create!(title: 'New blog post', content: 'Hello, world!')
47
+ it "acts like a normal ActiveRecord model" do
48
+ record = BlogPost.create!(title: "New blog post", content: "Hello, world!")
51
49
 
52
50
  record.reload
53
51
 
54
- expect(record.title).to eq 'New blog post'
55
- expect(record.content).to eq 'Hello, world!'
52
+ expect(record.title).to eq "New blog post"
53
+ expect(record.content).to eq "Hello, world!"
56
54
  expect(record.updated_at).to be_present
57
55
 
58
56
  record.destroy
@@ -60,232 +58,257 @@ describe 'a temporary ActiveRecord model created with with_model' do
60
58
  expect { record.reload }.to raise_error(ActiveRecord::RecordNotFound)
61
59
  end
62
60
 
63
- describe 'the class' do
61
+ describe "the class" do
64
62
  subject { BlogPost.new }
65
63
 
66
- it_behaves_like 'ActiveModel'
64
+ it_behaves_like "ActiveModel"
67
65
  end
68
66
 
69
- it 'has the methods defined in its model block' do
70
- expect(BlogPost.new(title: 'New blog post').fancy_title).to eq 'Title: New blog post'
67
+ it "has the methods defined in its model block" do
68
+ expect(BlogPost.new(title: "New blog post").fancy_title).to eq "Title: New blog post"
71
69
  end
72
70
 
73
- it 'defines a constant' do
71
+ it "defines a constant" do
74
72
  expect(BlogPost).to be_a(Class)
75
73
  end
76
74
 
77
- describe '.with_model?' do
78
- it 'returns true' do
75
+ describe ".with_model?" do
76
+ it "returns true" do
79
77
  expect(BlogPost.with_model?).to be true
80
78
  end
81
79
  end
82
80
 
83
- it 'is its own base_class' do
81
+ it "is its own base_class" do
84
82
  expect(BlogPost.base_class).to eq BlogPost
85
83
  end
86
84
  end
87
85
 
88
- context 'after an example which uses with_model without shadowing an existing constant' do
89
- it 'returns the constant to its undefined state' do
86
+ context "after an example which uses with_model without shadowing an existing constant" do
87
+ it "returns the constant to its undefined state" do
90
88
  expect(non_shadowing_example_ran).to be true
91
89
  expect(defined?(BlogPost)).to be_falsy
92
90
  end
93
91
  end
94
92
 
95
- describe 'constant restoration' do
96
- before { stub_const('MyConst', 1) }
93
+ describe "constant restoration" do
94
+ before { stub_const("MyConst", 1) }
97
95
 
98
96
  shadowing_example_ran = false
99
97
 
100
- context 'with the with_model block' do
101
- with_model :MyConst
98
+ context "with the with_model block" do
99
+ with_model :MyConst do
100
+ table
101
+ end
102
102
 
103
103
  after do
104
104
  shadowing_example_ran = true
105
105
  end
106
106
 
107
- it 'shadows that constant' do
107
+ it "shadows that constant" do
108
108
  expect(MyConst).to be_a(Class)
109
109
  end
110
110
  end
111
111
 
112
- context 'without the with_model block' do
113
- it 'returns the constant to its original value' do
112
+ context "without the with_model block" do
113
+ it "returns the constant to its original value" do
114
114
  expect(shadowing_example_ran).to be true
115
115
  expect(MyConst).to eq 1
116
116
  end
117
117
  end
118
118
  end
119
119
 
120
- describe 'with a plural name' do
121
- with_model :BlogPosts
120
+ describe "with a plural name" do
121
+ with_model :BlogPosts do
122
+ table
123
+ end
122
124
 
123
- it 'does not singularize the constant name' do
125
+ it "does not singularize the constant name" do
124
126
  expect(BlogPosts).to be
125
127
  expect(-> { BlogPost }).to raise_error(NameError)
126
128
  end
127
129
  end
128
130
 
129
- describe 'with a name containing capital letters' do
130
- with_model :BlogPost
131
+ describe "with a name containing capital letters" do
132
+ with_model :BlogPost do
133
+ table
134
+ end
131
135
 
132
- it 'tableizes the table name' do
136
+ it "tableizes the table name" do
133
137
  expect(BlogPost.table_name).to match(/_blog_posts_/)
134
138
  expect(BlogPost.table_name).to eq BlogPost.table_name.downcase
135
139
  end
136
140
  end
137
141
 
138
- describe 'with a name with underscores' do
139
- with_model :blog_post
142
+ describe "with a name with underscores" do
143
+ with_model :blog_post do
144
+ table
145
+ end
140
146
 
141
- it 'constantizes the name' do
147
+ it "constantizes the name" do
142
148
  expect(BlogPost).to be
143
149
  end
144
150
 
145
- it 'tableizes the table name' do
151
+ it "tableizes the table name" do
146
152
  expect(BlogPost.table_name).to match(/_blog_posts_/)
147
153
  expect(BlogPost.table_name).to eq BlogPost.table_name.downcase
148
154
  end
149
155
  end
150
156
 
151
- describe 'with a name which is namespaced' do
152
- before { stub_const('Stuff', Module.new) }
157
+ describe "with a name which is namespaced" do
158
+ before { stub_const("Stuff", Module.new) }
153
159
 
154
- with_model :'Stuff::BlogPost'
160
+ with_model :"Stuff::BlogPost" do
161
+ table
162
+ end
155
163
 
156
- it 'creates the model in the namespace' do
164
+ it "creates the model in the namespace" do
157
165
  expect(defined?(BlogPost)).to be_falsey
158
166
  expect(defined?(Stuff::BlogPost)).to be_truthy
159
167
  end
160
168
  end
161
169
 
162
- describe 'using the constant in the model block' do
170
+ describe "using the constant in the model block" do
163
171
  with_model :BlogPost do
172
+ table
164
173
  model do
165
- raise 'I am not myself!' unless self == BlogPost
174
+ raise "I am not myself!" unless self == BlogPost
166
175
  end
167
176
  end
168
177
 
169
- it 'is available' do
178
+ it "is available" do
170
179
  expect(BlogPost).to be
171
180
  end
172
181
  end
173
182
 
174
- context 'with a mixin' do
183
+ context "with a mixin" do
175
184
  let(:mixin) do
176
- Module.new { def foo; end }
185
+ Module.new {
186
+ def foo
187
+ end
188
+ }
177
189
  end
178
190
 
179
- before { stub_const('AMixin', mixin) }
191
+ before { stub_const("AMixin", mixin) }
180
192
 
181
193
  with_model :WithAMixin do
194
+ table
182
195
  model do
183
196
  include AMixin
184
197
  end
185
198
  end
186
199
 
187
- it 'has the mixin' do
200
+ it "has the mixin" do
188
201
  expect(-> { WithAMixin.new.foo }).not_to raise_error
189
202
  expect(WithAMixin.include?(AMixin)).to be true
190
203
  end
191
204
  end
192
205
 
193
- context 'with a mixin that has a class_eval' do
206
+ context "with a mixin that has a class_eval" do
194
207
  subject { WithAClassEval.new }
195
208
 
196
209
  let(:mixin) do
197
210
  Module.new do
198
211
  def self.included(klass)
199
212
  klass.class_eval do
200
- after_save { |object| object.my_method } # rubocop:disable Style/SymbolProc
213
+ after_save { |object| object.my_method }
201
214
  end
202
215
  end
203
216
  end
204
217
  end
205
218
 
206
- before { stub_const('AMixin', mixin) }
219
+ before { stub_const("AMixin", mixin) }
207
220
 
208
221
  with_model :WithAClassEval do
222
+ table
209
223
  model do
210
224
  include AMixin
211
- def my_method; end
225
+
226
+ def my_method
227
+ end
212
228
  end
213
229
  end
214
230
 
215
- it 'only has one after_save callback' do
231
+ it "only has one after_save callback" do
216
232
  expect(subject).to receive(:my_method).once
217
233
  subject.save
218
234
  end
219
235
 
220
- it 'still only has one after_save callback in future tests' do
236
+ it "still only has one after_save callback in future tests" do
221
237
  expect(subject).to receive(:my_method).once
222
238
  subject.save
223
239
  end
224
240
  end
225
241
 
226
- context 'with table options' do
242
+ context "with table options" do
227
243
  with_model :WithOptions do
228
244
  table id: false do |t|
229
- t.string 'foo'
245
+ t.string "foo"
230
246
  t.timestamps null: false
231
247
  end
232
248
  end
233
249
 
234
- it 'respects the additional options' do
235
- expect(WithOptions.columns.map(&:name)).not_to include('id')
250
+ it "respects the additional options" do
251
+ expect(WithOptions.columns.map(&:name)).not_to include("id")
236
252
  end
237
253
  end
238
254
 
239
- context 'without a block' do
240
- with_model :BlogPost
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.
258
+ context "without a block" do
259
+ WithModel.deprecator.silence do
260
+ with_model :BlogPost
261
+ end
241
262
 
242
- it 'acts like a normal ActiveRecord model' do
263
+ it "acts like a normal ActiveRecord model" do
243
264
  record = BlogPost.create!
244
265
  record.reload
245
266
  record.destroy
246
267
  expect { record.reload }.to raise_error(ActiveRecord::RecordNotFound)
247
268
  end
248
269
 
249
- describe 'the class' do
270
+ describe "the class" do
250
271
  subject { BlogPost.new }
251
272
 
252
- it_behaves_like 'ActiveModel'
273
+ it_behaves_like "ActiveModel"
253
274
  end
254
275
  end
255
276
 
256
- context 'with an empty block' do
257
- with_model(:BlogPost) {} # rubocop:disable Lint/EmptyBlock
277
+ context "with an empty block" do
278
+ WithModel.deprecator.silence do
279
+ with_model(:BlogPost) {}
280
+ end
258
281
 
259
- it 'acts like a normal ActiveRecord model' do
282
+ it "acts like a normal ActiveRecord model" do
260
283
  record = BlogPost.create!
261
284
  record.reload
262
285
  record.destroy
263
286
  expect { record.reload }.to raise_error(ActiveRecord::RecordNotFound)
264
287
  end
265
288
 
266
- describe 'the class' do
289
+ describe "the class" do
267
290
  subject { BlogPost.new }
268
291
 
269
- it_behaves_like 'ActiveModel'
292
+ it_behaves_like "ActiveModel"
270
293
  end
271
294
  end
272
295
 
273
- context 'without a model block' do
296
+ context "without a model block" do
274
297
  with_model :BlogPost do
275
298
  table do |t|
276
- t.string 'title'
277
- t.text 'content'
299
+ t.string "title"
300
+ t.text "content"
278
301
  t.timestamps null: false
279
302
  end
280
303
  end
281
304
 
282
- it 'acts like a normal ActiveRecord model' do
283
- record = BlogPost.create!(title: 'New blog post', content: 'Hello, world!')
305
+ it "acts like a normal ActiveRecord model" do
306
+ record = BlogPost.create!(title: "New blog post", content: "Hello, world!")
284
307
 
285
308
  record.reload
286
309
 
287
- expect(record.title).to eq 'New blog post'
288
- expect(record.content).to eq 'Hello, world!'
310
+ expect(record.title).to eq "New blog post"
311
+ expect(record.content).to eq "Hello, world!"
289
312
  expect(record.updated_at).to be_present
290
313
 
291
314
  record.destroy
@@ -293,56 +316,34 @@ describe 'a temporary ActiveRecord model created with with_model' do
293
316
  expect { record.reload }.to raise_error(ActiveRecord::RecordNotFound)
294
317
  end
295
318
 
296
- describe 'the class' do
319
+ describe "the class" do
297
320
  subject { BlogPost.new }
298
321
 
299
- it_behaves_like 'ActiveModel'
322
+ it_behaves_like "ActiveModel"
300
323
  end
301
324
  end
302
325
 
303
- context 'without a table or model block' do
304
- with_model :BlogPost
326
+ context "without a table or model block" do
327
+ WithModel.deprecator.silence do
328
+ with_model :BlogPost
329
+ end
305
330
 
306
- it 'acts like a normal ActiveRecord model' do
307
- expect(BlogPost.columns.map(&:name)).to eq ['id']
331
+ it "acts like a normal ActiveRecord model" do
332
+ expect(BlogPost.columns.map(&:name)).to eq ["id"]
308
333
  record = BlogPost.create!
309
334
  record.reload
310
335
  record.destroy
311
336
  expect { record.reload }.to raise_error(ActiveRecord::RecordNotFound)
312
337
  end
313
338
 
314
- describe 'the class' do
339
+ describe "the class" do
315
340
  subject { BlogPost.new }
316
341
 
317
- it_behaves_like 'ActiveModel'
318
- end
319
- end
320
-
321
- context 'with ActiveSupport::DescendantsTracker' do
322
- with_model :BlogPost do
323
- model do
324
- def self.inspect
325
- "BlogPost class #{object_id}"
326
- end
327
- end
328
- end
329
-
330
- def blog_post_classes
331
- ActiveRecord::Base.descendants.select do |c|
332
- c.table_name == BlogPost.table_name
333
- end
334
- end
335
-
336
- it 'includes the correct model class in descendants on the first test run' do
337
- expect(blog_post_classes).to eq [BlogPost]
338
- end
339
-
340
- it 'includes the correct model class in descendants on the second test run' do
341
- expect(blog_post_classes).to eq [BlogPost]
342
+ it_behaves_like "ActiveModel"
342
343
  end
343
344
  end
344
345
 
345
- context 'with_model can be run within RSpec :all hook' do
346
+ context "with_model can be run within RSpec :all hook" do
346
347
  with_model :BlogPost, scope: :all do
347
348
  table do |t|
348
349
  t.string :title
@@ -353,42 +354,55 @@ describe 'a temporary ActiveRecord model created with with_model' do
353
354
  BlogPost.create # without scope: :all these will fail
354
355
  end
355
356
 
356
- it 'has been initialized within before(:all)' do
357
+ it "has been initialized within before(:all)" do
357
358
  expect(BlogPost.count).to eq 1
358
359
  end
359
360
  end
360
361
 
361
362
  context "with 'superclass' option" do
362
- class BlogPostParent < ActiveRecord::Base
363
+ class BlogPostParent < ActiveRecord::Base # standard:disable Lint/ConstantDefinitionInBlock
363
364
  self.abstract_class = true
364
365
  end
365
366
 
366
- after(:all) do
367
- Object.__send__(:remove_const, 'BlogPostParent')
368
- end
369
-
370
367
  with_model :BlogPost, superclass: BlogPostParent do
371
368
  table do |t|
372
- t.string 'title'
369
+ t.string "title"
373
370
  end
374
371
  end
375
372
 
376
- describe 'the class' do
373
+ describe "the class" do
377
374
  subject { BlogPost.new }
378
375
 
379
- it_behaves_like 'ActiveModel'
376
+ it_behaves_like "ActiveModel"
380
377
  end
381
378
 
382
- it 'is a subclass of the supplied superclass' do
379
+ it "is a subclass of the supplied superclass" do
383
380
  expect(BlogPost < BlogPostParent).to be true
384
381
  end
385
382
 
386
- it 'is its own base_class' do
383
+ it "is its own base_class" do
387
384
  expect(BlogPost.base_class).to eq BlogPost
388
385
  end
389
386
 
390
- it 'responds to .with_model? with true' do
387
+ it "responds to .with_model? with true" do
391
388
  expect(BlogPost.with_model?).to be true
392
389
  end
393
390
  end
391
+
392
+ context "with 'superclass' that connects to a different database" do
393
+ class ApplicationRecordInDifferentDatabase < ActiveRecord::Base # standard:disable Lint/ConstantDefinitionInBlock
394
+ self.abstract_class = true
395
+ establish_connection(ActiveRecord::Base.connection_pool.db_config.configuration_hash)
396
+ end
397
+
398
+ with_model :BlogPost, superclass: ApplicationRecordInDifferentDatabase do
399
+ table do |t|
400
+ t.string "title"
401
+ end
402
+ end
403
+
404
+ it "uses the superclass connection" do
405
+ expect(BlogPost.connection.tables).to include(BlogPost.table_name)
406
+ end
407
+ end
394
408
  end
@@ -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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # The README offers Minitest::Spec as well as Minitest::Test. Minitest::Spec
6
+ # inherits from Minitest::Test, so extending Minitest::Test reaches describe
7
+ # blocks too, and with_model needs no separate wiring for them.
8
+ describe "with_model in a spec-style suite" do
9
+ with_model :Sofa do
10
+ table do |t|
11
+ t.string "type"
12
+ t.string "fabric"
13
+ end
14
+ end
15
+
16
+ with_model :Loveseat, superclass: :Sofa do
17
+ table(false)
18
+ end
19
+
20
+ it "creates the models a describe block declares" do
21
+ assert_empty Sofa.all, "another test's rows outlived it"
22
+
23
+ Loveseat.create!(fabric: "tweed")
24
+
25
+ assert_equal Sofa.table_name, Loveseat.table_name
26
+ assert_instance_of Loveseat, Sofa.first
27
+ end
28
+
29
+ # Every describe is its own subclass of the one around it, so a nested block
30
+ # inherits the models declared above it and can add more of its own.
31
+ describe "and a nested describe" do
32
+ with_model :Ottoman do
33
+ table do |t|
34
+ t.string "name"
35
+ end
36
+ end
37
+
38
+ it "sees the models from both levels" do
39
+ assert_empty Sofa.all, "another test's rows outlived it"
40
+
41
+ Ottoman.create!(name: "pouf")
42
+ Loveseat.create!(fabric: "velvet")
43
+
44
+ assert_equal 1, Ottoman.count
45
+ assert_equal 1, Sofa.count
46
+ end
47
+ end
48
+ end