Skip to content

Commit 5dff6d6

Browse files
authored
MONGOID-5654: refactor and deprecate Hash#__consolidate__ (#5740)
* refactor and deprecate Hash#__consolidate__ * fix linter complaints * another minor refactoring for optimization * remove pre-refactoring docs * fix failing specs
1 parent 67c8035 commit 5dff6d6

File tree

5 files changed

+181
-121
lines changed

5 files changed

+181
-121
lines changed

lib/mongoid/atomic_update_preparer.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
# A singleton class to assist with preparing attributes for atomic
5+
# updates.
6+
#
7+
# Once the deprecated Hash#__consolidate__ method is removed entirely,
8+
# these methods may be moved into Mongoid::Contextual::Mongo as private
9+
# methods.
10+
#
11+
# @api private
12+
class AtomicUpdatePreparer
13+
class << self
14+
# Convert the key/values in the attributes into a hash of atomic updates.
15+
# Non-operator keys are assumed to use $set operation.
16+
#
17+
# @param [ Class ] klass The model class.
18+
# @param [ Hash ] attributes The attributes to convert.
19+
#
20+
# @return [ Hash ] The prepared atomic updates.
21+
def prepare(attributes, klass)
22+
attributes.each_pair.with_object({}) do |(key, value), atomic_updates|
23+
key = klass.database_field_name(key.to_s)
24+
25+
if key.to_s.start_with?('$')
26+
(atomic_updates[key] ||= {}).update(prepare_operation(klass, key, value))
27+
else
28+
(atomic_updates['$set'] ||= {})[key] = mongoize_for(key, klass, key, value)
29+
end
30+
end
31+
end
32+
33+
private
34+
35+
# Treats the key as if it were a MongoDB operator and prepares
36+
# the value accordingly.
37+
#
38+
# @param [ Class ] klass the model class
39+
# @param [ String | Symbol ] key the operator
40+
# @param [ Hash ] value the operand
41+
#
42+
# @return [ Hash ] the prepared value.
43+
def prepare_operation(klass, key, value)
44+
value.each_with_object({}) do |(key2, value2), hash|
45+
key2 = klass.database_field_name(key2)
46+
hash[key2] = value_for(key, klass, value2)
47+
end
48+
end
49+
50+
# Get the value for the provided operator, klass, key and value.
51+
#
52+
# This is necessary for special cases like $rename, $addToSet and $push.
53+
#
54+
# @param [ String ] operator The operator.
55+
# @param [ Class ] klass The model class.
56+
# @param [ Object ] value The original value.
57+
#
58+
# @return [ Object ] Value prepared for the provided operator.
59+
def value_for(operator, klass, value)
60+
case operator
61+
when '$rename' then value.to_s
62+
when '$addToSet', '$push' then value.mongoize
63+
else mongoize_for(operator, klass, operator, value)
64+
end
65+
end
66+
67+
# Mongoize for the klass, key and value.
68+
#
69+
# @param [ String ] operator The operator.
70+
# @param [ Class ] klass The model class.
71+
# @param [ String | Symbol ] key The field key.
72+
# @param [ Object ] value The value to mongoize.
73+
#
74+
# @return [ Object ] The mongoized value.
75+
def mongoize_for(operator, klass, key, value)
76+
field = klass.fields[key.to_s]
77+
return value unless field
78+
79+
mongoized = field.mongoize(value)
80+
if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable? && !value.is_a?(Array)
81+
return mongoized.first
82+
end
83+
84+
mongoized
85+
end
86+
end
87+
end
88+
end

lib/mongoid/contextual/mongo.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22
# rubocop:todo all
33

4+
require 'mongoid/atomic_update_preparer'
45
require "mongoid/contextual/mongo/documents_loader"
56
require "mongoid/contextual/atomic"
67
require "mongoid/contextual/aggregable/mongo"
@@ -817,8 +818,8 @@ def load_async
817818
# @return [ true | false ] If the update succeeded.
818819
def update_documents(attributes, method = :update_one, opts = {})
819820
return false unless attributes
820-
attributes = Hash[attributes.map { |k, v| [klass.database_field_name(k.to_s), v] }]
821-
view.send(method, attributes.__consolidate__(klass), opts)
821+
822+
view.send(method, AtomicUpdatePreparer.prepare(attributes, klass), opts)
822823
end
823824

824825
# Apply the field limitations.

lib/mongoid/extensions/hash.rb

Lines changed: 7 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,20 @@ def __mongoize_object_id__
3232
end
3333

3434
# Consolidate the key/values in the hash under an atomic $set.
35+
# DEPRECATED. This was never intended to be a public API and
36+
# the functionality will no longer be exposed once this method
37+
# is eventually removed.
3538
#
3639
# @example Consolidate the hash.
3740
# { name: "Placebo" }.__consolidate__
3841
#
3942
# @return [ Hash ] A new consolidated hash.
43+
#
44+
# @deprecated
4045
def __consolidate__(klass)
41-
consolidated = {}
42-
each_pair do |key, value|
43-
if key =~ /\$/
44-
value.keys.each do |key2|
45-
value2 = value[key2]
46-
real_key = klass.database_field_name(key2)
47-
48-
value.delete(key2) if real_key != key2
49-
value[real_key] = value_for(key, klass, real_key, value2)
50-
end
51-
consolidated[key] ||= {}
52-
consolidated[key].update(value)
53-
else
54-
consolidated["$set"] ||= {}
55-
consolidated["$set"].update(key => mongoize_for(key, klass, key, value))
56-
end
57-
end
58-
consolidated
46+
Mongoid::AtomicUpdatePreparer.prepare(self, klass)
5947
end
48+
Mongoid.deprecate(self, :__consolidate__)
6049

6150
# Checks whether conditions given in this hash are known to be
6251
# unsatisfiable, i.e., querying with this hash will always return no
@@ -166,50 +155,6 @@ def to_criteria
166155

167156
private
168157

169-
# Get the value for the provided operator, klass, key and value.
170-
#
171-
# This is necessary for special cases like $rename, $addToSet and $push.
172-
#
173-
# @param [ String ] operator The operator.
174-
# @param [ Class ] klass The model class.
175-
# @param [ String | Symbol ] key The field key.
176-
# @param [ Object ] value The original value.
177-
#
178-
# @return [ Object ] Value prepared for the provided operator.
179-
def value_for(operator, klass, key, value)
180-
case operator
181-
when "$rename" then value.to_s
182-
when "$addToSet", "$push" then value.mongoize
183-
else mongoize_for(operator, klass, operator, value)
184-
end
185-
end
186-
187-
# Mongoize for the klass, key and value.
188-
#
189-
# @api private
190-
#
191-
# @example Mongoize for the klass, field and value.
192-
# {}.mongoize_for("$push", Band, "name", "test")
193-
#
194-
# @param [ String ] operator The operator.
195-
# @param [ Class ] klass The model class.
196-
# @param [ String | Symbol ] key The field key.
197-
# @param [ Object ] value The value to mongoize.
198-
#
199-
# @return [ Object ] The mongoized value.
200-
def mongoize_for(operator, klass, key, value)
201-
field = klass.fields[key.to_s]
202-
if field
203-
val = field.mongoize(value)
204-
if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable?
205-
val = val.first if !value.is_a?(Array)
206-
end
207-
val
208-
else
209-
value
210-
end
211-
end
212-
213158
module ClassMethods
214159

215160
# Turn the object from the ruby type we deal with to a Mongo friendly
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Mongoid::AtomicUpdatePreparer do
6+
describe '#prepare' do
7+
let(:prepared) { described_class.prepare(hash, Band) }
8+
9+
context 'when the hash already contains $set' do
10+
context 'when the $set is first' do
11+
let(:hash) do
12+
{ '$set' => { name: 'Tool' }, likes: 10, '$inc' => { plays: 1 } }
13+
end
14+
15+
it 'moves the non hash values under the provided key' do
16+
expect(prepared).to eq(
17+
'$set' => { 'name' => 'Tool', 'likes' => 10 },
18+
'$inc' => { 'plays' => 1 }
19+
)
20+
end
21+
end
22+
23+
context 'when the $set is not first' do
24+
let(:hash) do
25+
{ likes: 10, '$inc' => { plays: 1 }, '$set' => { name: 'Tool' } }
26+
end
27+
28+
it 'moves the non hash values under the provided key' do
29+
expect(prepared).to eq(
30+
'$set' => { 'likes' => 10, 'name' => 'Tool' },
31+
'$inc' => { 'plays' => 1 }
32+
)
33+
end
34+
end
35+
end
36+
37+
context 'when the hash does not contain $set' do
38+
let(:hash) do
39+
{ likes: 10, '$inc' => { plays: 1 }, name: 'Tool' }
40+
end
41+
42+
it 'moves the non hash values under the provided key' do
43+
expect(prepared).to eq(
44+
'$set' => { 'likes' => 10, 'name' => 'Tool' },
45+
'$inc' => { 'plays' => 1 }
46+
)
47+
end
48+
end
49+
50+
context 'when the hash contains $rename' do
51+
let(:hash) { { likes: 10, '$rename' => { old: 'new' } } }
52+
53+
it 'preserves the $rename operator' do
54+
expect(prepared).to eq(
55+
'$set' => { 'likes' => 10 },
56+
'$rename' => { 'old' => 'new' }
57+
)
58+
end
59+
end
60+
61+
context 'when the hash contains $addToSet' do
62+
let(:hash) { { likes: 10, '$addToSet' => { list: 'new' } } }
63+
64+
it 'preserves the $addToSet operator' do
65+
expect(prepared).to eq(
66+
'$set' => { 'likes' => 10 },
67+
'$addToSet' => { 'list' => 'new' }
68+
)
69+
end
70+
end
71+
72+
context 'when the hash contains $push' do
73+
let(:hash) { { likes: 10, '$push' => { list: 14 } } }
74+
75+
it 'preserves the $push operator' do
76+
expect(prepared).to eq(
77+
'$set' => { 'likes' => 10 },
78+
'$push' => { 'list' => 14 }
79+
)
80+
end
81+
end
82+
end
83+
end

spec/mongoid/extensions/hash_spec.rb

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -163,63 +163,6 @@
163163
end
164164
end
165165

166-
describe "#__consolidate__" do
167-
168-
context "when the hash already contains the key" do
169-
170-
context "when the $set is first" do
171-
172-
let(:hash) do
173-
{ "$set" => { name: "Tool" }, likes: 10, "$inc" => { plays: 1 }}
174-
end
175-
176-
let(:consolidated) do
177-
hash.__consolidate__(Band)
178-
end
179-
180-
it "moves the non hash values under the provided key" do
181-
expect(consolidated).to eq({
182-
"$set" => { 'name' => "Tool", likes: 10 }, "$inc" => { 'plays' => 1 }
183-
})
184-
end
185-
end
186-
187-
context "when the $set is not first" do
188-
189-
let(:hash) do
190-
{ likes: 10, "$inc" => { plays: 1 }, "$set" => { name: "Tool" }}
191-
end
192-
193-
let(:consolidated) do
194-
hash.__consolidate__(Band)
195-
end
196-
197-
it "moves the non hash values under the provided key" do
198-
expect(consolidated).to eq({
199-
"$set" => { likes: 10, 'name' => "Tool" }, "$inc" => { 'plays' => 1 }
200-
})
201-
end
202-
end
203-
end
204-
205-
context "when the hash does not contain the key" do
206-
207-
let(:hash) do
208-
{ likes: 10, "$inc" => { plays: 1 }, name: "Tool"}
209-
end
210-
211-
let(:consolidated) do
212-
hash.__consolidate__(Band)
213-
end
214-
215-
it "moves the non hash values under the provided key" do
216-
expect(consolidated).to eq({
217-
"$set" => { likes: 10, name: "Tool" }, "$inc" => { 'plays' => 1 }
218-
})
219-
end
220-
end
221-
end
222-
223166
describe ".demongoize" do
224167

225168
let(:hash) do

0 commit comments

Comments
 (0)