subhash 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4438f0e738721e6c962927ac4ca4c9d532bd1257
4
+ data.tar.gz: c70b1bb50ed69081842f8e27ec5f0589b0544d50
5
+ SHA512:
6
+ metadata.gz: 0a6d52b5a2a87cc274534d49a5b53bcd9a834512ef3b6b25861eb660536e06614c84ad72c448b6ea237f913b1d9aa6d17e9fa985a2198f7f5bf398721bbdb172
7
+ data.tar.gz: 04c7cad6557626bcb02e479ae44bf155e9f810c843f80b7e2769c3f8cc99fd4062e3891eec8890dea383eb0a590115a6b43f6758180479e80d77938bf9cec72b
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,4 @@
1
+ [gerrit]
2
+ host=review.forj.forj.io
3
+ port=29418
4
+ project=forj-oss/rhash.git
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,33 @@
1
+ # Copyright 2013 Hewlett-Packard Development Company, L.P.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # use: rubocop --show-cops
16
+ # to validate configuration.
17
+ # rubocop config
18
+ AllCops:
19
+ Include:
20
+ - '**/Rakefile'
21
+ Style/HashSyntax:
22
+ EnforcedStyle: hash_rockets
23
+
24
+ # lets start with 40, but 10 is way to small..
25
+ Metrics/MethodLength:
26
+ Max: 40
27
+ # If Method length is increased, class length need to be extended as well.
28
+ Metrics/ClassLength:
29
+ Max: 150
30
+
31
+ # allow arguments to be longer than 15
32
+ Metrics/AbcSize:
33
+ Max: 40
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rh.gemspec
4
+ gemspec
@@ -0,0 +1,115 @@
1
+ # Recursive Hash of Hashes/Arrays
2
+
3
+ SubHash is a set of tools which facilitate data structured as tree of Hash or Array to be
4
+ set and get.
5
+
6
+ Imagine you have a yaml file to load. If your yaml file is well structured, it will be stored
7
+ in memory as Hash of Hashes, or even Arrays or any kind of type recognized by ruby.
8
+
9
+ So, if you want to access a data in a strong tree of hash, how do you write this?
10
+
11
+ puts data[key_l1][key_l2][key_l3][mykey]
12
+ => myvalue
13
+
14
+ What's happen if key\_l1 doesn't exist? or exist but contains a nil or any other value instead of a Hash?
15
+ An exception is generated. So, you need to add exception to avoid this.
16
+
17
+ But your code may become a little complex if you need to check if those layers exists or not...
18
+
19
+ So, imagine that instead of that, you do:
20
+
21
+ puts data.rh_get(key_l1, key_l2, key_l3, mykey)
22
+
23
+ Interesting, right?
24
+
25
+ Imagine the set, now:
26
+
27
+ data.rh_set(MyNewValue, key_l1, key_l2, key_l3, mykey)
28
+
29
+ Seems easier, right?
30
+
31
+ And you can check if keys exists, as well as until which level of Hash, I found the path to my key:
32
+
33
+ data.rh_exist?(key_l1, key_l2, key_l3, mykey)
34
+ => true/false
35
+
36
+ data.rh_lexist?(key_l1, key_l2, key_l3, mykey)
37
+ => can be 0, 1, 2 ,3 or 4, depending on the path existence to access mykey...
38
+
39
+ If you think this can help you, subhash provides the following functions to the Ruby Hash/Array object:
40
+
41
+ - Hash.rh\_get
42
+ - Hash.rh\_set
43
+ - Hash.rh\_exist? or Hash.rh\_lexist?
44
+ - Hash.rh\_del
45
+ - Hash.rh\_merge
46
+ - Array.rh\_merge
47
+
48
+ Thanks to set of functions, you can easily create a tree of Hash or Array.
49
+
50
+ Ex:
51
+
52
+ data = {}
53
+
54
+ data.rh_set(:value, :level1, :level2, :level3)
55
+ # => { :level1 => { :level2 => { :level3 => :value } } }
56
+
57
+ # And it is easy to add or remove
58
+ data.rh_set(:value2, :level1, :level2, :level3)
59
+ # => { :level1 => { :level2 => { :level3 => :value } } }
60
+ data.rh_set(:value, :level1, :level2, :level3_second)
61
+ # => { :level1 => { :level2 => { :level3 => :value, :level3_second => :value } } }
62
+ data.rh_del(:level1, :level2, :level3)
63
+ # => { :level1 => { :level2 => { } } }
64
+
65
+ ## Installation
66
+
67
+ Add this line to your application's Gemfile:
68
+
69
+ ```ruby
70
+ gem 'subhash'
71
+ ```
72
+
73
+ And then execute:
74
+
75
+ $ bundle
76
+
77
+ Or install it yourself as:
78
+
79
+ $ gem install subhash
80
+
81
+ ## Usage
82
+
83
+ To use Recursive Hash, just add this require in your code:
84
+
85
+ require 'subhash'
86
+
87
+ Hash and Array object are enhanced and provide those features to any Hash or Array. So, you just need to
88
+ call wanted functions.
89
+
90
+ Ex:
91
+
92
+ data = Hash.new
93
+ data.rh_set(:value, :key_lvl1)
94
+
95
+ ## Development
96
+
97
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
98
+
99
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
100
+
101
+ ## Contributing
102
+
103
+ 1. clone it ( https://review.forj.io/forj-oss/rhash )
104
+ 2. add an alias to push your code
105
+
106
+ alias git-push='git push origin HEAD:refs/for/$(git branch -v|grep "^\*"|awk '\''{printf $2}'\'')'
107
+
108
+ or install git-review
109
+
110
+ 3. push your code
111
+ * with git-push
112
+ * with git-review. See http://www.mediawiki.org/wiki/Gerrit/git-review
113
+
114
+
115
+ Enjoy!!!
@@ -0,0 +1,27 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+ require 'rdoc/task'
5
+
6
+ task :default => [:lint, :spec]
7
+
8
+ desc 'Run the specs.'
9
+ RSpec::Core::RakeTask.new do |t|
10
+ t.pattern = 'spec/*_spec.rb'
11
+ t.rspec_opts = '-f doc'
12
+ end
13
+
14
+ desc 'Generate lorj documentation'
15
+ RDoc::Task.new do |rdoc|
16
+ rdoc.main = 'README.md'
17
+ rdoc.rdoc_files.include('README.md', 'lib', 'example', 'bin')
18
+ end
19
+
20
+ desc 'Run RuboCop on the project'
21
+ RuboCop::RakeTask.new(:lint) do |task|
22
+ task.formatters = ['progress']
23
+ task.verbose = true
24
+ task.fail_on_error = true
25
+ end
26
+
27
+ task :build => [:lint, :spec]
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'rh'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ puts "Recursive Hash module
14
+ Short list of functions:
15
+ - Hash.rh_set(value, *keys) => value
16
+ - Hash.rh_get(*keys) => value found or nil
17
+ - Hash.rh_exist?(*keys) => true/false
18
+ - Hash.rh_lexist?(*keys) => number
19
+ - Hash.rh_clone() => Hash
20
+ - Hash.rh_del(*keys) => data removed
21
+ - Hash.rh_merge(hash) => Hash/Array merged
22
+ - Hash.rh_merge!(hash) => Hash/Array merged
23
+ - Array.rh_merge(Array) => Array/Hash merged
24
+ - Array.rh_merge!(Array) => Array/Hash merged"
25
+
26
+ require 'irb'
27
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,844 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'rubygems'
19
+ require 'yaml'
20
+
21
+ # Adding rh_clone at object level. This be able to use a generic rh_clone
22
+ # redefined per object Hash and Array.
23
+ class Object
24
+ alias_method :rh_clone, :clone
25
+ end
26
+
27
+ # Rh common module included in Hash and Array class.
28
+ module Rh
29
+ public
30
+
31
+ # Function which will parse arrays in hierarchie and will remove any control
32
+ # element (index 0)
33
+ def rh_remove_control(result)
34
+ return unless [Hash, Array].include?(result.class)
35
+
36
+ if result.is_a?(Hash)
37
+ result.each { |elem| rh_remove_control(elem) }
38
+ else
39
+ result.delete_at(0) if result[0].is_a?(Hash) && result[0].key?(:__control)
40
+ result.each_index { |index| rh_remove_control(result[index]) }
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Internal function to determine if result and data key contains both Hash or
47
+ # Array and if so, do the merge task on those sub Hash/Array
48
+ #
49
+ def _rh_merge_recursive(result, key, data)
50
+ return false unless [Array, Hash].include?(data.class)
51
+
52
+ value = data[key]
53
+ return false unless [Array, Hash].include?(value.class) &&
54
+ value.class == result[key].class
55
+
56
+ if object_id == result.object_id
57
+ result[key].rh_merge!(value)
58
+ else
59
+ result[key] = result[key].rh_merge(value)
60
+ end
61
+
62
+ true
63
+ end
64
+
65
+ # Internal function to determine if changing from Hash/Array to anything else
66
+ # is authorized or not.
67
+ #
68
+ # The structure is changing if `result` or `value` move from Hash/Array to any
69
+ # other type.
70
+ #
71
+ # * *Args*:
72
+ # - result: Merged Hash or Array structure.
73
+ # - key : Key in result and data.
74
+ # - data : Hash or Array structure to merge.
75
+ #
76
+ # * *returns*:
77
+ # - +true+ : if :__struct_changing == true
78
+ # - +false+ : otherwise.
79
+ def _rh_struct_changing_ok?(result, key, data)
80
+ return true unless [Array, Hash].include?(data[key].class) ||
81
+ [Array, Hash].include?(result[key].class)
82
+
83
+ # result or value are structure (Hash or Array)
84
+ if result.is_a?(Hash)
85
+ control = result[:__struct_changing]
86
+ else
87
+ control = result[0][:__struct_changing]
88
+ key -= 1
89
+ end
90
+ return true if control.is_a?(Array) && control.include?(key)
91
+
92
+ false
93
+ end
94
+
95
+ # Internal function to determine if a data merged can be updated by any
96
+ # other object like Array, String, etc...
97
+ #
98
+ # The decision is given by a :__unset setting.
99
+ #
100
+ # * *Args*:
101
+ # - Hash/Array data to replace.
102
+ # - key: string or symbol.
103
+ #
104
+ # * *returns*:
105
+ # - +false+ : if key is found in :__protected Array.
106
+ # - +true+ : otherwise.
107
+ def _rh_merge_ok?(result, key)
108
+ if result.is_a?(Hash)
109
+ control = result[:__protected]
110
+ else
111
+ control = result[0][:__protected]
112
+ key -= 1
113
+ end
114
+
115
+ return false if control.is_a?(Array) && control.include?(key)
116
+
117
+ true
118
+ end
119
+
120
+ def _rh_control_tags
121
+ [:__remove, :__remove_index, :__add, :__add_index,
122
+ :__protected, :__struct_changing, :__control]
123
+ end
124
+ end
125
+
126
+ # Recursive Hash added to the Hash class
127
+ class Hash
128
+ # Recursive Hash deep level found counter
129
+ # This function will returns the count of deep level of recursive hash.
130
+ # * *Args* :
131
+ # - +p+ : Array of string or symbols. keys tree to follow and check
132
+ # existence in yVal.
133
+ #
134
+ # * *Returns* :
135
+ # - +integer+ : Represents how many deep level was found in the recursive
136
+ # hash
137
+ #
138
+ # * *Raises* :
139
+ # No exceptions
140
+ #
141
+ # Example: (implemented in spec)
142
+ #
143
+ # yVal = { :test => {:test2 => 'value1', :test3 => 'value2'},
144
+ # :test4 => 'value3'}
145
+ #
146
+ # yVal can be represented like:
147
+ #
148
+ # yVal:
149
+ # test:
150
+ # test2 = 'value1'
151
+ # test3 = 'value2'
152
+ # test4 = 'value3'
153
+ #
154
+ # so:
155
+ # # test is found
156
+ # yVal.rh_lexist?(:test) => 1
157
+ #
158
+ # # no test5
159
+ # yVal.rh_lexist?(:test5) => 0
160
+ #
161
+ # # :test/:test2 tree is found
162
+ # yVal.rh_lexist?(:test, :test2) => 2
163
+ #
164
+ # # :test/:test2 is found (value = 2), but :test5 was not found in this tree
165
+ # yVal.rh_lexist?(:test, :test2, :test5) => 2
166
+ #
167
+ # # :test was found. but :test/:test5 tree was not found. so level 1, ok.
168
+ # yVal.rh_lexist?(:test, :test5 ) => 1
169
+ #
170
+ # # it is like searching for nothing...
171
+ # yVal.rh_lexist? => 0
172
+
173
+ def rh_lexist?(*p)
174
+ p = p.flatten
175
+
176
+ return 0 if p.length == 0
177
+
178
+ if p.length == 1
179
+ return 1 if self.key?(p[0])
180
+ return 0
181
+ end
182
+ return 0 unless self.key?(p[0])
183
+ ret = 0
184
+ ret = self[p[0]].rh_lexist?(p.drop(1)) if self[p[0]].is_a?(Hash)
185
+ 1 + ret
186
+ end
187
+
188
+ # Recursive Hash deep level existence
189
+ #
190
+ # * *Args* :
191
+ # - +p+ : Array of string or symbols. keys tree to follow and check
192
+ # existence in yVal.
193
+ #
194
+ # * *Returns* :
195
+ # - +boolean+ : Returns True if the deep level of recursive hash is found.
196
+ # false otherwise
197
+ #
198
+ # * *Raises* :
199
+ # No exceptions
200
+ #
201
+ # Example:(implemented in spec)
202
+ #
203
+ # yVal = { :test => {:test2 => 'value1', :test3 => 'value2'},
204
+ # :test4 => 'value3'}
205
+ #
206
+ # yVal can be represented like:
207
+ #
208
+ # yVal:
209
+ # test:
210
+ # test2 = 'value1'
211
+ # test3 = 'value2'
212
+ # test4 = 'value3'
213
+ #
214
+ # so:
215
+ # # test is found
216
+ # yVal.rh_exist?(:test) => True
217
+ #
218
+ # # no test5
219
+ # yVal.rh_exist?(:test5) => False
220
+ #
221
+ # # :test/:test2 tree is found
222
+ # yVal.rh_exist?(:test, :test2) => True
223
+ #
224
+ # # :test/:test2 is found (value = 2), but :test5 was not found in this tree
225
+ # yVal.rh_exist?(:test, :test2, :test5) => False
226
+ #
227
+ # # :test was found. but :test/:test5 tree was not found. so level 1, ok.
228
+ # yVal.rh_exist?(:test, :test5 ) => False
229
+ #
230
+ # # it is like searching for nothing...
231
+ # yVal.rh_exist? => nil
232
+ def rh_exist?(*p)
233
+ p = p.flatten
234
+
235
+ return nil if p.length == 0
236
+
237
+ count = p.length
238
+ (rh_lexist?(*p) == count)
239
+ end
240
+
241
+ # Recursive Hash Get
242
+ # This function will returns the level of recursive hash was found.
243
+ # * *Args* :
244
+ # - +p+ : Array of string or symbols. keys tree to follow and check
245
+ # existence in yVal.
246
+ #
247
+ # * *Returns* :
248
+ # - +value+ : Represents the data found in the tree. Can be of any type.
249
+ #
250
+ # * *Raises* :
251
+ # No exceptions
252
+ #
253
+ # Example:(implemented in spec)
254
+ #
255
+ # yVal = { :test => {:test2 => 'value1', :test3 => 'value2'},
256
+ # :test4 => 'value3'}
257
+ #
258
+ # yVal can be represented like:
259
+ #
260
+ # yVal:
261
+ # test:
262
+ # test2 = 'value1'
263
+ # test3 = 'value2'
264
+ # test4 = 'value3'
265
+ #
266
+ # so:
267
+ # yVal.rh_get(:test) => {:test2 => 'value1', :test3 => 'value2'}
268
+ # yVal.rh_get(:test5) => nil
269
+ # yVal.rh_get(:test, :test2) => 'value1'
270
+ # yVal.rh_get(:test, :test2, :test5) => nil
271
+ # yVal.rh_get(:test, :test5 ) => nil
272
+ # yVal.rh_get => { :test => {:test2 => 'value1', :test3 => 'value2'},
273
+ # :test4 => 'value3'}
274
+ def rh_get(*p)
275
+ p = p.flatten
276
+ return self if p.length == 0
277
+
278
+ if p.length == 1
279
+ return self[p[0]] if self.key?(p[0])
280
+ return nil
281
+ end
282
+ return self[p[0]].rh_get(p.drop(1)) if self[p[0]].is_a?(Hash)
283
+ nil
284
+ end
285
+
286
+ # Recursive Hash Set
287
+ # This function will build a recursive hash according to the '*p' key tree.
288
+ # if yVal is not nil, it will be updated.
289
+ #
290
+ # * *Args* :
291
+ # - +p+ : Array of string or symbols. keys tree to follow and check
292
+ # existence in yVal.
293
+ #
294
+ # * *Returns* :
295
+ # - +value+ : the value set.
296
+ #
297
+ # * *Raises* :
298
+ # No exceptions
299
+ #
300
+ # Example:(implemented in spec)
301
+ #
302
+ # yVal = {}
303
+ #
304
+ # yVal.rh_set(:test) => nil
305
+ # # yVal = {}
306
+ #
307
+ # yVal.rh_set(:test5) => nil
308
+ # # yVal = {}
309
+ #
310
+ # yVal.rh_set(:test, :test2) => :test
311
+ # # yVal = {:test2 => :test}
312
+ #
313
+ # yVal.rh_set(:test, :test2, :test5) => :test
314
+ # # yVal = {:test2 => {:test5 => :test} }
315
+ #
316
+ # yVal.rh_set(:test, :test5 ) => :test
317
+ # # yVal = {:test2 => {:test5 => :test}, :test5 => :test }
318
+ #
319
+ # yVal.rh_set('blabla', :test2, 'text') => :test
320
+ # # yVal = {:test2 => {:test5 => :test, 'text' => 'blabla'},
321
+ # :test5 => :test }
322
+ def rh_set(value, *p)
323
+ p = p.flatten
324
+ return nil if p.length == 0
325
+
326
+ if p.length == 1
327
+ self[p[0]] = value
328
+ return value
329
+ end
330
+
331
+ self[p[0]] = {} unless self[p[0]].is_a?(Hash)
332
+ self[p[0]].rh_set(value, p.drop(1))
333
+ end
334
+
335
+ # Recursive Hash delete
336
+ # This function will remove the last key defined by the key tree
337
+ #
338
+ # * *Args* :
339
+ # - +p+ : Array of string or symbols. keys tree to follow and check
340
+ # existence in yVal.
341
+ #
342
+ # * *Returns* :
343
+ # - +value+ : The Hash updated.
344
+ #
345
+ # * *Raises* :
346
+ # No exceptions
347
+ #
348
+ # Example:(implemented in spec)
349
+ #
350
+ # yVal = {{:test2 => { :test5 => :test,
351
+ # 'text' => 'blabla' },
352
+ # :test5 => :test}}
353
+ #
354
+ #
355
+ # yVal.rh_del(:test) => nil
356
+ # # yVal = no change
357
+ #
358
+ # yVal.rh_del(:test, :test2) => nil
359
+ # # yVal = no change
360
+ #
361
+ # yVal.rh_del(:test2, :test5) => {:test5 => :test}
362
+ # # yVal = {:test2 => {:test5 => :test} }
363
+ #
364
+ # yVal.rh_del(:test, :test2)
365
+ # # yVal = {:test2 => {:test5 => :test} }
366
+ #
367
+ # yVal.rh_del(:test, :test5)
368
+ # # yVal = {:test2 => {} }
369
+ #
370
+ def rh_del(*p)
371
+ p = p.flatten
372
+
373
+ return nil if p.length == 0
374
+
375
+ return delete(p[0]) if p.length == 1
376
+
377
+ return nil if self[p[0]].nil?
378
+ self[p[0]].rh_del(p.drop(1))
379
+ end
380
+
381
+ # Move levels (default level 1) of tree keys to become symbol.
382
+ #
383
+ # * *Args* :
384
+ # - +levels+: level of key tree to update.
385
+ # * *Returns* :
386
+ # - a new hash of hashes updated. Original Hash is not updated anymore.
387
+ #
388
+ # examples:
389
+ # With hdata = { :test => { :test2 => { :test5 => :test,
390
+ # 'text' => 'blabla' },
391
+ # 'test5' => 'test' }}
392
+ #
393
+ # hdata.rh_key_to_symbol(1) return no diff
394
+ # hdata.rh_key_to_symbol(2) return "test5" is replaced by :test5
395
+ # # hdata = { :test => { :test2 => { :test5 => :test,
396
+ # # 'text' => 'blabla' },
397
+ # # :test5 => 'test' }}
398
+ # rh_key_to_symbol(3) return "test5" replaced by :test5, and "text" to :text
399
+ # # hdata = { :test => { :test2 => { :test5 => :test,
400
+ # # :text => 'blabla' },
401
+ # # :test5 => 'test' }}
402
+ # rh_key_to_symbol(4) same like rh_key_to_symbol(3)
403
+
404
+ def rh_key_to_symbol(levels = 1)
405
+ result = {}
406
+ each do |key, value|
407
+ new_key = key
408
+ new_key = key.to_sym if key.is_a?(String)
409
+ if value.is_a?(Hash) && levels > 1
410
+ value = value.rh_key_to_symbol(levels - 1)
411
+ end
412
+ result[new_key] = value
413
+ end
414
+ result
415
+ end
416
+
417
+ # Check if levels of tree keys are all symbols.
418
+ #
419
+ # * *Args* :
420
+ # - +levels+: level of key tree to update.
421
+ # * *Returns* :
422
+ # - true : one key path is not symbol.
423
+ # - false : all key path are symbols.
424
+ # * *Raises* :
425
+ # Nothing
426
+ #
427
+ # examples:
428
+ # With hdata = { :test => { :test2 => { :test5 => :test,
429
+ # 'text' => 'blabla' },
430
+ # 'test5' => 'test' }}
431
+ #
432
+ # hdata.rh_key_to_symbol?(1) return false
433
+ # hdata.rh_key_to_symbol?(2) return true
434
+ # hdata.rh_key_to_symbol?(3) return true
435
+ # hdata.rh_key_to_symbol?(4) return true
436
+ def rh_key_to_symbol?(levels = 1)
437
+ each do |key, value|
438
+ return true if key.is_a?(String)
439
+
440
+ res = false
441
+ if levels > 1 && value.is_a?(Hash)
442
+ res = value.rh_key_to_symbol?(levels - 1)
443
+ end
444
+ return true if res
445
+ end
446
+ false
447
+ end
448
+
449
+ # return an exact clone of the recursive Array and Hash contents.
450
+ #
451
+ # * *Args* :
452
+ #
453
+ # * *Returns* :
454
+ # - Recursive Array/Hash cloned. Other kind of objects are kept referenced.
455
+ # * *Raises* :
456
+ # Nothing
457
+ #
458
+ # examples:
459
+ # hdata = { :test => { :test2 => { :test5 => :test,
460
+ # 'text' => 'blabla' },
461
+ # 'test5' => 'test' },
462
+ # :array => [{ :test => :value1 }, 2, { :test => :value3 }]}
463
+ #
464
+ # hclone = hdata.rh_clone
465
+ # hclone[:test] = "test"
466
+ # hdata[:test] == { :test2 => { :test5 => :test,'text' => 'blabla' }
467
+ # # => true
468
+ # hclone[:array].pop
469
+ # hdata[:array].length != hclone[:array].length
470
+ # # => true
471
+ # hclone[:array][0][:test] = "value2"
472
+ # hdata[:array][0][:test] != hclone[:array][0][:test]
473
+ # # => true
474
+ def rh_clone
475
+ result = {}
476
+ each do |key, value|
477
+ if [Array, Hash].include?(value.class)
478
+ result[key] = value.rh_clone
479
+ else
480
+ result[key] = value
481
+ end
482
+ end
483
+ result
484
+ end
485
+
486
+ # Merge the current Hash object (self) cloned with a Hash/Array tree contents
487
+ # (data).
488
+ #
489
+ # 'self' is used as original data to merge to.
490
+ # 'data' is used as data to merged to clone of 'self'. If you want to update
491
+ # 'self', use rh_merge!
492
+ #
493
+ # if 'self' or 'data' contains a Hash tree, the merge will be executed
494
+ # recursively.
495
+ #
496
+ # The current function will execute the merge of the 'self' keys with the top
497
+ # keys in 'data'
498
+ #
499
+ # The merge can be controlled by an additionnal Hash key '__*' in each
500
+ # 'self' key.
501
+ # If both a <key> exist in 'self' and 'data', the following decision is made:
502
+ # - if both 'self' and 'data' key contains an Hash or and Array, a recursive
503
+ # merge if Hash or update if Array, is started.
504
+ #
505
+ # - if 'self' <key> contains an Hash or an Array, but not 'data' <key>, then
506
+ # 'self' <key> will be set to the 'data' <key> except if 'self' <Key> has
507
+ # :__struct_changing: true
508
+ # data <key> value can set :unset value
509
+ #
510
+ # - if 'self' <key> is :unset and 'data' <key> is any value
511
+ # 'self' <key> value is set with 'data' <key> value.
512
+ # 'data' <key> value can contains a Hash with :__no_unset: true to
513
+ # protect this key against the next merge. (next config layer merge)
514
+ #
515
+ # - if 'data' <key> exist but not in 'self', 'data' <key> is just added.
516
+ #
517
+ # - if 'data' & 'self' <key> exist, 'self'<key> is updated except if key is in
518
+ # :__protected array list.
519
+ #
520
+ # * *Args* :
521
+ # - hash : Hash data to merge.
522
+ #
523
+ # * *Returns* :
524
+ # - Recursive Array/Hash merged.
525
+ #
526
+ # * *Raises* :
527
+ # Nothing
528
+ #
529
+ # examples:
530
+ #
531
+ def rh_merge(data)
532
+ _rh_merge(clone, data)
533
+ end
534
+
535
+ # Merge the current Hash object (self) with a Hash/Array tree contents (data).
536
+ #
537
+ # For details on this functions, see #rh_merge
538
+ #
539
+ def rh_merge!(data)
540
+ _rh_merge(self, data)
541
+ end
542
+ end
543
+
544
+ # Recursive Hash added to the Hash class
545
+ class Hash
546
+ private
547
+
548
+ # Internal function which do the real merge task by #rh_merge and #rh_merge!
549
+ #
550
+ # See #rh_merge for details
551
+ #
552
+ def _rh_merge(result, data)
553
+ return _rh_merge_choose_data(result, data) unless data.is_a?(Hash)
554
+
555
+ data.each do |key, _value|
556
+ next if [:__struct_changing, :__protected].include?(key)
557
+
558
+ _do_rh_merge(result, key, data)
559
+ end
560
+ [:__struct_changing, :__protected].each do |key|
561
+ # Refuse merge by default if key data type are different.
562
+ # This assume that the first layer merge has set
563
+ # :__unset as a Hash, and :__protected as an Array.
564
+
565
+ _do_rh_merge(result, key, data, true) if data.key?(key)
566
+
567
+ # Remove all control element in arrays
568
+ rh_remove_control(result[key]) if result.key?(key)
569
+ end
570
+
571
+ result
572
+ end
573
+
574
+ def _rh_merge_choose_data(result, data)
575
+ # return result as first one impose the type between Hash/Array.
576
+ return result if [Hash, Array].include?(result.class) ||
577
+ [Hash, Array].include?(data.class)
578
+
579
+ data
580
+ end
581
+ # Internal function to execute the merge on one key provided by #_rh_merge
582
+ #
583
+ # if refuse_discordance is true, then result[key] can't be updated if
584
+ # stricly not of same type.
585
+ def _do_rh_merge(result, key, data, refuse_discordance = false)
586
+ value = data[key]
587
+
588
+ return if _rh_merge_do_add_key(result, key, value)
589
+
590
+ return if _rh_merge_recursive(result, key, data)
591
+
592
+ return if refuse_discordance
593
+
594
+ return unless _rh_struct_changing_ok?(result, key, data)
595
+
596
+ return unless _rh_merge_ok?(result, key)
597
+
598
+ _rh_merge_do_upd_key(result, key, value)
599
+ end
600
+
601
+ def _rh_merge_do_add_key(result, key, value)
602
+ unless result.key?(key) || value == :unset
603
+ result[key] = value # New key added
604
+ return true
605
+ end
606
+ false
607
+ end
608
+
609
+ def _rh_merge_do_upd_key(result, key, value)
610
+ if value == :unset
611
+ result.delete(key) if result.key?(key)
612
+ return
613
+ end
614
+
615
+ result[key] = value # Key updated
616
+ end
617
+
618
+ include Rh
619
+ end
620
+
621
+ # Defines rh_clone for Array
622
+ class Array
623
+ # return an exact clone of the recursive Array and Hash contents.
624
+ #
625
+ # * *Args* :
626
+ #
627
+ # * *Returns* :
628
+ # - Recursive Array/Hash cloned.
629
+ # * *Raises* :
630
+ # Nothing
631
+ #
632
+ # examples:
633
+ # hdata = { :test => { :test2 => { :test5 => :test,
634
+ # 'text' => 'blabla' },
635
+ # 'test5' => 'test' },
636
+ # :array => [{ :test => :value1 }, 2, { :test => :value3 }]}
637
+ #
638
+ # hclone = hdata.rh_clone
639
+ # hclone[:test] = "test"
640
+ # hdata[:test] == { :test2 => { :test5 => :test,'text' => 'blabla' }
641
+ # # => true
642
+ # hclone[:array].pop
643
+ # hdata[:array].length != hclone[:array].length
644
+ # # => true
645
+ # hclone[:array][0][:test] = "value2"
646
+ # hdata[:array][0][:test] != hclone[:array][0][:test]
647
+ # # => true
648
+ def rh_clone
649
+ result = []
650
+ each do |value|
651
+ begin
652
+ result << value.rh_clone
653
+ rescue
654
+ result << value
655
+ end
656
+ end
657
+ result
658
+ end
659
+
660
+ # This function is part of the rh_merge functionnality adapted for Array.
661
+ #
662
+ # To provide Array recursivity, we uses the element index.
663
+ #
664
+ # **Warning!** If the Array order has changed (sort/random) the index changed
665
+ # and can generate unwanted result.
666
+ #
667
+ # To implement recursivity, and some specific Array management (add/remove)
668
+ # you have to create an Hash and insert it at position 0 in the 'self' Array.
669
+ #
670
+ # **Warning!** If you create an Array, where index 0 contains a Hash, this
671
+ # Hash will be considered as the Array control element.
672
+ # If the first index of your Array is not a Hash, an empty Hash will be
673
+ # inserted at position 0.
674
+ #
675
+ # 'data' has the same restriction then 'self' about the first element.
676
+ # 'data' can influence the rh_merge Array behavior, by updating the first
677
+ # element.
678
+ #
679
+ # The first Hash element has the following attributes:
680
+ #
681
+ # - :__struct_changing: Array of index which accepts to move from a structured
682
+ # data (Hash/Array) to another structure or type.
683
+ #
684
+ # Ex: Hash => Array, Array => Integer
685
+ #
686
+ # - :__protected: Array of index which protects against update from 'data'
687
+ #
688
+ # - :__remove: Array of elements to remove. each element are remove with
689
+ # Array.delete function. See Array delete function for details.
690
+ #
691
+ # - :__remove_index: Array of indexes to remove.
692
+ # Each element are removed with Array.delete_at function.
693
+ # It starts from the highest index until the lowest.
694
+ # See Array delete function for details.
695
+ #
696
+ # **NOTE**: __remove and __remove_index cannot be used together.
697
+ # if both are set, __remove is choosen
698
+ #
699
+ # **NOTE** : __remove* is executed before __add*
700
+ #
701
+ # - :__add: Array of elements to add. Those elements are systematically added
702
+ # at the end of the Array. See Array.<< for details.
703
+ #
704
+ # - :__add_index: Hash of index(key) + Array of data(value) to add.
705
+ # The index listed refer to merged 'self' Array. several elements with same
706
+ # index are grouply inserted in the index.
707
+ # ex:
708
+ # [:data3].rh_merge({:__add_index => [0 => [:data1, :data2]]})
709
+ # => [{}, :data1, :data2, :data3]
710
+ #
711
+ # **NOTE**: __add and __add_index cannot be used together.
712
+ # if both are set, __add is choosen
713
+ #
714
+ # How merge is executed:
715
+ #
716
+ # Starting at index 0, each index of 'data' and 'self' are used to compare
717
+ # indexed data.
718
+ # - If 'data' index 0 has not an Hash, the 'self' index 0 is just skipped.
719
+ # - If 'data' index 0 has the 'control' Hash, the array will be updated
720
+ # according to :__add and :__remove arrays.
721
+ # when done, those attributes are removed
722
+ #
723
+ # - For all next index (1 => 'data'.length), data are compared
724
+ #
725
+ # - If the 'data' length is > than 'self' length
726
+ # addtionnal indexed data are added to 'self'
727
+ #
728
+ # - If index element exist in both 'data' and 'self',
729
+ # 'self' indexed data is updated/merged according to control.
730
+ # 'data' indexed data can use :unset to remove the data at this index
731
+ # nil is also supported. But the index won't be removed. data will just
732
+ # be set to nil
733
+ #
734
+ # when all Arrays elements are merged, rh_merge will:
735
+ # - remove 'self' elements containing ':unset'
736
+ #
737
+ # - merge 'self' data at index 0 with 'data' found index 0
738
+ #
739
+ def rh_merge(data)
740
+ _rh_merge(clone, data)
741
+ end
742
+
743
+ def rh_merge!(data)
744
+ _rh_merge(self, data)
745
+ end
746
+
747
+ private
748
+
749
+ def _rh_merge(result, data)
750
+ data = data.clone
751
+ data_control = _rh_merge_control(data)
752
+ result_control = _rh_merge_control(result)
753
+
754
+ _rh_do_control_merge(result_control, result, data_control, data)
755
+
756
+ (1..(data.length - 1)).each do |index|
757
+ _rh_do_array_merge(result, index, data)
758
+ end
759
+
760
+ (-(result.length - 1)..-1).each do |index|
761
+ result.delete_at(index.abs) if result[index.abs] == :unset
762
+ end
763
+
764
+ _rh_do_array_merge(result, 0, [data_control])
765
+ rh_remove_control(result[0]) # Remove all control elements in tree of arrays
766
+
767
+ result
768
+ end
769
+
770
+ def _rh_do_array_merge(result, index, data)
771
+ return if _rh_merge_recursive(result, index, data)
772
+
773
+ return unless _rh_struct_changing_ok?(result, index, data)
774
+
775
+ return unless _rh_merge_ok?(result, index)
776
+
777
+ result[index] = data[index] unless data[index] == :kept
778
+ end
779
+
780
+ # Get the control element. or create it if missing.
781
+ def _rh_merge_control(array)
782
+ unless array[0].is_a?(Hash)
783
+ array.insert(0, :__control => true)
784
+ return array[0]
785
+ end
786
+
787
+ _rh_control_tags.each do |prop|
788
+ if array[0].key?(prop)
789
+ array[0][:__control] = true
790
+ return array[0]
791
+ end
792
+ end
793
+
794
+ array.insert(0, :__control => true)
795
+
796
+ array[0]
797
+ end
798
+
799
+ # Do the merge according to :__add and :__remove
800
+ def _rh_do_control_merge(_result_control, result, data_control, _data)
801
+ if data_control[:__remove].is_a?(Array)
802
+ _rh_do_control_remove(result, data_control[:__remove])
803
+ elsif data_control[:__remove_index].is_a?(Array)
804
+ index_to_remove = data_control[:__remove_index].uniq.sort.reverse
805
+ _rh_do_control_remove_index(result, index_to_remove)
806
+ end
807
+
808
+ data_control.delete(:__remove)
809
+ data_control.delete(:__remove_index)
810
+
811
+ if data_control[:__add].is_a?(Array)
812
+ data_control[:__add].each { |element| result << element }
813
+ elsif data_control[:__add_index].is_a?(Hash)
814
+ _rh_do_control_add_index(result, data_control[:__add_index].sort)
815
+ end
816
+
817
+ data_control.delete(:__add)
818
+ data_control.delete(:__add_index)
819
+ end
820
+
821
+ def _rh_do_control_add_index(result, add_index)
822
+ add_index.reverse_each do |elements_to_insert|
823
+ next unless elements_to_insert.is_a?(Array) &&
824
+ elements_to_insert[0].is_a?(Fixnum) &&
825
+ elements_to_insert[1].is_a?(Array)
826
+
827
+ index = elements_to_insert[0] + 1
828
+ elements = elements_to_insert[1]
829
+
830
+ elements.reverse_each { |element| result.insert(index, element) }
831
+ end
832
+ end
833
+
834
+ # do the element removal.
835
+ def _rh_do_control_remove(result, remove)
836
+ remove.each { |element| result.delete(element) }
837
+ end
838
+
839
+ def _rh_do_control_remove_index(result, index_to_remove)
840
+ index_to_remove.each { |index| result.delete_at(index + 1) }
841
+ end
842
+
843
+ include Rh
844
+ end
@@ -0,0 +1,4 @@
1
+ # Recursive Hash
2
+ module SubHash
3
+ VERSION = '0.1.0'
4
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'subhash/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'subhash'
8
+ spec.version = SubHash::VERSION
9
+ spec.authors = ['Christophe Larsonneur']
10
+ spec.email = ['clarsonneur@gmail.com']
11
+
12
+ spec.summary = 'Recursive Hash of hashes/Array management'
13
+ spec.description = 'Hash and Array object enhanced to manage Hash of Hash/Array easily.'
14
+ spec.homepage = "https://github.com/forj-oss/rhash"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.9'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency "rspec", "~> 3.1.0"
24
+ spec.add_development_dependency "rubocop", "~> 0.30.0"
25
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subhash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christophe Larsonneur
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-04-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 3.1.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 3.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.30.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 0.30.0
69
+ description: Hash and Array object enhanced to manage Hash of Hash/Array easily.
70
+ email:
71
+ - clarsonneur@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .gitreview
78
+ - .rspec
79
+ - .rubocop.yml
80
+ - Gemfile
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - lib/subhash.rb
86
+ - lib/subhash/version.rb
87
+ - subhash.gemspec
88
+ homepage: https://github.com/forj-oss/rhash
89
+ licenses: []
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 2.1.11
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Recursive Hash of hashes/Array management
111
+ test_files: []
112
+ has_rdoc: