mongoid 9.0.1 → 9.0.2

Sign up to get free protection for your applications and to get access to all the features.
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