# How to replace keywords with values to merge into an email template

**URL:** <https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832>\
**Category:** Rails\
**Created:** [January 2, 2008, 11:25pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832 "2008-01-02T23:25:51Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 2, 2008, 11:25pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/1 "2008-01-02T23:25:51Z")

</div>

I am making an email template system, in which certain keywords are to  
be replaced by values from the model. eg {ORDERNO}, {NAME} etc

My plan is to do the following.

1. find all keywords in the text (subject and body as separate  
methods)
2. check all keywords are valid (from a predefined list)
3. go through each keywords found and use gsub to substitute them
4. identify any non valid keywords and make them available to the  
calling method
5. return the text after substitution

I thought that maybe if I put any invalid keys into an attribute  
hash, :invalid\_keywords, that can be checked by the calling method.

I cannot think of the best approach to solve step 1. My immediate  
approach would be to recursively find each keyword in turn, recursing  
through the remaining string. I feel there is a neat way to write  
this (or can it be done directrly with regexp). Grateful for  
suggestions here.

Also, it seems to me that Rails must do similar sorts of things - I  
wonder if there is a better overall approach, or something in Rails  
that will help in achieving the overall requirement.

Thanks for any advice  
Tonypm

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 3, 2008, 12:24am UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/2 "2008-01-03T00:24:30Z")

</div>

On Jan 2, 2008 5:25 PM, tonypm [removed\_email\_address@domain.invalid](mailto:removed_email_address@domain.invalid) wrote:

> 1. identify any non valid keywords and make them available to the  
> suggestions here.
> 
> Also, it seems to me that Rails must do similar sorts of things - I  
> wonder if there is a better overall approach, or something in Rails  
> that will help in achieving the overall requirement.

Ummmm, it sounds to me as though you are trying to write a  
replacement for ActionMailer. Why?

–  
Rick DeNatale

My blog on Ruby  
[http://talklikeaduck.denhaven2.com/](http://talklikeaduck.denhaven2.com/)

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 4, 2008, 4:26pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/3 "2008-01-04T16:26:58Z")

</div>

> Ummmm, it sounds to me as though you are trying to write a  
> replacement for ActionMailer. Why?
> 
> –  
> Rick DeNatale
> 
> My blog on Rubyhttp://talklikeaduck.denhaven2.com/

Rick,

I am using actionmailer to create and deliver the email. When the  
email is created, the user is able to choose from one of a set of user  
templates from a model email\_template. The template has a name, and  
defines text for subject and body. Within the template, the user is  
able to include fields which are to be substituted with things like  
customer name, order number, order details etc. He can insert these  
into the template so that he gets a form of mail merge.

So when the email is being created, the user selects the email  
template by name from a select drop down. This then updates the page  
with the text from the template. At this point I want to substitute  
in the mail merge values before the text is displayed.

I was working on the premise that I would have to do the substiutions  
fo the fields for the tags. But I may be missing something that  
AcionMailer can do for me

Thanks  
Tony

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 4, 2008, 5:59pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/4 "2008-01-04T17:59:43Z")

</div>

maybe you could use ERB manually to do the job.  
it offers a few options to do things like that (eg changing the the type  
of recognized tags and how it’s handling the usage of local variables  
when resolving the template).

[http://www.ruby-doc.org/core/classes/ERB.html](http://www.ruby-doc.org/core/classes/ERB.html)

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 4, 2008, 5:35pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/5 "2008-01-04T17:35:41Z")

</div>

Hi Tony,

I had to do something similar a couple of months ago, and I tackled  
it like this:

I created a module that defined an array of hashes which represented  
the supported tags. Here are some examples:

```
tags << {
  :find => 'order.id',
  :verify => 'id',
  :replace => 'id.to_s',
  :default => '',
  :description => 'The ID of the order.',
  :domain => 'Orders'
}

tags << {
  :find => 'order.status',
  :verify => 'status',
  :replace => 'status.name',
  :default => '',
  :description => 'The status of the order.',
  :domain => 'Orders'
}

tags << {
  :find => 'order.user.name',
  :verify => 'user',
  :replace => 'user.name',
  :default => '',
  :description => 'The name of the user who submitted the order.',
  :domain => 'Orders'
}

```

and then to make parsing a little easier, wrap each “find” in some  
kind of marker

```
tags.each do |tag|
  tag[:find] = "{t:#{tag[:find]}}"
end

```

this happens to be similar to RadiantCMS’s style, so your the tag in  
text will look like

Hi {t:order.user.name}, your order shipped on {t:order.shipped}.

I included “description” and “domain” for building a small help popup  
for the user when inserting tags. I had tags in two main domains,  
Users and Orders, but this way I could add whatever else I needed.

Now, to parse these, I came up with this method:

def TemplateTags.replace\_tags(object, template, content = nil)  
tags = supported\_tags # this method returns the array of support  
tags  
tags.each do |tag|  
find = tag[:find]  
replace = tag[:replace]  
verify = tag[:verify]  
default = tag[:default]

```
  if find.include?('content')
    if content
      template.gsub!(find, content)
    end
  elsif replace
    begin
      replace = if object.instance_eval(verify) then

```

object.instance\_eval(replace) else default end  
rescue  
replace = default  
end

```
    template.gsub!(find, replace)
  end
end

return template

```

end

To this, you pass an object (order, user, whatever), the template  
(with all of the tags), and, in my case, an optional “content”.  
Content was something that the system needed to put in, and it’s  
template tag was defined as

```
tags << {
  :find => 'content',
  :verify => nil,
  :replace => nil,
  :default => nil,
  :description => 'Place holder for system generated messages.',
  :domain => 'Orders'
}

```

This is kind of a kludge, because the user had to know to put  
{t:content} in their template. But in the particular case in which  
this was implemented, that was a reasonable request.

The trick to this is verifying that the object can actually respond  
to the method that you might want to call on it, which is the purpose of

if object.instance\_eval(verify)

Essentially what you are doing is saying "check to see if the object  
has , and if it does, then ask it for ". Two  
examples are the tags noted above, order.id and order.status. First,  
order.id:

the template will look like this:

“Your order, id #{t:order.id}, shipped yesterday”

so in the code, you pass an order object to replace\_tags.  
replace\_tags needs to make sure that object can respond to id, and if  
it can, it calls id. So in this case you are doing something like

if object responds id, then call id

In the second example, order.status, my status was an association, so  
to get to it normally, you’d use order.status.name. The tag would  
look like

“Your order is now {t:order.status}”

so in replace\_tags, I first had to check to see if the object had the  
proper association, which is the verify:

if object responds to status, call status.name

I hope that makes sense. This is the first time I’ve had to try to  
explain it to anyone, and typing it out at that! To sit down with  
someone and explain it would, I think, be much easier. I also admit  
that this might not be the best way to address this problem, but this  
worked for what I needed. The main downside is you have to create an  
array of hashes that represent all of the tags you support, but I  
don’t know how to get around that.

Let me know if you have any questions. I can create a small working  
example if you need me to.

Peace,  
Phillip

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 17, 2008, 9:48am UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/6 "2008-01-17T09:48:43Z")

</div>

Phillip/Thorsten,

Have just got back onto this project. Thank you both for your  
responses, both have merit. I was not aware of instance\_eval and that  
makes generalizing the solution really nice (the thing I really like  
about Ruby).

My template requirement is fairly simple and is now working. I may  
also employ the erb solution, since one of the replacements may work  
best as a partial, since I may want to expand a single keyword into  
multiple order details. I figure if I make the first replacement say  
{ORDERDETAILS} to become \<%= render :partial=\>‘order\_details’ %\> and  
then run the result through erb, I will get quite a neat solution.

I have put the initial keyword replacement stuff into the email  
template model and that works quite nicely, though at the moment, I  
think I will need to do the pass through erb from the controller.  
Still trying to get my head fully around setting up an erb template at  
the moment though.

If anyone could suggest a simple bit of code to include in my  
controller I would be grateful. If I have the string containing the \<  
%= render … line, and I have the object @order in the controller  
from which I can derive everything for the partial, I just need the  
code to call erb with the string and the @order object. (I am not  
sure what the ‘binding’ thing is all about and whether I would need  
it.

Many thanks  
Tonypm

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 22, 2008, 7:38pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/7 "2008-01-22T19:38:06Z")

</div>

I have just come back to this one to try to solve the partial issue.

> partial\_output = render\_to\_string :partial =\>  
> ‘order\_details’, :locals =\> {:collection =\> order\_details)  
> template\_output = TemplateEngine.replace(order,template,  
> partial\_output)

What you suggest is helpful. Since I am only likely to have one  
partial, I can do that. Render the partial into a string before I do  
the tag replacement and pass that into the replacement method.

Excellent!

Many thanks for your suggestions.  
Tony

---

<div class="post-metadata">

**Author:** ![tonypm](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/a587f6/32.png) [@tonypm](https://www.ruby-forum.com/u/tonypm)\
**Post date:** [January 17, 2008, 1:34pm UTC](https://www.ruby-forum.com/t/how-to-replace-keywords-with-values-to-merge-into-an-email-template/124832/8 "2008-01-17T13:34:50Z")

</div>

On Jan 17, 2008, at 2:47 AM, tonypm wrote:

> Phillip/Thorsten,
> 
> Have just got back onto this project. Thank you both for your  
> responses, both have merit. I was not aware of instance\_eval and that  
> makes generalizing the solution really nice (the thing I really like  
> about Ruby).

After I sent that message, I got to thinking that this approach might  
also work with respond\_to? instead of instance\_eval. It might be a  
little simpler that way. I’m not sure.

> My template requirement is fairly simple and is now working. I may  
> also employ the erb solution, since one of the replacements may work  
> best as a partial, since I may want to expand a single keyword into  
> multiple order details. I figure if I make the first replacement say  
> {ORDERDETAILS} to become \<%= render :partial=\>‘order\_details’ %\> and  
> then run the result through erb, I will get quite a neat solution.

When I created my little “engine”, I considered the possibility of  
needing to do this. Fortunately (or not, depending on how you look  
at it), I didn’t have an actual need at the time, so I didn’t push  
forward. However, just bouncing it around quickly right now, it  
might be possible to use the concept of “content” that I have. The  
downside is that you’d have to know ahead of time which partials  
you’d need to generate for a particular template. But if that were  
possible, you could do something like

partial\_output = render\_to\_string :partial =\>  
‘order\_details’, :locals =\> {:collection =\> order\_details)  
template\_output = TemplateEngine.replace(order, template,  
partial\_output)

I’m sure it would have some kinks to work out, but I bet you could do  
it. If I ever come across a need, I’ll probably do it.

Peace,  
Phillip
