Skip to content

Commit f674f2e

Browse files
johnnyshieldsjamis
andauthored
MONGOID-5653 - Move Hash#__nested__ monkey patch method to new module Mongoid::Attributes::Embedded.traverse (#5692)
* Move Hash#__nested__ monkey patch method to new module Mongoid::Attributes::Embedded.traverse * Fix whitespace * Fix rubocop whitespace warning * Update embedded.rb * minor changes to test names --------- Co-authored-by: Jamis Buck <[email protected]>
1 parent 1efecc9 commit f674f2e

File tree

8 files changed

+158
-119
lines changed

8 files changed

+158
-119
lines changed

lib/mongoid/attributes.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
require "active_model/attribute_methods"
55
require "mongoid/attributes/dynamic"
6+
require "mongoid/attributes/embedded"
67
require "mongoid/attributes/nested"
78
require "mongoid/attributes/processing"
89
require "mongoid/attributes/projector"
@@ -299,7 +300,7 @@ def read_raw_attribute(name)
299300
if fields.key?(normalized)
300301
attributes[normalized]
301302
else
302-
attributes.__nested__(normalized)
303+
Embedded.traverse(attributes, normalized)
303304
end
304305
else
305306
attributes[normalized]

lib/mongoid/attributes/embedded.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Attributes
5+
# Utility module for working with embedded attributes.
6+
module Embedded
7+
extend self
8+
9+
# Fetch an embedded value or subset of attributes via dot notation.
10+
#
11+
# @example Fetch an embedded value via dot notation.
12+
# Embedded.traverse({ 'name' => { 'en' => 'test' } }, 'name.en')
13+
# #=> 'test'
14+
#
15+
# @param [ Hash ] attributes The document attributes.
16+
# @param [ String ] path The dot notation string.
17+
#
18+
# @return [ Object | nil ] The attributes at the given path,
19+
# or nil if the path doesn't exist.
20+
def traverse(attributes, path)
21+
path.split('.').each do |key|
22+
break if attributes.nil?
23+
24+
attributes = if attributes.try(:key?, key)
25+
attributes[key]
26+
elsif attributes.respond_to?(:each) && key.match?(/\A\d+\z/)
27+
attributes[key.to_i]
28+
end
29+
end
30+
attributes
31+
end
32+
end
33+
end
34+
end

lib/mongoid/attributes/nested.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
module Mongoid
55
module Attributes
66

7-
# Defines behavior around that lovel Rails feature nested attributes.
7+
# Defines behavior for the Rails nested attributes feature.
88
module Nested
99
extend ActiveSupport::Concern
1010

lib/mongoid/extensions/hash.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,28 +127,6 @@ def extract_id
127127
self["_id"] || self[:_id] || self["id"] || self[:id]
128128
end
129129

130-
# Fetch a nested value via dot syntax.
131-
#
132-
# @example Fetch a nested value via dot syntax.
133-
# { "name" => { "en" => "test" }}.__nested__("name.en")
134-
#
135-
# @param [ String ] string the dot syntax string.
136-
#
137-
# @return [ Object ] The matching value.
138-
def __nested__(string)
139-
keys = string.split(".")
140-
value = self
141-
keys.each do |key|
142-
return nil if value.nil?
143-
value_for_key = value[key]
144-
if value_for_key.nil? && key.to_i.to_s == key
145-
value_for_key = value[key.to_i]
146-
end
147-
value = value_for_key
148-
end
149-
value
150-
end
151-
152130
# Turn the object from the ruby type we deal with to a Mongo friendly
153131
# type.
154132
#

lib/mongoid/reloadable.rb

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,10 @@ def reload_root_document
9191
#
9292
# @return [ Hash ] The reloaded attributes.
9393
def reload_embedded_document
94-
extract_embedded_attributes(
95-
collection(_root).find(_root.atomic_selector, session: _session).read(mode: :primary).first
94+
Mongoid::Attributes::Embedded.traverse(
95+
collection(_root).find(_root.atomic_selector, session: _session).read(mode: :primary).first,
96+
atomic_position
9697
)
9798
end
98-
99-
# Extract only the desired embedded document from the attributes.
100-
#
101-
# @example Extract the embedded document.
102-
# document.extract_embedded_attributes(attributes)
103-
#
104-
# @param [ Hash ] attributes The document in the db.
105-
#
106-
# @return [ Hash | nil ] The document's extracted attributes or nil if the
107-
# document doesn't exist.
108-
def extract_embedded_attributes(attributes)
109-
# rubocop:disable Lint/UnmodifiedReduceAccumulator
110-
atomic_position.split('.').inject(attributes) do |attrs, part|
111-
attrs[Utils.maybe_integer(part)]
112-
end
113-
# rubocop:enable Lint/UnmodifiedReduceAccumulator
114-
end
11599
end
116100
end

lib/mongoid/utils.rb

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,6 @@ def placeholder?(value)
2222
value == PLACEHOLDER
2323
end
2424

25-
# If value can be coerced to an integer, return it as an integer.
26-
# Otherwise, return the value itself.
27-
#
28-
# @param [ String ] value the string to possibly coerce.
29-
#
30-
# @return [ String | Integer ] the result of the coercion.
31-
def maybe_integer(value)
32-
if value.match?(/^\d/)
33-
value.to_i
34-
else
35-
value
36-
end
37-
end
38-
3925
# This function should be used if you need to measure time.
4026
# @example Calculate elapsed time.
4127
# starting = Utils.monotonic_time
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Mongoid::Attributes::Embedded do
6+
describe '.traverse' do
7+
subject(:embedded) { described_class.traverse(attributes, path) }
8+
9+
let(:path) { '100.name' }
10+
11+
context 'when the attribute key is a string' do
12+
let(:attributes) { { '100' => { 'name' => 'hundred' } } }
13+
14+
it 'retrieves an embedded value under the provided key' do
15+
expect(embedded).to eq 'hundred'
16+
end
17+
18+
context 'when the value is false' do
19+
let(:attributes) { { '100' => { 'name' => false } } }
20+
21+
it 'retrieves the embedded value under the provided key' do
22+
expect(embedded).to be false
23+
end
24+
end
25+
26+
context 'when the value does not exist' do
27+
let(:attributes) { { '100' => { 0 => 'Please do not return this value!' } } }
28+
29+
it 'returns nil' do
30+
expect(embedded).to be_nil
31+
end
32+
end
33+
end
34+
35+
context 'when the attribute key is an integer' do
36+
let(:attributes) { { 100 => { 'name' => 'hundred' } } }
37+
38+
it 'retrieves an embedded value under the provided key' do
39+
expect(embedded).to eq 'hundred'
40+
end
41+
end
42+
43+
context 'when the attribute value is nil' do
44+
let(:attributes) { { 100 => { 'name' => nil } } }
45+
46+
it 'returns nil' do
47+
expect(embedded).to be_nil
48+
end
49+
end
50+
51+
context 'when both string and integer keys are present' do
52+
let(:attributes) { { '100' => { 'name' => 'Fred' }, 100 => { 'name' => 'Daphne' } } }
53+
54+
it 'returns the string key value' do
55+
expect(embedded).to eq 'Fred'
56+
end
57+
58+
context 'when the string key value is nil' do
59+
let(:attributes) { { '100' => nil, 100 => { 'name' => 'Daphne' } } }
60+
61+
it 'returns nil' do
62+
expect(embedded).to be_nil
63+
end
64+
end
65+
end
66+
67+
context 'when attributes is an array' do
68+
let(:attributes) do
69+
[ { 'name' => 'Fred' }, { 'name' => 'Daphne' }, { 'name' => 'Velma' }, { 'name' => 'Shaggy' } ]
70+
end
71+
let(:path) { '2.name' }
72+
73+
it 'retrieves the nth value' do
74+
expect(embedded).to eq 'Velma'
75+
end
76+
77+
context 'when the member does not exist' do
78+
let(:attributes) { [ { 'name' => 'Fred' }, { 'name' => 'Daphne' } ] }
79+
80+
it 'returns nil' do
81+
expect(embedded).to be_nil
82+
end
83+
end
84+
end
85+
86+
context 'when the path includes a scalar value' do
87+
let(:attributes) { { '100' => 'name' } }
88+
89+
it 'returns nil' do
90+
expect(embedded).to be_nil
91+
end
92+
end
93+
94+
context 'when the parent key is not present' do
95+
let(:attributes) { { '101' => { 'name' => 'hundred and one' } } }
96+
97+
it 'returns nil' do
98+
expect(embedded).to be_nil
99+
end
100+
end
101+
102+
context 'when the attributes are deeply nested' do
103+
let(:attributes) { { '100' => { 'name' => { 300 => %w[a b c] } } } }
104+
105+
it 'retrieves the embedded subset of attributes' do
106+
expect(embedded).to eq(300 => %w[a b c])
107+
end
108+
109+
context 'when the path is deeply nested' do
110+
let(:path) { '100.name.300.1' }
111+
112+
it 'retrieves the embedded value' do
113+
expect(embedded).to eq 'b'
114+
end
115+
end
116+
end
117+
end
118+
end

spec/mongoid/extensions/hash_spec.rb

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -220,68 +220,6 @@
220220
end
221221
end
222222

223-
context "when the hash key is a string" do
224-
225-
let(:hash) do
226-
{ "100" => { "name" => "hundred" } }
227-
end
228-
229-
let(:nested) do
230-
hash.__nested__("100.name")
231-
end
232-
233-
it "should retrieve a nested value under the provided key" do
234-
expect(nested).to eq "hundred"
235-
end
236-
237-
context 'and the value is falsey' do
238-
let(:hash) do
239-
{ "100" => { "name" => false } }
240-
end
241-
it "should retrieve the falsey nested value under the provided key" do
242-
expect(nested).to eq false
243-
end
244-
end
245-
246-
context 'and the value is nil' do
247-
let(:hash) do
248-
{ "100" => { 0 => "Please don't return this value!" } }
249-
end
250-
it "should retrieve the nil nested value under the provided key" do
251-
expect(nested).to eq nil
252-
end
253-
end
254-
end
255-
256-
context "when the hash key is an integer" do
257-
let(:hash) do
258-
{ 100 => { "name" => "hundred" } }
259-
end
260-
261-
let(:nested) do
262-
hash.__nested__("100.name")
263-
end
264-
265-
it "should retrieve a nested value under the provided key" do
266-
expect(nested).to eq("hundred")
267-
end
268-
end
269-
270-
context "when the parent key is not present" do
271-
272-
let(:hash) do
273-
{ "101" => { "name" => "hundred and one" } }
274-
end
275-
276-
let(:nested) do
277-
hash.__nested__("100.name")
278-
end
279-
280-
it "should return nil" do
281-
expect(nested).to eq(nil)
282-
end
283-
end
284-
285223
describe ".demongoize" do
286224

287225
let(:hash) do

0 commit comments

Comments
 (0)