lib/iron/extensions/dsl_proxy.rb in iron-extensions-1.1.0 vs lib/iron/extensions/dsl_proxy.rb in iron-extensions-1.1.1
- old
+ new
@@ -1,9 +1,9 @@
# Specialty helper class for building elegant DSLs (domain-specific languages)
# The purpose of the class is to allow seamless DSL's by allowing execution
# of blocks with the instance variables of the calling context preserved, but
-# all method calls be proxied to a given receiver. This sounds pretty abstract,
+# all method calls proxied to a given receiver. This sounds pretty abstract,
# so here's an example:
#
# class ControlBuilder
# def initialize; @controls = []; end
# def control_list; @controls; end
@@ -24,23 +24,23 @@
# @knob_count.times { knob }
# button
# end
#
# Notice the lack of explicit builder receiver to the calls to #switch, #knob and #button.
-# Those calls are automatically proxied to the @builder we passed to the DslProxy.
+# Those calls are automatically proxied to the receiver we passed to the DslProxy.
#
# In quick and dirty DSLs, like Rails' migrations, you end up with a lot of
# pointless receiver declarations for each method call, like so:
#
-# def change
-# create_table do |t|
-# t.integer :counter
-# t.text :title
-# t.text :desc
-# # ... tired of typing "t." yet? ...
+# def change
+# create_table do |t|
+# t.integer :counter
+# t.text :title
+# t.text :desc
+# # ... tired of typing "t." yet? ...
+# end
# end
-# end
#
# This is not a big deal if you're using a simple DSL, but when you have multiple nested
# builders going on at once, it is ugly, pointless, and can cause bugs when
# the throwaway arg names you choose (eg 't' above) overlap in scope.
#
@@ -58,55 +58,119 @@
# Pass in a builder-style class, or other receiver you want set as "self" within the
# block, and off you go. The passed block will be executed with all
# block-context local and instance variables available, but with all
# method calls sent to the receiver you pass in. The block's result will
- # be returned. If the receiver doesn't
+ # be returned.
+ #
+ # If the receiver doesn't respond_to? a method, any missing methods
+ # will be proxied to the enclosing context.
def self.exec(receiver, &block) # :yields: receiver
- proxy = DslProxy.new(receiver, &block)
- return proxy._result
+ # Find the context within which the block was defined
+ context = ::Kernel.eval('self', block.binding)
+
+ # Create or re-use our proxy object
+ if context.respond_to?(:_to_dsl_proxy)
+ # If we're nested, we don't want/need a new dsl proxy, just re-use the existing one
+ proxy = context._to_dsl_proxy
+ else
+ # Not nested, create a new proxy for our use
+ proxy = DslProxy.new(context)
+ end
+
+ # Exec the block and return the result
+ proxy._proxy(receiver, &block)
end
- # Create a new proxy and execute the passed block
- def initialize(builder, &block) # :yields: receiver
+ # Simple state setup
+ def initialize(context)
+ @_receivers = []
+ @_instance_original_values = {}
+ @_context = context
+ end
+
+ def _proxy(receiver, &block) # :yields: receiver
+ # Sanity!
+ raise 'Cannot proxy with a DslProxy as receiver!' if receiver.respond_to?(:_to_dsl_proxy)
+
+ if @_receivers.empty?
+ # On first proxy call, run each context instance variable,
+ # and set it to ourselves so we can proxy it
+ @_context.instance_variables.each do |var|
+ unless var.starts_with?('@_')
+ value = @_context.instance_variable_get(var.to_s)
+ @_instance_original_values[var] = value
+ #instance_variable_set(var, value)
+ instance_eval "#{var} = value"
+ end
+ end
+ end
+
# Save the dsl target as our receiver for proxying
- @_receiver = builder
+ _push_receiver(receiver)
- # Find the context within which the block was defined
- @_context = ::Kernel.eval('self', block.binding)
- # Run each instance variable, and set it to ourselves so we can proxy it
- @_context.instance_variables.each do |var|
- value = @_context.instance_variable_get(var.to_s)
- instance_eval "#{var} = value"
- end
-
# Run the block with ourselves as the new "self", passing the receiver in case
# the code wants to disambiguate for some reason
- @_result = instance_exec(@_receiver, &block)
+ result = instance_exec(@_receivers.last, &block)
- # Run each instance variable, and set it to ourselves so we can proxy it
- @_context.instance_variables.each do |var|
- @_context.instance_variable_set(var.to_s, instance_eval("#{var}"))
+ # Pop the last receiver off the stack
+ _pop_receiver
+
+ if @_receivers.empty?
+ # Run each local instance variable and re-set it back to the context if it has changed during execution
+ #instance_variables.each do |var|
+ @_context.instance_variables.each do |var|
+ unless var.starts_with?('@_')
+ value = instance_eval("#{var}")
+ #value = instance_variable_get("#{var}")
+ if @_instance_original_values[var] != value
+ @_context.instance_variable_set(var.to_s, value)
+ end
+ end
+ end
end
+
+ return result
end
- # Returns value of the exec'd block
- def _result
- @_result
+ # For nesting multiple proxies
+ def _to_dsl_proxy
+ self
end
+
+ # Set the currently active receiver
+ def _push_receiver(receiver)
+ @_receivers.push receiver
+ end
+
+ # Remove the currently active receiver, restore old receiver if nested
+ def _pop_receiver
+ @_receivers.pop
+ end
# Proxies all calls to our receiver, or to the block's context
# if the receiver doesn't respond_to? it.
def method_missing(method, *args, &block)
- if @_receiver.respond_to?(method)
- @_receiver.send(method, *args, &block)
+ #$stderr.puts "Method missing: #{method}"
+ if @_receivers.last.respond_to?(method)
+ #$stderr.puts "Proxy [#{method}] to receiver"
+ @_receivers.last.__send__(method, *args, &block)
else
- @_context.send(method, *args, &block)
+ #$stderr.puts "Proxy [#{method}] to context"
+ @_context.__send__(method, *args, &block)
end
end
- # Proxies searching for constants to the context
+ # Let anyone who's interested know what our proxied objects will accept
+ def respond_to?(method, include_private = false)
+ return true if method == :_to_dsl_proxy
+ @_receivers.last.respond_to?(method, include_private) || @_context.respond_to?(method, include_private)
+ end
+
+ # Proxies searching for constants to the context, so that eg Kernel::foo can actually
+ # find Kernel - BasicObject does not partake in the global scope!
def self.const_missing(name)
+ #$stderr.puts "Constant missing: #{name} - proxy to context"
@_context.class.const_get(name)
end
end
\ No newline at end of file