lib | ||
spec | ||
.coveralls.yml | ||
.gitignore | ||
.rspec | ||
.ruby-gemset | ||
.ruby-version | ||
.travis.yml | ||
.yardopts | ||
docile.gemspec | ||
Gemfile | ||
HISTORY.md | ||
LICENSE | ||
Rakefile | ||
README.md |
Docile
Ruby makes it possible to create very expressive Domain Specific Languages, or DSL's for short. However, it requires some deep knowledge and somewhat hairy meta-programming to get the interface just right.
"Docile" means Ready to accept control or instruction; submissive [1]
Instead of each Ruby project reinventing this wheel, let's make our Ruby DSL coding a bit more docile...
Basic Usage
Let's say that we want to make a DSL for modifying Array objects. Wouldn't it be great if we could just treat the methods of Array as a DSL?
with_array([]) do
push 1
push 2
pop
push 3
end
#=> [1, 3]
No problem, just define the method with_array
like this:
def with_array(arr=[], &block)
Docile.dsl_eval(arr, &block)
end
Easy!
Advanced Usage
Mutating (changing) an Array instance is fine, but what usually makes a good DSL is a Builder Pattern.
For example, let's say you want a DSL to specify how you want to build a Pizza:
@sauce_level = :extra
pizza do
cheese
pepperoni
sauce @sauce_level
end
#=> #<Pizza:0x00001009dc398 @cheese=true, @pepperoni=true, @bacon=false, @sauce=:extra>
And let's say we have a PizzaBuilder, which builds a Pizza like this:
Pizza = Struct.new(:cheese, :pepperoni, :bacon, :sauce)
class PizzaBuilder
def cheese(v=true); @cheese = v; end
def pepperoni(v=true); @pepperoni = v; end
def bacon(v=true); @bacon = v; end
def sauce(v=nil); @sauce = v; end
def build
Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce)
end
end
PizzaBuilder.new.cheese.pepperoni.sauce(:extra).build
#=> #<Pizza:0x00001009dc398 @cheese=true, @pepperoni=true, @bacon=false, @sauce=:extra>
Then implement your DSL like this:
def pizza(&block)
Docile.dsl_eval(PizzaBuilder.new, &block).build
end
It's just that easy!
Block parameters
Parameters can be passed to the DSL block.
Supposing you want to make some sort of cheap Sinatra knockoff:
@last_request = nil
respond '/path' do |request|
puts "Request received: #{request}"
@last_request = request
end
def ride bike
# Play with your new bike
end
respond '/new_bike' do |bike|
ride(bike)
end
You'd put together a dispatcher something like this:
require 'singleton'
class DispatchScope
def a_method_you_can_call_from_inside_the_block
:useful_huh?
end
end
class MessageDispatch
include Singleton
def initialize
@responders = {}
end
def add_responder path, &block
@responders[path] = block
end
def dispatch path, request
Docile.dsl_eval(DispatchScope.new, request, &@responders[path])
end
end
def respond path, &handler
MessageDispatch.instance.add_responder path, handler
end
def send_request path, request
MessageDispatch.instance.dispatch path, request
end
Functional-Style DSL Objects
Sometimes, you want to use an object as a DSL, but it doesn't quite fit the imperative pattern shown above.
Instead of methods like Array#push, which modifies the object at hand, it has methods like String#reverse, which returns a new object without touching the original. Perhaps it's even frozen in order to enforce immutability.
Wouldn't it be great if we could just treat these methods as a DSL as well?
s = "I'm immutable!".freeze
with_immutable_string(s) do
reverse
upcase
end
#=> "!ELBATUMMI M'I"
s
#=> "I'm immutable!"
No problem, just define the method with_immutable_string
like this:
def with_immutable_string(str="", &block)
Docile.dsl_eval_immutable(str, &block)
end
All set!
Features
- Method lookup falls back from the DSL object to the block's context
- Local variable lookup falls back from the DSL object to the block's context
- Instance variables are from the block's context only
- Nested DSL evaluation, correctly chaining method and variable handling from the inner to the outer DSL scopes
- Alternatives for both imperative and functional styles of DSL objects
Installation
$ gem install docile
Documentation
Documentation hosted on rubydoc.info: Docile Documentation
Or, read the code hosted on github.com: Docile Code
Status
Version 1.0.x works on all ruby versions since 1.8.7.
Note on Patches/Pull Requests
- Fork the project.
- Setup your development environment with:
gem install bundler; bundle install
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
- Send me a pull request. Bonus points for topic branches.
Copyright
Copyright (c) 2012-2013 Marc Siegel. See LICENSE for details.