recursive-open-struct-sd 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|