Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
92: Building a Simple DSL in Ruby
You've probably used a DSL—a Domain Specific Language—without even thinking about it. Every time you write a Rails route or a RSpec test, you're using a Ruby-based DSL. The goal is to make the code read more like a description of what to do, rather than a set of instructions on how to do it. I've always found that the best way to understand them is to try and break the language until it does what you want.
Trying to make Ruby look like a config file
Let's say we want to build a simple tool for defining email templates. In a perfect world, I'd love to write something that looks like this:
email "Welcome" do
from "noreply@startup.com"
to "customer@gmail.com"
body "Thanks for joining!"
end
If I try to run that right now, Ruby is going to scream at me. I'll get a NoMethodError for email. Obviously, these methods don't exist. I could just define global methods, but that's a recipe for a namespace collision nightmare. I want these methods to be grouped together, specifically for the context of an email.
Wrapping the state in a class
My first instinct is to create a class to hold the data. I'll create an EmailBuilder class with attributes for the from, to, and body fields. I'll add some setter methods that feel "DSL-ish" by removing the equals sign.
class EmailBuilder
attr_reader :from, :to, :body
def from(value)
@from = value
end
def to(value)
@to = value
end
def body(value)
@body = value
end
end
Now, I can do this: builder = EmailBuilder.new; builder.from("me@test.com"). But that's not a DSL; that's just calling methods on an object. I want that clean block syntax. I'll try to create a top-level email method that instantiates the builder and takes a block.
def email(name, &block)
builder = EmailBuilder.new
block.call(builder)
puts "Email #{name} configured: #{builder.from} -> #{builder.to}"
end
If I run my original example now, it still fails. Why? Because inside the block, Ruby doesn't know that from and to belong to the builder object. I'd have to write builder.from "..." inside the block. It's better, but it's still not the "clean" look we're after.
The magic of instance_eval
This is where things get interesting. I want the code inside the block to be executed as if it were inside the EmailBuilder instance. In Ruby, we have a tool for exactly this: instance_eval.
Let's tweak the email method. Instead of passing the builder to the block, I'll tell the builder to evaluate the block within its own context.
def email(name, &block)
builder = EmailBuilder.new
builder.instance_eval(&block)
puts "Email #{name} configured: #{builder.from} -> #{builder.to}"
end
Now, when I run the original example, it works perfectly. When Ruby enters that block, self is no longer the main object; self is the EmailBuilder instance. So when it sees from "...", it looks for a method named from on the builder. This is the core secret of most Ruby DSLs.
Polishing the interface
One thing that bothers me is that my email method is just a global function. In a real project, I'd wrap this in a module or a class to keep things tidy. Also, I want to be able to actually do something with the resulting object rather than just printing a string.
I'll move the logic into a MailGenerator class and add a render method to the builder so it can output a final string.
class EmailBuilder
attr_reader :from, :to, :body
def from(val); @from = val; end
def to(val); @to = val; end
def body(val); @body = val; end
def render
"From: #{@from}\nTo: #{@to}\n\n#{@body}"
end
end
module MailGenerator
def self.email(name, &block)
builder = EmailBuilder.new
builder.instance_eval(&block)
builder.render
end
end
# Now we use it like this:
content = MailGenerator.email "Welcome" do
from "support@app.com"
to "user@example.com"
body "Welcome to the platform!"
end
puts content
It's elegant, it's readable, and it hides the plumbing. Just a word of caution: instance_eval is powerful, but it opens up the entire object to the block. If EmailBuilder had a method like destroy_database!, the user of your DSL could call it inside that block. Always be mindful of what methods you expose in your builder classes.
📋 Practical Task
Build a Custom Playlist DSL
Your task is to create a DSL for defining a music playlist. The goal is to allow a user to define a playlist with a title and multiple songs, and then print the final list in a formatted way.
Requirements:
- Create a
PlaylistBuilderclass that can store a title and an array of songs. - Implement a method
song(title, artist)inside the builder that adds a song to the list. - Implement a
rendermethod in the builder that returns a string like:"Playlist: [Title]\n- [Artist]: [Song Title]..." - Create a module called
MusicAppwith a class methodplaylist(name, &block)that usesinstance_evalto configure the builder and returns the rendered string.
Example Usage:
puts MusicApp.playlist "Morning Coffee" do
song "Bitter Sweet Symphony", "The Verve"
song "Fast Car", "Tracy Chapman"
endThere are no comments for now.