DotKey
DotKey is a Ruby gem that allows you to easily interact with Ruby objects using dot notation.
Getting started
Add DotKey to your Rails project by adding it to your Gemfile:
gem install dotkey
Or using bundler:
bundle add dotkey
Usage
get
Retrieves a value from a data structure (Hash, Array, or a nested combination) using a dot-delimited key.
data = {a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]}
DotKey.get(data, "a")
DotKey.get(data, "a.b")
DotKey.get(data, "a.b.0")
DotKey.get(data, "c.0.d")
If any values along the path are nil, nil is returned. However, trying to traverse something that is not a Hash or Array will cause an error:
DotKey.get({b: {}}, "b.c.d")
DotKey.get({b: []}, "b.c.d")
DotKey.get({b: []}, "b.0.d")
DotKey.get({a: "a string"}, "a.b")
This behaviour can be disabled by specifying the raise_on_invalid parameter:
DotKey.get({b: []}, "b.c.d", raise_on_invalid: false)
DotKey.get({a: "a string"}, "a.b", raise_on_invalid: false)
get_all
Retrieves all matching values from a data structure (Hash, Array, or a nested combination) using a dot-delimited key with wildcards.
* and ** can be used as wildcards for Array items and Hash keys respectively:
data = {a: [{b: 1}, {b: 2, c: 3}], d: [4, 5]}
DotKey.get_all(data, "a.0.b")
DotKey.get_all(data, "a.*.b")
DotKey.get_all(data, "a.1.**")
DotKey.get_all(data, "**.*")
If any values along the path are nil, nil is returned. However, trying to traverse something that is not a Hash or Array will cause an error:
DotKey.get_all({b: {}}, "b.c.d")
DotKey.get_all({b: []}, "b.c.d")
DotKey.get_all({b: []}, "b.0.d")
DotKey.get_all({a: "a string"}, "a.b")
This behaviour can be disabled by specifying the raise_on_invalid parameter.
DotKey.get_all({b: []}, "b.c.d", raise_on_invalid: false)
DotKey.get_all({a: "a string"}, "a.b", raise_on_invalid: false)
Missing values are included in the result as nil values, but these can be omitted by specifying the include_missing parameter:
data = {a: [{b: 1}, {b: 2, c: 3}], d: 4}
DotKey.get_all(data, "a.*.c")
DotKey.get_all(data, "a.*.c", include_missing: false)
DotKey.get_all(data, "d.*", raise_on_invalid: false)
DotKey.get_all(data, "d.*", raise_on_invalid: false, include_missing: false)
DotKey.get_all({a: nil}, "**", include_missing: false)
flatten
Converts a nested structure into a flat Hash, with the dot-delimited path to the value as the key.
DotKey.flatten({a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]})
set!
Sets a value in a data structure (Hash, Array, or a nested combination) using a dot-delimited key.
data = {a: {b: [1]}}
DotKey.set!(data, "a.b.0", "a")
DotKey.set!(data, "a.b.1", "b")
DotKey.set!(data, "c", "d")
data
Intermediate structures are created as needed when traversing a path that includes
missing elements:
data = {}
DotKey.set!(data, "a.b.c.0", 42)
data
DotKey.set!(data, "a.b.c.2", 44)
data
By default, keys are created as symbols, but string keys can by specified using the
string_keys parameter:
data = {}
DotKey.set!(data, "a", :symbol)
DotKey.set!(data, "b", "string", string_keys: true)
data
If a key along the path refers to a structure that is neither a Hash nor an Array,
an error is raised:
data = {a: "string"}
DotKey.set!(data, "a.b", 42)
delete!
Removes a value from a data structure (Hash, Array, or a nested combination) using a dot-delimited key and returns the deleted value.
data = {a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]}
DotKey.delete!(data, "a.b.0")
data
DotKey.delete!(data, "c.0.d")
data
If any values along the path are nil, nothing happens and nil is returned. However, if a key along the path refers to a structure that is neither a Hash nor an Array, an error is raised:
DotKey.delete!({b: {}}, "b.c.d")
DotKey.delete!({b: []}, "b.c.d")
DotKey.delete!({b: []}, "b.0.d")
DotKey.delete!({a: "a string"}, "a.b")
This behaviour can be disabled by specifying the raise_on_invalid parameter:
DotKey.delete!({b: []}, "b.c.d", raise_on_invalid: false)
DotKey.delete!({a: "a string"}, "a.b", raise_on_invalid: false)
Configuration
The default delimiter for keys is a dot, .. However, this can be changed to any other String using the DotKey.delimiter option:
DotKey.delimiter = "_"
DotKey.get({a: {b: [1]}}, "a_b_0")
DotKey.flatten({a: {b: [1]}})
Performance
Due to the parsing of string keys, DotKey won't be the most performant option when accessing data in nested objects:
object = {a: {b: {c: {d: {e: {f: {g: [[[1]]]}}}}}}}
Benchmark.ips do |bm|
bm.report("dotkey") { DotKey.get(object, "a.b.c.d.e.f.g.0.0.0") }
bm.report("dig") { object.dig(:a, :b, :c, :d, :e, :f, :g, 0, 0, 0) }
bm.report("brackets") { object[:a][:b][:c][:d][:e][:f][:g][0][0][0] }
bm.report("fetch") { object.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e).fetch(:f).fetch(:g).fetch(0).fetch(0).fetch(0) }
bm.compare!
end
However, DotKey excels at providing a concise and flexible approach to working with nested data structures:
it offers customisable handling of missing values and error conditions, while seamlessly supporting
both string and symbol keys without requiring explicit type conversion.
While much slower than the alternatives, it is still quick and efficient enough for most use cases. In fact, comparing DotKey.get again using Rails' HashWithIndifferentAccess, the performance is comparable to using dig:
object = {a: {"b" => {c: {"d" => {e: {"f" => {g: [[[1]]]}}}}}}}
indifferent = ActiveSupport::HashWithIndifferentAccess.new(
{a: {"b" => {c: {"d" => {e: {"f" => {g: [[[1]]]}}}}}}},
)
Benchmark.ips do |bm|
bm.report("dotkey") { DotKey.get(object, "a.b.c.d.e.f.g.0.0.0") }
bm.report("indifferent dotkey") { DotKey.get(indifferent, "a.b.c.d.e.f.g.0.0.0") }
bm.report("indifferent dig") { indifferent.dig(:a, :b, :c, :d, :e, :f, :g, 0, 0, 0) }
bm.report("indifferent brackets") { indifferent[:a][:b][:c][:d][:e][:f][:g][0][0][0] }
bm.report("indifferent fetch") { indifferent.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e).fetch(:f).fetch(:g).fetch(0).fetch(0).fetch(0) }
bm.compare!
end
The performance for setting values is much more comparable, but with significantly more succinct code:
# brackets set:
# 431898.9 i/s
# brackets set with missing intermediate values:
# 394861.3 i/s - 1.09x slower
# dotkey set:
# 212933.4 i/s - 2.03x slower
# dotkey set with missing intermediate values:
# 168456.3 i/s - 2.56x slower
See the performance test suite for more details.