Netskin Logo

DSLs for structuring code

#ruby
#rails
#dsl
by Tom Rothe on 04.05.2023

DSLs are a great way to structure code. We all have come to love and appreciate the directives used in Rails:

class Article
  belongs_to :blog
  has_many :comments
  scope :published, -> { where(published: true) }
end

Expressive, clear and concise. In my opinion, it does not get much better than this.

However, providing DSLs introduces boilerplate code. When I am thinking about using this tool to structure code, I tend to think that it is overkill if it is just for a single class. Making up a specific language for just one class does not seem pragmatic. So, in my head, DSL are reserved for Gems or modules that are excessively used.

Nowadays, I use this tool much more liberally. I made a little Gem (dsl_factory) which reduces the boilerplate for defining a DSL. The gem is nothing fancy (~100 lines of code). Yet, it standardizes the way how to define DSLs in my applications.

Let’s look at this example:

class ArticleExporter
  # define the DSL
  ExportDsl = DslFactory.define_dsl do
    string :filename
    hash   :columns, String, Proc
  end
  extend ExportDsl

  # intent
  filename 'out.csv'
  column 'Blog',          -> { _1.blog.name }
  column 'Title',         -> { _1.title }
  column 'Excerpt',       -> { _1.excerpt }
  column '# of Comments', -> { _1.comments_count }

  # implementation
  def write
    CSV.open(self.class.filename, 'w') do |csv|
      csv << self.class.columns.keys # write headers

      Article.find_each do |article|
        csv << self.class.columns.values.map { _1.call(article) } # write values
      end
    end
  end
end

Instead of collecting all the columns in the write method, we can separate intent and implementation. This is nice to read and easy to maintain.

Here another example, which I used to define a menu structure, which is being used in one form or another throughout the application:

module MenuHelper
  # define the DSL
  MenuDsl = DslFactory.define_dsl do
    array :links do
      string :label
      callable :url
      callable :condition
    end
  end
  extend MenuDsl

  # intent
  link do
    label 'Articles'
    url -> { articles_path }
    condition -> { true }
  end

  link do
    label 'Review'
    url -> { reviews_path }
    condition -> { current_user.can_review? }
  end

  link do
    label 'Blog Administration'
    url -> { blogs_path }
    condition -> { current_user.admin? }
  end
end

The DSL definition documents the data structure and the subsequent use of the DSL contract states the intention clearly.

I believe that the usage of DSLs is a useful tool. They are not only reserved for Gems – we can also use them in much smaller scopes.

Happy Coding!

References

❮ Understanding the HTTP 418 I'm a Teapot Status
Digging Deeper into implicit_order_column in Ruby on Rails ❯
Netskin Logo