lib/xkeys.rb in xkeys-1.0.1 vs lib/xkeys.rb in xkeys-2.0.0
- old
+ new
@@ -1,7 +1,7 @@
# XKeys - Extended keys to facilitate fetching and storing in nested
-# hash and array structures with Perl-ish auto-vivification.
+# hash- and array-like structures with Perl-ish auto-vivification.
#
# Synopsis:
# root = {}.extend XKeys::Hash
# root[:my, :list, :[]] = 'value 1'
# root[:my, :list, :[]] = 'value 2'
@@ -18,73 +18,80 @@
# # => [ nil, [ 'value 1', nil, nil, 'value 2' ] ]
# root[0, 1] # => [ nil ] (slice of length 1 at 0)
# root[1, 0, {}] # => 'value 1'
# root[1, 4, {}] # => nil
#
+# As of version 2, other types with array- or hash-like behavior are
+# supported as well.
+#
+# Version 2.0.0 2014-03-21
+#
# @author Brian Katzung <briank@kappacs.com>, Kappa Computer Solutions, LLC
-# @copyright 2013 Brian Katzung and Kappa Computer Solutions, LLC
+# @copyright 2013-2014 Brian Katzung and Kappa Computer Solutions, LLC
# @license MIT License
module XKeys; end
# Extended fetch and get ([])
module XKeys::Get
# Perform an extended fetch using successive keys to traverse a tree
- # of nested hashes and/or arrays.
+ # of nested hash- and/or array-like objects.
#
# xfetch(key1, ..., keyN [, option_hash])
#
- # Options:
+ # Options:
#
# :else => default value
- # The default value to return if the specified keys do not exist.
+ # The default value to return if any of the keys do not exist
+ # (when an underlying #fetch generates a KeyError or IndexError).
# The :raise option takes precedence.
#
# :raise => true
- # Raise a KeyError or IndexError if the specified keys do not
- # exist. This is the default behavior for xfetch in the absence
- # of an :else option.
+ # Re-raise the original KeyError or IndexError if any of the keys
+ # do not exist. This is the default behavior for xfetch in the
+ # absence of an :else option.
#
# :raise => *parameters
# Like :raise => true but does raise *parameters instead, e.g.
# :raise => RuntimeError or :raise => [RuntimeError, 'SNAFU']
def xfetch (*args)
if args[-1].is_a?(Hash) then options, last = args[-1], -2
else options, last = {}, -1
end
+
args[0..last].inject(self) do |node, key|
begin node.fetch key
rescue KeyError, IndexError
- if options[:raise] and options[:raise] != true
+ if options[:raise] && options[:raise] != true
raise *options[:raise]
- elsif options[:raise] or !options.has_key? :else
+ elsif options[:raise] || !options.has_key?(:else)
raise
else return options[:else]
end
end
end
end
# Perform an extended get using successive keys to traverse a tree of
# nested hashes and/or arrays.
#
- # [key] returns the hash or array element (or range-based array slice)
- # as normal.
+ # [key] or [range] returns the normal hash or array element (or
+ # range-based array slice).
#
- # array[int1, int2] returns a length-based array slice as normal.
- # Append an option hash to force nested index behavior for two
- # integer array indexes: array[index1, index2, {}].
+ # [int1, int2] for arrays (or other objects responding to the #slice
+ # method) returns the object's normal two-parameter (e.g. start + length
+ # slice) index value.
#
- # [key1, ..., keyN[, option_hash]] traverses a tree of nested
- # hashes and/or arrays using xfetch.
+ # [key1, ..., keyN[, option_hash]] traverses a tree of nested
+ # hash- and/or array-like objects using xfetch.
#
- # Option :else => nil is used if no :else option is supplied.
- # See xfetch for option details.
+ # Option :else => nil is used if no :else option is supplied.
+ # See xfetch for option details.
def [] (*args)
- if args.count == 1 or (self.is_a?(Array) and args.count == 2 and
- args[0].is_a?(Integer) and args[1].is_a?(Integer))
+ if args.count == 1 || (respond_to?(:slice) && args.count == 2 &&
+ args[0].is_a?(Integer) && args[1].is_a?(Integer))
# [key] or array[start, length]
super *args
else
def_opts = { :else => nil } # Default options
if args[-1].is_a? Hash
@@ -99,65 +106,119 @@
# "Private" module for XKeys::Set_* common code
module XKeys::Set_
# Common code for XKeys::Set_Hash and XKeys::Set_Auto. This method
- # returns true if it is handling the set, or false if super should
- # handle the set.
+ # returns true if it is handling the set, or false if the caller
+ # should super to handle the set.
#
- # _xset(key1, ..., keyN[, options_hash], value) { |key, options| block }
+ # _xkeys_set(key1, ..., keyN[, option_hash], value) { |key, options| block }
#
- # The block should return true to auto-vivify an array or false to
- # auto-vivify a hash.
+ # If the root of the tree responds to the #xkeys_new method, it will
+ # be called as follows whenever a new node needs to be created:
#
- # Options:
+ # xkeys_new(key2, info_hash, option_hash)
#
+ # where info_hash contains
+ #
+ # :node => The current node
+ # :key1 => The key in the current node, or :[]
+ # :block => The block passed to _xkeys_set
+ #
+ # The returned new node will be assigned to node[key1] (or pushed onto
+ # the end of the array) and should be appropriate to accept key2.
+ #
+ # Otherwise, the block should return true for array-like keys or false
+ # for hash-like keys. An array or hash node will be added accordingly.
+ #
+ # If a key is :[], the current node responds to the #push method, and
+ # push mode has not been disabled (see below), a new node will be
+ # pushed onto the end of the current node.
+ #
+ # Options:
+ #
# :[] => false
- # Disable :[] auto-indexing
- def _xset (*args)
+ # Disable :[] push mode
+ def _xkeys_set (*args, &block)
if args[-2].is_a?(Hash) then options, last = args[-2], -3
else options, last = {}, -2
end
+
+ push_mode = options[:[]] != false
+
if args.count + last == 0
- if self.is_a?(Array) && args[0] == :[]
- self << args[-1] # array[:[]] = value
- else return false # [key] = value ==> super
+ if args[0] == :[] && push_mode && respond_to?(:push)
+ push args[-1] # array[:[]] = value
+ true # done--don't caller-super
+ else false # use caller-super to do it
end
else
# root[key1, ..., keyN[, option_hash]] = value
- (node, key) = args[1..last].inject([self, args[0]]) do |node, key|
- if yield key, options
- node[0][node[1]] ||= []
- [node[0][node[1]], (key != :[]) ? key :
- node[0][node[1]].size]
+ (node, key) = args[1..last].inject([self, args[0]]) do |nk1, k2|
+ if nk1[1] == :[] && push_mode && nk1[0].respond_to?(:push)
+ # Push a new node onto an array-like node
+ node = _xkeys_new(k2, { :node => nk1[0],
+ :key1 => nk1[1], :block => block }, options)
+ nk1[0].push node
+ [node, k2]
+ elsif nk1[0][nk1[1]].nil?
+ # Auto-vivify the specified key/index
+ node = _xkeys_new(k2, { :node => nk1[0],
+ :key1 => nk1[1], :block => block }, options)
+ nk1[0][nk1[1]] = node
+ [node, k2]
else
- node[0][node[1]] ||= {}
- [node[0][node[1]], key]
+ # Traverse an existing node
+ [nk1[0][nk1[1]], k2]
end
end
- if yield key, options
- node[(key != :[])? key : node.size] = args[-1]
- else node[key] = args[-1]
+
+ # Assign (or push) according to the final key.
+ if key == :[] && push_mode && node.respond_to?(:push)
+ node.push args[-1]
+ else
+ node[key] = args[-1]
end
+ true # done--don't caller-super
end
- true
end
+ # Return a new node for node[key1] suitable to hold key2.
+ # Either key1 or key2 (or both) may be :[].
+ def _xkeys_new (key2, info, options)
+ if respond_to? :xkeys_new
+ # Note: #xkeys_new is responsible for cloning extensions
+ # as desired or needed.
+ xkeys_new key2, info, options
+ else
+ node = info[:block].call(key2, options) ? [] : {}
+
+ # Clone XKeys extensions from the root node
+ node.extend XKeys::Get if is_a? XKeys::Get
+ node.extend XKeys::Set_Auto if is_a? XKeys::Set_Auto
+ node.extend XKeys::Set_Hash if is_a? XKeys::Set_Hash
+
+ node
+ end
+ end
+
end
# Extended set ([]=) with hash keys
module XKeys::Set_Hash
include XKeys::Set_
# Auto-vivify nested hash trees using extended hash key/array index
# assignment syntax. :[] keys create nested arrays as needed. Other
# keys, including integer keys, create nested hashes as needed.
#
- # root[key1, ..., keyN[, options_hash]] = value
+ # See XKeys::Set_ for additional information.
+ #
+ # root[key1, ..., keyN[, option_hash]] = value
def []= (*args)
- super args[0], args[-1] unless _xset(*args) do |key, opts|
- key == :[] and opts[:[]] != false
+ super args[0], args[-1] unless _xkeys_set(*args) do |key, options|
+ key == :[] && options[:[]] != false
end
args[-1]
end
end
@@ -169,13 +230,15 @@
# Auto-vivify nested hash and/or array trees using extended hash
# key/array index assignment syntax. :[] keys and integer keys
# create nested arrays as needed. Other keys create nested hashes
# as needed.
#
- # root[key1, ..., keyN[, options_hash]] = value
+ # See XKeys::Set_ for additional information.
+ #
+ # root[key1, ..., keyN[, option_hash]] = value
def []= (*args)
- super args[0], args[-1] unless _xset(*args) do |key, opts|
- (key == :[] and opts[:[]] != false) or key.is_a?(Integer)
+ super args[0], args[-1] unless _xkeys_set(*args) do |key, options|
+ (key == :[] && options[:[]] != false) || key.is_a?(Integer)
end
args[-1]
end
end