views:

101

answers:

2

I'm using Blockenspiel to create a DSL with Ruby. It works great and solves a lot of my problems, but I encountered the following problem that is not strictly related to Blockenspiel.

Suppose I have a DSL that looks like this:

dish do
  name = 'Pizza'
  ingredients = ...
  nutrition_facts = ...
end

dish do
  name = 'Doner'
  ingredients = ...
  nutrition_facts = ...
end

Now I have a menu compiler which takes the dishes and compiles them into a menu. The compiler should now be able to compile multiple menu files, so it has setup and clear a global context. This should preferably happen in parallel.

I found out that sinatra uses class variables, but this has the consequence that it can only do sequential processing and that you have to clear the class variables when you want to compile a new menu. An alternative would be to use global variables.

I would prefer to evaluate the DSL methods within the scope of an object, so that there's no global context and I could compile the menus in parallel, but the last time I tried this, I encountered some problems when declaring (helper-)methods in the menu file.

Which methods are possible? What is the recommended way to do this?

+1  A: 

There are basically two ways to archieve what you want.

Option a: You yield an object with setter-methods:

Dish = Struct.new(:name, :ingredients, :nutrition_facts)
def dish
  d = Dish.new
  yield d
  d
end

dish do |d|
  d.name = 'Pizza'
  d.ingredients = ...
  d.nutrition_facts = ...
end

Option b: you use instance variables and instance_eval

class Dish
  attr_accessor :name, :ingredients, :nutrition_facts
end
def dish(&blk)
  d = Dish.new
  d.instance_eval(&blk)
  d
end

dish do
  @name = 'Doner'
  @ingredients = ...
  @nutrition_facts = ...
end

In both cases the dish method will return an instance of Dish on which you can call e.g. name to access the name set in the block (and multiple calls to dish will return independent objects). Note that with instance_eval the user will also be able to call private methods of the Dish class in the block and that misspelling variable names will not cause an error.

sepp2k
If you replace `@foo =` with `self.foo =`, you don't have to worry about misspellings.
jleedev
@jleedev: Good point.
sepp2k
But I still need a global menu, which I wanted to avoid.
ott
You could create a `menu(name='default') do ... end` component to the DSL. The `dish` method would only be available within a `Menu`. What problems, exactly, did you have with helper methods in the menu file?
James A. Rosen
I want the menu name to be taken from the menu file that I compile. I want to compile all menus in parallel without global state or context. A context should only be local to a menu file.
ott
+1  A: 

What a number of libraries I've seen do is take advantage of instance_eval for this sort of thing.

As long as performance isn't a huge issue, you can do stuff like:

class Menu
  def initialize file
    instance_eval File.read(file),file,1
  end

  def dish &block
    Dish.new &block
  end
  #....
end

class Dish
  def name(n=nil)
    @name = n if n
    @name
  end
  def ingredients(igrd=nil)
    @ingredients= igrd if igrd
    @ingredients
  end
end
#....

Menu.new 'menus/pizza_joint'

menus/pizza_joint

dish do
  name 'Cheese Pizza'
  ingredients ['Cheese','Dough','Sauce']
end

There are actually DSL libraries that add accessors like #name and #ingredients so you don't have to build them by hand. eg dslify

BaroqueBobcat
Can I also declare new functions and use them within a menu while it is evaluated?
ott
Yes. `instance_eval`, acting like a block lets you define classes modules and methods inside it.
BaroqueBobcat