diff --git a/lib/recursive_open_struct.rb b/lib/recursive_open_struct.rb index 03ad915..5551160 100644 --- a/lib/recursive_open_struct.rb +++ b/lib/recursive_open_struct.rb @@ -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 @@ -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 @@ -56,13 +60,8 @@ 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 @@ -70,6 +69,14 @@ def [](name) 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')) @@ -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) @@ -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 diff --git a/lib/recursive_open_struct/ruby_19_backport.rb b/lib/recursive_open_struct/ruby_19_backport.rb deleted file mode 100644 index bf95790..0000000 --- a/lib/recursive_open_struct/ruby_19_backport.rb +++ /dev/null @@ -1,27 +0,0 @@ -module RecursiveOpenStruct::Ruby19Backport - # Apply fix if necessary: - # https://github.com/ruby/ruby/commit/2d952c6d16ffe06a28bb1007e2cd1410c3db2d58 - def initialize_copy(orig) - super - @table.each_key{|key| new_ostruct_member(key)} - end - - def []=(name, value) - modifiable[new_ostruct_member(name)] = value - end - - def eql?(other) - return false unless other.kind_of?(OpenStruct) - @table.eql?(other.table) - end - - def hash - @table.hash - end - - def each_pair - return to_enum(:each_pair) { @table.size } unless block_given? - @table.each_pair{|p| yield p} - end -end - diff --git a/spec/recursive_open_struct/recursion_spec.rb b/spec/recursive_open_struct/recursion_spec.rb index bed2a03..198cc3f 100644 --- a/spec/recursive_open_struct/recursion_spec.rb +++ b/spec/recursive_open_struct/recursion_spec.rb @@ -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 } } @@ -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] }