how to replace keywords with values to merge into an email template

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

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

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

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 <verify>, and if it does, then ask it for <replace>". 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

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

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

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

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