Skip to content

fix sub-element bug and dry internal handling of options #58

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

Closed
Show file tree
Hide file tree
Changes from all 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
55 changes: 36 additions & 19 deletions lib/recursive_open_struct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

require 'recursive_open_struct/debug_inspect'
require 'recursive_open_struct/deep_dup'
require 'recursive_open_struct/ruby_19_backport'
require 'recursive_open_struct/dig'

# TODO: When we care less about Rubies before 2.4.0, match OpenStruct's method
Expand All @@ -14,23 +13,28 @@
# `#to_h`.

class RecursiveOpenStruct < OpenStruct
include Ruby19Backport if RUBY_VERSION =~ /\A1.9/
include Dig if OpenStruct.public_instance_methods.include? :dig

# TODO: deprecated, possibly remove or make optional an runtime so that it
# doesn't normally pollute the public method namespace
include DebugInspect

def initialize(hash=nil, args={})
def self.default_options
{
mutate_input_hash: false,
recurse_over_arrays: false,
preserve_original_keys: false
}
end

def initialize(hash=nil, passed_options={})
hash ||= {}
@recurse_over_arrays = args.fetch(:recurse_over_arrays, false)
@preserve_original_keys = args.fetch(:preserve_original_keys, false)
@deep_dup = DeepDup.new(
recurse_over_arrays: @recurse_over_arrays,
preserve_original_keys: @preserve_original_keys
)

@table = args.fetch(:mutate_input_hash, false) ? hash : @deep_dup.call(hash)
@options = self.class.default_options.merge!(passed_options).freeze

@deep_dup = DeepDup.new(@options)

@table = @options[:mutate_input_hash] ? hash : @deep_dup.call(hash)

@sub_elements = {}
end
Expand All @@ -56,20 +60,23 @@ def [](name)
key_name = _get_key_from_table_(name)
v = @table[key_name]
if v.is_a?(Hash)
@sub_elements[key_name] ||= self.class.new(
v,
recurse_over_arrays: @recurse_over_arrays,
preserve_original_keys: @preserve_original_keys,
mutate_input_hash: true
)
elsif v.is_a?(Array) and @recurse_over_arrays
@sub_elements[key_name] ||= _create_sub_element_(v, mutate_input_hash: true)
elsif v.is_a?(Array) and @options[:recurse_over_arrays]
@sub_elements[key_name] ||= recurse_over_array(v)
@sub_elements[key_name] = recurse_over_array(@sub_elements[key_name])
else
v
end
end

def []=(name, value)
modifiable do |tbl|
key_name = _get_key_from_table_(name)
@sub_elements.delete(key_name)
tbl[key_name] = value
end
end

# Makes sure ROS responds as expected on #respond_to? and #method requests
def respond_to_missing?(mid, include_private = false)
mname = _get_key_from_table_(mid.to_s.chomp('=').chomp('_as_a_hash'))
Expand Down Expand Up @@ -145,6 +152,13 @@ def delete_field(name)
@table.delete sym
end

protected

# TODO: Use modifiable? instead of modifiable once we care less about Rubies before 2.4.0.
def modifiable
block_given? ? yield(super) : super
end

private

def _get_key_from_table_(name)
Expand All @@ -153,11 +167,14 @@ def _get_key_from_table_(name)
name
end

def _create_sub_element_(hash, **overrides)
self.class.new(hash, @options.merge(overrides))
end

def recurse_over_array(array)
array.each_with_index do |a, i|
if a.is_a? Hash
array[i] = self.class.new(a, :recurse_over_arrays => true,
:mutate_input_hash => true, :preserve_original_keys => @preserve_original_keys)
array[i] = _create_sub_element_(a, mutate_input_hash: true, recurse_over_arrays: true)
elsif a.is_a? Array
array[i] = recurse_over_array a
end
Expand Down
27 changes: 0 additions & 27 deletions lib/recursive_open_struct/ruby_19_backport.rb

This file was deleted.

35 changes: 35 additions & 0 deletions spec/recursive_open_struct/recursion_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
expect(subject.blah_as_a_hash).to eq({ :another => 'value' })
end

it "handles sub-element replacement with dotted notation before member setup" do
expect(ros[:blah][:another]).to eql 'value'
expect(ros.methods).not_to include(:blah)
ros.blah = { changed: 'backing' }
expect(ros.blah.changed).to eql 'backing'
end

describe "handling loops in the original Hashes" do
let(:h1) { { :a => 'a'} }
let(:h2) { { :a => 'b', :h1 => h1 } }
Expand Down Expand Up @@ -55,6 +62,34 @@
expect(ros.blah.blargh).to eq "Janet"
end

describe 'subscript mutation notation' do
it 'handles the basic case' do
subject[:blah] = 12345
expect(subject.blah).to eql 12345
end

it 'recurses properly' do
subject[:blah][:another] = 'abc'
expect(subject.blah.another).to eql 'abc'
expect(subject.blah_as_a_hash).to eql({ :another => 'abc' })
end

let(:diff){ { :different => 'thing' } }

it 'can replace the entire hash' do
expect(subject.to_h).to eql(h)
subject[:blah] = diff
expect(subject.to_h).to eql({ :blah => diff })
end

it 'updates sub-element cache' do
expect(subject.blah.different).to be_nil
subject[:blah] = diff
expect(subject.blah.different).to eql 'thing'
expect(subject.blah_as_a_hash).to eql(diff)
end
end

context "after a sub-element has been modified" do
let(:hash) do
{ :blah => { :blargh => "Brad" }, :some_array => [ 1, 2, 3] }
Expand Down