# Blocks and DSLs — Ruby

Source: https://www.geekswithgeeks.com/en/ruby/x-dsl

> Understand how Ruby DSLs such as RSpec, Rake and Rails routes work.

## Configuration that reads like language

Ruby's flexible syntax (optional parentheses, blocks, keyword arguments, symbols) makes it ideal for **internal domain-specific languages (DSLs)**. Familiar examples: **RSpec** (`describe ... it ... expect(x).to eq(y)`), **Rake** tasks, **Rails routes** (`resources :orders`), **Bundler**'s Gemfile, Sinatra routes and many configuration files. Most DSLs combine **blocks** with **`instance_eval`** or `instance_exec`, which run a block with `self` set to a builder object, so bare method calls inside the block go to the builder. Alternatively, a DSL can **yield** a configuration object (`configure do |config| config.timeout = 5 end`), which keeps `self` unchanged and is easier to debug. Good DSLs are small, documented and validate their input; they make configuration and specifications readable for domain experts. The trade-off is indirection: when something goes wrong inside `instance_eval`, error messages and method lookup can be confusing, so prefer the yielding style unless the extra readability is worth it.

## A tiny pricing DSL

instance_eval lets rule definitions read like a specification.

```ruby
class PricingRules
  Rule = Data.define(:name, :condition, :discount)

  def self.define(&block)
    rules = new
    rules.instance_eval(&block)               # bare calls inside the block go to rules
    rules
  end

  def initialize = @rules = []

  def discount(name, percent:, condition:)
    @rules << Rule.new(name:, condition:, discount: percent)
  end

  def apply(order)
    rule = @rules.find { |r| r.condition.call(order) }
    rule ? order[:total] * (100 - rule.discount) / 100.0 : order[:total]
  end
end

rules = PricingRules.define do
  discount :festive, percent: 10, condition: ->(o) { o[:total] >= 2000 }
  discount :student, percent: 5,  condition: ->(o) { o[:student] }
end

puts rules.apply({ total: 2500 })                  # 2250.0
puts rules.apply({ total: 1000, student: true })   # 950.0

# a yielding style is often easier to debug:
# MyApp.configure { |c| c.timeout = 5 }
```

## Avoid reserved words as keyword names

Ruby lets you declare a keyword argument named `when:` or `if:`, but you cannot then read it as a local variable without `binding.local_variable_get`. Choose plain names such as `condition:` for DSL options.

**Quiz:** How do many Ruby DSLs make bare method calls inside a block reach a builder object?

- [ ] By using global variables
- [ ] By compiling the block to C
- [x] By running the block with instance_eval or instance_exec so self is the builder
- [ ] By using threads

*Answer:* By running the block with instance_eval or instance_exec so self is the builder. instance_eval changes self for the block, routing implicit calls to the builder.
