Liquid email templates in Rails

Liquid is a "safe, customer facing template language for flexible web apps". It's limited capabilities makes it the perfect email templating tool when you'd like designers and content producers to be able to edit and create email templates.

A brief introduction to Liquid

Liquid is a perfect match when:

As mentioned, Liquid syntax borrows heavily from Smarty, the PHP templating language, as well as the eZ Publish template language:

# array = %w{a, b, c, d, e, f}
{% for item in array limit:2 offset:2 %} 
    {{ item | upcase }}
{% endfor %} 

# Result: C D

Liquid for emails

When I first stumbled across Liquid I was looking for a solution to the following problem: An application sends out quite a few emails. The customer occasionally needs to adjust formatting, update content and perform other kinds of maintenance tasks. Given that the emails are dynamic (ie they embed user generated content) updating the content only isn't enough. The customer needs to edit templates.

So why not just give the customer access to your erb/haml templates?

I'll tell you why. Fullblown templating systems like erb and haml allows any piece of Ruby code to be run. Even stuff like this:

<h1>Hey there!</h1>
<%= User.delete_all %>

You basically have no (good) way of restricting users from doing that, so allowing them access to these templates isn't really a good idea. Liquid on the other hand, provides a sandbox environment where you can

With Liquid, you have full and complete control over the templating environment, and you can easily set up an environment which is safe to invite the customer into. Liquid does not allow any access to any object unless you tell it too.

An example

Let's assume we've got an application with accounts, projects and clients. You know the drill. When certain events occur, the application sends out emails to clients. The account owners should be able to tweak email templates for their own clients, create new ones and so on.

Installation

First, we'll need to install and setup Liquid. You can install through script/install:

ruby script/plugin install liquid

The next step is to expose data to Liquid templates. Liquid allows no object access by default, and so you have to actively allow this. The simplest way to get there is through the liquid_methods method. You call liquid_methods with a list of symbols, and these methods will be available from liquid templates:

class Person < ActiveRecord::Base
  liquid_methods :name, :email
end

If you only need to expose a few attributes this method works well. However, if you need a little more control, you can implement a to_liquid method, which should return a hash:

class Person < ActiveRecord::Base
  has_many :addresses

  # ...

  def to_liquid
    { :name => name, :email => email, :addresses => addresses }
  end
end

In this example I exposed addresses, which is an association. When accessing a person object from a liquid template you will have access to an addresses array, but in order to make any sense of these objects, Address needs to expose data to Liquid in the same way.

liquid_methods and to_liquid are quick and easy ways of exposing your data, but enabling template languages language directly in our model objects isn't really so great. Luckily, Liquid provides us with Liquid::Drop, which solves this exact problem. Take a look at the following example (taken from the docs):

class ProductDrop < Liquid::Drop
  def top_sales
     Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
  end
end

This decouples view data from the model objects in a clean way.

.liquid template files

Once the plugin is installed you can now use Liquid anywhere you use erb or haml templates simply by giving files the .liquid suffix. In our example scenario, though, end users are going to be editing templates, so the application source may not be the hottest place to put the templates.

Loading template strings from a database

Loading templates from strings are fortunately quite simple. Knowing this, we can now save templates to the database and catch syntax errors with ActiveRecord validations. That should provide a nice framework for allowing end users to create and edit templates.

The EmailTemplate class

The following is a database backed email template class, which uses Liquid. Parsing templates is fairly expensive, and a few quick benchmarks confirms my suspicions: When loading templates from the database, it's faster to deserialize readily parsed templates than to reparse them:

require 'benchmark'
n = 50000
template = "A short template, using a single lookup: {{ user.name }}"
serialized = Marshal.dump(Liquid::Template.parse(template))

Benchmark.bm do |x|
  x.report("deserialize: ") { n.times { Marshal.load(serialized) } }
  x.report("parse: ") { n.times { Liquid::Template.parse(template) } }
end

user     system      total        real
deserialize:   1.710000   0.030000   1.740000 (  1.733290)
parse:  13.800000   0.300000  14.100000 ( 14.119359)

In addition to taking the fastest route, the class tries to further enhance performance by only parsing or deserializing the template once each time it's fetched or updated. This is done by memoizing the output in the instance variable @template. This will only yield savings in cases where a template is rendered more than once per database fetch.

# Table name: email_templates
#
#  id       :integer(11)     not null, primary key
#  subject  :string(255)     not null
#  from     :string(255)     not null
#  bcc      :string(255)
#  cc       :string(255)
#  body     :text            not null
#  template :text
#

class EmailTemplate < ActiveRecord::Base
  ### Validation
  validates_presence_of :subject, :from, :body

  # http://code.dunae.ca/validates_email_format_of.html
  validates_email_format_of :from, :allow_nil => true
  validates_email_format_of :cc, :allow_nil => true
  validates_email_format_of :bcc, :allow_nil => true

  #
  # Puts the parse error from Liquid on the error list if parsing failed
  #
  def after_validation
    errors.add :template, @syntax_error unless @syntax_error.nil?
  end

  ### Attributes
  attr_protected :template

  #
  # body contains the raw template. When updating this attribute, the
  # email_template parses the template and stores the serialized object
  # for quicker rendering.
  #
  def body=(text)
    self[:body] = text

    begin
      @template = Liquid::Template.parse(text)
      self[:template] = Marshal.dump(@template)
    rescue Liquid::SyntaxError => error
      @syntax_error = error.message
    end
  end

  ### Methods

  #
  # Delivers the email
  #
  def deliver_to(address, options = {})
    options[:cc] ||= cc unless cc.blank?
    options[:bcc] ||= bcc unless bcc.blank?

    # Liquid doesn't like symbols as keys
    options.stringify_keys!
    ApplicationMailer.deliver_email_template(address, self, options)
  end

  #
  # Renders body as Liquid Markup template
  #
  def render(options = {})
    template.render options
  end

  #
  # Usable string representation
  #
  def to_s
    "[EmailTemplate] From: #{from}, '#{subject}': #{body}"
  end

 private
  #
  # Returns a usable Liquid:Template instance
  #
  def template
    return @template unless @template.nil?

    if self[:template].blank?
      @template = Liquid::Template.parse body
      self[:template] = Marshal.dump @template
      save
    else
      @template = Marshal.load self[:template]
    end

    @template
  end
end

The class is actually storing the template twice - both in fulltext, for easy editing, and as a serialized object. The class relies on an ApplicationMailer, which looks like:

class ApplicationMailer < ActionMailer::Base

  #                                                                                                                                                                              
  # Delivers an email template to one or more receivers                                                                                                                          
  #                                                                                                                                                                              
  def email_template(to, email_template, options = {})
    subject email_template.subject
    recipients to
    from email_template.from
    sent_on Time.now
    cc options['cc'] if options.key?('cc')
    bcc options['bcc'] if options.key?('bcc')
    body :body => email_template.render(options)
  end
end

Here's how you could use the EmailTemplate:

account = Account.find(1)

template = EmailTemplate.new({
  :subject => "New password",
  :from => "me@myplace.com",
  :body => "Your new password is {{account.generated_password}}"
})

template.deliver_to(account.email, { :account => self })

In addition, you can create a REST interface to allow users to view, create, edit and delete email templates. The validations will catch syntax errors, giving users a hint of what's wrong.

Liquid solved my problem exactly, and I think it provides foundation for a smooth solution where your end users can take control of their generated emails. Anyone else out there with experience using Liquid?

Published 2. February 2009 in rails og ruby.

Possibly related