Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MONGOID-5653 - Move Hash#__nested__ monkey patch method to new module Mongoid::Attributes::Embedded.traverse #5692

Merged
merged 5 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 'retrieves the nil embedded value under the provided key' 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 key 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
Loading