|
require 'set' |
|
|
|
module Util |
|
def self.instance_eval_or_call(object, &block) |
|
# Here we assume that if the block takes one or more arguments, |
|
# you want to execute the block in its own scope, yielding the |
|
# DSL constructor as an object. In this case the DSL would function |
|
# like this: |
|
# |
|
# Zoo.setup do |zoo| |
|
# zoo.pig likes: "slop" |
|
# end |
|
if block.arity > 0 |
|
block.call(object) |
|
|
|
# But that's boring. The real trick is this: we want to use this DSL |
|
# without yielding an argument, like this: |
|
# |
|
# Zoo.setup do |
|
# pig likes: "slop" |
|
# end |
|
# |
|
# `instance_eval` is the typical way to accomplish that, eval'ing the |
|
# block so that `pig` and `cow` are local methods. But doing that, you |
|
# lose the surrounding context. If this were in a Rails controller, |
|
# you couldn't use the `params` hash or any of your instance variables |
|
# because the `Zoo` instance doesn't know about them. |
|
# |
|
# Ruby has no real concept of closures, or anything like JavaScript's |
|
# `Function#apply()` function. So here Mat has created a proxy/delegate |
|
# class, `ContextBoundDelegate`. Inside your block it looks as if you |
|
# have access to both the DSL object _and_ its surrounding scope, but this |
|
# is an illusion. You're actually calling methods on `ContextBoundDelegate`, |
|
# which in turn forwards them to either the DSL object (if it responds to |
|
# a particular message) or the surrounding scope (if it's not). I will try |
|
# to explain how this works below. |
|
else |
|
ContextBoundDelegate.instance_eval_with_context(object, &block) |
|
end |
|
end |
|
|
|
class ContextBoundDelegate |
|
class <<self |
|
# Designated initializer for `ContextBoundDelegate` -- in fact, the only |
|
# public initializer, as the `#new` method has been made private below. |
|
# It's possible you could do this same job inside of `#initialize`, but |
|
# I like that this method is very, _very_ clear about the fact that it's |
|
# eval'ing something. You're not supposed to know about `ContextBoundDelegate`; |
|
# it's an implementation detail of your _real_ API. |
|
def instance_eval_with_context(receiver, &block) |
|
# Extract the block's context by eval'ing `self`, so we can store it in a variable. |
|
calling_context = eval('self', block.binding) |
|
|
|
# It's possible that one ContextBoundDelegate may be nested inside another. |
|
# In fact, Sunspot uses this feature in a few places such as boolean queries. |
|
# If the calling context has a calling context of its own (i.e., if it's another |
|
# ContextBoundDelegate) then we need to forward messages to _that_ object instead. |
|
# Consequently, syntax like this will Just Work: |
|
# |
|
# Zoo.setup do |
|
# monkey_house do |
|
# chimp eats: params[:chimp_food] |
|
# end |
|
# end |
|
# |
|
# In this example, `Zoo#monkey_house` is a DSL instance method that |
|
# opens a new ContextBoundDelegate, nested inside the first one. In order |
|
# to access the `params` object we need to still forward that message |
|
# to the original surrounding context. So we do. |
|
if parent_calling_context = calling_context.instance_eval{@__calling_context__} |
|
calling_context = parent_calling_context |
|
end |
|
|
|
# One reason why you never initialize this object directly is that it's |
|
# intended use is as a proxy. Here we construct a new ContextBoundDelegate, |
|
# then immediately `instance_eval` the block using _the delegate_ as context. |
|
# It is now aware of both our DSL and the controller around it, and can |
|
# serve as a kind of internal message bus, ensuring things are called on the |
|
# correct object. |
|
new(receiver, calling_context).instance_eval(&block) |
|
end |
|
private :new |
|
end |
|
|
|
BASIC_METHODS = Set[:==, :equal?, :"!", :"!=", :instance_eval, |
|
:object_id, :__send__, :__id__] |
|
|
|
# Like all Ruby objects, ContextBoundDelegate inherits from Object, |
|
# and so carries a lot of baggage in the form of standard instance |
|
# methods. But our intent is for ContextBoundDelegate to be more or |
|
# less invisible, to behave as if it _is_ one of our two contexts. |
|
# So here we're un-defining (i.e. deleting) *all* instance methods |
|
# except for a few specific ones we need, listed in BASIC_METHODS |
|
# above. Those are limited to methods responsible for telling whether |
|
# any two delegate proxies are the same, plus `instance_eval`. |
|
instance_methods.each do |method| |
|
unless BASIC_METHODS.include?(method.to_sym) |
|
undef_method(method) |
|
end |
|
end |
|
|
|
def initialize(receiver, calling_context) |
|
@__receiver__, @__calling_context__ = receiver, calling_context |
|
end |
|
|
|
# In the case of #id, we want to short-circuit the normal proxying |
|
# behavior (which favors our DSL receiver over the calling context) |
|
# and send any #id messages directly to the caller. I didn't understand |
|
# why at first, but then I did: it's for ActiveRecord and other ORMs |
|
# where you might call `self.id` or just `id` and expect it to return |
|
# a record/object identifier. Consequently, one constraint on your DSL |
|
# syntax is that it can't use the `#id` method for anything. |
|
def id |
|
@__calling_context__.__send__(:id) |
|
end |
|
|
|
# Special case due to `Kernel#sub`'s existence. Kernel methods (such as |
|
# `rand()`) are forwarded directly to Kernel, bypassing `method_missing` |
|
# and skipping this object completely unless it implements this method |
|
# itself. Here we're just forcing #sub to go through our cascading proxy |
|
# like any other method, just in case either context cares to implement it. |
|
def sub(*args, &block) |
|
__proxy_method__(:sub, *args, &block) |
|
end |
|
|
|
# Receives any methods that aren't explicitly implemented by this proxy |
|
# thingy, and forwards them to `__proxy_method__`. Why is `__proxy_method__` |
|
# separate? So it can be reused for special cases like `#sub` above. |
|
def method_missing(method, *args, &block) |
|
__proxy_method__(method, *args, &block) |
|
end |
|
|
|
# Where the magic happens. This method is actually really simple: _every_ |
|
# method or variable you call inside your block is run through here. The |
|
# proxy attempts to call it on the receiver (i.e. the DSL). If it's not |
|
# implemented there, it tries the calling context. If _that_ doesn't work, |
|
# NoMethodError is raised. It actually sends the message to the objects |
|
# (rather than check for them using `respond_to?()`) because you may want |
|
# to implement your DSL syntax using `method_missing`, or through some other |
|
# metaprogramming trick that might short-circuit Ruby's ways of detecting |
|
# the presence of a method. Hence the cascading rescue statements. With the |
|
# exception of #id (handled as a special case above), the receiver always |
|
# has precedence over the caller. |
|
def __proxy_method__(method, *args, &block) |
|
begin |
|
@__receiver__.__send__(method.to_sym, *args, &block) |
|
rescue ::NoMethodError => e |
|
begin |
|
@__calling_context__.__send__(method.to_sym, *args, &block) |
|
rescue ::NoMethodError |
|
raise(e) |
|
end |
|
end |
|
end |
|
end |
|
end |
|
|
|
# This is a DSL for describing a zoo, with animals and buildings. |
|
class Zoo |
|
# Constructor for our zoo. It takes a block, which is passed to |
|
# `instance_eval_or_call`, which does all the magical things discussed |
|
# above. Returns our constructed Zoo object. |
|
def self.setup(&block) |
|
Zoo.new.tap do |zoo| |
|
Util.instance_eval_or_call(zoo, &block) |
|
end |
|
end |
|
|
|
def initialize |
|
@data = {zoo:{}} |
|
# The __current_target__ variable is used to track where we are in |
|
# the zoo. If we enter a building, like the monkey house or a barn, |
|
# that will need to be represented here until we come out of it. |
|
@__current_target__ = @data[:zoo] |
|
end |
|
|
|
# You describe animals using these declarative methods: |
|
# |
|
# pig goes: "oink", eats: "slop" |
|
# |
|
def pig(thing) |
|
add_thing_to_animal(:pig, thing) |
|
end |
|
def cow(thing) |
|
add_thing_to_animal(:cow, thing) |
|
end |
|
def chimp(thing) |
|
add_thing_to_animal(:chimp, thing) |
|
end |
|
def elephant(thing) |
|
add_thing_to_animal(:elephant, thing) |
|
end |
|
|
|
# To enter a building, you use a handy block syntax: |
|
# |
|
# monkey_house do |
|
# orangutan is:"orange" |
|
# end |
|
# |
|
# Buildings can be nested inside one another: |
|
# |
|
# african_pavilion do |
|
# elephant size:"extra-large" |
|
# reptile_house do |
|
# snake is:"DO NOT TALK TO ME ABOUT SNAKES" |
|
# end |
|
# end |
|
# |
|
def monkey_house(&block) |
|
add_building(:monkey_house, &block) |
|
end |
|
def barn(&block) |
|
add_building(:barn, &block) |
|
end |
|
def african_pavilion(&block) |
|
add_building(:african_pavilion, &block) |
|
end |
|
def reptile_house(&block) |
|
add_building(:reptile_house, &block) |
|
end |
|
|
|
private |
|
|
|
def add_thing_to_animal(animal_name, thing) |
|
(@__current_target__[animal_name] ||= []) << thing |
|
end |
|
|
|
def add_building(building_name, &block) |
|
enter_building(building_name) |
|
Util.instance_eval_or_call(self, &block) |
|
exit_building |
|
end |
|
|
|
def enter_building(building_name) |
|
@__previous_target__ = @__current_target__ |
|
@__current_target__[building_name] ||= {} |
|
@__current_target__ = @__current_target__[building_name] |
|
end |
|
|
|
def exit_building |
|
raise "You're not currently in a building" if @__previous_target__.nil? |
|
@__current_target__ = @__previous_target__ |
|
end |
|
|
|
end |
|
|
|
what_the_pig_says = "oink" |
|
|
|
the_zoo = Zoo.setup do |
|
pig says: what_the_pig_says # Variable from outside the block |
|
cow says:"moo", provides: "milk" |
|
monkey_house do |
|
chimp eats: "bananas" |
|
end |
|
african_pavilion do |
|
reptile_house do |
|
cow "Why is a cow in the reptile house???" |
|
end |
|
end |
|
end |
|
|
|
require 'pp' |
|
pp the_zoo |
Putting "nicer" in scare quotes implies that you think I'm foolish and think my desire for beautiful code is more important than your time. I would say it's less about beauty than providing a syntax that behaves in exactly one way, that better represents common use cases.
The simpler form of Tire's DSL is cleaner and demos well, but can't be used in models to store index data from model attributes, or in controllers to query that data based on params or other values. Prawn's syntax doesn't do this crazy context trick, but then again its README doesn't demonstrate both variations of the syntax, and it does bug me that its developer doesn't use the more practical
Prawn::Document.new { |pdf| ... }
form in his README.You seem upset that I expected Tire to behave a certain way and phrased my stupid gist example of another library's DSL, which I prefer, as a complaint about that. You didn't tell me why your way is better, you told me I'm wrong. Is that your whole point?
I apologize for calling your DSL ugly.