Fetching deep in a dense structure. A kind of bastard of JSONPath.
Let
data = # taken from http://goessner.net/articles/JsonPath/
{ 'store' => {
'book' => [
{ 'category' => 'reference',
'author' => 'Nigel Rees',
'title' => 'Sayings of the Century',
'price' => 8.95
},
{ 'category' => 'fiction',
'author' => 'Evelyn Waugh',
'title' => 'Sword of Honour',
'price' => 12.99
},
{ 'category' => 'fiction',
'author' => 'Herman Melville',
'title' => 'Moby Dick',
'isbn' => '0-553-21311-3',
'price' => 8.99
},
{ 'category' => 'fiction',
'author' => 'J. R. R. Tolkien',
'title' => 'The Lord of the Rings',
'isbn' => '0-395-19395-8',
'price' => 22.99
}
],
'bicycle' => {
'color' => 'red',
'price' => 19.95,
'7' => 'seven'
}
}
}"store.book.1.title" # the title of the second book in the store
"store.book[1].title" # the title of the second book in the store
"store.book.1['french title']" # the french title of the 2nd book
"store.book.1[title,author]" # the title and the author of the 2nd book
"store.book[1,3].title" # the titles of the 2nd and 4th books
"store.book[1:8:2].title" # titles of books at offset 1, 3, 5, 7
"store.book[::3].title" # titles of books at offset 0, 3, 6, 9, ...
"store.book[:3].title" # titles of books at offset 0, 1, 2, 3
"store.*.price" # the price of everything directly in the store
"store..price" # the price of everything in the store
# ...Dense.get(data, 'store.book.1.title')
# => "Sword of Honour"
Dense.get(data, 'store.book.*.title')
# => [
# 'Sayings of the Century',
# 'Sword of Honour',
# 'Moby Dick',
# 'The Lord of the Rings' ]
Dense.get(data, 'store.bicycle.7')
# => "seven"When Dense.get(collection, path) doesn't find, it returns nil.
As seen above Dense.get might return a single value or an array of values. A "single" path like "store.book.1.title" will return a single value, while a "multiple" path like "store.book.*.title" or "store.book[1,2].title" will return an array of values.
Dense.has_key?(data, 'store.book.1.title')
# => true
Dense.has_key?(data, 'store.book.1["social security number"]')
# => falseDense.fetch is modelled after Hash.fetch.
Dense.fetch(data, 'store.book.1.title')
# => 'Sword of Honour'
Dense.fetch(data, 'store.book.*.title')
# => [ 'Sayings of the Century', 'Sword of Honour', 'Moby Dick',
# 'The Lord of the Rings' ]
Dense.fetch(data, 'store.bicycle.7')
# => 'seven'
Dense.fetch(data, 'store.bicycle[7]')
# => 'seven'When it doesn't find, it raises an instance of KeyError:
Dense.fetch({}, 'a.0.b')
# raises
# KeyError: found nothing at "a" ("0.b" remains)It might instead raise an instance of TypeError if a non-integer key is requested of an array:
Dense.fetch({ 'a' => [] }, 'a.k.b')
# raises
# TypeError: no key "k" for Array at "a"See KeyError and TypeError below for more details.
Dense.fetch(collection, path) raises when it doesn't find, while Dense.get(collection, path) returns nil.
Dense.fetch is modelled after Hash.fetch so it features a default optional argument.
If fetch doesn't find, it will return the provided default value.
Dense.fetch(data, 'store.book.1.title', -1)
# => "Sword of Honour" (found)
Dense.fetch(data, 'a.0.b', -1)
# => -1
Dense.fetch(data, 'store.nada', 'x')
# => "x"
Dense.fetch(data, 'store.bicycle.seven', false)
# => falseDense.fetch is modelled after Hash.fetch so it features a 'default' optional block.
Dense.fetch(data, 'store.book.1.title') do |coll, path|
"len:#{coll.length},path:#{path}"
end
# => "Sword of Honour" (found)
Dense.fetch(@data, 'store.bicycle.otto') do |coll, path|
"len:#{coll.length},path:#{path}"
end
# => "len:18,path:store.bicycle.otto" (not found)
not_found = lambda { |coll, path| "not found!" }
#
Dense.fetch(@data, 'store.bicycle.otto', not_found)
# => "not found!"
Dense.fetch(@data, 'store.bicycle.sept', not_found)
# => "not found!"Sets a value "deep" in a collection. Returns the value if successful.
c = {}
r = Dense.set(c, 'a', 1)
c # => { 'a' => 1 }
r # => 1
c = { 'h' => {} }
r = Dense.set(c, 'h.i', 1)
c # => { 'h' => { 'i' => 1 } }
r # => 1
c = { 'a' => [ 1, 2, 3 ] }
r = Dense.set(c, 'a.1', 1)
c # => { 'a' => [ 1, 1, 3 ] }
r # => 1
c = { 'h' => { 'a' => [ 1, 2, 3 ] } }
r = Dense.set(c, 'h.a.first', 'one')
c # => { 'h' => { 'a' => [ "one", 2, 3 ] } }
r # => 'one'
c = { 'h' => { 'a' => [ 1, 2, 3 ] } }
r = Dense.set(c, 'h.a.last', 'three')
c # => { 'h' => { 'a' => [ 1, 2, 'three' ] } }
r # => 'three'
c = { 'a' => [] }
Dense.set(c, 'a.b', 1)
# => TypeError: no key "b" for Array at "a"
c = { 'a' => {} }
r = Dense.set(c, 'a.1', 1)
c # => { 'a' => { '1' => 1 } }
r # => 1
c = {}
Dense.set(c, 'a.0', 1)
# => KeyError: found nothing at "a" ("0" remains)Setting at multiple places in one go is possible:
c = { 'h' => {} }
Dense.set(c, 'h[k0,k1,k2]', 123)
c
# => { 'h' => { 'k0' => 123, 'k1' => 123, 'k2' => 123 } }Creates the necessary collections on the way. A bit like mkdir -f x/y/z/
c = {}
r = Dense.force_set(c, 'a', 1)
r # => 1
c # => { 'a' => 1 }
c = {}
r = Dense.force_set(c, 'a.b.3.d.0', 1)
r # => 1
c # => { 'a' => { 'b' => [ nil, nil, nil, { 'd' => [ 1 ] } ] } }
c = { 'a' => [] }
Dense.force_set(c, 'a.b', 1)
# => TypeError: no key "b" for Array at "a"c = { 'a' => [ 0, 1, 2, 3 ] }
r = Dense.insert(c, 'b', 1234)
c
# => { "a" => [ 0, 1, 2, 3 ], "b" => 1234 }
c = { 'a' => [ 0, 1, 2, 3 ] }
r = Dense.insert(c, 'a.1', 'ONE')
c
# => { "a" => [ 0, "ONE", 1, 2, 3 ] }
c = { 'a' => [ 0, 1, 2, 3 ], 'a1' => [ 0, 1 ] }
r = Dense.insert(c, '.1', 'ONE')
c
# => { "a" => [ 0, "ONE", 1, 2, 3 ], "a1" => [ 0, "ONE", 1 ] }Removes an element deep in a collection.
c = { 'a' => 1 }
r = Dense.unset(c, 'a')
c # => {}
r # => 1
c = { 'h' => { 'i' => 1 } }
r = Dense.unset(c, 'h.i')
c # => { 'h' => {} }
r # => 1
c = { 'a' => [ 1, 2, 3 ] }
r = Dense.unset(c, 'a.1')
c # => { 'a' => [ 1, 3 ] }
r # => 2
c = { 'h' => { 'a' => [ 1, 2, 3 ] } }
r = Dense.unset(c, 'h.a.first')
c # => { 'h' => { 'a' => [ 2, 3 ] } }
r # => 1
c = { 'h' => { 'a' => [ 1, 2, 3 ] } }
r = Dense.unset(c, 'h.a.last')
c # => { 'h' => { 'a' => [ 1, 2 ] } }
r # => 3It fails with a KeyError or a TypeError if it cannot unset.
Dense.unset({}, 'a')
# => KeyError: found nothing at "a"
Dense.unset([], 'a')
# => TypeError: no key "a" for Array at root
Dense.unset([], '1')
# => KeyError: found nothing at "1"Unsetting multiple values is OK:
c = { 'h' => { 'a' => [ 1, 2, 3, 4, 5 ] } }
r = Dense.unset(c, 'h.a[2,3]')
c
# => { 'h' => { 'a' => [ 1, 2, 5 ] } }Dense might raise instances of KeyError and TypeError. Those instances have extra #full_path and #miss methods.
e =
begin
Dense.fetch({}, 'a.b')
rescue => err
err
end
# => #<KeyError: found nothing at "a" ("b" remains)>
e.full_path
# => "a"
e.miss
# => [false, [], {}, "a", [ "b" ]]The "miss" is an array [ false, path-to-miss, collection-at-miss, key-at-miss, path-post-miss ].
MIT, see LICENSE.txt