recursive-open-struct-sd 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +50 -0
- data/.rspec +1 -0
- data/.travis.yml +13 -0
- data/AUTHORS.txt +15 -0
- data/CHANGELOG.md +151 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +60 -0
- data/Rakefile +60 -0
- data/lib/recursive-open-struct.rb +1 -0
- data/lib/recursive_open_struct.rb +130 -0
- data/lib/recursive_open_struct/debug_inspect.rb +38 -0
- data/lib/recursive_open_struct/deep_dup.rb +32 -0
- data/lib/recursive_open_struct/ruby_19_backport.rb +27 -0
- data/lib/recursive_open_struct/version.rb +7 -0
- data/recursive-open-struct-sd.gemspec +46 -0
- data/spec/recursive_open_struct/debug_inspect_spec.rb +70 -0
- data/spec/recursive_open_struct/indifferent_access_spec.rb +166 -0
- data/spec/recursive_open_struct/open_struct_behavior_spec.rb +92 -0
- data/spec/recursive_open_struct/ostruct_2_0_0_spec.rb +105 -0
- data/spec/recursive_open_struct/recursion_and_subclassing_spec.rb +14 -0
- data/spec/recursive_open_struct/recursion_spec.rb +299 -0
- data/spec/spec_helper.rb +19 -0
- metadata +174 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module RecursiveOpenStruct::DebugInspect
|
2
|
+
def debug_inspect(io = STDOUT, indent_level = 0, recursion_limit = 12)
|
3
|
+
display_recursive_open_struct(io, @table, indent_level, recursion_limit)
|
4
|
+
end
|
5
|
+
|
6
|
+
def display_recursive_open_struct(io, ostrct_or_hash, indent_level, recursion_limit)
|
7
|
+
|
8
|
+
if recursion_limit <= 0 then
|
9
|
+
# protection against recursive structure (like in the tests)
|
10
|
+
io.puts ' '*indent_level + '(recursion limit reached)'
|
11
|
+
else
|
12
|
+
#puts ostrct_or_hash.inspect
|
13
|
+
if ostrct_or_hash.is_a?(self.class) then
|
14
|
+
ostrct_or_hash = ostrct_or_hash.marshal_dump
|
15
|
+
end
|
16
|
+
|
17
|
+
# We'll display the key values like this : key = value
|
18
|
+
# to align display, we look for the maximum key length of the data that will be displayed
|
19
|
+
# (everything except hashes)
|
20
|
+
data_indent = ostrct_or_hash \
|
21
|
+
.reject { |k, v| v.is_a?(self.class) || v.is_a?(Hash) } \
|
22
|
+
.max {|a,b| a[0].to_s.length <=> b[0].to_s.length}[0].to_s.length
|
23
|
+
# puts "max length = #{data_indent}"
|
24
|
+
|
25
|
+
ostrct_or_hash.each do |key, value|
|
26
|
+
if (value.is_a?(self.class) || value.is_a?(Hash)) then
|
27
|
+
io.puts ' '*indent_level + key.to_s + '.'
|
28
|
+
display_recursive_open_struct(io, value, indent_level + 1, recursion_limit - 1)
|
29
|
+
else
|
30
|
+
io.puts ' '*indent_level + key.to_s + ' '*(data_indent - key.to_s.length) + ' = ' + value.inspect
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class RecursiveOpenStruct::DeepDup
|
2
|
+
def initialize(opts={})
|
3
|
+
@recurse_over_arrays = opts.fetch(:recurse_over_arrays, false)
|
4
|
+
@preserve_original_keys = opts.fetch(:preserve_original_keys, false)
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(obj)
|
8
|
+
deep_dup(obj)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def deep_dup(obj, visited=[])
|
14
|
+
if obj.is_a?(Hash)
|
15
|
+
obj.each_with_object({}) do |(key, value), h|
|
16
|
+
h[@preserve_original_keys ? key : key.to_sym] = value_or_deep_dup(value, visited)
|
17
|
+
end
|
18
|
+
elsif obj.is_a?(Array) && @recurse_over_arrays
|
19
|
+
obj.each_with_object([]) do |value, arr|
|
20
|
+
value = value.is_a?(RecursiveOpenStruct) ? value.to_h : value
|
21
|
+
arr << value_or_deep_dup(value, visited)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
obj
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def value_or_deep_dup(value, visited)
|
29
|
+
obj_id = value.object_id
|
30
|
+
visited.include?(obj_id) ? value : deep_dup(value, visited << obj_id)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RecursiveOpenStruct::Ruby19Backport
|
2
|
+
# Apply fix if necessary:
|
3
|
+
# https://github.com/ruby/ruby/commit/2d952c6d16ffe06a28bb1007e2cd1410c3db2d58
|
4
|
+
def initialize_copy(orig)
|
5
|
+
super
|
6
|
+
@table.each_key{|key| new_ostruct_member(key)}
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(name, value)
|
10
|
+
modifiable[new_ostruct_member(name)] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def eql?(other)
|
14
|
+
return false unless other.kind_of?(OpenStruct)
|
15
|
+
@table.eql?(other.table)
|
16
|
+
end
|
17
|
+
|
18
|
+
def hash
|
19
|
+
@table.hash
|
20
|
+
end
|
21
|
+
|
22
|
+
def each_pair
|
23
|
+
return to_enum(:each_pair) { @table.size } unless block_given?
|
24
|
+
@table.each_pair{|p| yield p}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require './lib/recursive_open_struct/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "recursive-open-struct-sd"
|
7
|
+
s.version = RecursiveOpenStruct::VERSION
|
8
|
+
s.authors = ["William (B.J.) Snow Orvis"]
|
9
|
+
s.email = "aetherknight@gmail.com"
|
10
|
+
s.date = Time.now.utc.strftime("%Y-%m-%d")
|
11
|
+
s.homepage = "http://github.com/aetherknight/recursive-open-struct"
|
12
|
+
s.licenses = ["MIT"]
|
13
|
+
|
14
|
+
s.summary = "OpenStruct subclass that returns nested hash attributes as RecursiveOpenStructs"
|
15
|
+
s.description = <<-QUOTE .gsub(/^ /,'')
|
16
|
+
RecursiveOpenStruct is a subclass of OpenStruct. It differs from
|
17
|
+
OpenStruct in that it allows nested hashes to be treated in a recursive
|
18
|
+
fashion. For example:
|
19
|
+
|
20
|
+
ros = RecursiveOpenStruct.new({ :a => { :b => 'c' } })
|
21
|
+
ros.a.b # 'c'
|
22
|
+
|
23
|
+
Also, nested hashes can still be accessed as hashes:
|
24
|
+
|
25
|
+
ros.a_as_a_hash # { :b => 'c' }
|
26
|
+
|
27
|
+
> This is a fork of the original recursive-open-struct
|
28
|
+
> to include a fix for https://github.com/aetherknight/recursive-open-struct/issues/46
|
29
|
+
QUOTE
|
30
|
+
|
31
|
+
s.files = `git ls-files`.split("\n")
|
32
|
+
s.test_files = `git ls-files spec`.split("\n")
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
s.extra_rdoc_files = [
|
35
|
+
"CHANGELOG.md",
|
36
|
+
"LICENSE.txt",
|
37
|
+
"README.md"
|
38
|
+
]
|
39
|
+
|
40
|
+
s.add_development_dependency('bundler', [">= 0"])
|
41
|
+
s.add_development_dependency('pry', [">= 0"])
|
42
|
+
s.add_development_dependency('rake', [">= 0"])
|
43
|
+
s.add_development_dependency('rdoc', [">= 0"])
|
44
|
+
s.add_development_dependency('rspec', "~> 3.2")
|
45
|
+
s.add_development_dependency('simplecov', [">= 0"])
|
46
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'recursive_open_struct'
|
3
|
+
|
4
|
+
describe RecursiveOpenStruct do
|
5
|
+
describe "#debug_inspect" do
|
6
|
+
before(:each) do
|
7
|
+
h1 = { :a => 'a'}
|
8
|
+
h2 = { :a => 'b', :h1 => h1 }
|
9
|
+
h1[:h2] = h2
|
10
|
+
@ros = RecursiveOpenStruct.new(h2)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should have a simple way of display" do
|
14
|
+
@output = <<-QUOTE
|
15
|
+
a = "b"
|
16
|
+
h1.
|
17
|
+
a = "a"
|
18
|
+
h2.
|
19
|
+
a = "b"
|
20
|
+
h1.
|
21
|
+
a = "a"
|
22
|
+
h2.
|
23
|
+
a = "b"
|
24
|
+
h1.
|
25
|
+
a = "a"
|
26
|
+
h2.
|
27
|
+
a = "b"
|
28
|
+
h1.
|
29
|
+
a = "a"
|
30
|
+
h2.
|
31
|
+
a = "b"
|
32
|
+
h1.
|
33
|
+
a = "a"
|
34
|
+
h2.
|
35
|
+
a = "b"
|
36
|
+
h1.
|
37
|
+
a = "a"
|
38
|
+
h2.
|
39
|
+
(recursion limit reached)
|
40
|
+
QUOTE
|
41
|
+
@io = StringIO.new
|
42
|
+
@ros.debug_inspect(@io)
|
43
|
+
expect(@io.string).to match /^a = "b"$/
|
44
|
+
expect(@io.string).to match /^h1\.$/
|
45
|
+
expect(@io.string).to match /^ a = "a"$/
|
46
|
+
expect(@io.string).to match /^ h2\.$/
|
47
|
+
expect(@io.string).to match /^ a = "b"$/
|
48
|
+
expect(@io.string).to match /^ h1\.$/
|
49
|
+
expect(@io.string).to match /^ a = "a"$/
|
50
|
+
expect(@io.string).to match /^ h2\.$/
|
51
|
+
expect(@io.string).to match /^ a = "b"$/
|
52
|
+
expect(@io.string).to match /^ h1\.$/
|
53
|
+
expect(@io.string).to match /^ a = "a"$/
|
54
|
+
expect(@io.string).to match /^ h2\.$/
|
55
|
+
expect(@io.string).to match /^ a = "b"$/
|
56
|
+
expect(@io.string).to match /^ h1\.$/
|
57
|
+
expect(@io.string).to match /^ a = "a"$/
|
58
|
+
expect(@io.string).to match /^ h2\.$/
|
59
|
+
expect(@io.string).to match /^ a = "b"$/
|
60
|
+
expect(@io.string).to match /^ h1\.$/
|
61
|
+
expect(@io.string).to match /^ a = "a"$/
|
62
|
+
expect(@io.string).to match /^ h2\.$/
|
63
|
+
expect(@io.string).to match /^ a = "b"$/
|
64
|
+
expect(@io.string).to match /^ h1\.$/
|
65
|
+
expect(@io.string).to match /^ a = "a"$/
|
66
|
+
expect(@io.string).to match /^ h2\.$/
|
67
|
+
expect(@io.string).to match /^ \(recursion limit reached\)$/
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'recursive_open_struct'
|
3
|
+
|
4
|
+
describe RecursiveOpenStruct do
|
5
|
+
let(:value) { 'foo' }
|
6
|
+
let(:symbol) { :bar }
|
7
|
+
let(:new_value) { 'bar' }
|
8
|
+
let(:new_symbol) { :foo }
|
9
|
+
|
10
|
+
describe 'indifferent access' do
|
11
|
+
let(:hash) { {:foo => value, 'bar' => symbol} }
|
12
|
+
let(:hash_ros_opts) { {} }
|
13
|
+
subject(:hash_ros) { RecursiveOpenStruct.new(hash, hash_ros_opts) }
|
14
|
+
|
15
|
+
context 'setting value with method' do
|
16
|
+
before(:each) do
|
17
|
+
subject.foo = value
|
18
|
+
end
|
19
|
+
|
20
|
+
it('allows getting with method') { expect(subject.foo).to be value }
|
21
|
+
it('allows getting with symbol') { expect(subject[:foo]).to be value }
|
22
|
+
it('allows getting with string') { expect(subject['foo']).to be value }
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'setting value with symbol' do
|
27
|
+
before(:each) do
|
28
|
+
subject[:foo] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
it('allows getting with method') { expect(subject.foo).to be value }
|
32
|
+
it('allows getting with symbol') { expect(subject[:foo]).to be value }
|
33
|
+
it('allows getting with string') { expect(subject['foo']).to be value }
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'setting value with string' do
|
38
|
+
before(:each) do
|
39
|
+
subject['foo'] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
it('allows getting with method') { expect(subject.foo).to be value }
|
43
|
+
it('allows getting with symbol') { expect(subject[:foo]).to be value }
|
44
|
+
it('allows getting with string') { expect(subject['foo']).to be value }
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'overwriting values' do
|
49
|
+
context 'set with method' do
|
50
|
+
before(:each) do
|
51
|
+
subject.foo = value
|
52
|
+
end
|
53
|
+
|
54
|
+
it('overrides with symbol') do
|
55
|
+
subject[:foo] = new_value
|
56
|
+
expect(subject.foo).to be new_value
|
57
|
+
end
|
58
|
+
|
59
|
+
it('overrides with string') do
|
60
|
+
subject['foo'] = new_value
|
61
|
+
expect(subject.foo).to be new_value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'set with symbol' do
|
66
|
+
before(:each) do
|
67
|
+
subject[:foo] = value
|
68
|
+
end
|
69
|
+
|
70
|
+
it('overrides with method') do
|
71
|
+
subject.foo = new_value
|
72
|
+
expect(subject[:foo]).to be new_value
|
73
|
+
end
|
74
|
+
|
75
|
+
it('overrides with string') do
|
76
|
+
subject['foo'] = new_value
|
77
|
+
expect(subject[:foo]).to be new_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'set with string' do
|
82
|
+
before(:each) do
|
83
|
+
subject['foo'] = value
|
84
|
+
end
|
85
|
+
|
86
|
+
it('overrides with method') do
|
87
|
+
subject.foo = new_value
|
88
|
+
expect(subject['foo']).to be new_value
|
89
|
+
end
|
90
|
+
|
91
|
+
it('overrides with symbol') do
|
92
|
+
subject[:foo] = new_value
|
93
|
+
expect(subject['foo']).to be new_value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'set with hash' do
|
98
|
+
it('overrides with method') do
|
99
|
+
hash_ros.foo = new_value
|
100
|
+
expect(hash_ros[:foo]).to be new_value
|
101
|
+
|
102
|
+
hash_ros.bar = new_symbol
|
103
|
+
expect(hash_ros['bar']).to be new_symbol
|
104
|
+
end
|
105
|
+
|
106
|
+
it('overrides with symbol') do
|
107
|
+
hash_ros[:bar] = new_symbol
|
108
|
+
expect(hash_ros['bar']).to be new_symbol
|
109
|
+
end
|
110
|
+
|
111
|
+
it('overrides with string') do
|
112
|
+
hash_ros['foo'] = new_value
|
113
|
+
expect(hash_ros[:foo]).to be new_value
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when preserve_original_keys is not enabled' do
|
118
|
+
context 'transforms original keys to symbols' do
|
119
|
+
subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true) }
|
120
|
+
let(:recursive_hash) { {:foo => [ {'bar' => [ { 'foo' => :bar} ] } ] } }
|
121
|
+
let(:symbolized_recursive_hash) { {:foo => [ {:bar => [ { :foo => :bar} ] } ] } }
|
122
|
+
let(:symbolized_modified_hash) { {:foo => [ {:bar => [ { :foo => :foo} ] } ] } }
|
123
|
+
let(:symbolized_hash) { Hash[hash.map{|(k,v)| [k.to_sym,v]}] }
|
124
|
+
|
125
|
+
specify 'after initialization' do
|
126
|
+
expect(hash_ros.to_h).to eq symbolized_hash
|
127
|
+
end
|
128
|
+
|
129
|
+
specify 'in recursive hashes' do
|
130
|
+
expect(recursive.to_h).to eq symbolized_recursive_hash
|
131
|
+
end
|
132
|
+
|
133
|
+
specify 'after resetting value' do
|
134
|
+
recursive.foo.first[:bar].first[:foo] = :foo
|
135
|
+
expect(recursive.to_h).to eq symbolized_modified_hash
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'when preserve_original_keys is enabled' do
|
141
|
+
context 'preserves the original keys' do
|
142
|
+
subject(:recursive) { RecursiveOpenStruct.new(recursive_hash, recurse_over_arrays: true, preserve_original_keys: true) }
|
143
|
+
let(:recursive_hash) { {:foo => [ {'bar' => [ { 'foo' => :bar} ] } ] } }
|
144
|
+
let(:modified_hash) { {:foo => [ {'bar' => [ { 'foo' => :foo} ] } ] } }
|
145
|
+
|
146
|
+
let(:hash_ros_opts) { { preserve_original_keys: true }}
|
147
|
+
|
148
|
+
specify 'after initialization' do
|
149
|
+
expect(hash_ros.to_h).to eq hash
|
150
|
+
end
|
151
|
+
|
152
|
+
specify 'in recursive hashes' do
|
153
|
+
expect(recursive.to_h).to eq recursive_hash
|
154
|
+
end
|
155
|
+
|
156
|
+
specify 'after resetting value' do
|
157
|
+
recursive.foo.first[:bar].first[:foo] = :foo
|
158
|
+
expect(recursive.to_h).to eq modified_hash
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'recursive_open_struct'
|
3
|
+
|
4
|
+
describe RecursiveOpenStruct do
|
5
|
+
let(:hash) { {} }
|
6
|
+
subject(:ros) { RecursiveOpenStruct.new(hash) }
|
7
|
+
|
8
|
+
describe "behavior it inherits from OpenStruct" do
|
9
|
+
context 'when not initialized from anything' do
|
10
|
+
subject(:ros) { RecursiveOpenStruct.new }
|
11
|
+
it "can represent arbitrary data objects" do
|
12
|
+
ros.blah = "John Smith"
|
13
|
+
expect(ros.blah).to eq "John Smith"
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns nil for missing attributes' do
|
17
|
+
expect(ros.foo).to be_nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when initialized with nil' do
|
22
|
+
let(:hash) { nil }
|
23
|
+
it 'returns nil for missing attributes' do
|
24
|
+
expect(ros.foo).to be_nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when initialized with an empty hash' do
|
29
|
+
it 'returns nil for missing attributes' do
|
30
|
+
expect(ros.foo).to be_nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when initialized from a hash" do
|
35
|
+
let(:hash) { { :asdf => 'John Smith' } }
|
36
|
+
|
37
|
+
context 'that contains symbol keys' do
|
38
|
+
it "turns those symbol keys into method names" do
|
39
|
+
expect(ros.asdf).to eq "John Smith"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "can modify an existing key" do
|
44
|
+
ros.asdf = "George Washington"
|
45
|
+
expect(ros.asdf).to eq "George Washington"
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'that contains string keys' do
|
49
|
+
let(:hash) { { 'asdf' => 'John Smith' } }
|
50
|
+
it "turns those string keys into method names" do
|
51
|
+
expect(ros.asdf).to eq "John Smith"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'that contains keys that mirror existing private methods' do
|
56
|
+
let(:hash) { { test: :foo, rand: 'not a number' } }
|
57
|
+
|
58
|
+
# https://github.com/aetherknight/recursive-open-struct/issues/42
|
59
|
+
it 'handles subscript notation without calling the method name first (#42)' do
|
60
|
+
expect(ros['test']).to eq :foo
|
61
|
+
expect(ros['rand']).to eq 'not a number'
|
62
|
+
|
63
|
+
expect(ros.test).to eq :foo
|
64
|
+
expect(ros.rand).to eq 'not a number'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
describe "handling of arbitrary attributes" do
|
72
|
+
subject { RecursiveOpenStruct.new }
|
73
|
+
before(:each) do
|
74
|
+
subject.blah = "John Smith"
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "#respond?" do
|
78
|
+
it { expect(subject).to respond_to :blah }
|
79
|
+
it { expect(subject).to respond_to :blah= }
|
80
|
+
it { expect(subject).to_not respond_to :asdf }
|
81
|
+
it { expect(subject).to_not respond_to :asdf= }
|
82
|
+
end # describe #respond?
|
83
|
+
|
84
|
+
describe "#methods" do
|
85
|
+
it { expect(subject.methods.map(&:to_sym)).to include :blah }
|
86
|
+
it { expect(subject.methods.map(&:to_sym)).to include :blah= }
|
87
|
+
it { expect(subject.methods.map(&:to_sym)).to_not include :asdf }
|
88
|
+
it { expect(subject.methods.map(&:to_sym)).to_not include :asdf= }
|
89
|
+
end # describe #methods
|
90
|
+
end # describe handling of arbitrary attributes
|
91
|
+
end # describe behavior it inherits from OpenStruct
|
92
|
+
end
|