Skip to content

Commit 4e1da0a

Browse files
committed
Merge branch 'fledman-dry_options_handling_and_subelement_creation' into main
* fledman-dry_options_handling_and_subelement_creation: Fix up David Feldman's PR to work better than my online editor attempt at resolving merge conflicts one more test case remove ruby 1.9 backport unify all mutation code paths to fix bug setup failing test case for subscript mutation notation bug explicitly merge to avoid kwargs bug in rubies before 2.2 dry options handling & subelement creation
2 parents ac06eb8 + f9f4888 commit 4e1da0a

File tree

3 files changed

+73
-52
lines changed

3 files changed

+73
-52
lines changed

lib/recursive_open_struct.rb

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
require 'recursive_open_struct/debug_inspect'
55
require 'recursive_open_struct/deep_dup'
6-
require 'recursive_open_struct/ruby_19_backport'
76
require 'recursive_open_struct/dig'
87

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

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

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

24-
def initialize(hash=nil, args={})
22+
def self.default_options
23+
{
24+
mutate_input_hash: false,
25+
recurse_over_arrays: false,
26+
preserve_original_keys: false
27+
}
28+
end
29+
30+
def initialize(hash=nil, passed_options={})
2531
hash ||= {}
26-
@recurse_over_arrays = args.fetch(:recurse_over_arrays, false)
27-
@preserve_original_keys = args.fetch(:preserve_original_keys, false)
28-
@deep_dup = DeepDup.new(
29-
recurse_over_arrays: @recurse_over_arrays,
30-
preserve_original_keys: @preserve_original_keys
31-
)
3232

33-
@table = args.fetch(:mutate_input_hash, false) ? hash : @deep_dup.call(hash)
33+
@options = self.class.default_options.merge!(passed_options).freeze
34+
35+
@deep_dup = DeepDup.new(@options)
36+
37+
@table = @options[:mutate_input_hash] ? hash : @deep_dup.call(hash)
3438

3539
@sub_elements = {}
3640
end
@@ -56,20 +60,22 @@ def [](name)
5660
key_name = _get_key_from_table_(name)
5761
v = @table[key_name]
5862
if v.is_a?(Hash)
59-
@sub_elements[key_name] ||= self.class.new(
60-
v,
61-
recurse_over_arrays: @recurse_over_arrays,
62-
preserve_original_keys: @preserve_original_keys,
63-
mutate_input_hash: true
64-
)
65-
elsif v.is_a?(Array) and @recurse_over_arrays
63+
@sub_elements[key_name] ||= _create_sub_element_(v, mutate_input_hash: true)
64+
elsif v.is_a?(Array) and @options[:recurse_over_arrays]
6665
@sub_elements[key_name] ||= recurse_over_array(v)
6766
@sub_elements[key_name] = recurse_over_array(@sub_elements[key_name])
6867
else
6968
v
7069
end
7170
end
7271

72+
def []=(name, value)
73+
key_name = _get_key_from_table_(name)
74+
tbl = modifiable? # Ensure we are modifiable
75+
@sub_elements.delete(key_name)
76+
tbl[key_name] = value
77+
end
78+
7379
# Makes sure ROS responds as expected on #respond_to? and #method requests
7480
def respond_to_missing?(mid, include_private = false)
7581
mname = _get_key_from_table_(mid.to_s.chomp('=').chomp('_as_a_hash'))
@@ -95,13 +101,16 @@ def method_missing(mid, *args)
95101
if len != 1
96102
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
97103
end
98-
modifiable?[new_ostruct_member!($1.to_sym)] = args[0]
104+
# self[$1.to_sym] = args[0]
105+
# modifiable?[new_ostruct_member!($1.to_sym)] = args[0]
106+
new_ostruct_member!($1.to_sym)
107+
public_send(mid, args[0])
99108
elsif len == 0
100109
key = mid
101110
key = $1 if key =~ /^(.*)_as_a_hash$/
102111
if @table.key?(_get_key_from_table_(key))
103112
new_ostruct_member!(key)
104-
send(mid)
113+
public_send(mid)
105114
end
106115
else
107116
err = NoMethodError.new "undefined method `#{mid}' for #{self}", mid, args
@@ -120,8 +129,7 @@ class << self; self; end.class_eval do
120129
self[key_name]
121130
end
122131
define_method("#{name}=") do |x|
123-
@sub_elements.delete(key_name)
124-
modifiable?[key_name] = x
132+
self[key_name] = x
125133
end
126134
define_method("#{name}_as_a_hash") { @table[key_name] }
127135
end
@@ -141,8 +149,8 @@ class << self; self; end.class_eval do
141149
def delete_field(name)
142150
sym = _get_key_from_table_(name)
143151
singleton_class.__send__(:remove_method, sym, "#{sym}=") rescue NoMethodError # ignore if methods not yet generated.
144-
@sub_elements.delete sym
145-
@table.delete sym
152+
@sub_elements.delete(sym)
153+
@table.delete(sym)
146154
end
147155

148156
private
@@ -153,11 +161,14 @@ def _get_key_from_table_(name)
153161
name
154162
end
155163

164+
def _create_sub_element_(hash, **overrides)
165+
self.class.new(hash, @options.merge(overrides))
166+
end
167+
156168
def recurse_over_array(array)
157169
array.each_with_index do |a, i|
158170
if a.is_a? Hash
159-
array[i] = self.class.new(a, :recurse_over_arrays => true,
160-
:mutate_input_hash => true, :preserve_original_keys => @preserve_original_keys)
171+
array[i] = _create_sub_element_(a, mutate_input_hash: true, recurse_over_arrays: true)
161172
elsif a.is_a? Array
162173
array[i] = recurse_over_array a
163174
end

lib/recursive_open_struct/ruby_19_backport.rb

Lines changed: 0 additions & 27 deletions
This file was deleted.

spec/recursive_open_struct/recursion_spec.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
expect(subject.blah_as_a_hash).to eq({ :another => 'value' })
2929
end
3030

31+
it "handles sub-element replacement with dotted notation before member setup" do
32+
expect(ros[:blah][:another]).to eql 'value'
33+
expect(ros.methods).not_to include(:blah)
34+
35+
ros.blah = { changed: 'backing' }
36+
37+
expect(ros.blah.changed).to eql 'backing'
38+
end
39+
3140
describe "handling loops in the original Hashes" do
3241
let(:h1) { { :a => 'a'} }
3342
let(:h2) { { :a => 'b', :h1 => h1 } }
@@ -55,6 +64,34 @@
5564
expect(ros.blah.blargh).to eq "Janet"
5665
end
5766

67+
describe 'subscript mutation notation' do
68+
it 'handles the basic case' do
69+
subject[:blah] = 12345
70+
expect(subject.blah).to eql 12345
71+
end
72+
73+
it 'recurses properly' do
74+
subject[:blah][:another] = 'abc'
75+
expect(subject.blah.another).to eql 'abc'
76+
expect(subject.blah_as_a_hash).to eql({ :another => 'abc' })
77+
end
78+
79+
let(:diff){ { :different => 'thing' } }
80+
81+
it 'can replace the entire hash' do
82+
expect(subject.to_h).to eql(h)
83+
subject[:blah] = diff
84+
expect(subject.to_h).to eql({ :blah => diff })
85+
end
86+
87+
it 'updates sub-element cache' do
88+
expect(subject.blah.different).to be_nil
89+
subject[:blah] = diff
90+
expect(subject.blah.different).to eql 'thing'
91+
expect(subject.blah_as_a_hash).to eql(diff)
92+
end
93+
end
94+
5895
context "after a sub-element has been modified" do
5996
let(:hash) do
6097
{ :blah => { :blargh => "Brad" }, :some_array => [ 1, 2, 3] }

0 commit comments

Comments
 (0)