Skip to content

Commit

Permalink
MONGOID-5653 - Move Hash#__nested__ monkey patch method to new module…
Browse files Browse the repository at this point in the history
… 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]>
  • Loading branch information
johnnyshields and jamis authored Aug 28, 2023
1 parent 1efecc9 commit f674f2e
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 119 deletions.
3 changes: 2 additions & 1 deletion lib/mongoid/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

require "active_model/attribute_methods"
require "mongoid/attributes/dynamic"
require "mongoid/attributes/embedded"
require "mongoid/attributes/nested"
require "mongoid/attributes/processing"
require "mongoid/attributes/projector"
Expand Down Expand Up @@ -299,7 +300,7 @@ def read_raw_attribute(name)
if fields.key?(normalized)
attributes[normalized]
else
attributes.__nested__(normalized)
Embedded.traverse(attributes, normalized)
end
else
attributes[normalized]
Expand Down
34 changes: 34 additions & 0 deletions lib/mongoid/attributes/embedded.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Mongoid
module Attributes
# Utility module for working with embedded attributes.
module Embedded
extend self

# Fetch an embedded value or subset of attributes via dot notation.
#
# @example Fetch an embedded value via dot notation.
# Embedded.traverse({ 'name' => { 'en' => 'test' } }, 'name.en')
# #=> 'test'
#
# @param [ Hash ] attributes The document attributes.
# @param [ String ] path The dot notation string.
#
# @return [ Object | nil ] The attributes at the given path,
# or nil if the path doesn't exist.
def traverse(attributes, path)
path.split('.').each do |key|
break if attributes.nil?

attributes = if attributes.try(:key?, key)
attributes[key]
elsif attributes.respond_to?(:each) && key.match?(/\A\d+\z/)
attributes[key.to_i]
end
end
attributes
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mongoid/attributes/nested.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module Mongoid
module Attributes

# Defines behavior around that lovel Rails feature nested attributes.
# Defines behavior for the Rails nested attributes feature.
module Nested
extend ActiveSupport::Concern

Expand Down
22 changes: 0 additions & 22 deletions lib/mongoid/extensions/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,28 +127,6 @@ def extract_id
self["_id"] || self[:_id] || self["id"] || self[:id]
end

# Fetch a nested value via dot syntax.
#
# @example Fetch a nested value via dot syntax.
# { "name" => { "en" => "test" }}.__nested__("name.en")
#
# @param [ String ] string the dot syntax string.
#
# @return [ Object ] The matching value.
def __nested__(string)
keys = string.split(".")
value = self
keys.each do |key|
return nil if value.nil?
value_for_key = value[key]
if value_for_key.nil? && key.to_i.to_s == key
value_for_key = value[key.to_i]
end
value = value_for_key
end
value
end

# Turn the object from the ruby type we deal with to a Mongo friendly
# type.
#
Expand Down
22 changes: 3 additions & 19 deletions lib/mongoid/reloadable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,10 @@ def reload_root_document
#
# @return [ Hash ] The reloaded attributes.
def reload_embedded_document
extract_embedded_attributes(
collection(_root).find(_root.atomic_selector, session: _session).read(mode: :primary).first
Mongoid::Attributes::Embedded.traverse(
collection(_root).find(_root.atomic_selector, session: _session).read(mode: :primary).first,
atomic_position
)
end

# Extract only the desired embedded document from the attributes.
#
# @example Extract the embedded document.
# document.extract_embedded_attributes(attributes)
#
# @param [ Hash ] attributes The document in the db.
#
# @return [ Hash | nil ] The document's extracted attributes or nil if the
# document doesn't exist.
def extract_embedded_attributes(attributes)
# rubocop:disable Lint/UnmodifiedReduceAccumulator
atomic_position.split('.').inject(attributes) do |attrs, part|
attrs[Utils.maybe_integer(part)]
end
# rubocop:enable Lint/UnmodifiedReduceAccumulator
end
end
end
14 changes: 0 additions & 14 deletions lib/mongoid/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,6 @@ def placeholder?(value)
value == PLACEHOLDER
end

# If value can be coerced to an integer, return it as an integer.
# Otherwise, return the value itself.
#
# @param [ String ] value the string to possibly coerce.
#
# @return [ String | Integer ] the result of the coercion.
def maybe_integer(value)
if value.match?(/^\d/)
value.to_i
else
value
end
end

# This function should be used if you need to measure time.
# @example Calculate elapsed time.
# starting = Utils.monotonic_time
Expand Down
118 changes: 118 additions & 0 deletions spec/mongoid/attributes/embedded_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# frozen_string_literal: true

require 'spec_helper'

describe Mongoid::Attributes::Embedded do
describe '.traverse' do
subject(:embedded) { described_class.traverse(attributes, path) }

let(:path) { '100.name' }

context 'when the attribute key is a string' do
let(:attributes) { { '100' => { 'name' => 'hundred' } } }

it 'retrieves an embedded value under the provided key' do
expect(embedded).to eq 'hundred'
end

context 'when the value is false' do
let(:attributes) { { '100' => { 'name' => false } } }

it 'retrieves the embedded value under the provided key' do
expect(embedded).to be false
end
end

context 'when the value does not exist' do
let(:attributes) { { '100' => { 0 => 'Please do not return this value!' } } }

it 'returns nil' do
expect(embedded).to be_nil
end
end
end

context 'when the attribute key is an integer' do
let(:attributes) { { 100 => { 'name' => 'hundred' } } }

it 'retrieves an embedded value under the provided key' do
expect(embedded).to eq 'hundred'
end
end

context 'when the attribute value is nil' do
let(:attributes) { { 100 => { 'name' => nil } } }

it 'returns nil' do
expect(embedded).to be_nil
end
end

context 'when both string and integer keys are present' do
let(:attributes) { { '100' => { 'name' => 'Fred' }, 100 => { 'name' => 'Daphne' } } }

it 'returns the string key value' do
expect(embedded).to eq 'Fred'
end

context 'when the string key value is nil' do
let(:attributes) { { '100' => nil, 100 => { 'name' => 'Daphne' } } }

it 'returns nil' do
expect(embedded).to be_nil
end
end
end

context 'when attributes is an array' do
let(:attributes) do
[ { 'name' => 'Fred' }, { 'name' => 'Daphne' }, { 'name' => 'Velma' }, { 'name' => 'Shaggy' } ]
end
let(:path) { '2.name' }

it 'retrieves the nth value' do
expect(embedded).to eq 'Velma'
end

context 'when the member does not exist' do
let(:attributes) { [ { 'name' => 'Fred' }, { 'name' => 'Daphne' } ] }

it 'returns nil' do
expect(embedded).to be_nil
end
end
end

context 'when the path includes a scalar value' do
let(:attributes) { { '100' => 'name' } }

it 'returns nil' do
expect(embedded).to be_nil
end
end

context 'when the parent key is not present' do
let(:attributes) { { '101' => { 'name' => 'hundred and one' } } }

it 'returns nil' do
expect(embedded).to be_nil
end
end

context 'when the attributes are deeply nested' do
let(:attributes) { { '100' => { 'name' => { 300 => %w[a b c] } } } }

it 'retrieves the embedded subset of attributes' do
expect(embedded).to eq(300 => %w[a b c])
end

context 'when the path is deeply nested' do
let(:path) { '100.name.300.1' }

it 'retrieves the embedded value' do
expect(embedded).to eq 'b'
end
end
end
end
end
62 changes: 0 additions & 62 deletions spec/mongoid/extensions/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,68 +220,6 @@
end
end

context "when the hash key is a string" do

let(:hash) do
{ "100" => { "name" => "hundred" } }
end

let(:nested) do
hash.__nested__("100.name")
end

it "should retrieve a nested value under the provided key" do
expect(nested).to eq "hundred"
end

context 'and the value is falsey' do
let(:hash) do
{ "100" => { "name" => false } }
end
it "should retrieve the falsey nested value under the provided key" do
expect(nested).to eq false
end
end

context 'and the value is nil' do
let(:hash) do
{ "100" => { 0 => "Please don't return this value!" } }
end
it "should retrieve the nil nested value under the provided key" do
expect(nested).to eq nil
end
end
end

context "when the hash key is an integer" do
let(:hash) do
{ 100 => { "name" => "hundred" } }
end

let(:nested) do
hash.__nested__("100.name")
end

it "should retrieve a nested value under the provided key" do
expect(nested).to eq("hundred")
end
end

context "when the parent key is not present" do

let(:hash) do
{ "101" => { "name" => "hundred and one" } }
end

let(:nested) do
hash.__nested__("100.name")
end

it "should return nil" do
expect(nested).to eq(nil)
end
end

describe ".demongoize" do

let(:hash) do
Expand Down

0 comments on commit f674f2e

Please sign in to comment.