jiujitsumind
jiujitsumind

Reputation: 101

How to structure Util classes in RoR

I have a template that users can upload that generates a report. They can put special tags into the html template and it will replace with data from the db. Quick example:

<div class="customer-info">
  <h1>{customer_name}</h1>
  <h2>{customer_address_line1}</h2>
  <h2>{customer_address_line2}</h2>
  <h2>{customer_address_city}, {customer_address_state} {customer_address_zip}</h2>
</div>

I have a controller that looks up the customer and then parses the template and replaces the tokens.

Right now I have the parse code in the controller creating a fat controller. Not good.

But where should I move the code? Model folder? Create a Util folder and put it there?

Just not sure what the Rails Way would be.

Upvotes: 3

Views: 303

Answers (2)

spike
spike

Reputation: 10014

I agree that the logic shouldn't be in the controller, but let's get a little more specific about how you'd go about implementing this.

First, where are you storing your templates in the database? They should be stored in their own model, let's call it CustomerTemplate and give an attribute :template of type Text.

So now we have two types of objects, Customers and CustomerTemplates. How to render a customer given a template? It honestly wouldn't be terrible to just have a render function in the CustomerTemplate model that takes a customer and renders it, but it is putting some logic inside your app that doesn't strictly belong there. You should separate out the "customer specific rendering logic" from the "rendering my simple custom template language".

So, let's create a simple template handler for your custom language, which I'm going to nickname Curly. This handler should know nothing about customers. All it does is take a string and interpolate values inside {}'s. This way if you want to add new template types in the future — say, to render another model like an invoice — you can use the same template type.

Templates in Rails are classes which respond to call and are registered with ActionView::Template. The simplest example is Builder.

Here's a quickly written Template handler which renders Curly. The call function returns a string which is eval'd, so the string has to be valid ruby code. The string eval in scoped by the render call, so it has access to any variables passed in via the { locals: {} } option to render.

# In lib/curly_template_handler.rb
class CurlyTemplateHandler
  def self.call(template)
    src = template.source
    """
    r = '#{src}'.gsub(/{([^}]*)}/) { |s|
       local_assigns[$1.to_sym] || s
    }
    raw r
"""
  end
end

Make sure the handler is initialized, and let's set it to listen for the :curly type.

# In config/initializers/register_curly_template.rb
ActionView::Template.register_template_handler(:curly, CurlyTemplateHandler)

We need to add lib/ to autoload_paths so the class is loaded:

# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)

Finally, we can render our template in our view! I'm embedding the string here, but you'd really get it from a CustomerTemplate object:

<%= render(inline: "<h2>{customer_name}</h2><p>{customer_address}</p>",
           type: :curly,
           locals: { customer_name: @customer.name,
           customer_address: @customer.address }) %>

DO NOT USE MY EXAMPLE CODE IN PRODUCTION! I left out a bunch of corner cases which you'll need to handle, like sanitizing user input.

Upvotes: 0

Ozzie Gooen
Ozzie Gooen

Reputation: 122

I was curious about this too, and found a very similar discussion here. Honestly, I think it depends on how much parse code there is. If there are only a very few lines, then the model is a safe place. If it's going to be a large package, especially a re-usable one, the /lib/ folder may be a better bet for the parsing itself. However, you definitely should remove it from the controller, as you suggested.

Upvotes: 1

Related Questions