mongoid 9.0.1 → 9.0.2

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +16 -0
  3. data/lib/mongoid/association/accessors.rb +7 -2
  4. data/lib/mongoid/association/nested/one.rb +14 -1
  5. data/lib/mongoid/association/referenced/belongs_to/binding.rb +7 -1
  6. data/lib/mongoid/association/referenced/belongs_to/buildable.rb +1 -1
  7. data/lib/mongoid/association/referenced/belongs_to.rb +15 -0
  8. data/lib/mongoid/association/referenced/has_many.rb +9 -8
  9. data/lib/mongoid/association/referenced/has_one/buildable.rb +3 -8
  10. data/lib/mongoid/association/referenced/with_polymorphic_criteria.rb +41 -0
  11. data/lib/mongoid/attributes/nested.rb +2 -1
  12. data/lib/mongoid/clients/sessions.rb +12 -15
  13. data/lib/mongoid/composable.rb +2 -0
  14. data/lib/mongoid/document.rb +2 -0
  15. data/lib/mongoid/errors/unrecognized_model_alias.rb +53 -0
  16. data/lib/mongoid/errors/unrecognized_resolver.rb +27 -0
  17. data/lib/mongoid/errors/unregistered_class.rb +47 -0
  18. data/lib/mongoid/errors.rb +3 -0
  19. data/lib/mongoid/identifiable.rb +28 -0
  20. data/lib/mongoid/model_resolver.rb +154 -0
  21. data/lib/mongoid/persistence_context.rb +2 -1
  22. data/lib/mongoid/traversable.rb +5 -0
  23. data/lib/mongoid/version.rb +1 -1
  24. data/spec/integration/associations/belongs_to_spec.rb +129 -0
  25. data/spec/integration/persistence/collection_options_spec.rb +36 -0
  26. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +4 -0
  27. data/spec/mongoid/association/referenced/belongs_to/proxy_spec.rb +1 -0
  28. data/spec/mongoid/association/referenced/belongs_to_spec.rb +58 -21
  29. data/spec/mongoid/association/referenced/has_many/buildable_spec.rb +4 -0
  30. data/spec/mongoid/attributes/nested_spec.rb +1 -0
  31. data/spec/mongoid/clients/transactions_spec.rb +2 -2
  32. data/spec/mongoid/model_resolver_spec.rb +167 -0
  33. data/spec/mongoid/monkey_patches_spec.rb +1 -1
  34. data/spec/mongoid/persistence_context_spec.rb +17 -4
  35. data/spec/mongoid/serializable_spec.rb +16 -9
  36. metadata +14 -4
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Mongoid
6
+ # The default class for resolving model classes based on discriminant keys.
7
+ # Given a key, it will return the corresponding model class, if any. By
8
+ # default, it looks for classes with names that match the given keys, but
9
+ # additional mappings may be provided.
10
+ #
11
+ # It is also possible to instantiate multiple resolvers---and even implement
12
+ # your own---so that different sets of classes can use independent resolution
13
+ # mechanics.
14
+ class ModelResolver
15
+ # The mutex instance used to make the `.instance` method thread-safe.
16
+ #
17
+ # @api private
18
+ INSTANCE_MUTEX = Mutex.new
19
+
20
+ class << self
21
+ extend Forwardable
22
+ def_delegators :instance, :register
23
+
24
+ # Returns the default instance of the ModelResolver.
25
+ #
26
+ # @return [ Mongoid::ModelResolver ] the default ModelResolver instance.
27
+ def instance
28
+ @instance ||= INSTANCE_MUTEX.synchronize { @instance ||= new }
29
+ end
30
+
31
+ # Returns the map of registered resolvers. The default resolver is not
32
+ # included here.
33
+ #
34
+ # @return [ Hash<Symbol => Mongoid::ModelResolver::Interface> ] the hash of
35
+ # resolver instances, mapped by symbol identifier.
36
+ def resolvers
37
+ @resolvers ||= {}
38
+ end
39
+
40
+ # Returns the resolver instance that corresponds to the argument.
41
+ #
42
+ # @param [ nil | true | false Symbol | String | Mongoid::ModelResolver::Interface ] identifier_or_object
43
+ # When nil or false, returns nil. When true or :default, corresponds to the default resolver.
44
+ # When any other symbol or string, corresponds to the registered resolver with that identifier.
45
+ # Otherwise, it must be a resolver instance itself.
46
+ #
47
+ # @raise Mongoid::Errors::UnrecognizedResolver if the given identifier is a
48
+ # symbol or string and it does not match any registered resolver.
49
+ #
50
+ # @return [ Mongoid::ModelResolver::Interface ] the resolver instance corresponding to the
51
+ # given argument.
52
+ def resolver(identifier_or_object = :default)
53
+ case identifier_or_object
54
+ when nil, false then nil
55
+ when true, :default then instance
56
+ when String, Symbol
57
+ resolvers.fetch(identifier_or_object.to_sym) do |key|
58
+ raise Mongoid::Errors::UnrecognizedResolver, key
59
+ end
60
+ else identifier_or_object
61
+ end
62
+ end
63
+
64
+ # Register the given resolver under the given name.
65
+ #
66
+ # @param [ Mongoid::ModelResolver::Interface ] resolver the resolver to register.
67
+ # @param [ String | Symbol ] name the identifier to use to register the resolver.
68
+ def register_resolver(resolver, name)
69
+ resolvers[name.to_sym] = resolver
70
+ self
71
+ end
72
+ end
73
+
74
+ # Instantiates a new ModelResolver instance.
75
+ def initialize
76
+ @key_to_model = {}
77
+ @model_to_keys = {}
78
+ end
79
+
80
+ # Registers the given model class with the given keys. In addition to the given keys, the
81
+ # class name itself will be included as a key to identify the class. Keys are given in priority
82
+ # order, with highest priority keys first and lowest last. The class name, if not given explicitly,
83
+ # is always given lowest priority.
84
+ #
85
+ # If called more than once, newer keys have higher priority than older keys. All duplicate keys will
86
+ # be removed.
87
+ #
88
+ # @param [ Mongoid::Document ] klass the document class to register
89
+ # @param [ Array<String> ] *keys the list of keys to use as an alias (optional)
90
+ def register(klass, *keys)
91
+ default_key = klass.name
92
+
93
+ @model_to_keys[klass] = [ *keys, *@model_to_keys[klass], default_key ].uniq
94
+ @key_to_model[default_key] = klass
95
+
96
+ keys.each do |key|
97
+ @key_to_model[key] = klass
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ # The `Interface` concern represents the interface that custom resolvers
104
+ # must implement.
105
+ concerning :Interface do
106
+ # Returns the default (highest priority) key for the given record. This is typically
107
+ # the key that will be used when saving a new polymorphic association.
108
+ #
109
+ # @param [ Mongoid::Document ] record the record instance for which to query the default key.
110
+ #
111
+ # @raise Mongoid::Errors::UnregisteredClass if the record's class has not been registered with this resolver.
112
+ #
113
+ # @return [ String ] the default key for the record's class.
114
+ def default_key_for(record)
115
+ keys_for(record).first
116
+ end
117
+
118
+ # Returns the list of all keys for the given record's class, in priority order (with highest
119
+ # priority keys first).
120
+ #
121
+ # @param [ Mongoid::Document] record the record instance for which to query the registered keys.
122
+ #
123
+ # @raise Mongoid::Errors::UnregisteredClass if the record's class has not been registered with this resolver.
124
+ #
125
+ # @return [ Array<String> ] the list of keys that have been registered for the given class.
126
+ def keys_for(record)
127
+ @model_to_keys.fetch(record.class) do |klass|
128
+ # figure out which resolver this is
129
+ resolver = if self == Mongoid::ModelResolver.instance
130
+ :default
131
+ else
132
+ Mongoid::ModelResolver.resolvers.keys.detect { |k| Mongoid::ModelResolver.resolvers[k] == self }
133
+ end
134
+ resolver ||= self # if it hasn't been registered, we'll show it the best we can
135
+ raise Mongoid::Errors::UnregisteredClass.new(klass, resolver)
136
+ end
137
+ end
138
+
139
+ # Returns the document class that has been registered by the given key.
140
+ #
141
+ # @param [ String ] key the key by which to query the corresponding class.
142
+ #
143
+ # @raise Mongoid::Errors::UnrecognizedModelAlias if the given key has not
144
+ # been registered with this resolver.
145
+ #
146
+ # @return [ Class ] the document class that has been registered with the given key.
147
+ def model_for(key)
148
+ @key_to_model.fetch(key) do
149
+ raise Mongoid::Errors::UnrecognizedModelAlias, key
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -25,7 +25,8 @@ module Mongoid
25
25
  # @return [ Array<Symbol> ] The list of extra options besides client options
26
26
  # that determine the persistence context.
27
27
  EXTRA_OPTIONS = [ :client,
28
- :collection
28
+ :collection,
29
+ :collection_options
29
30
  ].freeze
30
31
 
31
32
  # The full list of valid persistence context options.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'mongoid/fields/validators/macro'
4
+ require 'mongoid/model_resolver'
4
5
 
5
6
  module Mongoid
6
7
  # Mixin module included in Mongoid::Document to provide behavior
@@ -32,6 +33,10 @@ module Mongoid
32
33
  # rubocop:disable Metrics/AbcSize
33
34
  def inherited(subclass)
34
35
  super
36
+
37
+ # Register the new subclass with the resolver subsystem
38
+ Mongoid::ModelResolver.register(subclass)
39
+
35
40
  @_type = nil
36
41
  subclass.aliased_fields = aliased_fields.dup
37
42
  subclass.localized_fields = localized_fields.dup
@@ -2,5 +2,5 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  module Mongoid
5
- VERSION = "9.0.1"
5
+ VERSION = "9.0.2"
6
6
  end
@@ -2,8 +2,40 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require 'spec_helper'
5
+ require 'support/feature_sandbox'
6
+
5
7
  require_relative '../../mongoid/association/referenced/has_one_models'
6
8
 
9
+ def quarantine(context, polymorphic:, dept_aliases:, team_aliases:)
10
+ state = {}
11
+
12
+ context.before(:context) do
13
+ state[:quarantine] = FeatureSandbox.start_quarantine
14
+
15
+ # Have to eval this, because otherwise we get syntax errors when defining a class
16
+ # inside a method.
17
+ #
18
+ # I know the scissors are sharp! But I want to run with them anwyay!
19
+ Object.class_eval <<-RUBY
20
+ class SandboxManager; include Mongoid::Document; end
21
+ class SandboxDepartment; include Mongoid::Document; end
22
+ class SandboxTeam; include Mongoid::Document; end
23
+ RUBY
24
+
25
+ SandboxManager.belongs_to :unit, polymorphic: polymorphic
26
+
27
+ SandboxDepartment.identify_as *dept_aliases, resolver: polymorphic
28
+ SandboxDepartment.has_many :sandbox_managers, as: :unit
29
+
30
+ SandboxTeam.identify_as *team_aliases, resolver: polymorphic
31
+ SandboxTeam.has_one :sandbox_manager, as: :unit
32
+ end
33
+
34
+ context.after(:context) do
35
+ FeatureSandbox.end_quarantine(state[:quarantine])
36
+ end
37
+ end
38
+
7
39
  describe 'belongs_to associations' do
8
40
  context 'referencing top level classes when source class is namespaced' do
9
41
  let(:college) { HomCollege.create! }
@@ -31,4 +63,101 @@ describe 'belongs_to associations' do
31
63
  expect(instance.movie).to eq movie
32
64
  end
33
65
  end
66
+
67
+ context 'when the association is polymorphic' do
68
+ let(:dept_manager) { SandboxManager.create(unit: department) }
69
+ let(:team_manager) { SandboxManager.create(unit: team) }
70
+ let(:department) { SandboxDepartment.create }
71
+ let(:team) { SandboxTeam.create }
72
+
73
+ shared_context 'it finds the associated records' do
74
+ it 'successfully finds the manager\'s unit' do
75
+ expect(dept_manager.reload.unit).to be == department
76
+ expect(team_manager.reload.unit).to be == team
77
+ end
78
+
79
+ it 'successfully finds the unit\'s manager' do
80
+ dept_manager; team_manager # make sure these are created first...
81
+
82
+ expect(department.reload.sandbox_managers).to be == [ dept_manager ]
83
+ expect(team.reload.sandbox_manager).to be == team_manager
84
+ end
85
+ end
86
+
87
+ shared_context 'it searches for alternative aliases' do
88
+ it 'successfully finds the corresponding unit when unit_type is a different alias' do
89
+ dept_manager.update unit_type: 'sandbox_dept'
90
+ dept_manager.reload
91
+
92
+ team_manager.update unit_type: 'group'
93
+ team_manager.reload
94
+
95
+ expect(dept_manager.reload.unit_type).to be == 'sandbox_dept'
96
+ expect(dept_manager.unit).to be == department
97
+
98
+ expect(team_manager.reload.unit_type).to be == 'group'
99
+ expect(team_manager.unit).to be == team
100
+ end
101
+ end
102
+
103
+ context 'when the association uses the default resolver' do
104
+ context 'when there are no aliases given' do
105
+ quarantine(self, polymorphic: true, dept_aliases: [], team_aliases: [])
106
+
107
+ it 'populates the unit_type with the class name' do
108
+ expect(dept_manager.unit_type).to be == 'SandboxDepartment'
109
+ expect(team_manager.unit_type).to be == 'SandboxTeam'
110
+ end
111
+
112
+ it_behaves_like 'it finds the associated records'
113
+ end
114
+
115
+ context 'when there are multiple aliases given' do
116
+ quarantine(self, polymorphic: true, dept_aliases: %w[ dept sandbox_dept ], team_aliases: %w[ team group ])
117
+
118
+ it 'populates the unit_type with the first alias' do
119
+ expect(dept_manager.unit_type).to be == 'dept'
120
+ expect(team_manager.unit_type).to be == 'team'
121
+ end
122
+
123
+ it_behaves_like 'it finds the associated records'
124
+ it_behaves_like 'it searches for alternative aliases'
125
+ end
126
+ end
127
+
128
+ context 'when the association uses a registered resolver' do
129
+ before(:context) { Mongoid::ModelResolver.register_resolver Mongoid::ModelResolver.new, :sandbox }
130
+ quarantine(self, polymorphic: :sandbox, dept_aliases: %w[ dept sandbox_dept ], team_aliases: %w[ team group ])
131
+
132
+ it 'does not include the aliases in the default resolver' do
133
+ expect(Mongoid::ModelResolver.instance.keys_for(SandboxDepartment.new)).not_to include('dept')
134
+ end
135
+
136
+ it 'populates the unit_type with the first alias' do
137
+ expect(dept_manager.unit_type).to be == 'dept'
138
+ expect(team_manager.unit_type).to be == 'team'
139
+ end
140
+
141
+ it_behaves_like 'it finds the associated records'
142
+ it_behaves_like 'it searches for alternative aliases'
143
+ end
144
+
145
+ context 'when the association uses an unregistered resolver' do
146
+ quarantine(self, polymorphic: Mongoid::ModelResolver.new,
147
+ dept_aliases: %w[ dept sandbox_dept ],
148
+ team_aliases: %w[ team group ])
149
+
150
+ it 'does not include the aliases in the default resolver' do
151
+ expect(Mongoid::ModelResolver.instance.keys_for(SandboxDepartment.new)).not_to include('dept')
152
+ end
153
+
154
+ it 'populates the unit_type with the first alias' do
155
+ expect(dept_manager.unit_type).to be == 'dept'
156
+ expect(team_manager.unit_type).to be == 'team'
157
+ end
158
+
159
+ it_behaves_like 'it finds the associated records'
160
+ it_behaves_like 'it searches for alternative aliases'
161
+ end
162
+ end
34
163
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # rubocop:disable RSpec/LeakyConstantDeclaration
6
+ # rubocop:disable Lint/ConstantDefinitionInBlock
7
+ describe 'Collection options' do
8
+ before(:all) do
9
+ class CollectionOptionsCapped
10
+ include Mongoid::Document
11
+
12
+ store_in collection_options: {
13
+ capped: true,
14
+ size: 25_600
15
+ }
16
+ end
17
+ end
18
+
19
+ after(:all) do
20
+ CollectionOptionsCapped.collection.drop
21
+ Mongoid.deregister_model(CollectionOptionsCapped)
22
+ Object.send(:remove_const, :CollectionOptionsCapped)
23
+ end
24
+
25
+ before do
26
+ CollectionOptionsCapped.collection.drop
27
+ # We should create the collection explicitly to apply collection options.
28
+ CollectionOptionsCapped.create_collection
29
+ end
30
+
31
+ it 'creates a document' do
32
+ expect { CollectionOptionsCapped.create! }.not_to raise_error
33
+ end
34
+ end
35
+ # rubocop:enable Lint/ConstantDefinitionInBlock
36
+ # rubocop:enable RSpec/LeakyConstantDeclaration
@@ -28,6 +28,10 @@ describe Mongoid::Association::Embedded::EmbedsMany do
28
28
  expect(legislator.attributes.keys).to eq(['_id', 'a'])
29
29
  end
30
30
 
31
+ it 'allows accessing the parent' do
32
+ expect { legislator.congress }.not_to raise_error
33
+ end
34
+
31
35
  context 'when using only with $' do
32
36
  before do
33
37
  Patient.destroy_all
@@ -2,6 +2,7 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require "spec_helper"
5
+ require 'support/models/canvas'
5
6
  require_relative '../belongs_to_models.rb'
6
7
 
7
8
  describe Mongoid::Association::Referenced::BelongsTo::Proxy do
@@ -4,6 +4,10 @@
4
4
  require "spec_helper"
5
5
  require_relative './has_one_models'
6
6
 
7
+ BELONGS_TO_RESOLVER_ID__ = :__belongs_to_resolver_id
8
+ BELONGS_TO_RESOLVER = Mongoid::ModelResolver.new
9
+ Mongoid::ModelResolver.register_resolver BELONGS_TO_RESOLVER, BELONGS_TO_RESOLVER_ID__
10
+
7
11
  describe Mongoid::Association::Referenced::BelongsTo do
8
12
 
9
13
  before do
@@ -199,47 +203,76 @@ describe Mongoid::Association::Referenced::BelongsTo do
199
203
 
200
204
  context 'when the polymorphic option is provided' do
201
205
 
202
- context 'when the polymorphic option is true' do
206
+ [ true, :default ].each do |opt|
207
+ context "when the polymorphic option is #{opt.inspect}" do
208
+ let(:options) { { polymorphic: opt } }
209
+ before { association }
203
210
 
204
- let(:options) do
205
- {
206
- polymorphic: true
207
- }
211
+ it 'set the polymorphic attribute on the owner class' do
212
+ expect(belonging_class.polymorphic).to be(true)
213
+ end
214
+
215
+ it 'sets up a field for the inverse type' do
216
+ expect(belonging_class.fields.keys).to include(association.inverse_type)
217
+ end
218
+
219
+ it 'uses the default resolver' do
220
+ expect(association.resolver).to be == Mongoid::ModelResolver.instance
221
+ end
208
222
  end
223
+ end
209
224
 
210
- before do
211
- association
225
+ [ false, nil ].each do |opt|
226
+ context "when the polymorphic option is #{opt.inspect}" do
227
+ let(:options) { { polymorphic: opt } }
228
+
229
+ it 'does not set the polymorphic attribute on the owner class' do
230
+ expect(belonging_class.polymorphic).to be(false)
231
+ end
232
+
233
+ it 'does not set up a field for the inverse type' do
234
+ expect(belonging_class.fields.keys).not_to include(association.inverse_type)
235
+ end
236
+
237
+ it 'does not use a resolver' do
238
+ expect(association.resolver).to be_nil
239
+ end
212
240
  end
241
+ end
213
242
 
214
- it 'set the polymorphic attribute on the owner class' do
215
- expect(belonging_class.polymorphic).to be(true)
243
+ context 'when the polymorphic option is set to an unregistered id' do
244
+ let(:options) { { polymorphic: :bogus } }
245
+
246
+ # This behavior is intentional, so that the resolver can be registered after the classes
247
+ # are loaded.
248
+ it 'does not immediately raise an exception' do
249
+ expect { association }.not_to raise_error
216
250
  end
217
251
 
218
- it 'sets up a field for the inverse type' do
219
- expect(belonging_class.fields.keys).to include(association.inverse_type)
252
+ it 'raises error when resolver is accessed' do
253
+ expect { association.resolver }.to raise_error(Mongoid::Errors::UnrecognizedResolver)
220
254
  end
221
255
  end
222
256
 
223
- context 'when the polymorphic option is false' do
257
+ context 'when the polymorphic option is set to a registered id' do
258
+ let(:options) { { polymorphic: BELONGS_TO_RESOLVER_ID__ } }
259
+ before { association }
224
260
 
225
- let(:options) do
226
- {
227
- polymorphic: false
228
- }
261
+ it 'set the polymorphic attribute on the owner class' do
262
+ expect(belonging_class.polymorphic).to be(true)
229
263
  end
230
264
 
231
- it 'does not set the polymorphic attribute on the owner class' do
232
- expect(belonging_class.polymorphic).to be(false)
265
+ it 'sets up a field for the inverse type' do
266
+ expect(belonging_class.fields.keys).to include(association.inverse_type)
233
267
  end
234
268
 
235
- it 'does not set up a field for the inverse type' do
236
- expect(belonging_class.fields.keys).not_to include(association.inverse_type)
269
+ it 'connects the association to the corresponding resolver' do
270
+ expect(association.resolver).to be == BELONGS_TO_RESOLVER
237
271
  end
238
272
  end
239
273
  end
240
274
 
241
275
  context 'when the polymorphic option is not provided' do
242
-
243
276
  it 'does not set the polymorphic attribute on the owner class' do
244
277
  expect(belonging_class.polymorphic).to be(false)
245
278
  end
@@ -247,6 +280,10 @@ describe Mongoid::Association::Referenced::BelongsTo do
247
280
  it 'does not set up a field for the inverse type' do
248
281
  expect(belonging_class.fields.keys).not_to include(association.inverse_type)
249
282
  end
283
+
284
+ it 'does not use a resolver' do
285
+ expect(association.resolver).to be_nil
286
+ end
250
287
  end
251
288
  end
252
289
 
@@ -100,6 +100,10 @@ describe Mongoid::Association::Referenced::HasMany::Buildable do
100
100
  Post.where(association.foreign_key => object, 'ratable_type' => 'Rating')
101
101
  end
102
102
 
103
+ before do
104
+ Post.belongs_to :ratable, polymorphic: true
105
+ end
106
+
103
107
  it "adds the type to the criteria" do
104
108
  expect(documents).to eq(criteria)
105
109
  end
@@ -2,6 +2,7 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require "spec_helper"
5
+ require 'support/models/sandwich'
5
6
  require_relative '../association/referenced/has_many_models'
6
7
  require_relative '../association/referenced/has_and_belongs_to_many_models'
7
8
  require_relative './nested_spec_models'
@@ -282,7 +282,7 @@ describe Mongoid::Clients::Sessions do
282
282
  end
283
283
  end
284
284
 
285
- include_examples 'it aborts the transaction', Mongoid::Errors::InvalidTransactionNesting
285
+ include_examples 'it aborts the transaction', Mongoid::Errors::TransactionError
286
286
  end
287
287
  end
288
288
  end
@@ -591,7 +591,7 @@ describe Mongoid::Clients::Sessions do
591
591
  end
592
592
 
593
593
  it 'raises an error' do
594
- expect(error).to be_a(Mongoid::Errors::InvalidTransactionNesting)
594
+ expect(error).to be_a(Mongoid::Errors::TransactionError)
595
595
  end
596
596
 
597
597
  it 'does not execute any operations' do