Class: DslProxy
- Inherits:
- BasicObject
- Defined in:
- lib/iron/extensions/dsl_proxy.rb
Overview
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 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
def knob; @controls << :knob; end
def ; @controls << :button; end
def switch; @controls << :switch; end
def self.define(&block)
@builder = self.new
DslProxy.exec(@builder, &block)
# Do something here with the builder's list of controls
@builder.control_list
end
end
@knob_count = 5
new_list = ControlBuilder.define do
switch
@knob_count.times { knob }
end
Notice the lack of explicit builder receiver to the calls to #switch, #knob and #button. 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? ...
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.
In addition, simply using a yield statment loses the instance variables set in the calling context. This is a major pain in eg Rails views, where most of the interesting data resides in instance variables. You can get around this when #yield-ing by explicitly creating a local variable to be picked up by the closure created in the block, but it kind of sucks.
In summary, DslProxy allows you to keep all the local and instance variable context from your block declarations, while proxying all method calls to a given receiver. If you’re not building DSLs, this class is not for you, but if you are, I hope it helps!
Class Method Summary collapse
-
.const_missing(name) ⇒ Object
Proxies searching for constants to the context, so that eg Kernel::foo can actually find Kernel - BasicObject does not partake in the global scope!.
-
.exec(receiver, *to_yield, &block) ⇒ Object
Pass in a builder-style class, or other receiver you want set as “self” within the block, and off you go.
Instance Method Summary collapse
-
#_pop_receiver ⇒ Object
Remove the currently active receiver, restore old receiver if nested.
-
#_proxy(receiver, *to_yield, &block) ⇒ Object
:yields: receiver.
-
#_push_receiver(receiver) ⇒ Object
Set the currently active receiver.
-
#_to_dsl_proxy ⇒ Object
For nesting multiple proxies.
-
#initialize(context) ⇒ DslProxy
constructor
Simple state setup.
-
#method_missing(method, *args, &block) ⇒ Object
Proxies all calls to our receiver, or to the block’s context if the receiver doesn’t respond_to? it.
-
#respond_to?(method, include_private = false) ⇒ Boolean
Let anyone who’s interested know what our proxied objects will accept.
Constructor Details
#initialize(context) ⇒ DslProxy
Simple state setup
85 86 87 88 89 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 85 def initialize(context) @_receivers = [] @_instance_original_values = {} @_context = context end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method, *args, &block) ⇒ Object
Proxies all calls to our receiver, or to the block’s context if the receiver doesn’t respond_to? it.
152 153 154 155 156 157 158 159 160 161 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 152 def method_missing(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 #$stderr.puts "Proxy [#{method}] to context" @_context.__send__(method, *args, &block) end end |
Class Method Details
.const_missing(name) ⇒ Object
Proxies searching for constants to the context, so that eg Kernel::foo can actually find Kernel - BasicObject does not partake in the global scope!
171 172 173 174 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 171 def self.const_missing(name) #$stderr.puts "Constant missing: #{name} - proxy to context" @_context.class.const_get(name) end |
.exec(receiver, *to_yield, &block) ⇒ Object
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 respond_to? a method, any missing methods will be proxied to the enclosing context.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 67 def self.exec(receiver, *to_yield, &block) # :yields: receiver # 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, *to_yield, &block) end |
Instance Method Details
#_pop_receiver ⇒ Object
Remove the currently active receiver, restore old receiver if nested
146 147 148 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 146 def _pop_receiver @_receivers.pop end |
#_proxy(receiver, *to_yield, &block) ⇒ Object
:yields: receiver
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 91 def _proxy(receiver, *to_yield, &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_eval "#{var} = value" end end end # Save the dsl target as our receiver for proxying _push_receiver(receiver) # Run the block with ourselves as the new "self", passing the given yieldable(s) or # the receiver in case the code wants to disambiguate for some reason to_yield = [receiver] if to_yield.empty? to_yield = to_yield.first(block.arity) result = instance_exec(*to_yield, &block) # 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}") if @_instance_original_values[var] != value @_context.instance_variable_set(var.to_s, value) end end end end return result end |
#_push_receiver(receiver) ⇒ Object
Set the currently active receiver
141 142 143 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 141 def _push_receiver(receiver) @_receivers.push receiver end |
#_to_dsl_proxy ⇒ Object
For nesting multiple proxies
136 137 138 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 136 def _to_dsl_proxy self end |
#respond_to?(method, include_private = false) ⇒ Boolean
Let anyone who’s interested know what our proxied objects will accept
164 165 166 167 |
# File 'lib/iron/extensions/dsl_proxy.rb', line 164 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 |