Low commit activity in last 3 years
Access deeply nested hash values using dot notation (config.database.host) with nil-safe traversal that never raises on missing keys. Supports path-based get/set, YAML/JSON loading, and immutable updates.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-dot_access

Tests Gem Version Last updated

philiprehberger-dot_access

Dot-notation accessor for nested hashes with nil-safe traversal

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-dot_access"

Or install directly:

gem install philiprehberger-dot_access

Usage

require "philiprehberger/dot_access"

config = Philiprehberger::DotAccess.wrap({ database: { host: 'localhost', port: 5432 } })
config.database.host  # => "localhost"
config.database.port  # => 5432

Nil-Safe Traversal

config = Philiprehberger::DotAccess.wrap({ name: 'app' })
config.missing.nested.value  # => nil (never raises)

Path-Based Access

config = Philiprehberger::DotAccess.wrap({ a: { b: { c: 'deep' } } })
config.get('a.b.c')                        # => "deep"
config.get('a.b.missing', default: 'nope') # => "nope"

Immutable Updates

config  = Philiprehberger::DotAccess.wrap({ database: { port: 3306 } })
updated = config.set('database.port', 5432)

updated.database.port  # => 5432
config.database.port   # => 3306 (unchanged)

Array Element Access

Integer path segments are treated as array indices when the current value is an Array. Negative indices count from the end, following Ruby conventions.

config = Philiprehberger::DotAccess.wrap(
  { items: [{ name: 'a' }, { name: 'b' }] }
)

config.get('items.0.name')   # => "a"
config.get('items.-1.name')  # => "b"
config.exists?('items.1')    # => true
config.get('items.99.name')  # => nil

updated = config.set('items.0.name', 'A')
updated.get('items.0.name')  # => "A"

config.delete('items.0').get('items').map { |i| i[:name] }
# => ["b"]

Out-of-bounds indices raise ArgumentError on set/update and return nil on get.

Batch Updates

config  = Philiprehberger::DotAccess.wrap({ a: { b: 1, c: 2 }, d: 3 })
updated = config.update('a.b' => 10, 'd' => 30)

updated.get('a.b')  # => 10
updated.get('a.c')  # => 2 (unchanged)
updated.get('d')    # => 30

YAML Loading

config = Philiprehberger::DotAccess.from_yaml('config.yml')
config.database.host  # => value from YAML file

JSON Loading

config = Philiprehberger::DotAccess.from_json('{"database": {"host": "localhost"}}')
config.database.host  # => "localhost"

Path Existence Check

config = Philiprehberger::DotAccess.wrap({ a: { b: 1 }, x: nil })
config.exists?('a.b')     # => true
config.exists?('x')       # => true  (nil values still exist)
config.exists?('missing') # => false

Key Listing

config = Philiprehberger::DotAccess.wrap({ a: { b: { c: 1 } }, d: 2 })
config.keys              # => ["a.b.c", "d"]
config.keys(depth: 1)    # => ["a", "d"]

Strict Fetch

config = Philiprehberger::DotAccess.wrap({ database: { host: 'localhost' } })
config.fetch!('database.host')  # => "localhost"
config.fetch!('database.port')  # raises KeyError

Slice and Values At

config = Philiprehberger::DotAccess.wrap({ a: { b: 1, c: 2 }, d: 3 })
config.slice('a.b', 'd').to_h   # => { a: { b: 1 }, d: 3 }
config.values_at('a.b', 'd')    # => [1, 3]

Immutable Deletion

config  = Philiprehberger::DotAccess.wrap({ a: { b: 1, c: 2 }, d: 3 })
updated = config.delete('a.b')

updated.to_h  # => { a: { c: 2 }, d: 3 }
config.to_h   # => { a: { b: 1, c: 2 }, d: 3 } (unchanged)

Flatten

config = Philiprehberger::DotAccess.wrap({ a: { b: 1 }, c: 2 })
config.flatten  # => { "a.b" => 1, "c" => 2 }

From Flat

Inverse of #flatten — rebuild a wrapper from a hash of dot-paths to values. Useful for reconstituting data from flat storage like environment variables or columnar files.

flat = { "database.host" => "localhost", "database.port" => 5432 }
config = Philiprehberger::DotAccess.from_flat(flat)
config.database.host  # => "localhost"

# Round-trip works for nested hash structures
original = { a: { b: 1 }, c: 2 }
Philiprehberger::DotAccess.from_flat(
  Philiprehberger::DotAccess.wrap(original).flatten
).to_h  # => { a: { b: 1 }, c: 2 }

Arrays are carried through as opaque values — #flatten does not explode array elements into separate dot-paths, and from_flat cannot create new array slots from integer-only segments.

Compact

config = Philiprehberger::DotAccess.wrap({ a: 1, b: nil, c: { d: nil, e: 2 }, f: [1, nil, 2] })
config.compact.to_h  # => { a: 1, c: { e: 2 }, f: [1, 2] }

Deep Merge

config = Philiprehberger::DotAccess.wrap({ a: { b: 1, c: 2 } })
merged = config.merge({ a: { c: 3, d: 4 } })

merged.to_h  # => { a: { b: 1, c: 3, d: 4 } }

Iteration

config = Philiprehberger::DotAccess.wrap({ a: 1, b: { c: 2 } })
config.each { |key, value| puts "#{key}: #{value}" }
config.map { |_key, value| value }   # Enumerable methods included
config.select { |key, _value| key == :a }

Size and Emptiness

config = Philiprehberger::DotAccess.wrap({ a: 1, b: 2 })
config.size    # => 2
config.empty?  # => false

Serialization

config = Philiprehberger::DotAccess.wrap({ database: { host: "localhost" } })
config.to_json  # => '{"database":{"host":"localhost"}}'
config.to_yaml  # => "---\ndatabase:\n  host: localhost\n"

Converting Back to Hash

config = Philiprehberger::DotAccess.wrap({ a: { b: 1 } })
config.to_h  # => { a: { b: 1 } }

JSON Pointer (RFC 6901) Access

Useful when interoperating with JSON Schema, JSON Patch, or any tooling that already speaks JSON Pointer paths.

data = Philiprehberger::DotAccess.wrap(
  users: [{ name: "alice" }, { name: "bob" }],
  "a/b": { "c~d": 1 }
)

data.get_pointer("/users/0/name")          # => "alice"
data.get_pointer("")                        # => the wrapper itself (root)
data.get_pointer("/missing", default: :nf)  # => :nf

# ~1 escapes /, ~0 escapes ~ — so the key "a/b" → "a~1b"
data.get_pointer("/a~1b/c~0d")  # => 1

data.has_pointer?("/users/1/name")  # => true
updated = data.set_pointer("/users/0/name", "alex")
trimmed = data.delete_pointer("/users/0/name")

API

Philiprehberger::DotAccess

Method Description
.wrap(hash) Wrap a hash for dot-notation access
.from_yaml(str_or_path) Parse YAML string or file and wrap the result
.from_json(str) Parse JSON string and wrap the result
.from_flat(hash) Build a wrapper from a hash of dot-paths to values (inverse of #flatten)

Philiprehberger::DotAccess::Wrapper

Method Description
#get(path, default: nil) Retrieve a value by dot-separated path (supports array indices)
#fetch!(path) Retrieve a value or raise KeyError if the path is missing
#set(path, value) Return a new wrapper with the value set at the path (supports array indices)
#update(paths_hash) Batch-set multiple dot-paths, returning a new wrapper
#slice(*paths) Return a new wrapper containing only the given paths
#values_at(*paths) Return an array of values at the given paths
#exists?(path) Check if a dot-separated path exists (even if value is nil)
#keys(depth: nil) List all dot-path keys, optionally limited by depth
#delete(path) Return a new wrapper without the specified path (supports array indices)
#flatten Convert to a flat hash with dot-path string keys
#merge(other) Deep merge with another Wrapper or Hash, returning a new Wrapper
#compact Return a new wrapper with all nil values removed at every depth
#each / #each_pair Iterate over top-level key-value pairs
#empty? Returns true if the wrapped hash has no keys
#size / #count Returns the number of top-level keys
#to_json Serialize back to a JSON string
#to_yaml Serialize back to a YAML string
#to_h Convert back to a plain hash
#get_pointer(pointer, default: nil) Retrieve a value by a JSON Pointer (RFC 6901) path
#has_pointer?(pointer) Check if a JSON Pointer path exists
#set_pointer(pointer, value) Return a new wrapper with the value set at a JSON Pointer path
#delete_pointer(pointer) Return a new wrapper without the value at a JSON Pointer path

Philiprehberger::DotAccess::NullAccess

Method Description
#nil? Returns true
#<any_method> Returns nil for any method call

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT