Dottie
Dottie lets you access a Hash or Array (possibly containing other Hashes and Arrays) using a dot-delimited string as the key. The string is parsed into individual keys, and Dottie traverses the data structure to find the target value. If at any point along the way a node is not found, Dottie will return nil
rather than raising an error.
A great place Dottie is useful is when accessing a data structure parsed from a JSON string, such as when consuming a JSON API. Since the only structural elements JSON can contain are objects and arrays, Dottie can easily access anything parsed from JSON.
Here's a simple example showing a data structure as well as why Dottie is a nice way of accessing the values, especially when certain parts of the data are not guaranteed to be present:
car = {
'color' => 'black',
'type' => {
'make' => 'Tesla',
'model' => 'Model S'
}
}
car['color']
car['type']['make']
car['specs']['mileage']
d = Dottie(car)
d['color']
d['type.make']
d['specs.mileage']
Here's another example showing a hash containing an array:
family = {
'mom' => 'Alice',
'dad' => 'Bob',
'kids' => [
{ 'name' => 'Carol' },
{ 'name' => 'Dan' }
]
}
family['kids'][0]['name']
family['kids'][2]['name']
family['pets'][0]['name']
d = Dottie(family)
d['kids[0].name']
d['kids[2].name']
d['pets[0].name']
Installation
Add this line to your application's Gemfile:
gem 'dottie'
And then execute:
$ bundle
Or install it yourself as:
$ gem install dottie
If you want the mixin behavior described below (where you can call dottie
and dottie!
on Hash
and Array
objects), require 'dottie/ext'
in your Gemfile:
gem 'dottie', require: 'dottie/ext'
Usage
First, let's start with some examples of Dottie's behaviors. After that, we'll see how we can access those behaviors.
data = Dottie({
'a' => { 'b' => 'c' }
})
data['a']
data['a.b']
data.fetch('a.b')
data.has_key?('a.b')
data['a.b'] = 'd'
data['a.b']
data.hash
data['a.e'] = 'f'
data.hash
h = Dottie({})
h['a.b.c'] = 'd'
h.hash
complex = Dottie({
'a' => [{ 'b' => 'c' }, { 'd' => 'e' }]
})
complex['a[1].d']
complex['a[first].b'] = 'x'
complex.hash
complex['a[2]'] = 'y'
complex.hash
complex['a[+]'] = 'z'
complex.hash
complex['a[-]'] = 'p'
complex.hash
complex.delete('a[1].b')
complex.hash
complex.delete('a[1]')
complex.hash
Dottie Usage Options
There are three basic ways of using Dottie:
- By mixing Dottie's behavior into a
Hash
/Array
(most versatile)
- As a wrapper around a Hash or Array (doesn't modify the
Hash
/Array
)
- By calling Dottie's module methods directly (more verbose, good for one-off uses)
Here are examples of each of these usage options:
1. Using Dottie as a Mixin
This is the simplest usage. You must require 'dottie/ext'
in order for the dottie!
method to be added to Hash
and Array
. This can be done in your Gemfile as shown above.
require 'dottie/ext'
hash = {
'a' => 'b',
'c' => {
'd' => 'e'
}
}.dottie!
hash.class
hash['a']
hash['c.d']
The Hash
and Array
extensions are a nice but optional way to get convenient access to Dottie's behavior on built-in classes. To add Dottie's behavior to a Hash
or Array
without the use of the class extensions, you can extend the object with Dottie's methods. This is what the dottie!
extension methods do internally.
h = {
'a' => { 'b' => 'c' }
}
h.extend(Dottie::Methods)
h['a.b']
2. Using Dottie as a Wrapper
This is the preferred method if you do not wish to modify the Hash
or Array
you're working with. In this case, your object will be wrapped in a Dottie::Freckle
, which will more or less act like the original object. There are a few exceptions to this, such as when checking for equality. (For this, use .hash
or .array
to get access to the wrapped object.)
To wrap an object, use Dottie()
or Dottie[]
(which are equivalent), or manually create a Dottie::Freckle
instance. Or, if you have required 'dottie/ext'
, you can call dottie
(not dottie!
) on your object.
hash = {
'a' => { 'b' => 'c' }
}
d_hash = Dottie(hash)
d_hash = Dottie[Hash]
d_hash = hash.dottie
d_hash['a']
d_hash['a.b']
arr = ['a', { 'b' => 'c' }, 'd']
d_arr = Dottie(arr)
d_arr['[1].b']
d_hash = Dottie({ 'a' => 'b' })
d_arr = Dottie(['a', 'b', 'c'])
d_hash = { 'a' => 'b' }.dottie
d_arr = ['a', 'b', 'c'].dottie
3. Using Dottie's Methods Directly
This is an easy way to use Dottie's behaviors without modifying your objects and without wrapping them. For this, the syntax is more verbose, but if this suits you, here it is:
hash = {
'a' => { 'b' => 'c' }
}
Dottie.get(hash, 'a.b')
hash['a.b']
Dottie.set(hash, 'a.b', 'x')
hash
hash['a.b'] = 'z'
hash
Dottie.get(hash, 'a.b')
hash['a.b']
Traversing Arrays
Array elements can be targeted with a bracketed index, which is a positive or negative integer or a first
or last
named index.
me = Dottie({
'pets' => ['dog', 'cat', 'bird', 'fish']
})
me['pets[first]']
me['pets[1]']
me['pets[-2]']
me['pets[last]']
Dottie Key Format
Dottie uses periods and brackets to delimit key parts. In general, hash keys are strings separated by periods, and array indexes are integers surrounded by brackets. For example, in the key a.b[1]
, a
and b
are strings (hash keys) and 1
is a bracketed integer (an array index). Here's an example of how this key would be used:
d = Dottie({
'a' => {
'b' => ['x', 'y', 'z']
}
})
d['a.b[1]']
For readability and convenience, first
and last
, when enclosed with brackets (such as [first]
), are treated as named array indexes and are converted to 0
and -1
, respectively.
Besides first
and last
, any other non-integer string is handled like a normal, dot-delimited string. For example, the key a[b].c
is equivalent to a.b.c
.
Brackets can also be used to quote key segments that contain periods. For example, a[b.c].d
is interpreted internally as ['a', 'b.c', 'd']
. (If you need to see how a key will be interpreted, use Dottie#key_parts
in an IRB session.)
Dottie is forgiving in its key parsing. Periods are optional around brackets. For example, the key a[1]b[2]c
is the same as a.[1].b.[2].c
. Ruby-like syntax is preferred, where a period follows each closing bracket but does not preceed any opening brackets. The preferred key in this case is a[1].b[2].c
.
What Dottie Doesn't Do
When mixing Dottie into a Hash
or Array
, or when wrapping one in a Dottie::Freckle
(which passes unrecognized method calls on to the wrapped object), Dottie provides []
, []=
, fetch
, and has_key?
methods, with the latter two assuming you're working with a Hash
.
By design, Dottie does not provide an implementation for keys
, each
, or other built-in Hash
and Array
methods where the expected behavior might be ambiguous. Dottie is meant to be an easy way to store and retrieve data in a JSON-like data structure (hashes and/or arrays nested within each other) and is not meant as a replacement for any core classes.
FAQ
Q: Will Dottie make my life easier?
A: Probably.
Q: Will Dottie brush my teeth for me?
A: Probably not.
Q: Why is the wrapper class named Freckle
?
A: Why not?
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
)
- Commit your changes (
git commit -am 'Add some feature'
)
- Push to the branch (
git push origin my-new-feature
)
- Create new Pull Request