couch_potato 1.13.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c36fc0534a7564b1fcf3b2a59ca91e3e4e97452c2a74d77e30540eb4b7617211
4
- data.tar.gz: b1583818a743459e8ab86d88350f7ad5c6953c058c3b49c8b90da50610bdd197
3
+ metadata.gz: ffcaae6b2815d237b48cc3e4ad553f66920fe4df2c84c59ae38177260949ba6b
4
+ data.tar.gz: bd266a1033fa83fcba822a19e3de86b2c1aac2d80ba2dd1d3aef0920d4819d8f
5
5
  SHA512:
6
- metadata.gz: ceffb27b13b2cf9825cd5fcf507bb0e13a6ed9b5caa612fd1f5f27ba98934eb9dae34c4f4dc70835352a9176bc4562e49da12d489b925ebbeb37a3c9eaa964f4
7
- data.tar.gz: c5e01a2d0d83ea11b14a618f218fe3ff66af35be893a4d74644089938b852045c52a8e1d8ecf31b53e70396737f9db0a8849ef0fbacd843c89137ec4d8bfc0e2
6
+ metadata.gz: 16f3e1748e3b3314f1cf3d658fb175dd90c5e7c7cd717ab42477961c364bac527b262642a29f208c4aad200e9a31ce7470c0a538db8bf8d641e13da92f85debb
7
+ data.tar.gz: '089f1c9e5dc7238b659a4d1b2d0f3d341a084fe52f6d9c2d058ea8440f197f7d64b6f5a9d78c8cebbd19b131aede038329ab6c0f79b503d220f1f20d2e7d8a20'
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ pkg
7
7
 
8
8
  .ruby-version
9
9
  .tool-versions
10
+ .vscode/settings.json
data/CHANGES.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## Changes
2
2
 
3
+ # 1.15.0
4
+
5
+ - cache loading multiple documents
6
+ - keep the database cache when switching to another database and back to the original one
7
+
8
+ # 1.14.0
9
+
10
+ - add database_collection to models to help avoid n+1 requests
11
+
3
12
  # 1.13.0
4
13
 
5
14
  - add Ruby 3.2 support
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/enumerable'
4
+
3
5
  module CouchPotato
4
6
  class Database
5
7
  class ValidationsFailedError < ::StandardError; end
@@ -143,7 +145,9 @@ module CouchPotato
143
145
  # returns nil if the single document could not be found. when passing an array and some documents
144
146
  # could not be found these are omitted from the returned array
145
147
  def load_document(id)
146
- cached = cache && id.is_a?(String) && cache[id]
148
+ return load_documents(id) if id.is_a?(Array)
149
+
150
+ cached = cache && cache[id]
147
151
  if cache
148
152
  if cached
149
153
  ActiveSupport::Notifications.instrument('couch_potato.load.cached') do
@@ -159,6 +163,23 @@ module CouchPotato
159
163
  end
160
164
  alias load load_document
161
165
 
166
+ def load_documents(ids)
167
+ return [] if ids.empty?
168
+
169
+ uncached_ids = ids - (cache&.keys || [])
170
+ uncached_docs_by_id = bulk_load(uncached_ids).index_by {|doc| doc.id if doc.respond_to?(:id) }
171
+ if cache
172
+ uncached_ids.each do |id|
173
+ doc = uncached_docs_by_id[id]
174
+ cache[id] = doc if doc
175
+ end
176
+ end
177
+ cached_docs_by_id = ActiveSupport::Notifications.instrument('couch_potato.load.cached') do
178
+ cache&.slice(*ids) || {}
179
+ end
180
+ ids.filter_map { |id| (cached_docs_by_id[id]) || uncached_docs_by_id[id] }
181
+ end
182
+
162
183
  # loads one or more documents by its id(s)
163
184
  # behaves like #load except it raises a CouchPotato::NotFound if any of the documents could not be found
164
185
  def load!(id)
@@ -176,6 +197,8 @@ module CouchPotato
176
197
  # returns the underlying CouchRest::Database instance
177
198
  attr_reader :couchrest_database
178
199
 
200
+ attr_accessor :switched_databases
201
+
179
202
  # returns a new database instance connected to the CouchDB database
180
203
  # with the given name. the name is passed through the
181
204
  # additional_databases configuration to resolve it to a database
@@ -183,21 +206,29 @@ module CouchPotato
183
206
  # if the current database has a cache, the new database will receive
184
207
  # a cleared copy of it.
185
208
  def switch_to(database_name)
186
- resolved_database_name = CouchPotato.resolve_database_name(database_name)
187
- self
209
+ self.switched_databases ||= {}
210
+ if (db = switched_databases[database_name || :default])
211
+ return db
212
+ end
213
+ switched_databases[name || :default] ||= self
214
+
215
+
216
+ resolved_database_name = CouchPotato.resolve_database_name(database_name) unless database_name == :default
217
+ couchrest_database = resolved_database_name ? CouchPotato.couchrest_database_for_name(resolved_database_name) : CouchPotato.couchrest_database
218
+ new_db = self
188
219
  .class
189
- .new(CouchPotato.couchrest_database_for_name(resolved_database_name), name: database_name)
220
+ .new(couchrest_database, name: database_name == :default ? nil : database_name)
190
221
  .tap(&copy_clear_cache_proc)
222
+
223
+ new_db.switched_databases = switched_databases
224
+ new_db
191
225
  end
192
226
 
193
227
  # returns a new database instance connected to the default CouchDB database.
194
228
  # if the current database has a cache, the new database will receive
195
229
  # a cleared copy of it.
196
230
  def switch_to_default
197
- self
198
- .class
199
- .new(CouchPotato.couchrest_database)
200
- .tap(&copy_clear_cache_proc)
231
+ switch_to(:default)
201
232
  end
202
233
 
203
234
  private
@@ -224,6 +255,7 @@ module CouchPotato
224
255
  elsif processed_results.respond_to?(:each)
225
256
  processed_results.each do |document|
226
257
  document.database = self if document.respond_to?(:database=)
258
+ document.database_collection = processed_results if document.respond_to?(:database_collection=)
227
259
  end
228
260
  end
229
261
  processed_results
@@ -247,14 +279,10 @@ module CouchPotato
247
279
  raise "Can't load a document without an id (got nil)" if id.nil?
248
280
 
249
281
  ActiveSupport::Notifications.instrument('couch_potato.load') do
250
- if id.is_a?(Array)
251
- bulk_load id
252
- else
253
- instance = couchrest_database.get(id)
254
- instance.database = self if instance
255
- instance
256
- end
257
- end
282
+ instance = couchrest_database.get(id)
283
+ instance.database = self if instance
284
+ instance
285
+ end
258
286
  end
259
287
 
260
288
  def view_cache_id(spec)
@@ -293,10 +321,13 @@ module CouchPotato
293
321
  def bulk_load(ids)
294
322
  return [] if ids.empty?
295
323
 
296
- response = couchrest_database.bulk_load ids
297
- docs = response['rows'].map { |row| row['doc'] }.compact
298
- docs.each do |doc|
299
- doc.database = self if doc.respond_to?(:database=)
324
+ ActiveSupport::Notifications.instrument('couch_potato.load') do
325
+ response = couchrest_database.bulk_load ids
326
+ docs = response['rows'].map { |row| row['doc'] }.compact
327
+ docs.each do |doc|
328
+ doc.database = self if doc.respond_to?(:database=)
329
+ doc.database_collection = docs if doc.respond_to?(:database_collection=)
330
+ end
300
331
  end
301
332
  end
302
333
 
@@ -27,7 +27,7 @@ module CouchPotato
27
27
  ForbiddenAttributesProtection, Revisions
28
28
  base.send :include, Validation
29
29
  base.class_eval do
30
- attr_accessor :_id, :_rev, :_deleted, :database
30
+ attr_accessor :_id, :_rev, :_deleted, :database, :database_collection
31
31
  alias_method :id, :_id
32
32
  alias_method :id=, :_id=
33
33
 
@@ -1,4 +1,4 @@
1
1
  module CouchPotato
2
- VERSION = '1.13.0'.freeze
2
+ VERSION = '1.15.0'.freeze
3
3
  RSPEC_VERSION = '4.1.0'.freeze
4
4
  end
@@ -73,9 +73,11 @@ module CouchPotato
73
73
  end
74
74
 
75
75
  def docs
76
+ all_docs = rows.map { |r| r['doc'] }
76
77
  rows.map do |row|
77
78
  doc = row['doc']
78
79
  doc.database = database if doc.respond_to?(:database=)
80
+ doc.database_collection = all_docs if doc.respond_to?(:database_collection=)
79
81
  doc
80
82
  end
81
83
  end
data/lib/couch_potato.rb CHANGED
@@ -54,7 +54,7 @@ module CouchPotato
54
54
  def self.use(database_name)
55
55
  resolved_database_name = resolve_database_name(database_name)
56
56
  Thread.current[:__couch_potato_databases] ||= {}
57
- Thread.current[:__couch_potato_databases][resolved_database_name] ||= Database.new(couchrest_database_for_name!(resolved_database_name), name: database_name)
57
+ Thread.current[:__couch_potato_databases][resolved_database_name] ||= Database.new(couchrest_database_for_name(resolved_database_name), name: database_name)
58
58
  end
59
59
 
60
60
  # resolves a name to a database name/full url configured under additional databases
data/spec/create_spec.rb CHANGED
@@ -34,14 +34,24 @@ describe "create" do
34
34
  end
35
35
 
36
36
  describe "multi-db" do
37
- TEST_DBS = ['comment_a', 'comment_b', 'comment_c']
37
+ let(:test_dbs) do
38
+ ['comment_a', 'comment_b', 'comment_c'].map do |name|
39
+ if ENV['DATABASE']
40
+ uri = URI.parse(ENV['DATABASE'])
41
+ uri.path = "/#{name}"
42
+ uri.to_s
43
+ else
44
+ name
45
+ end
46
+ end
47
+ end
38
48
 
39
49
  before(:each) do
40
- TEST_DBS.each { |db_name| CouchPotato.couchrest_database_for_name(db_name).recreate! }
50
+ test_dbs.each { |db_name| CouchPotato.couchrest_database_for_name(db_name).recreate! }
41
51
  end
42
52
 
43
53
  it "should create documents in multiple dbs" do
44
- TEST_DBS.each do |db_name|
54
+ test_dbs.each do |db_name|
45
55
  @comment = Comment.new(:title => 'my_title')
46
56
  CouchPotato.with_database(db_name) do |couch|
47
57
  couch.save_document! @comment
@@ -18,41 +18,92 @@ RSpec.describe 'database caching' do
18
18
  {}
19
19
  end
20
20
 
21
- it 'gets an object from the cache the 2nd time via #load_documemt' do
22
- expect(couchrest_db).to receive(:get).with('1').exactly(1).times
21
+ context 'for a single document' do
22
+ it 'gets an object from the cache the 2nd time via #load_documemt' do
23
+ expect(couchrest_db).to receive(:get).with('1').exactly(1).times
23
24
 
24
- db.load_document '1'
25
- db.load_document '1'
26
- end
25
+ db.load_document '1'
26
+ db.load_document '1'
27
+ end
27
28
 
28
- it 'gets an object from the cache the 2nd time via #load' do
29
- expect(couchrest_db).to receive(:get).with('1').exactly(1).times
29
+ it 'gets an object from the cache the 2nd time via #load' do
30
+ expect(couchrest_db).to receive(:get).with('1').exactly(1).times
30
31
 
31
- db.load '1'
32
- db.load '1'
33
- end
32
+ db.load '1'
33
+ db.load '1'
34
+ end
34
35
 
35
- it 'gets an object from the cache the 2nd time via #load!' do
36
- expect(couchrest_db).to receive(:get).with('1').exactly(1).times
36
+ it 'gets an object from the cache the 2nd time via #load!' do
37
+ expect(couchrest_db).to receive(:get).with('1').exactly(1).times
37
38
 
38
- db.load! '1'
39
- db.load! '1'
40
- end
39
+ db.load! '1'
40
+ db.load! '1'
41
+ end
41
42
 
42
- it 'returns the correct object' do
43
- doc = double(:doc, 'database=': nil)
44
- allow(couchrest_db).to receive_messages(get: doc)
43
+ it 'returns the correct object' do
44
+ doc = double(:doc, 'database=': nil)
45
+ allow(couchrest_db).to receive_messages(get: doc)
45
46
 
46
- db.load_document '1'
47
- expect(db.load_document('1')).to eql(doc)
47
+ db.load_document '1'
48
+ expect(db.load_document('1')).to eql(doc)
49
+ end
48
50
  end
49
51
 
50
- it 'does not cache bulk loads' do
51
- allow(couchrest_db).to receive_messages(bulk_load: {'rows' => []})
52
- expect(couchrest_db).to receive(:bulk_load).with(['1']).exactly(2).times
52
+ context 'for multiple documents' do
53
+ let(:doc1) { double(:doc1, 'database=': nil, id: '1') }
54
+ let(:doc2) { double(:doc12, 'database=': nil, id: '2') }
55
+
56
+ it 'only loads uncached documents' do
57
+ allow(couchrest_db).to receive(:bulk_load).with(['1']).and_return('rows' => [{'doc' => doc1}])
58
+ allow(couchrest_db).to receive(:bulk_load).with(['2']).and_return('rows' => [{'doc' => doc2}])
59
+
60
+
61
+ db.load_document(['1'])
62
+ db.load_document(['1', '2'])
63
+
64
+ expect(couchrest_db).to have_received(:bulk_load).with(['1']).exactly(1).times
65
+ expect(couchrest_db).to have_received(:bulk_load).with(['2']).exactly(1).times
66
+ end
67
+
68
+ it 'loads nothing if all documents are cached' do
69
+ allow(couchrest_db).to receive(:bulk_load).with(['1', '2'])
70
+ .and_return('rows' => [{'doc' => doc1}, {'doc' => doc2}])
71
+
72
+ db.load_document(['1', '2'])
73
+ db.load_document(['1', '2'])
74
+
75
+ expect(couchrest_db).to have_received(:bulk_load).with(['1', '2']).exactly(1).times
76
+ end
77
+
78
+ it 'returns all requested documents' do
79
+ allow(couchrest_db).to receive(:bulk_load).with(['1']).and_return('rows' => [{'doc' => doc1}])
80
+ allow(couchrest_db).to receive(:bulk_load).with(['2']).and_return('rows' => [{'doc' => doc2}])
81
+
82
+
83
+ db.load_document(['1'])
84
+ result = db.load_document(['1', '2'])
85
+
86
+ expect(result).to eql([doc1, doc2])
87
+ end
88
+
89
+ it 'does not cache documents that do not respond to id' do
90
+ doc1 = {
91
+ 'id' => '1',
92
+ }
93
+ doc2 = {
94
+ 'id' => '2',
95
+ }
96
+ allow(couchrest_db).to receive(:bulk_load).with(['1', '2'])
97
+ .and_return('rows' => [{'doc' => doc1}, {'doc' => doc1}])
98
+
99
+ db.load_document(['1', '2'])
100
+ db.load_document(['1', '2'])
101
+
102
+ expect(couchrest_db).to have_received(:bulk_load).with(['1', '2']).exactly(2).times
103
+ end
104
+ end
53
105
 
54
- db.load_document ['1']
55
- db.load_document ['1']
106
+ context 'when switching the database' do
56
107
  end
57
108
 
58
109
  it 'clears the cache when destroying a document via #destroy_document' do
@@ -56,14 +56,24 @@ describe CouchPotato::Database, 'load' do
56
56
  db.load '1'
57
57
  end
58
58
 
59
+ it 'does not set database_collection on the model' do
60
+ user = double('user', 'database_collection=': nil).as_null_object
61
+ allow(DbTestUser).to receive(:new).and_return(user)
62
+ allow(couchrest_db).to receive(:get).and_return DbTestUser.json_create({ JSON.create_id => 'DbTestUser' })
63
+
64
+ db.load '1'
65
+
66
+ expect(user).not_to have_received(:database_collection=).with(db)
67
+ end
68
+
59
69
  it 'should load namespaced models' do
60
70
  allow(couchrest_db).to receive(:get).and_return Parent::Child.json_create({ JSON.create_id => 'Parent::Child' })
61
71
  expect(db.load('1').class).to eq(Parent::Child)
62
72
  end
63
73
 
64
74
  context 'when several ids given' do
65
- let(:doc1) { DbTestUser.new }
66
- let(:doc2) { DbTestUser.new }
75
+ let(:doc1) { DbTestUser.new(id: '1') }
76
+ let(:doc2) { DbTestUser.new(id: '2') }
67
77
  let(:response) do
68
78
  { 'rows' => [{ 'doc' => nil }, { 'doc' => doc1 }, { 'doc' => doc2 }] }
69
79
  end
@@ -88,8 +98,8 @@ describe CouchPotato::Database, 'load' do
88
98
  end
89
99
 
90
100
  it 'does not write itself to a document that has no database= method' do
91
- doc1 = double(:doc1)
92
- allow(doc1).to receive(:respond_to?).with(:database=) { false }
101
+ doc1 = double(:doc1, id: '1')
102
+ allow(doc1).to receive(:respond_to?) { false }
93
103
  allow(couchrest_db).to receive(:bulk_load) do
94
104
  { 'rows' => [{ 'doc' => doc1 }] }
95
105
  end
@@ -99,6 +109,24 @@ describe CouchPotato::Database, 'load' do
99
109
  db.load(['1'])
100
110
  end
101
111
 
112
+ it 'sets database_collection on each of the documents' do
113
+ db.load(%w[1 2]).each do |doc|
114
+ expect(doc.database_collection).to eql([doc1, doc2])
115
+ end
116
+ end
117
+
118
+ it 'does not set database_collection on a document that has no database_collection= method' do
119
+ doc1 = double(:doc1, id: '1')
120
+ allow(doc1).to receive(:respond_to?) { false }
121
+ allow(couchrest_db).to receive(:bulk_load) do
122
+ { 'rows' => [{ 'doc' => doc1 }] }
123
+ end
124
+
125
+ expect(doc1).not_to receive(:database_collection=)
126
+
127
+ db.load(['1'])
128
+ end
129
+
102
130
  it 'returns an empty array when passing an empty array' do
103
131
  expect(db.load([])).to eq([])
104
132
  end
@@ -336,7 +364,7 @@ describe CouchPotato::Database, 'view' do
336
364
  allow(CouchPotato::View::ViewQuery).to receive_messages(new: double('view query', query_view!: { 'rows' => [@result] }))
337
365
  end
338
366
 
339
- it 'initialzes a view query with map/reduce/list/lib funtions' do
367
+ it 'initializes a view query with map/reduce/list/lib funtions' do
340
368
  allow(@spec).to receive_messages(design_document: 'design_doc', view_name: 'my_view',
341
369
  map_function: '<map_code>', reduce_function: '<reduce_code>',
342
370
  lib: { test: '<test_code>' },
@@ -355,7 +383,7 @@ describe CouchPotato::Database, 'view' do
355
383
  @db.view(@spec)
356
384
  end
357
385
 
358
- it 'initialzes a view query with map/reduce/list funtions' do
386
+ it 'initializes a view query with map/reduce/list funtions' do
359
387
  allow(@spec).to receive_messages(design_document: 'design_doc', view_name: 'my_view',
360
388
  map_function: '<map_code>', reduce_function: '<reduce_code>',
361
389
  lib: nil, list_name: 'my_list', list_function: '<list_code>',
@@ -374,7 +402,7 @@ describe CouchPotato::Database, 'view' do
374
402
  @db.view(@spec)
375
403
  end
376
404
 
377
- it 'initialzes a view query with only map/reduce/lib functions' do
405
+ it 'initializes a view query with only map/reduce/lib functions' do
378
406
  allow(@spec).to receive_messages(design_document: 'design_doc', view_name: 'my_view',
379
407
  map_function: '<map_code>', reduce_function: '<reduce_code>',
380
408
  list_name: nil, list_function: nil,
@@ -390,7 +418,7 @@ describe CouchPotato::Database, 'view' do
390
418
  @db.view(@spec)
391
419
  end
392
420
 
393
- it 'initialzes a view query with only map/reduce functions' do
421
+ it 'initializes a view query with only map/reduce functions' do
394
422
  allow(@spec).to receive_messages(design_document: 'design_doc', view_name: 'my_view',
395
423
  map_function: '<map_code>', reduce_function: '<reduce_code>',
396
424
  lib: nil, list_name: nil, list_function: nil)
@@ -405,18 +433,37 @@ describe CouchPotato::Database, 'view' do
405
433
  @db.view(@spec)
406
434
  end
407
435
 
408
- it 'sets itself on returned results that have an accessor' do
436
+ it 'sets itself on returned docs that have an accessor' do
437
+ allow(@result).to receive(:respond_to?).and_return(false)
409
438
  allow(@result).to receive(:respond_to?).with(:database=).and_return(true)
410
439
  expect(@result).to receive(:database=).with(@db)
411
440
  @db.view(@spec)
412
441
  end
413
442
 
414
- it "does not set itself on returned results that don't have an accessor" do
415
- allow(@result).to receive(:respond_to?).with(:database=).and_return(false)
443
+ it "does not set itself on returned docs that don't have an accessor" do
444
+ allow(@result).to receive(:respond_to?).and_return(false)
416
445
  expect(@result).not_to receive(:database=).with(@db)
417
446
  @db.view(@spec)
418
447
  end
419
448
 
449
+ it 'sets the result of the view call on each returned doc' do
450
+ allow(@result).to receive(:respond_to?).and_return(false)
451
+ allow(@result).to receive(:respond_to?).with(:database_collection=).and_return(true)
452
+ allow(@result).to receive(:database_collection=)
453
+
454
+ @db.view(@spec)
455
+
456
+ expect(@result).to have_received(:database_collection=).with([@result])
457
+ end
458
+
459
+ it "does not set the result of the view call on docs that don't have an accessor" do
460
+ allow(@result).to receive(:respond_to?).and_return(false)
461
+
462
+ @db.view(@spec)
463
+
464
+ expect(@result).not_to receive(:database_collection=).with([@result])
465
+ end
466
+
420
467
  it 'does not try to set itself on result sets that are not collections' do
421
468
  expect do
422
469
  allow(@spec).to receive_messages(process_results: 1)
@@ -524,6 +571,14 @@ describe CouchPotato::Database, '#switch_to' do
524
571
 
525
572
  expect(new_db.cache).to be_nil
526
573
  end
574
+
575
+ it 're-uses the original cache when switching back to the original database' do
576
+ db.cache = { key: 'value' }
577
+ new_db = db.switch_to('db2')
578
+ original_db = new_db.switch_to_default
579
+
580
+ expect(original_db.cache).to have_key(:key)
581
+ end
527
582
  end
528
583
 
529
584
  describe CouchPotato::Database, '#switch_to_default' do
@@ -15,3 +15,30 @@ RSpec.describe CouchPotato::View::FlexViewSpec::Results, '#reduce_count' do
15
15
  expect(result.reduce_count).to eq(0)
16
16
  end
17
17
  end
18
+
19
+ RSpec.describe CouchPotato::View::FlexViewSpec::Results, '#docs' do
20
+ it 'sets the database on each doc' do
21
+ db = double('db')
22
+ doc = double('doc', 'database=': nil)
23
+
24
+ result = CouchPotato::View::FlexViewSpec::Results.new 'rows' => [{ 'doc' => doc }]
25
+ result.database = db
26
+
27
+ result.docs
28
+
29
+ expect(doc).to have_received(:database=).with(db)
30
+ end
31
+
32
+ it 'sets all docs as database_collection on each doc' do
33
+ doc = double('doc', 'database_collection=': nil)
34
+
35
+ result = CouchPotato::View::FlexViewSpec::Results.new 'rows' => [{ 'doc' => doc }]
36
+
37
+ result.docs
38
+
39
+ expect(doc).to have_received(:database_collection=).with([doc])
40
+ end
41
+
42
+ it 'returns the docs' do
43
+ end
44
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couch_potato
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Lang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-18 00:00:00.000000000 Z
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel