A well-designed Domain Specific Language (DSL) can help you be more productive as a developer, thus making you, your team and your clients happier. In this post, I’ll guide you through the design and creation of a simple DSL to create EPUB files. We’ll start with a regular API and refactoring until we get to a DSL solution.

A short into to DSLs

At it’s very core, DSL is a fancy term for a very simple language designed to solve something in particular. It’s domain-specific because it works in a very particular use-case, the most common ones being configuration files and APIs. If you are a Ruby developer then you have most likely used a DSL already. RSpec is one of the most popular:

describe "Something" do subject { SomeClass.new } it { is_expected.not_to be_nil } it "passes" do subject.greet eq "Hello!" end end 1 2 3 4 5 6 7 8 9 10 describe "Something" do subject { SomeClass . new } it { is_expected . not_to be_nil } it "passes" do subject . greet eq "Hello!" end end

That code is in a language designed to helps us write tests in a more natural way following the BDD testing methodology.

The result is code that is more understandable to you as a human — programmer or otherwise. Even if you’ve never used Ruby before, or don’t know about RSpec, you get an idea of what it is, it describes the functionality of Something.

The biggest drawback of DSLs is that you need to learn a new language every time — it’s easier to always use the same interface for all libraries. The advantage, though, is that the API is much more friendly and easier to use in the long-run. It’s an investment, the easiest the library, the lesser bugs consumers have, and everyone loves having less bugs. 🙂

So let’s get starting building a DSL. I’ll guide you through the design and creation of a simple DSL to create EPUB files. Starting with a regular API, we’ll refactor until we get to a DSL solution.

The design

EPUB is a format for digital books used by iOS and macOS. It’s basically a bunch of HTML files zipped together, following certain naming rules and ceremony. Without getting too deep into the file format specification, let’s just assume for now that all EPUBs must have a title, a description and at least one chapter. Initially, one could think of an API design as follows:

generator = EPUBGenerator.new(title: "My Awesome Book", description: "An awesome book, really.") generator.chapter = Chapter.new(title: "Chapter 1", contents: "Once upon a time...") book_path = generator.generate puts "The book was created, it now lives in #{book_path}" 1 2 3 4 5 generator = EPUBGenerator . new ( title : "My Awesome Book" , description : "An awesome book, really." ) generator . chapter = Chapter . new ( title : "Chapter 1" , contents : "Once upon a time..." ) book_path = generator . generate puts "The book was created, it now lives in #{book_path}"

That looks good, right? If the problem is that simple, then we are done. But what if the generator needs more than just a title and a description. Let’s say we now also need an author and a URL. We could just add more arguments:

generator = EPUBGenerator.new(title: "My Awesome Book", description: "An awesome book, really.", author: "Federico Ramirez", url: "http://blog.beezwax.net") 1 2 generator = EPUBGenerator . new ( title : "My Awesome Book" , description : "An awesome book, really." , author : "Federico Ramirez" , url : "http://blog.beezwax.net" )

You might say “Meh it’s not that bad”. And you would be right! But we are taking an unnecessary risk, four arguments for a method is a red flag — it can get out of hand quite easily.

There are many ways to solve that issue, the most common of which is to “extract it into an object”. Let’s create a Book model. We just add the arguments as attributes, make sure the data is always consistent and just inject that object into our generator. Now our code is not only more solid and easier to maintain, but we have the added benefit of testability.

Now we are done… well, not really. Consider now that our EPUB generation library is a Ruby gem. We’ll force all our users to know all the class names: EPUBGenerator , Chapter and Book .

If the library is this small, it’s not really a big deal. If we know we’ll need to expose the user to more classes, then we might want to consider a better solution. This is where a DSL comes handy.

A DSL gives us yet another layer of abstraction. In this example, with a single class name, the user can easily use the library to create a new EPUB:

generator = EPUBGenerator do |g| g.title "My Awesome Book" g.description "An awesome book, really." g.author "Federico Ramirez" g.url "http://blog.beezwax.net" end 1 2 3 4 5 6 7 generator = EPUBGenerator do | g | g . title "My Awesome Book" g . description "An awesome book, really." g . author "Federico Ramirez" g . url "http://blog.beezwax.net" end

The way that looks is arbitraty, that’s just a common format for DSLs. With domain-specific languages it’s easier to start with “how it looks” and then move into the implementation, as the other way around might be harder if you have never made a DSLs before.

Now that’s a good enough solution. The code is simple and easy to read. We are still missing a few things though. What would a chapter definition look like? Easy!

generator = EPUBGenerator do |g| g.title "My Awesome Book" # ... g.chapter do |c| c.title "Chapter 1" c.contents "Lorem ipsum dolor sit amet..." end end 1 2 3 4 5 6 7 8 9 10 generator = EPUBGenerator do | g | g . title "My Awesome Book" # ... g . chapter do | c | c . title "Chapter 1" c . contents "Lorem ipsum dolor sit amet..." end end

You start to notice a pattern here, if chapters needed some dependency, we just pass a new block:

generator = EPUBGenerator do |g| g.title "My Awesome Book" # ... g.chapter do |c| c.title "Chapter 1" #... c.footnote do |f| f.contents "Hello! I'm a footnote." end end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 generator = EPUBGenerator do | g | g . title "My Awesome Book" # ... g . chapter do | c | c . title "Chapter 1" #... c . footnote do | f | f . contents "Hello! I'm a footnote." end end end

Good! We now have our general design, let’s make it happen!

The implementation

Ruby’s yield is what makes it so easy to write DSLs. You can think of it as a function which gets called with whatever arguments we give it.

class EPUBGenerator def self.generate book = Book.new yield book generator = Generator.new(book) generator.generate end end 1 2 3 4 5 6 7 8 9 class EPUBGenerator def self . generate book = Book . new yield book generator = Generator . new ( book ) generator . generate end end

In the code above, pass book , an instance of Book to a block of code. We don’t know what the code-block will do with it, that responsibility is up to the caller. The generate method call looks like this:

generator = EPUBGenerator.generate do |book| puts "I have a book! #{book}" end 1 2 3 4 generator = EPUBGenerator . generate do | book | puts "I have a book! #{book}" end

We’ve abstracted away the Book class name dependency! We’ve also reduced the ceremony for creating books, it’s much simpler now. Let’s repeat this process of yieding code blocks for the Book model:

class Book attr_reader :chapters def initialize @chapters = [] end # getter/setter def title(text = nil) return @title if text.nil? @title = text end def chapter chapter = Chapter.new yield chapter chapter.id(chapters.count + 1) chapters << chapter end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Book attr_reader : chapters def initialize @ chapters = [ ] end # getter/setter def title ( text = nil ) return @ title if text . nil ? @ title = text end def chapter chapter = Chapter . new yield chapter chapter . id ( chapters . count + 1 ) chapters < < chapter end end

Nice! Our generator now looks like this:

generator = EPUBGenerator.generate do |b| b.title "My Awesome Book" b.chapter do |c| # ... do something with chapter object end end 1 2 3 4 5 6 7 8 generator = EPUBGenerator . generate do | b | b . title "My Awesome Book" b . chapter do | c | # ... do something with chapter object end end

We are still lacking functionality, but the important thing is to realize that every time we write b.<something> in the generator, we are actually calling a method on a book instance.

That’s it! The hard part is done! From now on, it’s quite straightforward to implement the missing functionality. For the sake of completeness, let’s make another model, the Chapter :

class Chapter attr_reader :title def initalize @title = "Not defined" end def title(text = nil) return @title if text.nil? @title = text end end 1 2 3 4 5 6 7 8 9 10 11 12 class Chapter attr_reader : title def initalize @ title = "Not defined" end def title ( text = nil ) return @ title if text . nil ? @ title = text end end

The generator can now add titles to chapters:

generator = EPUBGenerator.generate do |b| b.title "My Awesome Book" b.chapter do |c| c.title "Chapter 1" end end 1 2 3 4 5 6 7 8 generator = EPUBGenerator . generate do | b | b . title "My Awesome Book" b . chapter do | c | c . title "Chapter 1" end end

Wrapping up

We’ve built our own DSL. And it wasn’t even hard! If you are curious and want the full source code, you can see a fully working gem on GitHub. The complete DSL looks like this:

path = Epubber.generate do |b| b.title 'My First EPUB book' b.author 'Ramirez, Federico' b.description 'This is an example EPUB' b.url 'http://my-url.com' b.cover do |c| c.file File.new('my-image.jpg') end b.introduction do |i| i.content '<p>This is an introduction.</p>' end b.chapter do |c| c.title 'Chapter 1' c.content '<p>This is some content!</p>' end b.chapter do |c| c.title 'Chapter 2' c.content '<p>Some more content this is.</p>' end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 path = Epubber . generate do | b | b . title 'My First EPUB book' b . author 'Ramirez, Federico' b . description 'This is an example EPUB' b . url 'http://my-url.com' b . cover do | c | c . file File . new ( 'my-image.jpg' ) end b . introduction do | i | i . content '<p>This is an introduction.</p>' end b . chapter do | c | c . title 'Chapter 1' c . content '<p>This is some content!</p>' end b . chapter do | c | c . title 'Chapter 2' c . content '<p>Some more content this is.</p>' end end

BONUS TIP

Yielding blocks is used everywhere in Ruby. It is particularly useful for making sure resources are beeing handled properly, the most common example is file manipulation. In order to write to a file we have to open it for writing, write stuff, and then close it.

file = open_file('my_file.txt', 'w') file.write("Something") file.close 1 2 3 4 file = open_file ( 'my_file.txt' , 'w' ) file . write ( "Something" ) file . close

If we forget to close the file, we won’t get any errors, but it might lead to unexpected behavior. That’s not good, we want all our users to always close the file after they write to it. We can easily solve this with yield :