Hacking EVE, Part 4 – Prettying Things Up with Mustache

orca1

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:

<!DOCTYPE html>
<html>
	<head>
		<title>{{title}}</title>
		<script src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
		<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
		<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css">
		<script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
 
		<style>
			body { margin: 1em auto; width: 800px; }
			thead { background: #ddd; }
			.table-condensed thead { background: #fff; }
			.label-column { width: 16em; }
			.all-items-row { background: #fff; }
			.market-row { background: #eee; }
		</style>
	</head>
	<body>
		{{#build}}
			{{{render}}}
		{{/build}}
	</body>
</html>

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:

<div id="build">
	<table class="table table-bordered jumbotron">
	<thead>
		<tr><th colspan="3">{{title}}</th></tr>
	</thead>
	<tbody>
		<tr><td class="label-column">Cost</td><td>{{cost}}</td></tr>
		<tr><td class="label-column">Value</td><td>{{value}}</td></tr>
		<tr><td class="label-column">Profit</td><td>{{profit}}</td></tr>
		<tr><td class="label-column">Profit margin</td><td>{{profit_margin}}</td></tr>
	</tbody>
	</table>
 
	{{#children}}
		{{{render}}}
	{{/children}}
</div>

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