lib/xf/scope.rb
module Xf
# A play on Lenses in Ruby, though not quite as powerful they can be
# useful for getting and setting deep values on collections.
#
# @author baweaver
# @since 0.0.2
#
class Scope
# Creates a Scope
# @param paths [Array[Any]] Paths to follow to reach the desired value
#
# @return [Xf::Scope]
def initialize(paths)
@paths = paths
end
# Gets a value from a Hash
#
# @return [type] [description]
def get(&fn)
Proc.new { |hash| get_value(hash, &fn) }
end
# Sets a value in a Hash
#
# @param value = nil [Any]
# Value to set at the target
#
# @param &fn [Proc]
# Block to yield target value to. Returned value will be set as the new
# value at the target.
#
# @return [Proc[Hash] -> Hash]
# New Hash with transformation applied
def set(value = nil, &fn)
Proc.new { |hash| set_value(hash, value, &fn) }
end
# Mutates a value in a Hash
#
# @see #set
#
# @note
# This method does the same thing as `#set`, except that it mutates the
# target value instead of creating a clone first.
#
def set!(value = nil, &fn)
Proc.new { |hash| set_value!(hash, value, &fn) }
end
# Direct value getter, though it may be wiser to use Hash#dig here
# instead if you're concerned about speed.
#
# If the object does not respond to dig, Xf will attempt to use an
# array bracket accessor dive instead for compatability reasons.
#
# @param hash [Hash] Hash to get value from
#
# @return [Any]
def get_value(hash, &fn)
value = if hash.respond_to?(:dig)
hash.dig(*@paths)
else
new_hash = hash
@paths.each { |s| new_hash = new_hash[s] }
new_hash
end
block_given? ? fn[value] : value
end
# Sets a value at the bottom of a path without mutating the original.
#
# @param hash [Hash]
# Hash to set value on
#
# @param value = nil [Any]
# Value to set
#
# @param &fn [Proc]
# If present, current value is yielded to it and the return
# value is the new set value
#
# @return [Hash]
# Clone of the original with the value set
def set_value(hash, value = nil, &fn)
set_value!(deep_clone(hash), value, &fn)
end
# Mutating form of `#set_value`
#
# @see set_value
def set_value!(hash, value = nil, &fn)
lead_in = @paths[0..-2]
target_key = @paths[-1]
new_hash = hash
lead_in.each { |s| new_hash = new_hash[s] }
new_value = block_given? ?
yield(new_hash[target_key]) :
value
new_hash[target_key] = new_value
hash
end
# Private API - Methods below here are subject to change, please don't use
# them directly.
private def deep_clone(hash)
Marshal.load(Marshal.dump(hash))
end
end
end