lib/key_tree/tree.rb
# frozen_string_literal: true
require 'forwardable'
require_relative 'meta_data'
require_relative 'path'
require_relative 'refinements'
require_relative 'refine/deep_hash'
module KeyTree # rubocop:disable Style/Documentation
using Refinements
using Refine::DeepHash
# A tree of key-value lookup tables (hashes)
class Tree
include MetaData
extend Forwardable
#
# KeyTree::Tree.new(+hash+)
#
# Initialize a new KeyTree from nested Hash:es
#
def self.[](hash = {})
new(hash)
end
def initialize(hash = {}, default = nil, &default_proc)
@hash = hash.to_h.deep_key_pathify
@default = default
@default_proc = default_proc
end
attr_reader :default, :default_proc
alias to_key_tree itself
alias to_key_wood itself
delegate %i[empty? to_h to_json] => :@hash
# Convert a Tree to YAML, with string keys
#
# :call-seq:
# to_yaml => String
def to_yaml
to_h.deep_transform_keys(&:to_s).to_yaml
end
def [](key_path)
fetch(key_path) do
next default if default_proc.nil?
default_proc.call(self, key_path)
end
end
def fetch(key_path, *args, &key_missing)
@hash.deep_fetch(key_path.to_key_path, *args, &key_missing)
end
def store(key_path, new_value)
@hash.deep_store(key_path.to_key_path, new_value)
end
def store!(key_path, new_value)
store(key_path, new_value)
rescue KeyError
delete!(key_path)
retry
end
alias []= store!
def delete(key_path)
@hash.deep_delete(key_path.to_key_path)
end
def delete!(key_path)
delete(key_path)
rescue KeyError
key_path = key_path[0..-2]
retry
end
def values_at(*key_paths)
key_paths.map { |key_path| self[key_path] }
end
# Return all maximal key paths in a tree
#
# :call-seq:
# keys => Array of KeyTree::Path
def keys
@hash.deep.each_with_object([]) do |(key_path, value), result|
result << key_path.to_key_path unless value.is_a?(Hash)
end
end
alias key_paths keys
def include?(key_path)
fetch(key_path)
true
rescue KeyError
false
end
alias key? include?
alias has_key? include?
alias key_path? include?
alias has_key_path? include?
def prefix?(key_path)
key_path.to_key_path.reduce(@hash) do |subtree, key|
return false unless subtree.is_a?(Hash)
return false unless subtree.key?(key)
subtree[key]
end
true
end
alias has_prefix? prefix?
def value?(needle)
@hash.deep.lazy.any? { |(_, straw)| straw == needle }
end
alias has_value? value?
# Merge values from +other+ tree into self
#
# :call-seq:
# merge!(other) => self
# merge!(other) { |key, lhs, rhs| } => self
def merge!(other, &block)
@hash.deep_merge!(other.to_h, &block)
self
end
alias << merge!
# Return a new tree by merging values from +other+ tree
#
# :call-seq:
# merge(other) => Tree
# merge(other) { |key, lhs, rhs| } => Tree
def merge(other, &block)
@hash.deep_merge(other.to_h, &block).to_key_tree
end
alias + merge
end
end