mongoid 8.0.7 → 8.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +65 -41
  3. data/lib/mongoid/association/accessors.rb +5 -1
  4. data/lib/mongoid/association/eager_loadable.rb +3 -0
  5. data/lib/mongoid/atomic.rb +9 -7
  6. data/lib/mongoid/config.rb +10 -0
  7. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +15 -1
  8. data/lib/mongoid/document.rb +8 -1
  9. data/lib/mongoid/fields.rb +11 -6
  10. data/lib/mongoid/interceptable.rb +10 -8
  11. data/lib/mongoid/timestamps/created.rb +8 -1
  12. data/lib/mongoid/traversable.rb +12 -0
  13. data/lib/mongoid/validatable/associated.rb +98 -17
  14. data/lib/mongoid/validatable.rb +8 -0
  15. data/lib/mongoid/version.rb +1 -1
  16. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +40 -0
  17. data/spec/integration/callbacks_models.rb +37 -0
  18. data/spec/integration/callbacks_spec.rb +27 -0
  19. data/spec/mongoid/association/eager_spec.rb +24 -2
  20. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +4 -0
  21. data/spec/mongoid/association_spec.rb +60 -0
  22. data/spec/mongoid/document_spec.rb +27 -0
  23. data/spec/mongoid/interceptable_spec.rb +100 -0
  24. data/spec/mongoid/interceptable_spec_models.rb +51 -111
  25. data/spec/mongoid/serializable_spec.rb +14 -14
  26. data/spec/mongoid/timestamps/created_spec.rb +23 -0
  27. data/spec/mongoid/validatable/associated_spec.rb +27 -34
  28. data/spec/support/models/name.rb +10 -0
  29. metadata +4 -80
  30. checksums.yaml.gz.sig +0 -0
  31. data/spec/shared/LICENSE +0 -20
  32. data/spec/shared/bin/get-mongodb-download-url +0 -17
  33. data/spec/shared/bin/s3-copy +0 -45
  34. data/spec/shared/bin/s3-upload +0 -69
  35. data/spec/shared/lib/mrss/child_process_helper.rb +0 -80
  36. data/spec/shared/lib/mrss/cluster_config.rb +0 -231
  37. data/spec/shared/lib/mrss/constraints.rb +0 -378
  38. data/spec/shared/lib/mrss/docker_runner.rb +0 -298
  39. data/spec/shared/lib/mrss/eg_config_utils.rb +0 -51
  40. data/spec/shared/lib/mrss/event_subscriber.rb +0 -210
  41. data/spec/shared/lib/mrss/lite_constraints.rb +0 -238
  42. data/spec/shared/lib/mrss/server_version_registry.rb +0 -113
  43. data/spec/shared/lib/mrss/session_registry.rb +0 -69
  44. data/spec/shared/lib/mrss/session_registry_legacy.rb +0 -60
  45. data/spec/shared/lib/mrss/spec_organizer.rb +0 -179
  46. data/spec/shared/lib/mrss/utils.rb +0 -37
  47. data/spec/shared/share/Dockerfile.erb +0 -321
  48. data/spec/shared/share/haproxy-1.conf +0 -16
  49. data/spec/shared/share/haproxy-2.conf +0 -17
  50. data/spec/shared/shlib/config.sh +0 -27
  51. data/spec/shared/shlib/distro.sh +0 -74
  52. data/spec/shared/shlib/server.sh +0 -416
  53. data/spec/shared/shlib/set_env.sh +0 -169
  54. data.tar.gz.sig +0 -4
  55. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ffe4422a33c5d676a4ae8e28c2d3ea748a738022b5fe0defa69a675fefbff8d
4
- data.tar.gz: d296900948d3e569beb33f4c63876ac24c45368dd0f3000efebecdcaed2cfbcf
3
+ metadata.gz: 49662a14c4c7135e3403cab365f7e46928baf2efca9ede4e673269d39c5a7292
4
+ data.tar.gz: a69c22af01dbba7be588e2b4a9a63a1f58323ddd3395771969652323b2d7ecaf
5
5
  SHA512:
6
- metadata.gz: 3632cdca025ba56792b041d07a560d00f3a561fa21ca9b0f44167fbf54b14fe0cc5ae040adeff7a24ac98795f6a54cfbfcd88ae3057906a44d606573e18ee398
7
- data.tar.gz: 243a702300881cc8ef82f7937e8ff3ea718b2c23aea8768a6392d47b6f3f841dba062e0133d142855b9f9ccf85f3bfbe0f45b743daed7f82813e02f6b9a1674b
6
+ metadata.gz: cdde64cee0c2f085b6f250aae4e0f31340a9b79d04177e69f9c287170d8bb2eaba435873d680f77181587624244c1ba5885eff2d3964f3ebbe5f0924a70b1347
7
+ data.tar.gz: 2d065d480c992dc3a2e772b7c33a0b50805d981c4f150f06b93f0ad9c89fd6b9c138c64354f8353c1a9849d9ddef45eae382bfc3ca1e4bb6aead94e60c5e6324
data/Rakefile CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler"
4
- require "bundler/gem_tasks"
5
4
  Bundler.setup
6
5
 
7
6
  ROOT = File.expand_path(File.join(File.dirname(__FILE__)))
@@ -10,34 +9,53 @@ $: << File.join(ROOT, 'spec/shared/lib')
10
9
 
11
10
  require "rake"
12
11
  require "rspec/core/rake_task"
13
- require 'mrss/spec_organizer'
14
- require 'rubygems/package'
15
- require 'rubygems/security/policies'
16
-
17
- def signed_gem?(path_to_gem)
18
- Gem::Package.new(path_to_gem, Gem::Security::HighSecurity).verify
19
- true
20
- rescue Gem::Security::Exception => e
21
- false
22
- end
23
-
24
- $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
25
- require "mongoid/version"
26
-
27
- tasks = Rake.application.instance_variable_get('@tasks')
28
- tasks['release:do'] = tasks.delete('release')
29
12
 
30
- task :gem => :build
13
+ # stands in for the Bundler-provided `build` task, which builds the
14
+ # gem for this project. Our release process builds the gems in a
15
+ # particular way, in a GitHub action. This task is just to help remind
16
+ # developers of that fact.
31
17
  task :build do
32
- system "gem build mongoid.gemspec"
18
+ abort <<~WARNING
19
+ `rake build` does nothing in this project. The gem must be built via
20
+ the `Mongoid Release` action on GitHub, which is triggered manually when
21
+ a new release is ready.
22
+ WARNING
33
23
  end
34
24
 
35
- task :install => :build do
36
- system "sudo gem install mongoid-#{Mongoid::VERSION}.gem"
25
+ # `rake version` is used by the deployment system so get the release version
26
+ # of the product beng deployed. It must do nothing more than just print the
27
+ # product version number.
28
+ #
29
+ # See the mongodb-labs/driver-github-tools/ruby/publish Github action.
30
+ desc "Print the current value of Mongoid::VERSION"
31
+ task :version do
32
+ require 'mongoid/version'
33
+
34
+ puts Mongoid::VERSION
37
35
  end
38
36
 
37
+ # overrides the default Bundler-provided `release` task, which also
38
+ # builds the gem. Our release process assumes the gem has already
39
+ # been built (and signed via GPG), so we just need `rake release` to
40
+ # push the gem to rubygems.
39
41
  task :release do
40
- raise "Please use ./release.sh to release"
42
+ require 'mongoid/version'
43
+
44
+ if ENV['GITHUB_ACTION'].nil?
45
+ abort <<~WARNING
46
+ `rake release` must be invoked from the `Mongoid Release` GitHub action,
47
+ and must not be invoked locally. This ensures the gem is properly signed
48
+ and distributed by the appropriate user.
49
+
50
+ Note that it is the `rubygems/release-gem@v1` step in the `Mongoid Release`
51
+ action that invokes this task. Do not rename or remove this task, or the
52
+ release-gem step will fail. Reimplement this task with caution.
53
+
54
+ mongoid-#{Mongoid::VERSION}.gem was NOT pushed to RubyGems.
55
+ WARNING
56
+ end
57
+
58
+ system 'gem', 'push', "mongoid-#{Mongoid::VERSION}.gem"
41
59
  end
42
60
 
43
61
  RSpec::Core::RakeTask.new("spec") do |spec|
@@ -64,6 +82,8 @@ RUN_PRIORITY = %i(
64
82
  )
65
83
 
66
84
  def spec_organizer
85
+ require 'mrss/spec_organizer'
86
+
67
87
  Mrss::SpecOrganizer.new(
68
88
  root: ROOT,
69
89
  classifiers: CLASSIFIERS,
@@ -99,32 +119,36 @@ task :docs => 'docs:yard'
99
119
  namespace :docs do
100
120
  desc "Generate yard documention"
101
121
  task :yard do
122
+ require "mongoid/version"
123
+
102
124
  out = File.join('yard-docs', Mongoid::VERSION)
103
125
  FileUtils.rm_rf(out)
104
126
  system "yardoc -o #{out} --title mongoid-#{Mongoid::VERSION}"
105
127
  end
106
128
  end
107
129
 
108
- namespace :release do
109
- task :check_private_key do
110
- unless File.exist?('gem-private_key.pem')
111
- raise "No private key present, cannot release"
112
- end
130
+ desc 'Build and validate the evergreen config'
131
+ task eg: %w[ eg:build eg:validate ]
132
+
133
+ # 'eg' == 'evergreen', but evergreen is too many letters for convenience
134
+ namespace :eg do
135
+ desc 'Builds the .evergreen/config.yml file from the templates'
136
+ task :build do
137
+ ruby '.evergreen/update-evergreen-configs'
113
138
  end
114
- end
115
139
 
116
- desc 'Verifies that all built gems in pkg/ are valid'
117
- task :verify do
118
- gems = Dir['pkg/*.gem']
119
- if gems.empty?
120
- puts 'There are no gems in pkg/ to verify'
121
- else
122
- gems.each do |gem|
123
- if signed_gem?(gem)
124
- puts "#{gem} is signed"
125
- else
126
- abort "#{gem} is not signed"
127
- end
128
- end
140
+ desc 'Validates the .evergreen/config.yml file'
141
+ task :validate do
142
+ system 'evergreen validate --project mongoid .evergreen/config.yml'
143
+ end
144
+
145
+ desc 'Updates the evergreen executable to the latest available version'
146
+ task :update do
147
+ system 'evergreen get-update --install'
148
+ end
149
+
150
+ desc 'Runs the current branch as an evergreen patch'
151
+ task :patch do
152
+ system 'evergreen patch --uncommitted --project mongoid --browse --auto-description --yes'
129
153
  end
130
154
  end
@@ -115,7 +115,11 @@ module Mongoid
115
115
  # during binding or when cascading callbacks. Whenever we retrieve
116
116
  # associations within the codebase, we use without_autobuild.
117
117
  if !without_autobuild? && association.embedded? && attribute_missing?(field_name)
118
- raise ActiveModel::MissingAttributeError, "Missing attribute: '#{field_name}'"
118
+ # We always allow accessing the parent document of an embedded one.
119
+ try_get_parent = association.is_a?(
120
+ Mongoid::Association::Embedded::EmbeddedIn
121
+ ) && field_name == association.key
122
+ raise ActiveModel::MissingAttributeError, "Missing attribute: '#{field_name}'" unless try_get_parent
119
123
  end
120
124
 
121
125
  if !reload && (value = ivar(name)) != false
@@ -31,6 +31,9 @@ module Mongoid
31
31
  docs_map = {}
32
32
  queue = [ klass.to_s ]
33
33
 
34
+ # account for single-collection inheritance
35
+ queue.push(klass.root_class.to_s) if klass != klass.root_class
36
+
34
37
  while klass = queue.shift
35
38
  if as = assoc_map.delete(klass)
36
39
  as.each do |assoc|
@@ -178,13 +178,15 @@ module Mongoid
178
178
  #
179
179
  # @return [ Object ] The associated path.
180
180
  def atomic_paths
181
- @atomic_paths ||= begin
182
- if _association
183
- _association.path(self)
184
- else
185
- Atomic::Paths::Root.new(self)
186
- end
187
- end
181
+ return @atomic_paths if @atomic_paths
182
+
183
+ paths = if _association
184
+ _association.path(self)
185
+ else
186
+ Atomic::Paths::Root.new(self)
187
+ end
188
+
189
+ paths.tap { @atomic_paths = paths unless new_record? }
188
190
  end
189
191
 
190
192
  # Get all the attributes that need to be pulled.
@@ -142,6 +142,16 @@ module Mongoid
142
142
  end
143
143
  end
144
144
 
145
+ # When this flag is true, callbacks for every embedded document will be
146
+ # called only once, even if the embedded document is embedded in multiple
147
+ # documents in the root document's dependencies graph.
148
+ # This will be the default in 9.0. Setting this flag to false restores the
149
+ # pre-9.0 behavior, where callbacks are called for every occurrence of an
150
+ # embedded document. The pre-9.0 behavior leads to a problem that for multi
151
+ # level nested documents callbacks are called multiple times.
152
+ # See https://jira.mongodb.org/browse/MONGOID-5542
153
+ option :prevent_multiple_calls_of_embedded_callbacks, default: false
154
+
145
155
  # When this flag is true, callbacks for embedded documents will not be
146
156
  # called. This is the default in 8.x, but will be changed to false in 9.0.
147
157
  #
@@ -43,7 +43,21 @@ module Mongoid
43
43
  #
44
44
  # @return [ Object ] The converted number.
45
45
  def __numeric__(object)
46
- object.to_s.match?(/\A[-+]?[0-9]*[0-9.]0*\z/) ? object.to_i : Float(object)
46
+ str = object.to_s
47
+ raise ArgumentError if str.empty?
48
+
49
+ # These requirements seem a bit odd, but they're explicitly specified in the tests,
50
+ # so we're obligated to keep them, for now. (This code was rewritten from a one-line
51
+ # regex, due to security concerns with a polynomial regex being used on uncontrolled
52
+ # data).
53
+
54
+ str = str.chop if str.end_with?('.')
55
+ return 0 if str.empty?
56
+
57
+ result = Integer(str) rescue Float(object)
58
+
59
+ integer = result.to_i
60
+ integer == result ? integer : result
47
61
  end
48
62
 
49
63
  # Evolve the object to an integer.
@@ -133,7 +133,14 @@ module Mongoid
133
133
  #
134
134
  # @return [ Hash ] A hash of all attributes in the hierarchy.
135
135
  def as_document
136
- BSON::Document.new(as_attributes)
136
+ attrs = as_attributes
137
+
138
+ # legacy attributes have a tendency to leak internal state via
139
+ # `as_document`; we have to deep_dup the attributes here to prevent
140
+ # that.
141
+ attrs = attrs.deep_dup if Mongoid.legacy_attributes
142
+
143
+ BSON::Document.new(attrs)
137
144
  end
138
145
 
139
146
  # Calls #as_json on the document with additional, Mongoid-specific options.
@@ -47,6 +47,11 @@ module Mongoid
47
47
  # @api private
48
48
  INVALID_BSON_CLASSES = [ BSON::Decimal128, BSON::Int32, BSON::Int64 ].freeze
49
49
 
50
+ # The suffix for generated translated fields.
51
+ #
52
+ # @api private
53
+ TRANSLATIONS_SFX = '_translations'
54
+
50
55
  module ClassMethods
51
56
  # Returns the list of id fields for this model class, as both strings
52
57
  # and symbols.
@@ -99,8 +104,8 @@ module Mongoid
99
104
  ar.each_with_index do |fn, i|
100
105
  key = fn
101
106
  unless klass.fields.key?(fn) || klass.relations.key?(fn)
102
- if tr = fn.match(/(.*)_translations\z/)&.captures&.first
103
- key = tr
107
+ if fn.end_with?(TRANSLATIONS_SFX)
108
+ key = fn.delete_suffix(TRANSLATIONS_SFX)
104
109
  else
105
110
  key = fn
106
111
  end
@@ -708,11 +713,11 @@ module Mongoid
708
713
  # @param [ String ] meth The name of the method.
709
714
  def create_translations_getter(name, meth)
710
715
  generated_methods.module_eval do
711
- re_define_method("#{meth}_translations") do
716
+ re_define_method("#{meth}#{TRANSLATIONS_SFX}") do
712
717
  attributes[name] ||= {}
713
718
  attributes[name].with_indifferent_access
714
719
  end
715
- alias_method :"#{meth}_t", :"#{meth}_translations"
720
+ alias_method :"#{meth}_t", :"#{meth}#{TRANSLATIONS_SFX}"
716
721
  end
717
722
  end
718
723
 
@@ -726,14 +731,14 @@ module Mongoid
726
731
  # @param [ Field ] field The field.
727
732
  def create_translations_setter(name, meth, field)
728
733
  generated_methods.module_eval do
729
- re_define_method("#{meth}_translations=") do |value|
734
+ re_define_method("#{meth}#{TRANSLATIONS_SFX}=") do |value|
730
735
  attribute_will_change!(name)
731
736
  value&.transform_values! do |_value|
732
737
  field.type.mongoize(_value)
733
738
  end
734
739
  attributes[name] = value
735
740
  end
736
- alias_method :"#{meth}_t=", :"#{meth}_translations="
741
+ alias_method :"#{meth}_t=", :"#{meth}#{TRANSLATIONS_SFX}="
737
742
  end
738
743
  end
739
744
 
@@ -141,9 +141,13 @@ module Mongoid
141
141
  # @api private
142
142
  def _mongoid_run_child_callbacks(kind, children: nil, &block)
143
143
  if Mongoid::Config.around_callbacks_for_embeds
144
- _mongoid_run_child_callbacks_with_around(kind, children: children, &block)
144
+ _mongoid_run_child_callbacks_with_around(kind,
145
+ children: children,
146
+ &block)
145
147
  else
146
- _mongoid_run_child_callbacks_without_around(kind, children: children, &block)
148
+ _mongoid_run_child_callbacks_without_around(kind,
149
+ children: children,
150
+ &block)
147
151
  end
148
152
  end
149
153
 
@@ -163,13 +167,14 @@ module Mongoid
163
167
  # @api private
164
168
  def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
165
169
  child, *tail = (children || cascadable_children(kind))
170
+ with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
166
171
  if child.nil?
167
172
  block&.call
168
173
  elsif tail.empty?
169
- child.run_callbacks(child_callback_type(kind, child), &block)
174
+ child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
170
175
  else
171
- child.run_callbacks(child_callback_type(kind, child)) do
172
- _mongoid_run_child_callbacks_with_around(kind, children: tail, &block)
176
+ child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
177
+ _mongoid_run_child_callbacks(kind, children: tail, &block)
173
178
  end
174
179
  end
175
180
  end
@@ -218,9 +223,6 @@ module Mongoid
218
223
  return false if env.halted
219
224
  env.value = !env.halted
220
225
  callback_list << [next_sequence, env]
221
- if (grandchildren = child.send(:cascadable_children, kind))
222
- _mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list)
223
- end
224
226
  end
225
227
  callback_list
226
228
  end
@@ -22,13 +22,20 @@ module Mongoid
22
22
  # @example Set the created at time.
23
23
  # person.set_created_at
24
24
  def set_created_at
25
- if !timeless? && !created_at
25
+ if able_to_set_created_at?
26
26
  time = Time.configured.now
27
27
  self.updated_at = time if is_a?(Updated) && !updated_at_changed?
28
28
  self.created_at = time
29
29
  end
30
30
  clear_timeless_option
31
31
  end
32
+
33
+ # Is the created timestamp able to be set?
34
+ #
35
+ # @return [ true, false ] If the timestamp can be set.
36
+ def able_to_set_created_at?
37
+ !frozen? && !timeless? && !created_at
38
+ end
32
39
  end
33
40
  end
34
41
  end
@@ -300,6 +300,18 @@ module Mongoid
300
300
  !!(Mongoid::Document > superclass)
301
301
  end
302
302
 
303
+ # Returns the root class of the STI tree that the current
304
+ # class participates in. If the class is not an STI subclass, this
305
+ # returns the class itself.
306
+ #
307
+ # @return [ Mongoid::Document ] the root of the STI tree
308
+ def root_class
309
+ root = self
310
+ root = root.superclass while root.hereditary?
311
+
312
+ root
313
+ end
314
+
303
315
  # When inheriting, we want to copy the fields from the parent class and
304
316
  # set the on the child to start, mimicking the behavior of the old
305
317
  # class_inheritable_accessor that was deprecated in Rails edge.
@@ -15,32 +15,113 @@ module Mongoid
15
15
  #
16
16
  # validates_associated :name, :addresses
17
17
  # end
18
- class AssociatedValidator < ActiveModel::EachValidator
18
+ class AssociatedValidator < ActiveModel::Validator
19
+ # Required by `validates_with` so that the validator
20
+ # gets added to the correct attributes.
21
+ def attributes
22
+ options[:attributes]
23
+ end
19
24
 
20
- # Validates that the associations provided are either all nil or all
21
- # valid. If neither is true then the appropriate errors will be added to
22
- # the parent document.
25
+ # Checks that the named associations of the given record
26
+ # (`attributes`) are valid. This does NOT load the associations
27
+ # from the database, and will only validate records that are dirty
28
+ # or unpersisted.
23
29
  #
24
- # @example Validate the association.
25
- # validator.validate_each(document, :name, name)
30
+ # If anything is not valid, appropriate errors will be added to
31
+ # the `document` parameter.
32
+ #
33
+ # @param [ Mongoid::Document ] document the document with the
34
+ # associations to validate.
35
+ def validate(document)
36
+ options[:attributes].each do |attr_name|
37
+ validate_association(document, attr_name)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Validates that the given association provided is either nil,
44
+ # persisted and unchanged, or invalid. Otherwise, the appropriate errors
45
+ # will be added to the parent document.
26
46
  #
27
47
  # @param [ Document ] document The document to validate.
28
48
  # @param [ Symbol ] attribute The association to validate.
29
- # @param [ Object ] value The value of the association.
30
- def validate_each(document, attribute, value)
31
- begin
32
- document.begin_validate
33
- valid = Array.wrap(value).collect do |doc|
34
- if doc.nil? || doc.flagged_for_destroy?
35
- true
49
+ def validate_association(document, attribute)
50
+ # grab the proxy from the instance variable directly; we don't want
51
+ # any loading logic to run; we just want to see if it's already
52
+ # been loaded.
53
+ proxy = document.ivar(attribute)
54
+ return unless proxy
55
+
56
+ # if the variable exists, now we see if it is a proxy, or an actual
57
+ # document. It might be a literal document instead of a proxy if this
58
+ # document was created with a Document instance as a provided attribute,
59
+ # e.g. "Post.new(message: Message.new)".
60
+ target = proxy.respond_to?(:_target) ? proxy._target : proxy
61
+
62
+ # Now, fetch the list of documents from the target. Target may be a
63
+ # single value, or a list of values, and in the case of HasMany,
64
+ # might be a rather complex collection. We need to do this without
65
+ # triggering a load, so it's a bit of a delicate dance.
66
+ list = get_target_documents(target)
67
+
68
+ valid = document.validating do
69
+ # Now, treating the target as an array, look at each element
70
+ # and see if it is valid, but only if it has already been
71
+ # persisted, or changed, and hasn't been flagged for destroy.
72
+ #
73
+ # use map.all? instead of just all?, because all? will do short-circuit
74
+ # evaluation and terminate on the first failed validation.
75
+ list.map do |value|
76
+ if value && !value.flagged_for_destroy?
77
+ value.validated? ? true : value.valid?
36
78
  else
37
- doc.validated? ? true : doc.valid?
79
+ true
38
80
  end
39
81
  end.all?
40
- ensure
41
- document.exit_validate
42
82
  end
43
- document.errors.add(attribute, :invalid, **options) unless valid
83
+
84
+ document.errors.add(attribute, :invalid) unless valid
85
+ end
86
+
87
+ private
88
+
89
+ # Examine the given target object and return an array of
90
+ # documents (possibly empty) that the target represents.
91
+ #
92
+ # @param [ Array | Mongoid::Document | Mongoid::Association::Proxy | HasMany::Enumerable ] target
93
+ # the target object to examine.
94
+ #
95
+ # @return [ Array<Mongoid::Document> ] the list of documents
96
+ def get_target_documents(target)
97
+ if target.respond_to?(:_loaded?)
98
+ get_target_documents_for_has_many(target)
99
+ else
100
+ get_target_documents_for_other(target)
101
+ end
102
+ end
103
+
104
+ # Returns the list of all currently in-memory values held by
105
+ # the target. The target will not be loaded.
106
+ #
107
+ # @param [ HasMany::Enumerable ] target the target that will
108
+ # be examined for in-memory documents.
109
+ #
110
+ # @return [ Array<Mongoid::Document> ] the in-memory documents
111
+ # held by the target.
112
+ def get_target_documents_for_has_many(target)
113
+ [ *target._loaded.values, *target._added.values ]
114
+ end
115
+
116
+ # Returns the target as an array. If the target represents a single
117
+ # value, it is wrapped in an array.
118
+ #
119
+ # @param [ Array | Mongoid::Document | Mongoid::Association::Proxy ] target
120
+ # the target to return.
121
+ #
122
+ # @return [ Array<Mongoid::Document> ] the target, as an array.
123
+ def get_target_documents_for_other(target)
124
+ Array.wrap(target)
44
125
  end
45
126
  end
46
127
  end
@@ -37,6 +37,14 @@ module Mongoid
37
37
  Threaded.exit_validate(self)
38
38
  end
39
39
 
40
+ # Perform a validation within the associated block.
41
+ def validating
42
+ begin_validate
43
+ yield
44
+ ensure
45
+ exit_validate
46
+ end
47
+
40
48
  # Given the provided options, are we performing validations?
41
49
  #
42
50
  # @example Are we performing validations?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mongoid
4
- VERSION = "8.0.7"
4
+ VERSION = "8.0.9"
5
5
  end
@@ -2,6 +2,28 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
+ module HabtmSpec
6
+ class Page
7
+ include Mongoid::Document
8
+ embeds_many :blocks, class_name: 'HabtmSpec::Block'
9
+ end
10
+
11
+ class Block
12
+ include Mongoid::Document
13
+ embedded_in :page, class_name: 'HabtmSpec::Page'
14
+ end
15
+
16
+ class ImageBlock < Block
17
+ has_and_belongs_to_many :attachments, inverse_of: nil, class_name: 'HabtmSpec::Attachment'
18
+ accepts_nested_attributes_for :attachments
19
+ end
20
+
21
+ class Attachment
22
+ include Mongoid::Document
23
+ field :file, type: String
24
+ end
25
+ end
26
+
5
27
  describe 'has_and_belongs_to_many associations' do
6
28
 
7
29
  context 'when an anonymous class defines a has_and_belongs_to_many association' do
@@ -18,4 +40,22 @@ describe 'has_and_belongs_to_many associations' do
18
40
  expect(klass.new.movies.build).to be_a Movie
19
41
  end
20
42
  end
43
+
44
+ context 'when an embedded has habtm relation' do
45
+ let(:attachment) { HabtmSpec::Attachment.create!(file: 'foo.jpg') }
46
+
47
+ let(:page) { HabtmSpec::Page.create! }
48
+
49
+ let(:image_block) do
50
+ image_block = page.blocks.build({
51
+ _type: 'HabtmSpec::ImageBlock',
52
+ attachment_ids: [ attachment.id.to_s ],
53
+ attachments_attributes: { '1234' => { file: 'bar.jpg', id: attachment.id.to_s } }
54
+ })
55
+ end
56
+
57
+ it 'does not raise on save' do
58
+ expect { image_block.save! }.not_to raise_error
59
+ end
60
+ end
21
61
  end
@@ -153,3 +153,40 @@ class Building
153
153
 
154
154
  has_and_belongs_to_many :architects, dependent: :nullify
155
155
  end
156
+
157
+ class Root
158
+ include Mongoid::Document
159
+ embeds_many :embedded_once, cascade_callbacks: true
160
+ after_save :trace
161
+
162
+ attr_accessor :logger
163
+
164
+ def trace
165
+ logger << :root
166
+ end
167
+ end
168
+
169
+ class EmbeddedOnce
170
+ include Mongoid::Document
171
+ embeds_many :embedded_twice, cascade_callbacks: true
172
+ embedded_in :root
173
+ after_save :trace
174
+
175
+ attr_accessor :logger
176
+
177
+ def trace
178
+ logger << :embedded_once
179
+ end
180
+ end
181
+
182
+ class EmbeddedTwice
183
+ include Mongoid::Document
184
+ embedded_in :embedded_once
185
+ after_save :trace
186
+
187
+ attr_accessor :logger
188
+
189
+ def trace
190
+ logger << :embedded_twice
191
+ end
192
+ end