Welcome back to our continuing series on writing custom tools for data mining and analysis!
If you’ve been following along with the posts so far, I know what you’re thinking: “All this data is helpful and all, but this is 2014, you can’t really expect me to look at plain text!” Well, plain text does have some advantages, but I hear ya. So today we’ll use Mustache templates and create a dynamic rendering system to output our build data in the formats we prefer.
As usual, the source we’ll be working with is available on github.
Wait, what’s Mustache?
Templates allow you to mix standard boilerplate content with dynamic content from a context object or hash. Mustache is a logic-less templating library, with support for a fairly wide range of programming languages. It allows you to invoke code like:
require_relative 'view'
View.new.render
with a template:
Mustache is {{adjective}}!
and a view class:
require 'mustache'
class View < Mustache
def adjective
"pretty good"
end
end
to render text that mixes the boilerplate with your dynamic content. In this case, we get the string "Mustache is pretty good!".
The template language allows you to do simple boolean logic and iterate over lists, but that's about it. All your logic goes into the view class. This is a good thing.
We certainly have other templating options available, but I like Mustache for this job since it's language-agnostic, and we know we probably want to support at least HTML and plain text output. It also happens to interact nicely with Sinatra, which (ahem) may be the subject of a future post.
Building a Polymorphic Rendering Engine
Currently, we have a build data tree that can give us information about the costs, profits, and materials required for various items we might be able to build. We are most likely going to want to render the tree nodes in different ways based on the node type, and we know we'll want to render the same nodes in different output formats.
To make this happen, we'll first modify the last line from our previous post's build script to include an output format option:
ConsoleSerializer.new.write(build, {output_format: 'html'})
Our ConsoleSerializer used to do a lot of work, and hard-code a lot of knowledge about how to render the tree. Now it will dispatch all this work to a ReportView instance:
class ConsoleSerializer
def write(build, options = {})
puts ReportView.new(build, options).render
end
end
The ReportView will in turn create a view for the build, which will walk the tree, creating a view for each of our nodes.
Loading Templates Based on Format Type
So far, so good, but how will our ReportView know how to render the different nodes correctly based on our expected output type? Well, you can tell Mustache what template to use to render your view by specifying the template_file
property. So we'll create a base class for our views that knows how to lookup the correct template for a node given the current output options:
require 'mustache'
require 'active_support/inflector'
require_relative "../formatting"
class View < Mustache
include ActiveSupport::Inflector
include Formatting
attr_accessor :template_folder
def initialize(options = {})
@options = options
self.template_file = resolve_template
end
def template_file_name
self.class.name.chomp("View").underscore
end
def resolve_template
format = @options[:output_format] || 'text'
report_type = @options[:report_type]
Pathname.new("#{format}/#{template_folder}/#{report_type}").ascend do |path|
file_name = "#{File.dirname(__FILE__)}/templates/#{path}/#{template_file_name}.mustache"
return file_name if File.exists?(file_name)
end
nil
end
end
So when we construct a View instance, we'll resolve the template file, based on the current format (html), the expected template folder, and file name. We'll also allow a different template per report type. The code walks from the most specific path (format/template_folder/report_type) to the most generic folder looking for a template that will match our node. When it finds one, it sets it to the View and we're in business.
So since we've passed an html output format, our view will look under the templates/html/build/manufacturing/ folder for a "report.mustache" file and walk back up the directory tree until it finds a template to render. When we pass no output format option, the lookup will start at templates/text/build/manufacturing/.
Mustache Templates
Ok, great, so we have a template lookup mechanism. What do these templates actually look like? In the case of our report.mustache
guy, we're looking at something like:
{{title}}
{{#build}}
{{{render}}}
{{/build}}
We'll include some jquery and bootstrap files, add a couple inline CSS classes, and then we have the Mustache tags. The {{#build}}
tag references the #build
method on our ReportView object. This returns the BuildView instance as the context for the wrapped block, which invoked the BuildView#render
method. Note that we've called the #render
method using triple mustaches, to let Mustache know not to html-escape that content. The BuildView template can be simpler:
{{title}}
Cost {{cost}}
Value {{value}}
Profit {{profit}}
Profit margin {{profit_margin}}
{{#children}}
{{{render}}}
{{/children}}
And so on.
Our Final Result
So now we can pass the output_format: 'text'
arg to continue getting our old text output, or an output_format: 'html'
arg to get output like:
Initial Build | ||
---|---|---|
Cost | 89,761,430.80 ISK | |
Value | 184,818,252.20 ISK | |
Profit | 95,056,821.40 ISK | |
Profit margin | 105.9% |
Warp Scrambler II - 100 | ||
---|---|---|
Cost per unit | 719,940.86 ISK | |
Value | 134,819,272.60 ISK | |
Value per unit | 1,348,192.73 ISK | |
Profit | 62,825,186.40 ISK | |
Profit per unit | 628,251.86 ISK | |
Profit margin | 87.26% |
Expanded Cargohold II - 100 | ||
---|---|---|
Cost per unit | 177,673.45 ISK | |
Value | 49,998,979.60 ISK | |
Value per unit | 499,989.80 ISK | |
Profit | 32,231,635.00 ISK | |
Profit per unit | 322,316.35 ISK | |
Profit margin | 181.41% |
Woah! Now that's an amazing looking report! That data must really be valuable! Ok, maybe it could use some additional styling, but we now have the tools to dynamically render our build tree to give us completely different output without having to modify the build tree itself. We can even use the same View classes to render plain-text and html, and only need to define a new set of templates to support a third format. Not terrible for a few hours of work.
Hacking EVE
- Part 1 – Reading SDD Data
- Part 2 – Reading Market Data
- Part 3 – Let’s Build Something!
- Part 4 – Prettying Things Up with Mustache