Skip to content
ches edited this page Sep 15, 2012 · 6 revisions

gorillib/builder -- construct elegant ruby Domain-Specific Languages (DSLs).

Gorillib::Builder provides foundation models for elegant ruby DSLs (Domain-Specific Languages). A builder block's relaxed ruby syntax enables highly-readable specification of complex behavior:

```ruby
garage do
  car :ford_tudor do
    manufacturer    'Ford Motor Company'
    car_model       'Tudor'
    year            1939
    doors           2
    style           :sedan
    engine do
      volume  350
      cylinders     8
    end
  end

  car :wildcat do
    manufacturer    'Buick'
    car_model       'Wildcat'
    year            1968
    doors           2
    style           :convertible
    engine do
      volume  455
      cylinders     8
    end
  end
end
```

Defining a builder

To make a class be a builder, simply include Gorillib::Builder:

  ```ruby
  class Garage
    include Gorillib::Builder
    collection :car
  end

  class Car
    include Gorillib::Builder
    field      :name,         String
    field      :manufacturer, String
    field      :car_model,    String
    field      :year,         Integer
    field      :doors,        Integer
    field      :style,        Symbol, :validates => { :inclusion_in => [:sedan, :coupe, :convertible] }
    member     :engine
    belongs_to :garage
  end

  class Engine
    include Gorillib::Builder
    field      :volume, Integer
    field      :cylinders,    Integer
    belongs_to :car
  end
  ```

getset accessors

Fields of a Builder class create a single "getset" accessor (and not the familiar 'foo/foo=' pair):

  • car_model.foo -- returns value of foo
  • car_model.foo(val) -- sets foo to val, which can be any value, even nil.

member fields

A builder class can have member fields; the type of a member field should be a builder class itself

With no arguments, the accessor returns the value of engine attribute, creating if necessary:

  ```ruby
  car.engine                    #=> #<Engine volume=~ cylinders=~>
  ```

If you pass in a hash of values, the engine is created if necessary and then asked to receive! the hash.

  ```ruby
  car.engine                    #=> #<Engine volume=~ cylinders=~>
  car.engine(:cylinders => 8)   #=> #<Engine volume=~ cylinders=8>
  car.engine.cylinders          #=> 8
  ```

If you provide a no-args block, it is instance_evaled in the context of the member object:

  ```ruby
  car :ford_tudor do
    engine(:cylinders => 8) do
      self                      #=> #<Engine volume=~ cylinders=8>
      volume  455
      cylinders     self.cylinders - 2
      self                      #=> #<Engine volume=455 cylinders=6>
    end
    engine                      #=> #<Engine volume=455 cylinders=6>
  end
  ```

Some people disapprove of instance_eval, as they consider it unseemly to mess around with self. If you instead provide a one-arg block, the member object is passed in:

  ```ruby
  car :ford_tudor do |c|
    c.engine(:cylinders => 8) do |eng|
      self                      #=> #<Car ...>
      eng.volume 455
      eng.cylinders    eng.cylinders - 2
      eng                       #=> #<Engine volume=455 cylinders=6>
    end
    c.engine                    #=> #<Engine volume=455 cylinders=6>
  end
  ```

collections

A builder class can also have collection fields, to contain named builder objects.

  ```ruby
  garage do
    car(:ford_tudor)            #=> #<Car name="ford tudor" ...>
    cars                        #=> { :ford_tudor => #<Car ...>, :wildcat => #<Car ...> }
  end
  ```

The collected items must respond to id (FIXME:). The singular accessor accepts arguments just like a member accessor:

  ```ruby
  garage do
    car(:ford_tudor, :year => 1939)
    #=> #<Car name=`<:ford_tudor year=1939 doors=~ ...>
    car(:ford_tudor, :year => 1939) do
      doors           2
      style           :convertible
    end
    #=> #<Car name="ford tudor" year=1939 doors=2 ...>
    car(:wildcat, :year => 1968) do |c|
      c.doors         2
      c.style         :convertible
    end
    #=> #<Car name= year=1939 doors=2 ...>
  end
  ```

Model methods

builders have the following:

  • Model::Defaults
  • Model::Naming
  • Model::Conversion

SubclassRegistry

When a subclass happens, decorates class with saucepot(...), equivalent to

    update_or_create
    registers
  • At class level, registry_for(CookingUtensil) gives
    • add_cooking_utensil() and so forth for adding and getting
    • Protected cooking_utensils hash class attribute
  • Enlisting a resource class (Kitchen.register(FryingPan) and gives you a magic frying_pan factory method with signature frying_pan(name, *args, &block)
    • If resource does not exist, calls FryingPan.new with full set of params and block. Add it to cooking_utensils registry.
    • If resource with that name exists, retrieve it. call merge() with the parameters, and run_in_scope with the block.