drain 0.1.0 → 0.2.0

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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +6 -2
  3. data/.travis.yml +10 -0
  4. data/.yardopts +6 -1
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +1 -1
  7. data/README.md +11 -5
  8. data/Rakefile +7 -12
  9. data/drain.gemspec +15 -5
  10. data/gemspec.yml +4 -3
  11. data/lib/dr.rb +1 -0
  12. data/lib/{drain → dr}/base.rb +0 -0
  13. data/lib/{drain → dr}/base/bool.rb +0 -0
  14. data/lib/dr/base/converter.rb +33 -0
  15. data/lib/{drain → dr}/base/encoding.rb +0 -0
  16. data/lib/dr/base/eruby.rb +284 -0
  17. data/lib/{drain → dr}/base/functional.rb +2 -2
  18. data/lib/dr/base/graph.rb +378 -0
  19. data/lib/dr/base/utils.rb +28 -0
  20. data/lib/dr/parse.rb +1 -0
  21. data/lib/dr/parse/simple_parser.rb +70 -0
  22. data/lib/{drain → dr}/parse/time_parse.rb +0 -0
  23. data/lib/dr/ruby_ext.rb +1 -0
  24. data/lib/dr/ruby_ext/core_ext.rb +7 -0
  25. data/lib/{drain/ruby_ext/core_ext.rb → dr/ruby_ext/core_modules.rb} +67 -27
  26. data/lib/{drain → dr}/ruby_ext/meta_ext.rb +57 -30
  27. data/lib/dr/tools.rb +1 -0
  28. data/lib/{drain → dr}/tools/gtk.rb +0 -0
  29. data/lib/dr/version.rb +4 -0
  30. data/lib/drain.rb +2 -1
  31. data/test/helper.rb +12 -1
  32. data/test/test_converter.rb +42 -0
  33. data/test/test_core_ext.rb +116 -0
  34. data/test/test_graph.rb +126 -0
  35. data/test/test_meta.rb +65 -0
  36. data/test/test_simple_parser.rb +41 -0
  37. metadata +45 -21
  38. data/.document +0 -3
  39. data/lib/drain/base/eruby.rb +0 -28
  40. data/lib/drain/base/graph.rb +0 -213
  41. data/lib/drain/parse.rb +0 -5
  42. data/lib/drain/parse/simple_parser.rb +0 -61
  43. data/lib/drain/ruby_ext.rb +0 -5
  44. data/lib/drain/tools.rb +0 -5
  45. data/lib/drain/tools/git.rb +0 -116
  46. data/lib/drain/version.rb +0 -4
File without changes
@@ -0,0 +1 @@
1
+ lib/dr/base.rb
@@ -0,0 +1,7 @@
1
+ require 'dr/ruby_ext/core_modules'
2
+
3
+ #automatically include the CoreExt modules in their class
4
+ DR::CoreExt.constants.each do |c|
5
+ Module.const_get(c).module_eval {include Module.const_get("DR::CoreExt::#{c}")}
6
+ end
7
+ [Hash, Array].each {|m| m.include(Enumerable)} #to reinclude
@@ -9,7 +9,7 @@ module DR
9
9
  default=h[:default]
10
10
  r={}
11
11
  each do |el|
12
- keys=invh.fetch(value,[default])
12
+ keys=invh.fetch(el,[default])
13
13
  keys.each do |key|
14
14
  (r[key]||=[]) << el
15
15
  end
@@ -67,8 +67,10 @@ module DR
67
67
  end
68
68
 
69
69
  #from a hash {key: [values]} produce a hash {value: [keys]}
70
- #there is already Hash#key which does that, but the difference here is
71
- #that we flatten Enumerable values
70
+ #there is already Hash#invert using Hash#key which does that, but the difference here is that we flatten Enumerable values
71
+ #h={ploum: 2, plim: 2, plam: 3}
72
+ #h.invert #=> {2=>:plim, 3=>:plam}
73
+ #h.inverse #=> {2=>[:ploum, :plim], 3=>[:plam]}
72
74
  def inverse
73
75
  r={}
74
76
  each_key do |k|
@@ -96,49 +98,83 @@ module DR
96
98
  def keyed_value(key, sep: "/")
97
99
  r=self.dup
98
100
  return r if key.empty?
99
- key.split(sep).each do |k|
101
+ key.to_s.split(sep).each do |k|
100
102
  k=k.to_sym if r.key?(k.to_sym) && !r.key?(k)
101
103
  r=r[k]
102
104
  end
103
105
  return r
104
106
  end
105
- end
106
107
 
107
- module Proc
108
- # Safely call our block, even if the user passed in something of a
109
- # different arity (lambda case)
110
- def call_block(*args,**opts)
111
- if block.arity >= 0
112
- case block.arity
113
- when 0
114
- block.call(**opts)
115
- else
116
- block.call(args[0...block.arity],**opts)
117
- end
118
- else
119
- block.call(*args,**opts)
108
+ #take a key of the form ploum/plam/plim
109
+ #and return self[:ploum][:plam][:plim]=value
110
+ def set_keyed_value(key,value, sep: "/", symbolize: true)
111
+ r=self
112
+ *keys,last=key.to_s.split(sep)
113
+ keys.each do |k|
114
+ k=k.to_sym if (symbolize || r.key?(k.to_sym)) and !r.key?(k)
115
+ r[k]={} unless r.key?(k)
116
+ r=r[k]
120
117
  end
118
+ last=last.to_sym if symbolize
119
+ r[last]=value
120
+ self
121
+ end
122
+
123
+ #from a hash {foo: [:bar, :baz], bar: [:plum, :qux]},
124
+ #then leaf [:foo] returns [:plum, :qux, :baz]
125
+ def leafs(nodes)
126
+ expanded=[] #prevent loops
127
+ r=nodes.dup
128
+ begin
129
+ s,r=r,r.map do |n|
130
+ if key?(n) && !expanded.include?(n)
131
+ expanded << n
132
+ fetch(n)
133
+ else
134
+ n
135
+ end
136
+ end.flatten
137
+ end until s==r
138
+ r
121
139
  end
122
140
  end
123
141
 
124
142
  module UnboundMethod
125
143
  #this should be in the stdlib...
144
+ #Note: this is similar to Symbol#to_proc which works like this:
145
+ # foo=:foo.to_proc; foo.call(obj,*args) #=> obj.method(:foo).call(*args)
146
+ # => :length.to_proc.call("foo") #=> 3
126
147
  def to_proc
127
148
  return lambda do |obj,*args,&b|
128
- self.bind(obj).call(*args,&b)
149
+ bind(obj).call(*args,&b)
129
150
  end
130
151
  end
131
152
  def call(*args,&b)
132
- self.to_proc.call(*args,&b)
153
+ to_proc.call(*args,&b)
133
154
  end
134
155
  end
135
156
 
136
157
  module Proc
158
+ # Safely call our block, even if the user passed in something of a
159
+ # different arity (lambda case)
160
+ def call_block(*args,**opts)
161
+ if arity >= 0
162
+ case arity
163
+ when 0
164
+ call(**opts)
165
+ else
166
+ call(args[0...arity],**opts)
167
+ end
168
+ else
169
+ call(*args,**opts)
170
+ end
171
+ end
172
+
137
173
  #similar to curry, but pass the provided arguments on the right
138
174
  #(a difference to Proc#curry is that we pass the argument directly, not
139
175
  #via .call)
140
176
  def rcurry(*args,&b)
141
- return Proc.new do |*a,&b|
177
+ return ::Proc.new do |*a,&b|
142
178
  self.call(*a,*args,&b)
143
179
  end
144
180
  end
@@ -147,7 +183,7 @@ module DR
147
183
  #f.compose(g).(5,6)
148
184
  def compose(g)
149
185
  lambda do |*a,&b|
150
- self.call(g.call(*a,&b))
186
+ self.call(*g.call(*a,&b))
151
187
  end
152
188
  end
153
189
 
@@ -183,6 +219,14 @@ module DR
183
219
  end
184
220
  end
185
221
 
222
+ module CoreRef
223
+ CoreExt.constants.select {|c| const_get("::#{c}").is_a?(Class)}.each do |c|
224
+ refine const_get("::#{c}") do
225
+ include CoreExt.const_get(c)
226
+ end
227
+ end
228
+ end
229
+
186
230
  module Recursive
187
231
  extend self
188
232
  def recursive_constructor(klass)
@@ -194,10 +238,6 @@ module DR
194
238
  end
195
239
  end
196
240
  RecursiveHash=Recursive.recursive_constructor(Hash)
241
+ #For an individual hash: hash = Hash.new {|h,k| h[k] = h.class.new(&h.default_proc) }
197
242
  #Arrays don't accept blocks in the same way as Hashs, we need to pass a length parameter, so we can't use DR::RecursiveHash(Array)
198
243
  end
199
-
200
- #automatically include the *Ext modules in their class
201
- DR::CoreExt.constants.each do |c|
202
- Module.const_get(c).module_eval {include Module.const_get("DR::CoreExt::#{c}")}
203
- end
@@ -2,17 +2,28 @@ module DR
2
2
  module Meta
3
3
  extend self
4
4
  #from http://stackoverflow.com/questions/18551058/better-way-to-turn-a-ruby-class-into-a-module-than-using-refinements
5
+ #See also http://stackoverflow.com/questions/28649472/ruby-refinements-subtleties
6
+ #
5
7
  #convert a class into a module using refinements
6
8
  #ex: (Class.new { include Meta.refined_module(String) { def length; super+5; end } }).new("foo").length #=> 8
7
- def refined_module(klass)
9
+ #This uses the fact that a refining module of klass behaves as if it had
10
+ #klass has his direct ancestor
11
+ def refined_module(klass,&b)
8
12
  klass=klass.singleton_class unless Module===klass
9
13
  Module.new do
14
+ #including the module rather than just returning it allow us to
15
+ #still be able to use 'using' ('using' does not work directly on
16
+ #refining modules only on the enclosing ones)
10
17
  include refine(klass) {
11
- yield if block_given?
18
+ module_eval(&b) if block_given?
12
19
  }
13
20
  end
14
21
  end
15
22
 
23
+ #find the ancestors of obj, its singleton class, its
24
+ #singleton_singleton_class. To avoid going to infinity, we only add a
25
+ #singleton_class when its ancestors contains new modules we have not
26
+ #seen.
16
27
  def all_ancestors(obj)
17
28
  obj=obj.singleton_class unless Module===obj
18
29
  found=[]
@@ -28,25 +39,24 @@ module DR
28
39
  return found
29
40
  end
30
41
 
31
- #add extend_ancestors and extend_complete to Object
42
+ # add extend_ancestors and full_extend to Object
32
43
  def extend_object
33
44
  include_ancestors=Meta.method(:include_ancestors)
34
- include_complete=Meta.method(:include_complete)
45
+ include_complete=Meta.method(:full_include)
35
46
  Object.define_method(:extend_ancestors) do |m|
36
47
  include_ancestors.bind(singleton_class).call(m)
37
48
  end
38
- Object.define_method(:extend_complete) do |m|
49
+ Object.define_method(:full_extend) do |m|
39
50
  include_complete.bind(singleton_class).call(m)
40
51
  end
41
52
  end
42
53
 
43
- #If we don't want to extend a module with Meta, we can still do
54
+ #apply is a 'useless' wrapper to .call, but it also works for UnboundMethod.
55
+ # See also dr/core_ext that adds 'UnboundMethod#call'
56
+ #=> If we don't want to extend a module with Meta, we can still do
44
57
  #Meta.apply(String,method: Meta.instance_method(:include_ancestors),to: self)
45
- #Note: another way is to use Symbold#to_proc which works like this:
46
- #foo=:foo.to_proc; foo.call(obj,*args) #=> obj.method(:foo).call(*args)
47
- #essentially apply is a 'useless' wrapper to .call, but it also works
48
- #for UnboundMethod. See also dr/core_ext that add
49
- #'UnboundMethod#call'
58
+ #(note that in 'Meta.apply', the default option to 'to:' is self=Meta,
59
+ #that's why we need to put 'to: self' again)
50
60
  def apply(*args,method: nil, to: self, **opts,&block)
51
61
  #note, in to self is Meta, except if we include it in another
52
62
  #module so that it would make sense
@@ -55,6 +65,7 @@ module DR
55
65
  when UnboundMethod
56
66
  method=method.bind(to)
57
67
  end
68
+ #We cannot call **opts if opts is empty in case of an empty args, cf https://bugs.ruby-lang.org/issues/10708
58
69
  if opts.empty?
59
70
  method.call(*args,&block)
60
71
  else
@@ -68,6 +79,32 @@ module DR
68
79
  obj.singleton_class.send(:remove_method,method_name)
69
80
  method
70
81
  end
82
+
83
+ #Taken from sinatra/base.rb: return an unbound method from a block, with
84
+ #owner the current module
85
+ #Conversely, from a (bound) method, calling to_proc (hence &m) gives a lambda
86
+ #Note: rather than doing
87
+ #m=get_unbound_method('',&block);m.bind(obj).call(args)
88
+ #one could do obj.instance_exec(args,&block)
89
+ def get_unbound_method(method_name, &block)
90
+ define_method(method_name, &block)
91
+ method = instance_method method_name
92
+ remove_method method_name
93
+ method
94
+ end
95
+
96
+ #like get_unbound_method except we pass a strng rather than a block
97
+ def get_unbound_evalmethod(method_name, method_str, args: '')
98
+ module_eval <<-RUBY
99
+ def #{method_name}(#{args})
100
+ #{method_str}
101
+ end
102
+ RUBY
103
+ method = instance_method method_name
104
+ remove_method method_name
105
+ method
106
+ end
107
+
71
108
  end
72
109
 
73
110
  #helping with metaprograming facilities
@@ -96,7 +133,7 @@ module DR
96
133
  end
97
134
 
98
135
  #include_ancestor includes all modules ancestor, so one can do
99
- #singleton_class.include_ancestors(m) to have a fully featured extend
136
+ #singleton_class.include_ancestors(String) to include the Module ancestors of String into the class
100
137
  def include_ancestors(m)
101
138
  ancestors=m.respond_to?(:ancestors) ? m.ancestors : m.singleton_class.ancestors
102
139
  ancestors.reverse.each do |m|
@@ -104,8 +141,7 @@ module DR
104
141
  end
105
142
  end
106
143
 
107
- #include a module and extend its singleton_class (along with its ancestors)
108
- def include_complete(obj)
144
+ def include_all_ancestors(obj)
109
145
  ancestors=Meta.all_ancestors(obj)
110
146
  ancestors.reverse.each do |m|
111
147
  include m if m.class==Module
@@ -113,11 +149,11 @@ module DR
113
149
  end
114
150
 
115
151
  # module Z
116
- # def x; "x"; end
152
+ # def x; "x"; end
117
153
  # end
118
154
  # module Enumerable
119
- # extend MetaModule
120
- # full_include Z
155
+ # extend MetaModule
156
+ # full_include Z
121
157
  # end
122
158
  # Array.new.x => "x"
123
159
  def full_include other
@@ -130,19 +166,6 @@ module DR
130
166
  end
131
167
  end
132
168
 
133
- #Taken from sinatra/base.rb: return an unbound method from a block, with
134
- #owner the current module
135
- #Conversely, from a (bound) method, calling to_proc (hence &m) gives a lambda
136
- #Note: rather than doing
137
- #m=get_unbound_method('',&block);m.bind(obj).call(args)
138
- #one could do obj.instance_exec(args,&block)
139
- def get_unbound_method(method_name, &block)
140
- define_method(method_name, &block)
141
- method = instance_method method_name
142
- remove_method method_name
143
- method
144
- end
145
-
146
169
  #essentially like define_method, but can pass a Method or an UnboundMethod
147
170
  #see also dr/core_ext which add UnboundMethod#to_proc so we could
148
171
  #instead use define_method(name,&method) and it would work
@@ -167,6 +190,10 @@ module DR
167
190
  end
168
191
  end
169
192
 
193
+ #add_methods(method1, method2, ...)
194
+ #add_methods({name1: method1, name2: method2, ...})
195
+ #add_methods(aClass, :method_name1, :method_name2)
196
+ #add_methods(aClass, {new_name1: :method_name1, new_name2: :method_name2})
170
197
  def add_methods(*args)
171
198
  return if args.empty?
172
199
  if Module === args.first
@@ -0,0 +1 @@
1
+ lib/dr/base.rb
File without changes
@@ -0,0 +1,4 @@
1
+ module DR
2
+ # drain version
3
+ VERSION = "0.2.0"
4
+ end
@@ -1 +1,2 @@
1
- require 'drain/version'
1
+ require 'dr.rb'
2
+ Drain=DR
@@ -1,2 +1,13 @@
1
- require 'rubygems'
2
1
  require 'minitest/autorun'
2
+
3
+ ## Uncomment to launch pry on a failure
4
+ #require 'pry-rescue/minitest'
5
+
6
+ begin
7
+ require 'minitest/reporters'
8
+ Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
9
+ #Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
10
+ #Minitest::Reporters.use! Minitest::Reporters::ProgressReporter.new
11
+ rescue LoadError => error
12
+ warn "minitest/reporters not found, not changing minitest reporter: #{error}"
13
+ end
@@ -0,0 +1,42 @@
1
+ require 'helper'
2
+ require 'dr/base/converter'
3
+
4
+ describe DR::Converter do
5
+ before do
6
+ klass=Class.new do
7
+ attr_accessor :a, :h
8
+ def initialize(a,h)
9
+ @a=a
10
+ @h=h
11
+ end
12
+ end
13
+ @obj1=klass.new(["foo","bar"],{foo: :bar})
14
+ @obj2=klass.new([@obj1],{})
15
+ @obj3=klass.new([],{@obj1 => @obj2})
16
+ @obj3.a << @obj3
17
+ end
18
+
19
+ it "Output a hash with the attributes" do
20
+ DR::Converter.to_hash(@obj1, methods: [:a,:h]).must_equal({@obj1 => {a: @obj1.a, h: @obj1.h}})
21
+ end
22
+
23
+ it ":compact compress the values when there is only one method" do
24
+ DR::Converter.to_hash(@obj1, methods: [:a,:h], compact: true).must_equal({@obj1 => {a: @obj1.a, h: @obj1.h}})
25
+ DR::Converter.to_hash(@obj1, methods: [:a], compact: true).must_equal({@obj1 => @obj1.a})
26
+ end
27
+
28
+ it ":check checks that the method exists" do
29
+ -> {DR::Converter.to_hash(@obj1, methods: [:none], check: false)}.must_raise NoMethodError
30
+ DR::Converter.to_hash(@obj1, methods: [:none], check: true).must_equal({@obj1 => {}})
31
+ end
32
+
33
+ it "accepts a list" do
34
+ DR::Converter.to_hash([@obj1,@obj2], methods: [:a,:h]).must_equal({@obj1 => {a: @obj1.a, h: @obj1.h}, @obj2 => {a: @obj2.a, h: @obj2.h}})
35
+ end
36
+
37
+ #this test also test that cycles work
38
+ it ":recursive generate the hash on the values" do
39
+ DR::Converter.to_hash(@obj3, methods: [:a,:h], recursive: true).must_equal({@obj1 => {a: @obj1.a, h: @obj1.h}, @obj2 => {a: @obj2.a, h: @obj2.h}, @obj3 => {a: @obj3.a, h: @obj3.h}})
40
+ end
41
+
42
+ end
@@ -0,0 +1,116 @@
1
+ require 'helper'
2
+ require 'dr/ruby_ext/core_modules'
3
+
4
+ module TestCoreExtRefinements
5
+ using DR::CoreRef
6
+ # TODO make refinements work nicely
7
+ end
8
+
9
+ module TestCoreExt
10
+ require 'dr/ruby_ext/core_ext'
11
+ describe DR::CoreExt do
12
+ describe Enumerable do
13
+ it "Can filter enumerable" do
14
+ [1,2,3,4].filter({odd: [1,3], default: :even}).must_equal({:odd=>[1, 3], :even=>[2, 4]})
15
+ end
16
+ end
17
+
18
+ describe Hash do
19
+ it "Implements Hash#deep_merge" do
20
+ h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
21
+ h2 = { x: { y: [7,8,9] }, z: 'xyz' }
22
+ h1.deep_merge(h2).must_equal({x: {y: [7, 8, 9]}, z: "xyz"})
23
+ h2.deep_merge(h1).must_equal({x: {y: [4, 5, 6]}, z: [7, 8, 9]})
24
+ h1.deep_merge(h2) { |key, old, new| Array(old) + Array(new) }.must_equal({:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]})
25
+ end
26
+
27
+ it "Hash#deep_merge merge array when they start with nil" do
28
+ h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
29
+ h2 = { x: { y: [nil, 7,8,9] }, z: 'xyz' }
30
+ h1.deep_merge(h2).must_equal({x: {y: [4,5,6,7, 8, 9]}, z: "xyz"})
31
+ {x: { y: []} }.deep_merge(h2).must_equal({x: {y: [7, 8, 9]}, z: "xyz"})
32
+ {z: "foo"}.deep_merge(h2).must_equal({x: {y: [7, 8, 9]}, z: "xyz"})
33
+ end
34
+
35
+ it "Implements Hash#inverse" do
36
+ h={ploum: 2, plim: 2, plam: 3}
37
+ h.inverse.must_equal({2=>[:ploum, :plim], 3=>[:plam]})
38
+ end
39
+
40
+ it "Implements Hash#keyed_value" do
41
+ h = { x: { y: { z: "foo" } } }
42
+ h.keyed_value("x/y/z").must_equal("foo")
43
+ end
44
+
45
+ it "Implements Hash#set_keyed_value" do
46
+ h = { x: { y: { z: "foo" } } }
47
+ h.set_keyed_value("x/y/z","bar").must_equal({ x: { y: { z: "bar" } } })
48
+ h.set_keyed_value("x/y","bar2").must_equal({ x: { y: "bar2" } })
49
+ h.set_keyed_value("z/y","bar3").must_equal({ x: { y: "bar2" } , z: {y: "bar3"}})
50
+ end
51
+
52
+ it "Implements Hash#leafs" do
53
+ {foo: [:bar, :baz], bar: [:plum, :qux]}.leafs([:foo]).must_equal([:plum, :qux, :baz])
54
+ end
55
+ end
56
+
57
+ describe UnboundMethod do
58
+ it "Can be converted to a proc" do
59
+ m=String.instance_method(:length)
60
+ ["foo", "ploum"].map(&m).must_equal([3,5])
61
+ end
62
+ it "Can call" do
63
+ String.instance_method(:length).call("foo").must_equal(3)
64
+ end
65
+ end
66
+
67
+ describe Proc do
68
+ # fails due to ruby bug on double splat
69
+ # it "call_block does not worry about arity of lambda" do
70
+ # (->(x,y) {x+y}).call_block(1,2,3).must_equal(3)
71
+ # end
72
+
73
+ it "Can do rcurry" do
74
+ l=->(x,y) {"#{x}: #{y}"}
75
+ m=l.rcurry("foo")
76
+ m.call("bar").must_equal("bar: foo")
77
+ end
78
+
79
+ it "Can compose functions" do
80
+ somme=->(x,y) {x+y}
81
+ carre=->(x) {x*x}
82
+ carre.compose(somme).(2,3).must_equal(25)
83
+ end
84
+
85
+ it "Can uncurry functions" do
86
+ (->(x) {->(y) {x+y}}).uncurry.(2,3).must_equal(5)
87
+ (->(x,y) {x+y}).curry.uncurry.(2,3).must_equal(5)
88
+ end
89
+ end
90
+
91
+ describe Array do
92
+ it "Can be converted to proc (providing extra arguments)" do
93
+ ["ploum","plam"].map(&[:+,"foo"]).must_equal(["ploumfoo", "plamfoo"])
94
+ end
95
+ end
96
+
97
+ describe Object do
98
+ it "this can change the object" do
99
+ "foo".this {|s| s.size}.+(1).must_equal(4)
100
+ end
101
+
102
+ it "and_this emulates the Maybe Monad" do
103
+ "foo".and_this {|s| s.size}.must_equal(3)
104
+ assert_nil nil.and_this {|s| s.size}
105
+ end
106
+ end
107
+
108
+ describe DR::RecursiveHash do
109
+ it "Generates keys when needed" do
110
+ h=DR::RecursiveHash.new
111
+ h[:foo][:bar]=3
112
+ h.must_equal({foo: {bar: 3}})
113
+ end
114
+ end
115
+ end
116
+ end