DSL in Ruby with Metaprogramming
Tags: ruby, dsl, metaprogramming
Published: 2024-07-04 17:14:46

Metaprogramming in Ruby allows developers to write programs that can modify themselves at runtime. This powerful feature can be leveraged to create expressive and flexible domain-specific languages (DSLs). 

#### Defining the Structure

First, let's define the core class of our DSL, `TravelSearch`, along with auxiliary classes `Air`, `Hotel`, and `Train`. These classes will encapsulate the details of each travel component.

```ruby
class TravelSearch
  attr_accessor :air, :hotel, :train

  def initialize(&block)
    instance_eval(&block)
  end

  def air(&block)
    @air = Air.new(&block)
  end

  def hotel(&block)
    @hotel = Hotel.new(&block)
  end

  def train(&block)
    @train = Train.new(&block)
  end

  def to_s
    "Air: #{@air}\nHotel: #{@hotel}\nTrain: #{@train}"
  end
end

class Air
  attr_accessor :from, :to, :date

  def initialize(&block)
    instance_eval(&block)
  end

  def from(from)
    @from = from
  end

  def to(to)
    @to = to
  end

  def date(date)
    @date = date
  end

  def to_s
    "from #{@from} to #{@to} on #{@date}"
  end
end

class Hotel
  attr_accessor :city, :date, :nights

  def initialize(&block)
    instance_eval(&block)
  end

  def city(city)
    @city = city
  end

  def date(date)
    @date = date
  end

  def nights(nights)
    @nights = nights
  end

  def to_s
    "in #{@city} on #{@date} for #{@nights} nights"
  end
end

class Train
  attr_accessor :from, :to, :via, :date, :with_seat_reservation

  def initialize(&block)
    instance_eval(&block)
  end

  def from(from)
    @from = from
  end

  def to(to)
    @to = to
  end

  def via(via)
    @via = via
  end

  def date(date)
    @date = date
  end

  def with_seat_reservation(with_seat_reservation)
    @with_seat_reservation = with_seat_reservation
  end

  def to_s
    "from #{@from} to #{@to} via #{@via} on #{@date} with seat reservation: #{@with_seat_reservation}"
  end
end
```

### Utilizing `instance_eval` for DSL Construction

The `instance_eval` method is a key component in our DSL. It allows us to evaluate the given block within the context of the current object, effectively changing the `self` to the object the method is called on. This is crucial for making the DSL syntax intuitive and clean.

### Creating the DSL Entry Point

We'll define a method `search_travel` as the entry point for our DSL. This method initializes a `TravelSearch` object and evaluates the block within its context.

```ruby
def search_travel(&block)
  travel_search = TravelSearch.new(&block)
  puts travel_search
end
```

### Example Usage

Here's an example of how our DSL can be used to define a travel search:

```ruby
search_travel do
  air do
    from "SFO"
    to "JFK"
    date "2011-12-25"
  end

  hotel do
    city "New York"
    date "2011-12-25"
    nights 3
  end

  train do
    from "Milan"
    to "Rome"
    via "Florence"
    date "2011-12-25"
    with_seat_reservation true
  end
end
```

### Output

Running the above code will produce the following output:

```
Air: from SFO to JFK on 2011-12-25
Hotel: in New York on 2011-12-25 for 3 nights
Train: from Milan to Rome via Florence on 2011-12-25 with seat reservation: true
```


Using `instance_eval` and metaprogramming, we created a flexible and readable DSL for travel searches in Ruby. This approach allows users to define complex data structures with minimal syntax, making the code more expressive and easier to understand.