15. Templates¶
Formatting pipeline output for humans!
In contrast to traditional command-line programs, the output of a Cog command is not plain text, but a JSON object. This common structure makes it easy to chain commands together into pipelines because downstream commands can easily reach into the JSON output to extract the data they need, without having to jump through the various text manipulation steps that are so frequently part of traditional command-line pipelines. When it’s time to present the final output of a pipeline, however, we often want something a bit more user-friendly than a dump of JSON data in our chat window.
To address this, Cog commands have the ability to specify a template that will be used to process the final pipeline output into a more readable format. This allows commands to return rich JSON objects for maximum flexibility in pipelines, but also condense these objects down to the salient information that humans need in chat (“Bundle foo, version 1.0.0 was enabled”, for example).
15.1. Basic Greenbar Syntax¶
The template language used by Cog is Greenbar, a language created by Operable for the needs of Cog. It is probably best described as a Markdown variant, with support for ERB-like tags.
Many familiar Markdown features are available, including boldface, italics, ordered and unordered lists, and monospace formatting; even tables. Iteration and conditional logic (among other features) are achieved through the use of “tags”. Data from result objects can be accessed through variable references. This example will illustrate many Greenbar features:
Example Greenbar Template.
~if cond=length($results) == 1~
The _only_ member of the group is **~$results[0].username~**.
~end~
~if cond=length($results) > 1~
The group has the following members:
~each var=$results as=user~
1. ~$user.username~
~end~
~end~
Note that all Greenbar instructions are enclosed in ~
characters.
Some, like ~if~
and ~each~
have bodies, with corresponding
~end~
terminators; bodies may contain plain text, or other Greenbar
instructions. Variable dereferencing is achieved with the use of the
$
operator within Greenbar instructions. Complex objects can be
navigated using familiar dot notation, and individual array members can
be addressed by a zero-based index. Markdown does not require special
Greenbar instructions, but is used directly (observe the italics on
“only”, the bolding of the single user’s name, and the generation of an
ordered list in the ~each~
iterator). Note also the presence of the
top-level “results” key, containing all the output of the Cog pipeline.
If you’re looking for more information on how to write templates, skip on down to Advanced Greenbar Usage.
15.2. Overall Template Processing Logic¶
In order to write effective templates, it helps to understand a bit about how Cog processes the output of pipelines, how templates are chosen, and how pipeline data is presented to the template.
15.2.1. All pipelines return a “results” list¶
For all successful pipeline runs, Cog will return a single JSON object to the template. This object will have a “results” key, which is always a list of objects returned from individual command invocations (it’s a list whether there is only a single result object or many).
15.2.2. Commands can specify a template when they return to Cog¶
When each command runs, it has the option of specifying a template to
use when formatting the output by returning a COG_TEMPLATE
value in
the output. The name of the template is resolved relative to the bundle
the command is a member of. If no template is specified, then Cog will
fall back to one of two “common” templates. If the command returns bare
text (instead of JSON, as is customary), then a special “text” template
is used. On the other hand, if JSON is returned, then the “raw” template
is used, which will pretty-print the results.
15.2.3. Last output “wins”¶
The return value of every Cog command invocation can specify a template to use, but pipelines can trigger multiple invocations in the course of processing. That can theoretically result in multiple templates being specified.
Templates are only truly needed at the end of a pipeline, however. If a command in the middle of a pipeline declares that its output should be formatted with the “foo” template, we don’t care, because we know that output is going to be modified further by the downstream commands. As soon as Cog wraps up one pipeline stage and moves on to the next, all template information collected to that point is discarded.
With respect to templating, it is the final stage that we care about. With Cog’s current implementation, each invocation can theoretically specify a different template; in reality, though, only the template specified by the final invocation is used to template all the data.
15.2.4. Meta-templates, not Templates¶
Cog’s templates are not actually templates, but rather “meta-templates”.
They do not generate text, but rather directives, instructions on how
to render text. This allows individual chat providers to determine
exactly how to format a given template. For example, the Slack provider
can interpret a bold
directive as *bold text*
, while a HipChat
provider can interpret the same directive as <b>bold text</b>
.
Note
You can see how Greenbar directives are processed for Slack in the code here.
By using this architecture, command authors only need to write a single template, which each chat provider can interpret in the best way for its host platform, instead of having to supply a template for each chat provider individually.
Note
The rendering of Greenbar templates to general directives, which are then processed by chat adapter-specific processors, is analogous to the interpretation of Java bytecode on platform-specific VMs, or the rendering of OpenGL directives by different graphics processors.
15.3. Advanced Greenbar Usage¶
Greenbar includs a variety of tags to help you better organze your output and also fully utilize the formatting options available from your chat provider. To view more information about all tags that come with Greenbar with examples for each, jump down to the Reference section titled Greenbar Tags. And, if you haven’t been able to find the tag you’re looking for, Greenbar also supports custom tags.
Note
While this document gives an overview of Greenbar and gives you a reference for tags you can use, we’re still pretty short on examples. If you want to see what some real life templates look like and all the ways tags can be used to accomplish normal formatting, take a look at all the templates used by commands included in Cog.
15.4. Writing a custom tag¶
All of the tags we’ve covered were implemented in Elixir using the
Greenbar.Tag
module, which you can also use to write your own custom
tags. Before we dive into writing our own, let’s take a look at a
super-simple example, the ~br~
tag:
defmodule Greenbar.Tags.Break do
use Greenbar.Tag, name: "br"
def render(_id, _attrs, scope) do
{:halt, %{name: :newline}, scope}
end
end
First, we use Greenbar.Tag
to set the name of the tag that we’ll use
in the template. Then, we implement render
which returns a newline.
The :halt
symbol in the tuple returned means that the tag has
finished rendering and we can continue processing the rest of the
template. There are a few more ways we can output values which are more
useful in tags that accept a body as we’ll see in the next example.
Now to implement our own tag. Let’s build a tag that converts the body to uppercase. For a template like this:
~upcase~
hello world
~end~
we’ll expect the final result to be:
HELLO WORLD
To start we can open up a new file named upcase.ex
and start out
with an empty module and use Greenbar.Tag
to set the name.
defmodule Upcase do
use Greenbar.Tag, name: "upcase"
end
Next, we need to implement the render
function using a new tuple,
{:once,
scope, child_scope}
. This creates a new scope for our tag body.
def render(_id, _attrs, scope) do
child_scope = new_scope(scope)
{:once, scope, child_scope}
end
I know what you’re thinking, “Where’s the String.upcase
call?” Well,
the render call is useful for changing scope and returning pre-defined
results, but if you want to modify the body of a tag, you’ll need to
implement a post_body
function. post_body
gives you access to
the attributes of the tag, the outside scope, the scope of the body and
a buffer containing all the parsed items from the template. All we need
to do is to iterate over the items in the buffer and upcase anything
that contains text.
def post_body(_id, _attrs, scope, _body_scope, %Buffer{items: items}) do
{:ok, scope, %Buffer{items: Enum.map(items, &upcase_directive/1)}}
end
def upcase_directive(%{name: :text, text: text} = directive),
do: %{directive | text: String.upcase(text)}
def upcase_directive(directive),
do: directive
Note
You’ll also have to include alias Greenbar.Runtime.Buffer
at the
top of the module.
And that should do it. Your final custom tag module will look like the following:
defmodule Cog.Tags.Upcase do
use Greenbar.Tag, name: "upcase", body: true
alias Greenbar.Runtime.Buffer
def render(_id, _attrs, scope) do
child_scope = new_scope(scope)
{:once, scope, child_scope}
end
def post_body(_id, _attrs, scope, _body_scope, %Buffer{items: items}) do
{:ok, scope, %Buffer{items: Enum.map(items, &upcase_directive/1)}}
end
def upcase_directive(%{name: :text, text: text} = directive),
do: %{directive | text: String.upcase(text)}
def upcase_directive(directive),
do: directive
end
Note
Modifying Cog’s source code to include custom tags is not ideal and wont be easy for everyone to include in their deploy process. Future versions of Cog will have a better way to include custom tags without modifying Cog or Greenbar, which can be more easily used with our Docker Compose install, for example.
To use this with Cog, we’re going to need to include this module in the
Cog codebase and set it as an available tag when creating the
Greenbar.Engine
. Move the upcase.ex
file we just created to
lib/cog/tags/upcase.ex
and rename the module to Cog.Tags.Upcase
.
Now open up lib/cog/template/new/evaluator.ex
and scroll down to the
bottom of the file to find the do_evaluate
function. We need to add
the upcase
tag to the engine. Directly after the line where we
create the engine, include this line to add our tag:
{:ok, engine} = Engine.add_tag(engine, Cog.Tags.Upcase)
The end result should look like:
def do_evaluate(name, source, data) do
{:ok, engine} = Engine.new
{:ok, engine} = Engine.add_tag(engine, Cog.Tags.Upcase)
engine
|> Engine.compile!(name, source)
|> Engine.eval!(name, data)
end
And that’s it, just restart Cog and you can use your new ~upcase~
tag in any template.
15.5. Customizing the standard error template¶
Cog uses a standard template to render errors that might occur when processing a pipeline. For example, when a user types the name of a command that does not exists, or if a command were to crash unexpectedly. The standard template contains a lot of information that is useful when developing bundles, but may a bit to much info for the average user. For this reason, it can be easily customized.
15.5.1. Configuring¶
Configuring Cog to use a custom error template is a two step process.
First create a template called error.greenbar
and place it in an
empty directory accessible to Cog. Then set
COG_CUSTOM_TEMPLATE_DIR to the path of said directory. After
setting the env var you can update or remove the custom template file
directly. No Cog restarts are required.
15.5.2. error.greenbar¶
Like all templates in Cog, the standard error template is written in greenbar. See Basic Greenbar Syntax for more info. Unlike templates defined for commands though, the standard error template does not receive a “results” list. Instead it receives a single object containing information about the error.
The error object contains the following keys:
- id
- The id of the pipeline.
- started
- The time stamp for the start of the pipeline.
- initiator
- The username of the one who initiated the pipeline.
- pipeline_text
- The complete text of the pipeline.
- error_message
- The error message returned by the pipeline.
- planning_failure
- When a pipeline fails during it’s planning stage, ie during variable
binding or when interpreting options, this will contain the portion
of the pipeline that generated the error. Otherwise this will be
false
. - execution_failure
- Similar to
$planning_failure
; when a pipeline fails during execution of the pipeline, this will contain the portion of the pipeline that caused the error. Otherwise this is set tofalse
.
The default error.greenbar as an example.
~attachment title="Command Error" color="#ff3333" Caller=$initiator Pipeline=$pipeline_text "Pipeline ID"=$id Started=$started~
~if cond=$planning_failure ~
The pipeline failed planning the invocation:
~br~
```
~$planning_failure~
```
~end~
~if cond=$execution_failure~
The pipeline failed executing the command:
~br~
```
~$execution_failure~
```
~end~
~br~
~br~
The specific error was:
~br~
```
~$error_message~
```
~end~