mongoid 9.0.6 → 9.0.8

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +9 -9
  3. data/lib/mongoid/association/embedded/batchable.rb +11 -10
  4. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +61 -0
  5. data/lib/mongoid/association/embedded/embeds_one/proxy.rb +1 -1
  6. data/lib/mongoid/association/nested/many.rb +2 -0
  7. data/lib/mongoid/association/nested/one.rb +1 -1
  8. data/lib/mongoid/association/referenced/has_many/proxy.rb +0 -4
  9. data/lib/mongoid/changeable.rb +10 -1
  10. data/lib/mongoid/clients/sessions.rb +3 -4
  11. data/lib/mongoid/config.rb +1 -1
  12. data/lib/mongoid/contextual/aggregable/mongo.rb +6 -1
  13. data/lib/mongoid/contextual/mongo.rb +1 -1
  14. data/lib/mongoid/railties/bson_object_id_serializer.rb +7 -0
  15. data/lib/mongoid/reloadable.rb +6 -0
  16. data/lib/mongoid/validatable/associated.rb +1 -1
  17. data/lib/mongoid/validatable/macros.rb +15 -0
  18. data/lib/mongoid/validatable/numericality.rb +19 -0
  19. data/lib/mongoid/validatable.rb +1 -0
  20. data/lib/mongoid/version.rb +5 -2
  21. data/spec/integration/app_spec.rb +6 -0
  22. data/spec/integration/associations/embeds_many_spec.rb +110 -0
  23. data/spec/integration/associations/embeds_one_spec.rb +25 -6
  24. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +81 -0
  25. data/spec/integration/associations/has_many_spec.rb +56 -0
  26. data/spec/integration/associations/has_one_spec.rb +55 -3
  27. data/spec/mongoid/association/referenced/has_many_models.rb +24 -0
  28. data/spec/mongoid/association/referenced/has_one_models.rb +10 -2
  29. data/spec/mongoid/association_spec.rb +0 -60
  30. data/spec/mongoid/clients/transactions_spec.rb +162 -1
  31. data/spec/mongoid/clients/transactions_spec_models.rb +93 -0
  32. data/spec/mongoid/contextual/aggregable/mongo_spec.rb +33 -0
  33. data/spec/mongoid/contextual/mongo_spec.rb +6 -0
  34. data/spec/mongoid/reloadable_spec.rb +24 -0
  35. data/spec/mongoid/validatable/numericality_spec.rb +16 -0
  36. data/spec/shared/LICENSE +20 -0
  37. data/spec/shared/bin/get-mongodb-download-url +17 -0
  38. data/spec/shared/bin/s3-copy +45 -0
  39. data/spec/shared/bin/s3-upload +69 -0
  40. data/spec/shared/lib/mrss/child_process_helper.rb +80 -0
  41. data/spec/shared/lib/mrss/cluster_config.rb +231 -0
  42. data/spec/shared/lib/mrss/constraints.rb +378 -0
  43. data/spec/shared/lib/mrss/docker_runner.rb +298 -0
  44. data/spec/shared/lib/mrss/eg_config_utils.rb +51 -0
  45. data/spec/shared/lib/mrss/event_subscriber.rb +210 -0
  46. data/spec/shared/lib/mrss/lite_constraints.rb +238 -0
  47. data/spec/shared/lib/mrss/release/candidate.rb +281 -0
  48. data/spec/shared/lib/mrss/release/product_data.rb +144 -0
  49. data/spec/shared/lib/mrss/server_version_registry.rb +113 -0
  50. data/spec/shared/lib/mrss/session_registry.rb +69 -0
  51. data/spec/shared/lib/mrss/session_registry_legacy.rb +60 -0
  52. data/spec/shared/lib/mrss/spec_organizer.rb +179 -0
  53. data/spec/shared/lib/mrss/utils.rb +37 -0
  54. data/spec/shared/lib/tasks/candidate.rake +64 -0
  55. data/spec/shared/share/Dockerfile.erb +251 -0
  56. data/spec/shared/share/haproxy-1.conf +16 -0
  57. data/spec/shared/share/haproxy-2.conf +17 -0
  58. data/spec/shared/shlib/config.sh +27 -0
  59. data/spec/shared/shlib/distro.sh +84 -0
  60. data/spec/shared/shlib/server.sh +423 -0
  61. data/spec/shared/shlib/set_env.sh +110 -0
  62. data/spec/support/expectations.rb +20 -18
  63. metadata +59 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de63ddee828740cd1ba5832d931cd2c9731f19c1435076527533fb7856b48f8c
4
- data.tar.gz: 4cfa7d1e9f00ec0fe57c7709c7147f9b214375de3e402d5a468e03cf6e61af40
3
+ metadata.gz: '032258516f15736f4121de82ee4cdcdb625515f653f85a66b0df613665236e4d'
4
+ data.tar.gz: 443ec24d9a54bb10c87023cd6304081aa48bf92bf5654044cc935f79ca954b9c
5
5
  SHA512:
6
- metadata.gz: 4542042a79b3a3162acd38d28898cd0755128b0a209d33e3ecf18b98d12e7ab1d220045156164ec235672cff337fc2d2a53a5779b37b601e9d903481e2793236
7
- data.tar.gz: 5bd06c74d154e09f12120d81813e6e2a1380d141cd3613350355e30e0ded7fc9774dd7dfb1ca335e00a7ad0c512e4a019e0df51bec7cb06e334cead745dd098c
6
+ metadata.gz: 3c875bf631d019da208fa59221a74e3468931a802664e43ce2e8ed669e990722a499d8976a8acafb8078dbeff8c02a76df96b9d76f8e33c0c979658f40748fd9
7
+ data.tar.gz: 46943ea7b081db4784159bacc2056b75b6511f8c8892910dc2c2bfc7ee1229a6ddf0d9141f52137a12a04eea841c24e16b4eae7fb8fc15bd26e2dec7d0273ecc
data/Rakefile CHANGED
@@ -11,16 +11,16 @@ $: << File.join(ROOT, 'spec/shared/lib')
11
11
  require "rake"
12
12
  require "rspec/core/rake_task"
13
13
 
14
- # stands in for the Bundler-provided `build` task, which builds the
15
- # gem for this project. Our release process builds the gems in a
16
- # particular way, in a GitHub action. This task is just to help remind
17
- # developers of that fact.
14
+ if File.exist?('./spec/shared/lib/tasks/candidate.rake')
15
+ load 'spec/shared/lib/tasks/candidate.rake'
16
+ end
17
+
18
+ desc 'Build the gem'
18
19
  task :build do
19
- abort <<~WARNING
20
- `rake build` does nothing in this project. The gem must be built via
21
- the `Mongoid Release` action on GitHub, which is triggered manually when
22
- a new release is ready.
23
- WARNING
20
+ command = %w[ gem build ]
21
+ command << "--output=#{ENV['GEM_FILE_NAME']}" if ENV['GEM_FILE_NAME']
22
+ command << (ENV['GEMSPEC'] || 'mongoid.gemspec')
23
+ system(*command)
24
24
  end
25
25
 
26
26
  # `rake version` is used by the deployment system so get the release version
@@ -313,18 +313,19 @@ module Mongoid
313
313
  #
314
314
  # @return [ Array<Hash> ] The documents as an array of hashes.
315
315
  def pre_process_batch_insert(docs)
316
- docs.map do |doc|
317
- next unless doc
318
- append(doc)
319
- if persistable? && !_assigning?
320
- self.path = doc.atomic_path unless path
321
- if doc.valid?(:create)
322
- doc.run_before_callbacks(:save, :create)
323
- else
324
- self.inserts_valid = false
316
+ [].tap do |results|
317
+ append_many(docs) do |doc|
318
+ if persistable? && !_assigning?
319
+ self.path = doc.atomic_path unless path
320
+ if doc.valid?(:create)
321
+ doc.run_before_callbacks(:save, :create)
322
+ else
323
+ self.inserts_valid = false
324
+ end
325
325
  end
326
+
327
+ results << doc.send(:as_attributes)
326
328
  end
327
- doc.send(:as_attributes)
328
329
  end
329
330
  end
330
331
 
@@ -443,6 +443,67 @@ module Mongoid
443
443
  execute_callback :after_add, document
444
444
  end
445
445
 
446
+ # Returns a unique id for the document, which is either
447
+ # its _id or its object_id.
448
+ def id_of(doc)
449
+ doc._id || doc.object_id
450
+ end
451
+
452
+ # Optimized version of #append that handles multiple documents
453
+ # in a more efficient way.
454
+ #
455
+ # @param [ Array<Document> ] documents The documents to append.
456
+ #
457
+ # @return [ EmbedsMany::Proxy ] This proxy instance.
458
+ def append_many(documents, &block)
459
+ unique_set = process_incoming_docs(documents, &block)
460
+
461
+ _unscoped.concat(unique_set)
462
+ _target.push(*scope(unique_set))
463
+ update_attributes_hash
464
+
465
+ unique_set.each { |doc| execute_callback :after_add, doc }
466
+
467
+ self
468
+ end
469
+
470
+ # Processes the list of documents, building a list of those
471
+ # that are not already in the association, and preparing
472
+ # each unique document to be integrated into the association.
473
+ #
474
+ # The :before_add callback is executed for each unique document
475
+ # as part of this step.
476
+ #
477
+ # @param [ Array<Document> ] documents The incoming documents to
478
+ # process.
479
+ #
480
+ # @yield [ Document ] Optional block to call for each unique
481
+ # document.
482
+ #
483
+ # @return [ Array<Document> ] The list of unique documents that
484
+ # do not yet exist in the association.
485
+ def process_incoming_docs(documents, &block)
486
+ visited_docs = Set.new(_target.map { |doc| id_of(doc) })
487
+ next_index = _unscoped.size
488
+
489
+ documents.select do |doc|
490
+ next unless doc
491
+
492
+ id = id_of(doc)
493
+ next if visited_docs.include?(id)
494
+
495
+ execute_callback :before_add, doc
496
+
497
+ visited_docs.add(id)
498
+ integrate(doc)
499
+
500
+ doc._index = next_index
501
+ next_index += 1
502
+
503
+ block&.call(doc) || true
504
+ end
505
+ end
506
+
446
507
  # Instantiate the binding associated with this association.
447
508
  #
448
509
  # @example Create the binding.
@@ -36,8 +36,8 @@ module Mongoid
36
36
  bind_one
37
37
  characterize_one(_target)
38
38
  update_attributes_hash(_target)
39
- _base._reset_memoized_descendants!
40
39
  _target.save if persistable?
40
+ _base._reset_memoized_descendants!
41
41
  end
42
42
  end
43
43
 
@@ -190,6 +190,8 @@ module Mongoid
190
190
  update_document(doc, attrs)
191
191
  existing.push(doc) unless destroyable?(attrs)
192
192
  end
193
+
194
+ parent.children_may_have_changed!
193
195
  end
194
196
  end
195
197
  end
@@ -37,7 +37,7 @@ module Mongoid
37
37
  parent.send(association.setter, nil)
38
38
  else
39
39
  check_for_id_violation!
40
- end
40
+ end.tap { parent.children_may_have_changed! }
41
41
  end
42
42
 
43
43
  # Create the new builder for nested attributes on one-to-one
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: consider refactoring this Proxy class, to satisfy the following
4
- # cops...
5
- # rubocop:disable Metrics/ClassLength
6
3
  module Mongoid
7
4
  module Association
8
5
  module Referenced
@@ -588,4 +585,3 @@ module Mongoid
588
585
  end
589
586
  end
590
587
  end
591
- # rubocop:enable Metrics/ClassLength
@@ -15,6 +15,14 @@ module Mongoid
15
15
  changed_attributes.keys.select { |attr| attribute_change(attr) }
16
16
  end
17
17
 
18
+ # Indicates that the children of this document may have changed, and
19
+ # ought to be checked when the document is validated.
20
+ #
21
+ # @api private
22
+ def children_may_have_changed!
23
+ @children_may_have_changed = true
24
+ end
25
+
18
26
  # Has the document changed?
19
27
  #
20
28
  # @example Has the document changed?
@@ -31,7 +39,7 @@ module Mongoid
31
39
  #
32
40
  # @return [ true | false ] If any children have changed.
33
41
  def children_changed?
34
- _children.any?(&:changed?)
42
+ @children_may_have_changed || _children.any?(&:changed?)
35
43
  end
36
44
 
37
45
  # Get the attribute changes.
@@ -69,6 +77,7 @@ module Mongoid
69
77
  @previous_changes = changes
70
78
  @attributes_before_last_save = @previous_attributes
71
79
  @previous_attributes = attributes.dup
80
+ @children_may_have_changed = false
72
81
  reset_atomic_updates!
73
82
  changed_attributes.clear
74
83
  end
@@ -92,8 +92,7 @@ module Mongoid
92
92
  begin
93
93
  session.with_transaction(options) do
94
94
  yield
95
- end
96
- run_commit_callbacks(session)
95
+ end.tap { run_commit_callbacks(session) }
97
96
  rescue *transactions_not_supported_exceptions
98
97
  raise Mongoid::Errors::TransactionsNotSupported
99
98
  rescue Mongoid::Errors::Rollback
@@ -213,8 +212,8 @@ module Mongoid
213
212
 
214
213
  # Transforms custom options for after_commit and after_rollback callbacks
215
214
  # into options for +set_callback+.
216
- def set_options_for_callbacks!(args)
217
- options = args.extract_options!
215
+ def set_options_for_callbacks!(args, enforced_options = {})
216
+ options = args.extract_options!.merge(enforced_options)
218
217
  args << options
219
218
 
220
219
  if options[:on]
@@ -102,7 +102,7 @@ module Mongoid
102
102
  #
103
103
  # - :immediate - Initializes a single +Concurrent::ImmediateExecutor+
104
104
  # - :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+
105
- # that uses the +async_query_concurrency+ for the +max_threads+ value.
105
+ # that uses the +global_executor_concurrency+ for the +max_threads+ value.
106
106
  option :async_query_executor, default: :immediate
107
107
 
108
108
  # Defines how many asynchronous queries can be executed concurrently.
@@ -27,7 +27,12 @@ module Mongoid
27
27
  # If no documents are found, then returned Hash will have
28
28
  # count, sum of 0 and max, min, avg of nil.
29
29
  def aggregates(field)
30
- result = collection.aggregate(pipeline(field), session: _session).to_a
30
+ result = collection.aggregate(
31
+ pipeline(field),
32
+ session: _session,
33
+ hint: view.hint
34
+ ).to_a
35
+
31
36
  if result.empty?
32
37
  Aggregable::EMPTY_RESULT.dup
33
38
  else
@@ -1063,7 +1063,7 @@ module Mongoid
1063
1063
  end
1064
1064
 
1065
1065
  def retrieve_nth_to_last_with_limit(n, limit)
1066
- v = view.sort(inverse_sorting).skip(n).limit(limit || 1)
1066
+ v = view.sort(inverse_sorting).limit(limit || 1)
1067
1067
  v = v.skip(n) if n > 0
1068
1068
  raw_docs = v.to_a.reverse
1069
1069
  process_raw_docs(raw_docs, limit)
@@ -33,6 +33,13 @@ module Mongoid
33
33
  def deserialize(string)
34
34
  BSON::ObjectId.from_string(string)
35
35
  end
36
+
37
+ # Returns the klass this serializer handles.
38
+ #
39
+ # @return [ BSON::ObjectId ] The class this serializer handles.
40
+ def klass
41
+ BSON::ObjectId
42
+ end
36
43
  end
37
44
  end
38
45
  end
@@ -17,6 +17,12 @@ module Mongoid
17
17
  reloaded = _reload
18
18
  check_for_deleted_document!(reloaded)
19
19
 
20
+ # In an instance where we create a new document, but set the ID to an existing one,
21
+ # when the document is reloaded, we want to set new_record to false.
22
+ # This is necessary otherwise saving will fail, as it will try to insert the document,
23
+ # instead of attempting to update the existing document.
24
+ @new_record = false unless reloaded.nil? || reloaded.empty?
25
+
20
26
  reset_object!(reloaded)
21
27
 
22
28
  run_callbacks(:find) unless _find_callbacks.empty?
@@ -74,7 +74,7 @@ module Mongoid
74
74
  # use map.all? instead of just all?, because all? will do short-circuit
75
75
  # evaluation and terminate on the first failed validation.
76
76
  list.map do |value|
77
- if value && !value.flagged_for_destroy?
77
+ if value && !value.flagged_for_destroy? && (!value.persisted? || value.changed?)
78
78
  value.validated? ? true : value.valid?
79
79
  else
80
80
  true
@@ -89,6 +89,21 @@ module Mongoid
89
89
  def validates_presence_of(*args)
90
90
  validates_with(PresenceValidator, _merge_attributes(args))
91
91
  end
92
+
93
+ # Validates whether or not a field contains a numeric value.
94
+ #
95
+ # @example
96
+ # class Person
97
+ # include Mongoid::Document
98
+ # field :cost
99
+ #
100
+ # validates_numericality_of :cost
101
+ # end
102
+ #
103
+ # @param [ Object... ] *args The names of the field(s) to validate.
104
+ def validates_numericality_of(*args)
105
+ validates_with(NumericalityValidator, _merge_attributes(args))
106
+ end
92
107
  end
93
108
  end
94
109
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Validatable
5
+ # A specialization of the ActiveModel numericality validator, which adds
6
+ # logic to recognize and accept BSON::Decimal128 as a number.
7
+ class NumericalityValidator < ActiveModel::Validations::NumericalityValidator
8
+ private
9
+
10
+ # Ensure that BSON::Decimal128 is treated as a BigDecimal during the
11
+ # validation step.
12
+ def prepare_value_for_validation(value, record, attr_name)
13
+ result = super
14
+
15
+ result.is_a?(BSON::Decimal128) ? result.to_big_decimal : result
16
+ end
17
+ end
18
+ end
19
+ end
@@ -6,6 +6,7 @@ require "mongoid/validatable/localizable"
6
6
  require "mongoid/validatable/associated"
7
7
  require "mongoid/validatable/format"
8
8
  require "mongoid/validatable/length"
9
+ require "mongoid/validatable/numericality"
9
10
  require "mongoid/validatable/queryable"
10
11
  require "mongoid/validatable/presence"
11
12
  require "mongoid/validatable/uniqueness"
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
- # rubocop:todo all
3
2
 
4
3
  module Mongoid
5
- VERSION = "9.0.6"
4
+ # The current version of Mongoid
5
+ #
6
+ # Note that this file is automatically updated via `rake candidate:create`.
7
+ # Manual changes to this file will be overwritten by that rake task.
8
+ VERSION = '9.0.8'
6
9
  end
@@ -127,6 +127,12 @@ describe 'Mongoid application tests' do
127
127
  end
128
128
 
129
129
  context 'new application - rails' do
130
+ before(:all) do
131
+ if SpecConfig.instance.rails_version < '7.1'
132
+ skip '`rails new` with rails < 7.1 fails because modern concurrent-ruby removed logger dependency'
133
+ end
134
+ end
135
+
130
136
  it 'creates' do
131
137
  prepare_new_rails_app 'mongoid-test' do
132
138
  check_call(%w(rails g model post), env: clean_env)
@@ -3,6 +3,24 @@
3
3
 
4
4
  require 'spec_helper'
5
5
 
6
+ module EmbedsManySpec
7
+ class Post
8
+ include Mongoid::Document
9
+ field :title, type: String
10
+ embeds_many :comments, class_name: 'EmbedsManySpec::Comment', as: :container
11
+ accepts_nested_attributes_for :comments
12
+ end
13
+
14
+ class Comment
15
+ include Mongoid::Document
16
+ field :content, type: String
17
+ validates :content, presence: true
18
+ embedded_in :container, polymorphic: true
19
+ embeds_many :comments, class_name: 'EmbedsManySpec::Comment', as: :container
20
+ accepts_nested_attributes_for :comments
21
+ end
22
+ end
23
+
6
24
  describe 'embeds_many associations' do
7
25
 
8
26
  context 're-associating the same object' do
@@ -201,6 +219,47 @@ describe 'embeds_many associations' do
201
219
  include_examples 'persists correctly'
202
220
  end
203
221
  end
222
+
223
+ context 'including duplicates in the assignment' do
224
+ let(:canvas) do
225
+ Canvas.create!(shapes: [Shape.new])
226
+ end
227
+
228
+ shared_examples 'persists correctly' do
229
+ it 'persists correctly' do
230
+ canvas.shapes.length.should eq 2
231
+ _canvas = Canvas.find(canvas.id)
232
+ _canvas.shapes.length.should eq 2
233
+ end
234
+ end
235
+
236
+ context 'via assignment operator' do
237
+ before do
238
+ canvas.shapes = [ canvas.shapes.first, Shape.new, canvas.shapes.first ]
239
+ canvas.save!
240
+ end
241
+
242
+ include_examples 'persists correctly'
243
+ end
244
+
245
+ context 'via attributes=' do
246
+ before do
247
+ canvas.attributes = { shapes: [ canvas.shapes.first, Shape.new, canvas.shapes.first ] }
248
+ canvas.save!
249
+ end
250
+
251
+ include_examples 'persists correctly'
252
+ end
253
+
254
+ context 'via assign_attributes' do
255
+ before do
256
+ canvas.assign_attributes(shapes: [ canvas.shapes.first, Shape.new, canvas.shapes.first ])
257
+ canvas.save!
258
+ end
259
+
260
+ include_examples 'persists correctly'
261
+ end
262
+ end
204
263
  end
205
264
 
206
265
  context 'when an anonymous class defines an embeds_many association' do
@@ -217,4 +276,55 @@ describe 'embeds_many associations' do
217
276
  expect(klass.new.addresses.build).to be_a Address
218
277
  end
219
278
  end
279
+
280
+ context 'with deeply nested trees' do
281
+ let(:post) { EmbedsManySpec::Post.create!(title: 'Post') }
282
+ let(:child) { post.comments.create!(content: 'Child') }
283
+
284
+ # creating grandchild will cascade to create the other documents
285
+ let!(:grandchild) { child.comments.create!(content: 'Grandchild') }
286
+
287
+ let(:updated_parent_title) { 'Post Updated' }
288
+ let(:updated_grandchild_content) { 'Grandchild Updated' }
289
+
290
+ context 'with nested attributes' do
291
+ let(:attributes) do
292
+ {
293
+ title: updated_parent_title,
294
+ comments_attributes: [
295
+ {
296
+ # no change for comment1
297
+ _id: child.id,
298
+ comments_attributes: [
299
+ {
300
+ _id: grandchild.id,
301
+ content: updated_grandchild_content,
302
+ }
303
+ ]
304
+ }
305
+ ]
306
+ }
307
+ end
308
+
309
+ context 'when the grandchild is invalid' do
310
+ let(:updated_grandchild_content) { '' } # invalid value
311
+
312
+ it 'will not save the parent' do
313
+ expect(post.update(attributes)).to be_falsey
314
+ expect(post.errors).not_to be_empty
315
+ expect(post.reload.title).not_to eq(updated_parent_title)
316
+ expect(grandchild.reload.content).not_to eq(updated_grandchild_content)
317
+ end
318
+ end
319
+
320
+ context 'when the grandchild is valid' do
321
+ it 'will save the parent' do
322
+ expect(post.update(attributes)).to be_truthy
323
+ expect(post.errors).to be_empty
324
+ expect(post.reload.title).to eq(updated_parent_title)
325
+ expect(grandchild.reload.content).to eq(updated_grandchild_content)
326
+ end
327
+ end
328
+ end
329
+ end
220
330
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
- # rubocop:todo all
3
2
 
4
3
  require 'spec_helper'
5
4
 
6
5
  describe 'embeds_one associations' do
7
-
8
- context 're-associating the same object' do
6
+ context 'when re-associating the same object' do
9
7
  context 'with dependent: destroy' do
10
8
  let(:canvas) do
11
9
  Canvas.create!(palette: Palette.new)
@@ -17,7 +15,7 @@ describe 'embeds_one associations' do
17
15
  canvas.palette = canvas.palette
18
16
  canvas.save!
19
17
  canvas.reload
20
- canvas.palette.should == palette
18
+ expect(canvas.palette).to eq palette
21
19
  end
22
20
  end
23
21
  end
@@ -31,12 +29,33 @@ describe 'embeds_one associations' do
31
29
  end
32
30
 
33
31
  it 'loads the association correctly' do
34
- expect { klass }.to_not raise_error
35
- expect { klass.new.address }.to_not raise_error
32
+ expect { klass }.not_to raise_error
33
+ expect { klass.new.address }.not_to raise_error
36
34
  instance = klass.new
37
35
  address = Address.new
38
36
  instance.address = address
39
37
  expect(instance.address).to eq address
40
38
  end
41
39
  end
40
+
41
+ context 'when parent is persisted' do
42
+ let!(:person) do
43
+ Person.create!
44
+ end
45
+
46
+ context 'when assigning the new child' do
47
+ context 'when assigning an attribute to the child' do
48
+ before do
49
+ # person.reload
50
+ person.name = Name.new
51
+ person.name.first_name = 'Dmitry'
52
+ person.save!
53
+ end
54
+
55
+ it 'persists the child' do
56
+ expect(person.reload.name.first_name).to eq 'Dmitry'
57
+ end
58
+ end
59
+ end
60
+ end
42
61
  end
@@ -23,6 +23,38 @@ module HabtmSpec
23
23
  include Mongoid::Document
24
24
  field :file, type: String
25
25
  end
26
+
27
+ class Item
28
+ include Mongoid::Document
29
+
30
+ field :title, type: String
31
+
32
+ has_and_belongs_to_many :colors, class_name: 'HabtmSpec::Color', inverse_of: :items
33
+
34
+ accepts_nested_attributes_for :colors
35
+ end
36
+
37
+ class Beam
38
+ include Mongoid::Document
39
+
40
+ field :name, type: String
41
+ validates :name, presence: true
42
+
43
+ has_and_belongs_to_many :colors, class_name: 'HabtmSpec::Color', inverse_of: :beams
44
+
45
+ accepts_nested_attributes_for :colors
46
+ end
47
+
48
+ class Color
49
+ include Mongoid::Document
50
+
51
+ field :name, type: String
52
+
53
+ has_and_belongs_to_many :items, class_name: 'HabtmSpec::Item', inverse_of: :colors
54
+ has_and_belongs_to_many :beams, class_name: 'HabtmSpec::Beam', inverse_of: :colors
55
+
56
+ accepts_nested_attributes_for :items, :beams
57
+ end
26
58
  end
27
59
 
28
60
  describe 'has_and_belongs_to_many associations' do
@@ -59,4 +91,53 @@ describe 'has_and_belongs_to_many associations' do
59
91
  expect { image_block.save! }.not_to raise_error
60
92
  end
61
93
  end
94
+
95
+ context 'with deeply nested trees' do
96
+ let(:item) { HabtmSpec::Item.create!(title: 'Item') }
97
+ let(:beam) { HabtmSpec::Beam.create!(name: 'Beam') }
98
+ let!(:color) { HabtmSpec::Color.create!(name: 'Red', items: [ item ], beams: [ beam ]) }
99
+
100
+ let(:updated_item_title) { 'Item Updated' }
101
+ let(:updated_beam_name) { 'Beam Updated' }
102
+
103
+ context 'with nested attributes' do
104
+ let(:attributes) do
105
+ {
106
+ title: updated_item_title,
107
+ colors_attributes: [
108
+ {
109
+ # no change for color
110
+ _id: color.id,
111
+ beams_attributes: [
112
+ {
113
+ _id: beam.id,
114
+ name: updated_beam_name,
115
+ }
116
+ ]
117
+ }
118
+ ]
119
+ }
120
+ end
121
+
122
+ context 'when the beam is invalid' do
123
+ let(:updated_beam_name) { '' } # invalid value
124
+
125
+ it 'will not save the parent' do
126
+ expect(item.update(attributes)).to be_falsey
127
+ expect(item.errors).not_to be_empty
128
+ expect(item.reload.title).not_to eq(updated_item_title)
129
+ expect(beam.reload.name).not_to eq(updated_beam_name)
130
+ end
131
+ end
132
+
133
+ context 'when the beam is valid' do
134
+ it 'will save the parent' do
135
+ expect(item.update(attributes)).to be_truthy
136
+ expect(item.errors).to be_empty
137
+ expect(item.reload.title).to eq(updated_item_title)
138
+ expect(beam.reload.name).to eq(updated_beam_name)
139
+ end
140
+ end
141
+ end
142
+ end
62
143
  end