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 }