Project

dense

0.0
No commit activity in last 3 years
No release in over 3 years
Fetching deep in a dense structure. A kind of bastard of JSONPath.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 3.7

Runtime

>= 1.1.5, ~> 1.1
 Project Readme

dense

Build Status Gem Version

Fetching deep in a dense structure. A kind of bastard of JSONPath.

usage

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'
        }
      }
    }

paths

"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(collection, path)

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?(collection, path)

Dense.has_key?(data, 'store.book.1.title')
  # => true
Dense.has_key?(data, 'store.book.1["social security number"]')
  # => false

Dense.fetch(collection, path)

Dense.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(collection, path, default)

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)
  # => false

Dense.fetch(collection, path) { block }

Dense.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!"

Dense.set(collection, path, value)

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 } }

Dense.force_set(collection, path, value)

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"

Dense.insert(collection, path, value)

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 ] }

Dense.unset(collection, path)

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   # => 3

It 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 ] } }

KeyError and TypeError

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 ].

LICENSE

MIT, see LICENSE.txt