hash-tree 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.md +37 -0
- data/Rakefile +11 -0
- data/lib/hash-tree.rb +450 -0
- data/spec/fixtures/book.json +14 -0
- data/spec/fixtures/books.json +41 -0
- data/spec/fixtures/books.xml +101 -0
- data/spec/fixtures/books.yml +73 -0
- data/spec/hash-tree_spec.rb +239 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/application_helpers.rb +15 -0
- metadata +137 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012 De Marque inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
Hash Tree
|
2
|
+
===============
|
3
|
+
|
4
|
+
[![Build Status](https://secure.travis-ci.org/demarque/hash-tree.png?branch=master)](http://travis-ci.org/demarque/hash-tree)
|
5
|
+
|
6
|
+
Hash Tree gives you the abilty to manipulate nested hashes. The nodes of the hashes
|
7
|
+
can be others hashes or even arrays of hashes.
|
8
|
+
|
9
|
+
Install
|
10
|
+
-------
|
11
|
+
|
12
|
+
```
|
13
|
+
gem install hash-tree
|
14
|
+
```
|
15
|
+
|
16
|
+
Rails 3
|
17
|
+
-------
|
18
|
+
|
19
|
+
In your Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'hash-tree'
|
23
|
+
```
|
24
|
+
|
25
|
+
Usage
|
26
|
+
-----
|
27
|
+
|
28
|
+
**FILL THE USAGE**
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
HashTree.new
|
32
|
+
```
|
33
|
+
|
34
|
+
Copyright
|
35
|
+
---------
|
36
|
+
|
37
|
+
Copyright (c) 2012 De Marque inc. See LICENSE for further details.
|
data/Rakefile
ADDED
data/lib/hash-tree.rb
ADDED
@@ -0,0 +1,450 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'nori'
|
3
|
+
|
4
|
+
class HashTree
|
5
|
+
#*************************************************************************************
|
6
|
+
# CONSTRUCTOR
|
7
|
+
#*************************************************************************************
|
8
|
+
def initialize(hash={})
|
9
|
+
hash = {} unless hash.is_a? Hash
|
10
|
+
|
11
|
+
@hash = hash
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
#*************************************************************************************
|
16
|
+
# PUBLIC CLASS METHODS
|
17
|
+
#*************************************************************************************
|
18
|
+
def self.from_json(json_data)
|
19
|
+
return nil if json_data.to_s.empty?
|
20
|
+
|
21
|
+
parsed_data = JSON.parse(json_data)
|
22
|
+
|
23
|
+
tree = self.new(parsed_data)
|
24
|
+
tree.replace_values!(nil, '')
|
25
|
+
|
26
|
+
return tree
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.from_json_path(json_path)
|
30
|
+
from_json File.read(json_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from_xml(xml_data)
|
34
|
+
return nil if xml_data.to_s.empty?
|
35
|
+
|
36
|
+
tree = self.new(Nori.parse(xml_data))
|
37
|
+
tree.replace_values!(nil, '')
|
38
|
+
|
39
|
+
return tree
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.from_xml_path(xml_path)
|
43
|
+
from_xml File.read(xml_path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.from_yml_path(yml_path)
|
47
|
+
yml_data = YAML.load_file(yml_path)
|
48
|
+
|
49
|
+
tree = self.new(yml_data)
|
50
|
+
|
51
|
+
return tree
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
#*************************************************************************************
|
56
|
+
# PUBLIC METHODS
|
57
|
+
#*************************************************************************************
|
58
|
+
def checksum
|
59
|
+
Digest::MD5.hexdigest(@hash.to_json.scan(/\S/).sort.join)
|
60
|
+
end
|
61
|
+
|
62
|
+
def children(name)
|
63
|
+
if @hash[name] and @hash[name][name.chop]
|
64
|
+
return [@hash[name][name.chop]].flatten
|
65
|
+
else
|
66
|
+
return []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def clone_tree
|
71
|
+
HashTree.new(Marshal.load(Marshal.dump(@hash)))
|
72
|
+
end
|
73
|
+
|
74
|
+
# Remove all key with a nil value
|
75
|
+
def compact!
|
76
|
+
@hash = compact
|
77
|
+
end
|
78
|
+
|
79
|
+
def each(options={}, &block)
|
80
|
+
options = { :hash => @hash, :key_path => [], :scope => nil }.merge(options)
|
81
|
+
|
82
|
+
options[:hash].each do |key, value|
|
83
|
+
key_path = [options[:key_path], key].flatten
|
84
|
+
key_path_string = key_path.join('.')
|
85
|
+
|
86
|
+
if in_scope?(key_path_string, options[:scope])
|
87
|
+
if (options[:scope] and options[:scope] == key_path_string)
|
88
|
+
yield options[:hash], key, value, key_path_string
|
89
|
+
else
|
90
|
+
cast(value, Array).each do |item|
|
91
|
+
if item.is_a? Hash
|
92
|
+
each(:hash => item, :key_path => key_path, :scope => options[:scope], &block)
|
93
|
+
else
|
94
|
+
yield options[:hash], key, item, key_path_string
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def empty?
|
103
|
+
@hash.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
def exists?(path='', hash=nil)
|
107
|
+
hash = @hash unless hash
|
108
|
+
|
109
|
+
path_parts = path.split('.')
|
110
|
+
|
111
|
+
hash.each do |key, value|
|
112
|
+
value_for_loop = (value.is_a? Array) ? value : [value]
|
113
|
+
|
114
|
+
if path_parts[0] == key
|
115
|
+
return true if path_parts.length == 1
|
116
|
+
|
117
|
+
value_for_loop.each do |item|
|
118
|
+
if item.is_a?(Hash)
|
119
|
+
return true if exists?(path_parts[1..-1].join('.'), item)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
return false
|
127
|
+
end
|
128
|
+
|
129
|
+
def get(path='', options={})
|
130
|
+
options = { :default => '', :force => nil }.merge(options)
|
131
|
+
|
132
|
+
if not path.empty?
|
133
|
+
data = []
|
134
|
+
self.each(:scope => path) { |parent, k, v| data << cast(v, options[:force]) }
|
135
|
+
data = (data.length <= 1 ? data.first : data)
|
136
|
+
else
|
137
|
+
data = @hash
|
138
|
+
end
|
139
|
+
|
140
|
+
return (data == nil ? options[:default] : data)
|
141
|
+
end
|
142
|
+
|
143
|
+
def keys_to_s!(options={})
|
144
|
+
options = { :hash => nil }.merge(options)
|
145
|
+
|
146
|
+
options[:hash] = @hash unless options[:hash]
|
147
|
+
|
148
|
+
options[:hash].keys_to_s!
|
149
|
+
|
150
|
+
options[:hash].each do |key, value|
|
151
|
+
value_for_loop = (value.is_a? Array) ? value : [value]
|
152
|
+
value_for_loop.each { |item| keys_to_s!(:hash => item) if item.is_a? Hash }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def id
|
157
|
+
#override the id method of all object
|
158
|
+
get('id')
|
159
|
+
end
|
160
|
+
|
161
|
+
def insert(path, content)
|
162
|
+
current_value = get(path)
|
163
|
+
|
164
|
+
if current_value.is_a? Array
|
165
|
+
if content.is_a? Array
|
166
|
+
current_value = current_value.concat(content)
|
167
|
+
else
|
168
|
+
current_value << content
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
set(path, current_value)
|
173
|
+
end
|
174
|
+
|
175
|
+
def inspect(options={})
|
176
|
+
options = { :hash => nil, :position => 0, :raw => true }.merge(options)
|
177
|
+
|
178
|
+
options[:hash] = @hash unless options[:hash]
|
179
|
+
|
180
|
+
return options[:hash].inspect if options[:raw]
|
181
|
+
|
182
|
+
content = ""
|
183
|
+
|
184
|
+
options[:hash].each do |key, value|
|
185
|
+
convert_to_array(value).each do |item|
|
186
|
+
content << "\n" + (' ' * options[:position]) + "#{key} : "
|
187
|
+
content << (item.is_a?(Hash) ? inspect(:hash => item, :position => options[:position]+1) : value.inspect)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
return content
|
192
|
+
end
|
193
|
+
|
194
|
+
def merge(other_hash)
|
195
|
+
@hash = merge_children(@hash, other_hash)
|
196
|
+
end
|
197
|
+
|
198
|
+
def remove(path, options={})
|
199
|
+
options = { :if => nil, :remove_leaf => true, :unless => nil }.merge(options)
|
200
|
+
|
201
|
+
set(path, nil, options) if exists?(path)
|
202
|
+
end
|
203
|
+
|
204
|
+
def rename_key!(path, new_name, hash=nil)
|
205
|
+
hash = @hash unless hash
|
206
|
+
|
207
|
+
path_parts = path.split('.')
|
208
|
+
|
209
|
+
renamed_keys = {}
|
210
|
+
|
211
|
+
hash.each do |key, value|
|
212
|
+
if path_parts[0] == key
|
213
|
+
if path_parts.length == 1
|
214
|
+
renamed_keys[new_name] = hash.delete path_parts[0]
|
215
|
+
else
|
216
|
+
convert_to_array(value).each { |i| rename_key!(path_parts[1..-1].join('.'), new_name, i) if i.is_a? Hash }
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
hash.merge! renamed_keys if not renamed_keys.empty?
|
222
|
+
end
|
223
|
+
|
224
|
+
def replace_values!(old_value, new_value, hash=nil)
|
225
|
+
hash = @hash unless hash
|
226
|
+
|
227
|
+
hash.each do |key, value|
|
228
|
+
if value.is_a? Array
|
229
|
+
value.each do |item|
|
230
|
+
if item.is_a?(Hash)
|
231
|
+
item = replace_values!(old_value, new_value, item)
|
232
|
+
else
|
233
|
+
item = new_value if item == old_value
|
234
|
+
end
|
235
|
+
end
|
236
|
+
elsif value.is_a? Hash
|
237
|
+
value = replace_values!(old_value, new_value, value)
|
238
|
+
elsif value == old_value
|
239
|
+
value = new_value
|
240
|
+
end
|
241
|
+
|
242
|
+
hash[key] = value
|
243
|
+
end
|
244
|
+
|
245
|
+
return hash
|
246
|
+
end
|
247
|
+
|
248
|
+
def set(path, value, options={})
|
249
|
+
options = { :accept_nil => true, :if => nil, :remove_leaf => false, :unless => nil }.merge(options)
|
250
|
+
|
251
|
+
set_children(@hash, path, value, options) if options[:accept_nil] or value
|
252
|
+
end
|
253
|
+
|
254
|
+
def slash(path)
|
255
|
+
if exists?(path)
|
256
|
+
slashed_tree = get(path)
|
257
|
+
slashed_tree = slashed_tree.first if slashed_tree.is_a? Array and slashed_tree.length == 1 and slashed_tree.first.is_a? Hash
|
258
|
+
else
|
259
|
+
slashed_tree = @hash
|
260
|
+
end
|
261
|
+
|
262
|
+
return HashTree.new(slashed_tree)
|
263
|
+
end
|
264
|
+
|
265
|
+
def slash!(path)
|
266
|
+
@hash = slash(path).get
|
267
|
+
end
|
268
|
+
|
269
|
+
def to_json
|
270
|
+
@hash.to_json
|
271
|
+
end
|
272
|
+
|
273
|
+
def to_yaml
|
274
|
+
self.keys_to_s!
|
275
|
+
|
276
|
+
return @hash.ya2yaml
|
277
|
+
end
|
278
|
+
|
279
|
+
#*************************************************************************************
|
280
|
+
# PRIVATE METHODS
|
281
|
+
#*************************************************************************************
|
282
|
+
private
|
283
|
+
|
284
|
+
def compact(hash=nil)
|
285
|
+
hash = @hash unless hash
|
286
|
+
|
287
|
+
hash.each do |key, value|
|
288
|
+
if value.is_a? Array
|
289
|
+
hash[key] = compact_array value
|
290
|
+
elsif value.is_a? Hash
|
291
|
+
hash[key] = compact value
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
hash = compact_simple_hash(hash)
|
296
|
+
|
297
|
+
return (hash.empty? ? nil : hash)
|
298
|
+
end
|
299
|
+
|
300
|
+
def compact_array(array)
|
301
|
+
array.each { |item| item = compact(item) if item.is_a? Hash }
|
302
|
+
array.compact!
|
303
|
+
|
304
|
+
return (array.empty? ? nil : array)
|
305
|
+
end
|
306
|
+
|
307
|
+
def compact_simple_hash(hash)
|
308
|
+
hash.delete_if { |k,v| not v }
|
309
|
+
end
|
310
|
+
|
311
|
+
def cast(value, type)
|
312
|
+
case type.to_s
|
313
|
+
when 'Array' then convert_to_array value
|
314
|
+
when 'HashTree' then convert_to_hash_tree value
|
315
|
+
when 'String' then value.to_s
|
316
|
+
else value
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def check_conditions(hash, conditions, comparaison = :if)
|
321
|
+
if conditions
|
322
|
+
conditions.each do |value, equal|
|
323
|
+
compare_values = hash
|
324
|
+
equal = [equal] unless equal.is_a? Array
|
325
|
+
|
326
|
+
path_value = value.split('.')
|
327
|
+
path_value.delete_at(0)
|
328
|
+
|
329
|
+
count = 0
|
330
|
+
path_value.each do |p|
|
331
|
+
if compare_values.is_a? Array
|
332
|
+
values = []
|
333
|
+
compare_values.each { |v| values << v[p] }
|
334
|
+
compare_values = values
|
335
|
+
elsif compare_values[p] != nil
|
336
|
+
compare_values = compare_values[p]
|
337
|
+
else
|
338
|
+
compare_values = nil
|
339
|
+
break
|
340
|
+
end
|
341
|
+
count += 1
|
342
|
+
end
|
343
|
+
|
344
|
+
if compare_values.is_a? Array
|
345
|
+
found = 0
|
346
|
+
|
347
|
+
compare_values.each { |v| found += 1 if equal.include?(v) }
|
348
|
+
|
349
|
+
if comparaison == :if and found == 0
|
350
|
+
return false
|
351
|
+
elsif comparaison == :unless and found > 0
|
352
|
+
return false
|
353
|
+
end
|
354
|
+
else
|
355
|
+
return false if comparaison == :if and not equal.include?(compare_values)
|
356
|
+
return false if comparaison == :unless and equal.include?(compare_values)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
return true
|
362
|
+
end
|
363
|
+
|
364
|
+
def check_mixed_conditions(hash, if_conditions, unless_conditions)
|
365
|
+
valid = true
|
366
|
+
|
367
|
+
valid = check_conditions(hash, if_conditions, :if) if if_conditions
|
368
|
+
valid = check_conditions(hash, unless_conditions, :unless) if valid and unless_conditions
|
369
|
+
|
370
|
+
return valid
|
371
|
+
end
|
372
|
+
|
373
|
+
def convert_to_array(value)
|
374
|
+
value.is_a?(Array) ? value : [value]
|
375
|
+
end
|
376
|
+
|
377
|
+
def convert_to_hash_tree(value)
|
378
|
+
if value.is_a? HashTree
|
379
|
+
value
|
380
|
+
elsif value.is_a? Array
|
381
|
+
value.map{ |v| HashTree.new(v) }
|
382
|
+
else
|
383
|
+
HashTree.new(value)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def in_scope?(target, scope)
|
388
|
+
not scope or "^#{scope}".include? "^#{target}"
|
389
|
+
end
|
390
|
+
|
391
|
+
def merge_children(hash, other_hash)
|
392
|
+
other_hash = other_hash.get if other_hash.is_a? HashTree
|
393
|
+
|
394
|
+
if other_hash
|
395
|
+
other_hash.each do |key, value|
|
396
|
+
value = merge_children(hash[key], other_hash[key]) if hash[key] and value.is_a? Hash
|
397
|
+
|
398
|
+
hash[key] = value
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
return hash
|
403
|
+
end
|
404
|
+
|
405
|
+
def set_children(root, path, value, options={})
|
406
|
+
path = path.split('.')
|
407
|
+
|
408
|
+
selection = path[0]
|
409
|
+
|
410
|
+
setted = false
|
411
|
+
|
412
|
+
if path.length > 1
|
413
|
+
children = path.drop(1).join('.')
|
414
|
+
|
415
|
+
root[selection] = {} if not root[selection] or (not root[selection].is_a? Hash and not root[selection].is_a? Array)
|
416
|
+
|
417
|
+
if root[selection].is_a? Array
|
418
|
+
root[selection].each_with_index do |item, index|
|
419
|
+
set_children_node(root[selection][index], path, children, value, options) if item.is_a? Hash
|
420
|
+
end
|
421
|
+
else
|
422
|
+
set_children_node(root[selection], path, children, value, options)
|
423
|
+
end
|
424
|
+
else
|
425
|
+
root[selection] = value if check_mixed_conditions(root, options[:if], options[:unless])
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
def set_children_node(hash, path, children, value, options)
|
430
|
+
if options[:remove_leaf] and path.length == 2
|
431
|
+
if hash[children].is_a? Array
|
432
|
+
hash[children].delete_if{ |i| check_mixed_conditions(i, options[:if], options[:unless]) }
|
433
|
+
hash.delete(children) if hash[children].empty?
|
434
|
+
else
|
435
|
+
hash.delete(children) if check_mixed_conditions(hash[children], options[:if], options[:unless])
|
436
|
+
end
|
437
|
+
else
|
438
|
+
set_children(hash, children, value, options)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def method_missing(m, *args, &block)
|
443
|
+
if exists?(m.to_s)
|
444
|
+
return get(m.to_s)
|
445
|
+
else
|
446
|
+
return nil
|
447
|
+
#raise "DIG : The method #{m} doesn't exist in HashTree"
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
"book":
|
3
|
+
{
|
4
|
+
"id": 2,
|
5
|
+
"title": "Steppenwolf",
|
6
|
+
"author": ["Hermann Hesse"],
|
7
|
+
"formats": [
|
8
|
+
{ "nature": "pdf", "price": [{ "amount": 799, "currency": "cad" }]},
|
9
|
+
{ "nature": "epub", "price": [{ "amount": 799, "currency": "cad" }, { "amount": 759, "currency": "usd" }]}
|
10
|
+
],
|
11
|
+
"tags": ["conflict", "isolation", "reality", "animalistic"],
|
12
|
+
"year": 1927
|
13
|
+
}
|
14
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
{
|
2
|
+
"books":[
|
3
|
+
{
|
4
|
+
"id": 1,
|
5
|
+
"title": "Don Quixote",
|
6
|
+
"summary": "The novel follows the adventures of Alonso Quijano, who reads too many chivalric novels, and sets out to revive chivalry under the name of Don Quixote.",
|
7
|
+
"author": ["Miguel de Cervantes"],
|
8
|
+
"formats": [
|
9
|
+
{ "nature": "pdf", "prices": [{ "amount": 999, "currency": "cad" }]},
|
10
|
+
{ "nature": "epub", "prices": [{ "amount": 1099, "currency": "cad" }, { "amount": 999, "currency": "usd" }]}
|
11
|
+
],
|
12
|
+
"tags": ["realism", "chivalry", "satire", "deception"],
|
13
|
+
"year": 1615
|
14
|
+
},
|
15
|
+
|
16
|
+
{
|
17
|
+
"id": 2,
|
18
|
+
"title": "Steppenwolf",
|
19
|
+
"summary": null,
|
20
|
+
"author": ["Hermann Hesse"],
|
21
|
+
"formats": [
|
22
|
+
{ "nature": "pdf", "prices": [{ "amount": 799, "currency": "cad" }]},
|
23
|
+
{ "nature": "epub", "prices": [{ "amount": 799, "currency": "cad" }, { "amount": 759, "currency": "usd" }]}
|
24
|
+
],
|
25
|
+
"tags": ["conflict", "isolation", "reality", "animalistic"],
|
26
|
+
"year": 1927
|
27
|
+
},
|
28
|
+
|
29
|
+
{
|
30
|
+
"id": 3,
|
31
|
+
"title": "The Idiot",
|
32
|
+
"summary": "",
|
33
|
+
"author": ["Fyodor Dostoyevsky"],
|
34
|
+
"formats": [
|
35
|
+
{ "nature": "epub", "prices": [{ "amount": 1599, "currency": "cad" }] }
|
36
|
+
],
|
37
|
+
"tags": [],
|
38
|
+
"year": 1869
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
@@ -0,0 +1,101 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<books>
|
3
|
+
<book>
|
4
|
+
<id>1</id>
|
5
|
+
<title>Don Quixote</title>
|
6
|
+
<authors>
|
7
|
+
<author>Miguel de Cervantes</author>
|
8
|
+
</authors>
|
9
|
+
<formats>
|
10
|
+
<format>
|
11
|
+
<nature>pdf</nature>
|
12
|
+
<prices>
|
13
|
+
<price>
|
14
|
+
<amount>999</amount>
|
15
|
+
<currency>cad</currency>
|
16
|
+
</price>
|
17
|
+
</prices>
|
18
|
+
</format>
|
19
|
+
<format>
|
20
|
+
<nature>epub</nature>
|
21
|
+
<prices>
|
22
|
+
<price>
|
23
|
+
<amount>1099</amount>
|
24
|
+
<currency>cad</currency>
|
25
|
+
</price>
|
26
|
+
<price>
|
27
|
+
<amount>999</amount>
|
28
|
+
<currency>usd</currency>
|
29
|
+
</price>
|
30
|
+
</prices>
|
31
|
+
</format>
|
32
|
+
</formats>
|
33
|
+
<tags>
|
34
|
+
<tag>realism</tag>
|
35
|
+
<tag>chivalry</tag>
|
36
|
+
<tag>satire</tag>
|
37
|
+
<tag>deception</tag>
|
38
|
+
</tags>
|
39
|
+
<year>1615</year>
|
40
|
+
</book>
|
41
|
+
|
42
|
+
<book>
|
43
|
+
<id>2</id>
|
44
|
+
<title>Steppenwolf</title>
|
45
|
+
<authors>
|
46
|
+
<author>Hermann Hesse</author>
|
47
|
+
</authors>
|
48
|
+
<formats>
|
49
|
+
<format>
|
50
|
+
<nature>pdf</nature>
|
51
|
+
<prices>
|
52
|
+
<price>
|
53
|
+
<amount>799</amount>
|
54
|
+
<currency>cad</currency>
|
55
|
+
</price>
|
56
|
+
</prices>
|
57
|
+
</format>
|
58
|
+
<format>
|
59
|
+
<nature>epub</nature>
|
60
|
+
<prices>
|
61
|
+
<price>
|
62
|
+
<amount>799</amount>
|
63
|
+
<currency>cad</currency>
|
64
|
+
</price>
|
65
|
+
<price>
|
66
|
+
<amount>759</amount>
|
67
|
+
<currency>usd</currency>
|
68
|
+
</price>
|
69
|
+
</prices>
|
70
|
+
</format>
|
71
|
+
</formats>
|
72
|
+
<tags>
|
73
|
+
<tag>conflict</tag>
|
74
|
+
<tag>isolation</tag>
|
75
|
+
<tag>reality</tag>
|
76
|
+
<tag>animalistic</tag>
|
77
|
+
</tags>
|
78
|
+
<year>1927</year>
|
79
|
+
</book>
|
80
|
+
|
81
|
+
<book>
|
82
|
+
<id>3</id>
|
83
|
+
<title>The Idiot</title>
|
84
|
+
<authors>
|
85
|
+
<author>Fyodor Dostoyevsky</author>
|
86
|
+
</authors>
|
87
|
+
<formats>
|
88
|
+
<format>
|
89
|
+
<nature>epub</nature>
|
90
|
+
<prices>
|
91
|
+
<price>
|
92
|
+
<amount>1599</amount>
|
93
|
+
<currency>cad</currency>
|
94
|
+
</price>
|
95
|
+
</prices>
|
96
|
+
</format>
|
97
|
+
</formats>
|
98
|
+
<tags></tags>
|
99
|
+
<year>1869</year>
|
100
|
+
</book>
|
101
|
+
</books>
|
@@ -0,0 +1,73 @@
|
|
1
|
+
---
|
2
|
+
"books":
|
3
|
+
-
|
4
|
+
"id": 1
|
5
|
+
"title": "Don Quixote"
|
6
|
+
"author":
|
7
|
+
- "Miguel de Cervantes"
|
8
|
+
"formats":
|
9
|
+
-
|
10
|
+
"nature": "pdf"
|
11
|
+
"price":
|
12
|
+
-
|
13
|
+
"amount": 999
|
14
|
+
"currency": "cad"
|
15
|
+
-
|
16
|
+
"nature": "epub"
|
17
|
+
"price":
|
18
|
+
-
|
19
|
+
"amount": 1099
|
20
|
+
"currency": "cad"
|
21
|
+
-
|
22
|
+
"amount": 999
|
23
|
+
"currency": "usd"
|
24
|
+
"tags":
|
25
|
+
- "realism"
|
26
|
+
- "chivalry"
|
27
|
+
- "satire"
|
28
|
+
- "deception"
|
29
|
+
"year": 1615
|
30
|
+
|
31
|
+
-
|
32
|
+
"id": 2
|
33
|
+
"title": "Steppenwolf"
|
34
|
+
"author":
|
35
|
+
- "Hermann Hesse"
|
36
|
+
"formats":
|
37
|
+
-
|
38
|
+
"nature": "pdf"
|
39
|
+
"price":
|
40
|
+
-
|
41
|
+
"amount": 799
|
42
|
+
"currency": "cad"
|
43
|
+
|
44
|
+
-
|
45
|
+
"nature": "epub"
|
46
|
+
"price":
|
47
|
+
-
|
48
|
+
"amount": 799
|
49
|
+
"currency": "cad"
|
50
|
+
-
|
51
|
+
"amount": 759
|
52
|
+
"currency": "usd"
|
53
|
+
"tags":
|
54
|
+
- "conflict"
|
55
|
+
- "isolation"
|
56
|
+
- "reality"
|
57
|
+
- "animalistic"
|
58
|
+
"year": 1927
|
59
|
+
|
60
|
+
-
|
61
|
+
"id": 3
|
62
|
+
"title": "The Idiot"
|
63
|
+
"author":
|
64
|
+
- "Fyodor Dostoyevsky"
|
65
|
+
"formats":
|
66
|
+
-
|
67
|
+
"nature": "epub"
|
68
|
+
"price":
|
69
|
+
-
|
70
|
+
"amount": 1599
|
71
|
+
"currency": "cad"
|
72
|
+
"tags": ""
|
73
|
+
"year": 1869
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include ApplicationHelpers
|
4
|
+
|
5
|
+
describe HashTree do
|
6
|
+
describe "::from_json" do
|
7
|
+
context "when using the books.json fixture" do
|
8
|
+
let(:hashtree) { HashTree.from_json(File.read('spec/fixtures/books.json')) }
|
9
|
+
|
10
|
+
subject { hashtree }
|
11
|
+
|
12
|
+
it { should_not be_empty }
|
13
|
+
the("hashtree.get('books').length") { should eql 3 }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "when using an empty fixture" do
|
17
|
+
subject { HashTree.from_json('') }
|
18
|
+
|
19
|
+
it { should be_nil }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "::from_json_path" do
|
24
|
+
context "when using the books.json fixture" do
|
25
|
+
subject { HashTree.from_json_path('spec/fixtures/books.json') }
|
26
|
+
it { should_not be_empty }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "::from_xml" do
|
31
|
+
context "when using the books.xml fixture" do
|
32
|
+
let(:hashtree) { HashTree.from_xml(File.read('spec/fixtures/books.xml')) }
|
33
|
+
|
34
|
+
subject { hashtree }
|
35
|
+
|
36
|
+
it { should_not be_empty }
|
37
|
+
the("hashtree.get('books.book').length") { should eql 3 }
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when using an empty fixture" do
|
41
|
+
subject { HashTree.from_xml('') }
|
42
|
+
|
43
|
+
it { should be_nil }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "::from_xml_path" do
|
48
|
+
context "when using the books.xml fixture" do
|
49
|
+
subject { HashTree.from_xml_path('spec/fixtures/books.xml') }
|
50
|
+
it { should_not be_empty }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "::from_yml_path" do
|
55
|
+
context "when using the books.yml fixture" do
|
56
|
+
let(:hashtree) { HashTree.from_yml_path('spec/fixtures/books.yml') }
|
57
|
+
|
58
|
+
subject { hashtree }
|
59
|
+
|
60
|
+
it { should_not be_empty }
|
61
|
+
the("hashtree.get('books').length") { should eql 3 }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#checksum" do
|
66
|
+
context "with an empty hash" do
|
67
|
+
specify { HashTree.new.checksum.should eql '99914b932bd37a50b983c5e7c90ae93b' }
|
68
|
+
end
|
69
|
+
|
70
|
+
with_books_fixture do
|
71
|
+
specify { hashtree.checksum.should eql 'c15fddfa0bea3610663d019b8b5b4a4d' }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#children" do
|
76
|
+
pending 'TOTEST'
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#clone_tree" do
|
80
|
+
with_books_fixture do
|
81
|
+
context "and cloning it" do
|
82
|
+
subject { hashtree.clone_tree }
|
83
|
+
|
84
|
+
its(:object_id) { should_not eql hashtree.object_id }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#compact!" do
|
90
|
+
fixture :hashtree, HashTree.new({ 'books' => [ { 'title' => 'Don Quixote' }, { 'title' => nil }, { 'title' => 'Steppenwolf', 'formats' => [nil, 'pdf', 'epub'] } ]}) do
|
91
|
+
before { hashtree.compact! }
|
92
|
+
|
93
|
+
the("hashtree.get('books.title')") { should eql ['Don Quixote', 'Steppenwolf'] }
|
94
|
+
the("hashtree.get('books.formats')") { should eql ['pdf', 'epub'] }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#each" do
|
99
|
+
pending 'TOTEST'
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "#empty?" do
|
103
|
+
fixture :ht, HashTree.new do
|
104
|
+
it { should be_empty }
|
105
|
+
end
|
106
|
+
|
107
|
+
fixture :ht, HashTree.new({ 'book' => 'Steppenwolf' }) do
|
108
|
+
it { should_not be_empty }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "#exists?" do
|
113
|
+
fixture :hashtree, HashTree.new({ 'books' => [ { 'title' => 'Don Quixote' }, { 'formats' => [{ 'price' => 999 }] } ]}) do
|
114
|
+
the("hashtree.exists?('books.title')") { should be_true }
|
115
|
+
the("hashtree.exists?('books.formats.price')") { should be_true }
|
116
|
+
the("hashtree.exists?('books.unknown')") { should be_false }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe "#get" do
|
121
|
+
fixture :books, HashTree.new({ 'books' => [ { 'title' => 'Don Quixote' }, { 'formats' => [{ 'nature' => 'pdf' }, {'nature' => 'epub' }] } ]}) do
|
122
|
+
the("books.get('books.title')") { should eql "Don Quixote" }
|
123
|
+
the("books.get('books.title', :force => Array)") { should eql ["Don Quixote"] }
|
124
|
+
the("books.get('books.formats')") { should eql [{ 'nature' => 'pdf' }, {'nature' => 'epub' }] }
|
125
|
+
the("books.get('books.formats.nature')") { should eql ['pdf', 'epub'] }
|
126
|
+
the("books.get('books.name')") { should eql "" }
|
127
|
+
the("books.get('books.name', :default => nil)") { should be_nil }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "#keys_to_s!" do
|
132
|
+
pending 'TOTEST'
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "#id" do
|
136
|
+
context "with an hash tree having the attribute id with the value 123" do
|
137
|
+
subject { HashTree.new(:id => 123, :name => 'test') }
|
138
|
+
|
139
|
+
its(:id) { should eql 123 }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe "#insert" do
|
144
|
+
pending 'TOTEST'
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "#inspect" do
|
148
|
+
pending 'TOTEST'
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "#merge" do
|
152
|
+
pending 'TOTEST'
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "#remove" do
|
156
|
+
pending 'TOTEST'
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "#rename_key!" do
|
160
|
+
with_books_fixture do
|
161
|
+
context "and renaming the key books.title for name" do
|
162
|
+
before { hashtree.rename_key! 'books.title', 'name' }
|
163
|
+
|
164
|
+
the("hashtree.exists?('books.title')") { should be_false }
|
165
|
+
the("hashtree.exists?('books.name')") { should be_true }
|
166
|
+
end
|
167
|
+
|
168
|
+
context "and renaming the key books.formats.prices.currency for specie" do
|
169
|
+
before { hashtree.rename_key! 'books.formats.prices.currency', 'specie' }
|
170
|
+
|
171
|
+
the("hashtree.exists?('books.formats.prices.currency')") { should be_false }
|
172
|
+
the("hashtree.exists?('books.formats.prices.specie')") { should be_true }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "#replace_values!" do
|
178
|
+
with_books_fixture do
|
179
|
+
the("hashtree.get('books.formats.nature')") { should include 'pdf' }
|
180
|
+
|
181
|
+
context "and replacing value pdf for paper" do
|
182
|
+
before { hashtree.replace_values!('pdf', 'paper') }
|
183
|
+
the("hashtree.get('books.formats.nature')") { should_not include 'pdf' }
|
184
|
+
end
|
185
|
+
|
186
|
+
context "and replacing value unknown for paper" do
|
187
|
+
before { hashtree.replace_values!('unknown', 'paper') }
|
188
|
+
the("hashtree.get('books.formats.nature')") { should include 'pdf' }
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "#set" do
|
194
|
+
pending 'TOTEST'
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "#slash" do
|
198
|
+
with_book_fixture do
|
199
|
+
context "and slashing book" do
|
200
|
+
subject { hashtree.slash 'book' }
|
201
|
+
|
202
|
+
its(:title) { should eql 'Steppenwolf' }
|
203
|
+
the("hashtree.book['title']") { should eql 'Steppenwolf' }
|
204
|
+
end
|
205
|
+
|
206
|
+
context "and not slashing book" do
|
207
|
+
its(:title) { should be_nil }
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "#slash!" do
|
213
|
+
with_book_fixture do
|
214
|
+
context "and slashing book" do
|
215
|
+
before { hashtree.slash! 'book' }
|
216
|
+
|
217
|
+
its(:title) { should eql 'Steppenwolf' }
|
218
|
+
the("hashtree.book") { should be_nil }
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "#to_json" do
|
224
|
+
with_books_fixture do
|
225
|
+
context "and converting it to json" do
|
226
|
+
subject { hashtree.to_json }
|
227
|
+
|
228
|
+
it { should_not be_empty }
|
229
|
+
it { should include '{"books":[{' }
|
230
|
+
it { should include '"tags":["conflict","isolation","reality","animalistic"]' }
|
231
|
+
it { should include '"author":["Fyodor Dostoyevsky"]' }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe "#to_yaml" do
|
237
|
+
pending 'TOTEST'
|
238
|
+
end
|
239
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'rspec-aspic'
|
6
|
+
|
7
|
+
require File.expand_path('../../lib/hash-tree', __FILE__)
|
8
|
+
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.mock_with :rspec
|
13
|
+
config.include RSpecAspic
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ApplicationHelpers
|
2
|
+
def with_book_fixture(&block)
|
3
|
+
with_books_fixture('spec/fixtures/book.json', &block)
|
4
|
+
end
|
5
|
+
|
6
|
+
def with_books_fixture(file_path='spec/fixtures/books.json', &block)
|
7
|
+
context "when using the " + file_path.split('/').last + " fixture" do
|
8
|
+
let(:hashtree) { HashTree.from_json(File.read(file_path)) }
|
9
|
+
|
10
|
+
subject { hashtree }
|
11
|
+
|
12
|
+
self.instance_exec &block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hash-tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sebastien Rosa
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: &2157134360 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.5.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2157134360
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: ya2yaml
|
27
|
+
requirement: &2157133800 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.30'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2157133800
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: nori
|
38
|
+
requirement: &2157133320 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.1.0
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2157133320
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &2157132840 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.8.7
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2157132840
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &2157132320 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '2.0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2157132320
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-aspic
|
71
|
+
requirement: &2157131820 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.0.2
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2157131820
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: rspec-compact-doc-formatter
|
82
|
+
requirement: &2157131240 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.0.3
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *2157131240
|
91
|
+
description: HashTree help you to work with nested hashes and arrays.
|
92
|
+
email:
|
93
|
+
- sebastien@demarque.com
|
94
|
+
executables: []
|
95
|
+
extensions: []
|
96
|
+
extra_rdoc_files:
|
97
|
+
- LICENSE
|
98
|
+
- README.md
|
99
|
+
files:
|
100
|
+
- lib/hash-tree.rb
|
101
|
+
- spec/fixtures/book.json
|
102
|
+
- spec/fixtures/books.json
|
103
|
+
- spec/fixtures/books.xml
|
104
|
+
- spec/fixtures/books.yml
|
105
|
+
- spec/hash-tree_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- spec/support/application_helpers.rb
|
108
|
+
- LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- Gemfile
|
112
|
+
homepage: https://github.com/demarque/hash-tree
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project: hash-tree
|
133
|
+
rubygems_version: 1.8.17
|
134
|
+
signing_key:
|
135
|
+
specification_version: 3
|
136
|
+
summary: Manage nested hash
|
137
|
+
test_files: []
|