lib/hashformer/generate.rb in hashformer-0.2.2 vs lib/hashformer/generate.rb in hashformer-0.3.0
- old
+ new
@@ -60,27 +60,58 @@
end
# Internal representation of a method call and array lookup chainer. Do
# not use this directly; instead use HF::G.chain().
class Chain
- # Receiver for chaining calls that has no methods of its own except
- # initialize. This allows methods like :call to be chained.
- #
- # IMPORTANT: No methods other than .__chain can be called on this object,
- # because they will be chained! Instead, use === to detect the object's
- # type, for example.
- class Receiver < BasicObject
+ # Base module that defines methods included by BasicReceiver and
+ # DebuggableReceiver.
+ module ReceiverMethods
# An oddly named accessor is used instead of #initialize to avoid
# conflicts with any methods that might be chained.
attr_accessor :__chain
- # Adds a method call or array dereference to the list of calls to apply.
+ # Adds a method call or array dereference to the list of calls to
+ # apply. Does nothing if #__end has been called. Returns self for
+ # more chaining.
def method_missing(name, *args, &block)
- @__chain << {name: name, args: args, block: block}
+ @__ended ||= false
+ @__chain << {name: name, args: args, block: block} unless @__ended
self
end
+ # Adds a call to the given +block+ to the chain like Object#tap, but
+ # returns the result of the block instead of the original object. Any
+ # arguments given will be passed to the +block+ after the current
+ # value. Does nothing if #__end has been called. Returns self for
+ # more chaining.
+ #
+ # This is similar in spirit (but not implementation) to
+ # http://stackoverflow.com/a/12849214
+ def __as(*args, &block)
+ ::Kernel.raise 'No block given to #__as' unless ::Kernel.block_given?
+ @__ended ||= false
+ @__chain << {args: args, block: block} unless @__ended
+ self
+ end
+
+ # Disables further chaining. Any future method calls will just return
+ # the existing chain without modifying it.
+ def __end
+ @__ended = true
+ self
+ end
+ end
+
+ # Receiver for chaining calls that has no methods of its own except
+ # initialize. This allows methods like :call to be chained.
+ #
+ # IMPORTANT: No methods other than #__chain, #__as, or #__end should be
+ # called on this object, because they will be chained! Instead, use ===
+ # to detect the object's type, for example.
+ class BasicReceiver < BasicObject
+ include ReceiverMethods
+
undef !=
undef ==
undef !
undef instance_exec
undef instance_eval
@@ -88,29 +119,95 @@
undef singleton_method_added
undef singleton_method_removed
undef singleton_method_undefined
end
- # Returns the call chaining receiver.
+ # Debuggable chain receiver that inherits from Object. This will break a
+ # lot of chains (e.g. any chain using #to_s or #inspect), but will allow
+ # some debugging tools to operate without crashing. See
+ # Hashformer::Generate::Chain.enable_debugging.
+ class DebuggableReceiver
+ include ReceiverMethods
+
+ # Overrides ReceiverMethods#method_missing to print out methods as they
+ # are added to the chain.
+ def method_missing(name, *args, &block)
+ __dbg_msg(name, args, block)
+ super
+ end
+
+ # Overrides ReceiverMethods#__as to print out blocks as they are added
+ # to the chain.
+ def __as(*args, &block)
+ __dbg_msg('__as', args, block)
+ super
+ end
+
+ # Overrides ReceiverMethods#__end to print a message when a chain is
+ # ended.
+ def __end
+ $stdout.puts "Ending chain #{__id__}"
+ super
+ end
+
+ private
+
+ # Prints a debugging message for the addition of the given method
+ # +name+, +args+, and +block+. Prints "Adding..." for active chains,
+ # "Ignoring..." for ended chains.
+ def __dbg_msg(name, args, block)
+ $stdout.puts "#{@__ended ? 'Ignoring' : 'Adding'} " \
+ "#{name.inspect}(#{args.map(&:inspect).join(', ')}){#{block}} " \
+ "to chain #{__id__}"
+ end
+ end
+
+ class << self
+ # The chaining receiver class that will be used by newly created chains
+ # (must include ReceiverMethods).
+ def receiver_class
+ @receiver_class ||= BasicReceiver
+ end
+
+ # Switches Receiver to an Object (DebuggableReceiver) for debugging.
+ # debugging tools to introspect Receiver without crashing.
+ def enable_debugging
+ @receiver_class = DebuggableReceiver
+ end
+
+ # Switches Receiver back to a BasicObject (BasicReceiver).
+ def disable_debugging
+ @receiver_class = BasicReceiver
+ end
+ end
+
+
+ # Returns the call chaining receiver for this chain.
attr_reader :receiver
+ # Initializes an empty chain.
def initialize
@calls = []
- @receiver = Receiver.new
+ @receiver = self.class.receiver_class.new
@receiver.__chain = self
end
# Applies the methods stored by #method_missing
def call(input_hash)
value = input_hash
@calls.each do |c|
- value = value.send(c[:name], *c[:args], &c[:block])
+ if c[:name]
+ value = value.send(c[:name], *c[:args], &c[:block])
+ else
+ # Support #__as
+ value = c[:block].call(value, *c[:args])
+ end
end
value
end
- # Adds the given call info (used by Receiver).
+ # Adds the given call info (used by ReceiverMethods).
def <<(info)
@calls << info
self
end
@@ -176,9 +273,11 @@
end
# Generates a method call chain to apply to the input hash given to a
# transformation. This allows path references (as with HF::G.path) and
# method calls to be stored and applied later.
+ #
+ # See Hashformer::Generate::Chain.enable_debugging if you run into issues.
#
# Example:
# data = { in1: { in2: [1, 2, 3, [4, 5, 6, 7]] } }
# xform = { out1: HF::G.chain[:in1][:in2][3].reduce(&:+) }
# Hashformer.transform(data, xform) # Returns { out1: 22 }