Skip to content

Instantly share code, notes, and snippets.

@roktas
Created August 6, 2026 12:12
Show Gist options
  • Select an option

  • Save roktas/763555b415daba1ff3184e5da50eab89 to your computer and use it in GitHub Desktop.

Select an option

Save roktas/763555b415daba1ff3184e5da50eab89 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
# Hash-backed, dot-accessible options object. A field name that would
# collide with a real method on this object raises instead of silently
# becoming unreachable via dot-notation.
class DotHash
# Raised when a field name collides with an existing method.
class ReservedNameError < NameError
end
# @param hash [Hash] initial fields
def initialize(hash = {})
@table = {}
hash.each { |key, value| self[key] = value }
end
# @param key [Symbol, String]
# @return [Object, nil]
def [](key)
@table[key.to_sym]
end
# @param key [Symbol, String]
# @raise [ReservedNameError] if key collides with an existing method
def []=(key, value)
key = key.to_sym
if reserved?(key)
raise(
ReservedNameError,
"#{key.inspect} collides with a real #{self.class} method -- " \
"obj.#{key} would call that method, not return this value. " \
"Use a plain Hash if you need this exact key."
)
end
@table[key] = value
end
# @param key [Symbol, String]
# @return [Boolean]
def key?(key) = @table.key?(key.to_sym)
alias has_key? key?
# @param key [Symbol, String]
# @return [Object, nil] the removed value
def delete_field(key) = @table.delete(key.to_sym)
# @yieldparam pair [Array(Symbol, Object)]
# @return [Enumerator, self]
def each_pair(&)
return to_enum(__method__) { @table.size } unless block_given?
@table.each_pair(&)
self
end
alias each each_pair
# @param other [#to_hash]
# @raise [ReservedNameError] if any key in other collides with an existing method
# @return [self]
def merge!(other)
other.to_hash.each { |key, value| self[key] = value }
self
end
# @param other [#to_hash]
# @return [DotHash]
def merge(other) = dup.merge!(other)
# @return [Hash] a dup of the internal table, never the live one
def to_h(&block) = block ? @table.to_h(&block) : @table.dup
alias to_hash to_h
# @return [Hash] enables `case obj; in {foo:, bar:}`
def deconstruct_keys(_keys) = @table
def ==(other)
other.is_a?(DotHash) && @table == other.to_hash
end
def inspect
"#<#{self.class} #{@table.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")}>"
end
alias to_s inspect
# @raise [ReservedNameError] if the accessed name collides with an existing method
def method_missing(method, *args)
name = method.to_s
if name.end_with?("=")
self[name.delete_suffix("=")] = args.first
else
self[name]
end
end
def respond_to_missing?(_method, _include_private = false) = true
private
# @return [Boolean] whether key is already a real method on this object
def reserved?(key)
self.class.method_defined?(key) || self.class.private_method_defined?(key)
end
def initialize_copy(other)
super
@table = other.to_hash
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment