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