<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <id>tag:www.benpickles.com:,2005:/articles</id>
  <link rel="alternate" type="text/html" href="https://www.benpickles.com:"/>
  <link rel="self" type="application/atom+xml" href="https://www.benpickles.com/articles.atom"/>
  <title>Saying stuff about stuff.</title>
  <updated>2025-06-30T11:00:00Z</updated>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/95</id>
    <published>2025-06-30T11:00:00Z</published>
    <updated>2025-06-30T11:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/95-introducing-decant"/>
    <title>Introducing Decant</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;&lt;a href="https://github.com/benpickles/decant"&gt;Decant&lt;/a&gt; is a dependency-free frontmatter-aware framework-agnostic wrapper around a directory of static content (probably Markdown). It pairs perfectly with &lt;a href="https://parklife.dev"&gt;Parklife&lt;/a&gt; plus your favourite Ruby web framework to help you make a content-driven static website.&lt;/p&gt;

&lt;h2&gt;Usage&lt;/h2&gt;

&lt;p&gt;Start by defining a Decant model-like content class that points to a directory of files and their extension. You can declare convenience readers for common frontmatter data, and add your own methods – it’s a standard Ruby class.&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Decant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;dir: &lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ext: &lt;/span&gt;&lt;span class="s1"&gt;'md'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Declare frontmatter convenience readers.&lt;/span&gt;
  &lt;span class="n"&gt;frontmatter&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;

  &lt;span class="c1"&gt;# Add custom methods - it's a standard Ruby class.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shouty&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!!!"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then, given the file &lt;code&gt;content/about.md&lt;/code&gt; with the following contents:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;---
title: About
stuff: nonsense
---
# About

More words.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can &lt;code&gt;find&lt;/code&gt; the item by its extension-less path within the directory, read its content and frontmatter, and call &lt;code&gt;frontmatter&lt;/code&gt; convenience methods or your own manually-defined methods:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'about'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; "# About\n\nMore words.\n"&lt;/span&gt;
&lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frontmatter&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; {:title=&amp;gt;"About", :stuff=&amp;gt;"nonsense"}&lt;/span&gt;
&lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;       &lt;span class="c1"&gt;# =&amp;gt; "About"&lt;/span&gt;
&lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouty&lt;/span&gt;      &lt;span class="c1"&gt;# =&amp;gt; "ABOUT!!!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Nesting&lt;/h3&gt;

&lt;p&gt;Files can be nested so with the following layout:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;└ content/
  ├ about.md
  ├ features/
  │ ├ frontmatter.md
  │ ├ globs.md
  │ ├ nesting
  │ │ ├ lots.md
  │ │ └ more.md
  │ └ slugs.md
  └ usage.md
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A nested file can be targeted by its full path:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'features/nesting/more'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "More Nesting"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Which makes it easy to support catch-all-style routes, like this one in Rails:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Route.&lt;/span&gt;
&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/*slug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#show'&lt;/span&gt;

&lt;span class="c1"&gt;# Controller action.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="vi"&gt;@page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:slug&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or Sinatra:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/*slug'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vi"&gt;@page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:slug&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;erb&lt;/span&gt; &lt;span class="ss"&gt;:page&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Collections&lt;/h2&gt;

&lt;p&gt;Each class provides a few methods for accessing its collection of files. You’ve already seen &lt;code&gt;find&lt;/code&gt; which returns a single matching item, there’s also &lt;code&gt;all&lt;/code&gt; which returns all of the collection’s matching items, and &lt;code&gt;glob&lt;/code&gt;. With &lt;code&gt;glob&lt;/code&gt; you pass a shell-like pattern that can include some special characters, it uses &lt;code&gt;Pathname#glob&lt;/code&gt; so some &lt;a href="https://docs.ruby-lang.org/en/3.4/Dir.html#method-c-glob"&gt;knowledge of Ruby’s glob behaviour&lt;/a&gt; can be useful – though I imagine &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;**/*&lt;/code&gt; will be most commonly used. The other thing to note is that including the file extension isn’t necessary – in fact it &lt;em&gt;mustn’t&lt;/em&gt; be included because it will be added for you from the configured &lt;code&gt;ext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here are a couple of examples using the same nested file layout from earlier. You can target a subdirectory’s immediate files:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'features/*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Returns Page objects for:&lt;/span&gt;
&lt;span class="c1"&gt;# - features/frontmatter.md&lt;/span&gt;
&lt;span class="c1"&gt;# - features/globs.md&lt;/span&gt;
&lt;span class="c1"&gt;# - features/slugs.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or include everything from a directory down:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'features/**/*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Returns Page objects for:&lt;/span&gt;
&lt;span class="c1"&gt;# - features/frontmatter.md&lt;/span&gt;
&lt;span class="c1"&gt;# - features/globs.md&lt;/span&gt;
&lt;span class="c1"&gt;# - features/nesting/lots.md&lt;/span&gt;
&lt;span class="c1"&gt;# - features/nesting/more.md&lt;/span&gt;
&lt;span class="c1"&gt;# - features/slugs.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;The Content object&lt;/h2&gt;

&lt;p&gt;As well as the file’s content, its frontmatter, &lt;code&gt;frontmatter&lt;/code&gt;-added convenience methods, and your own manually-defined methods, a &lt;code&gt;Decant::Content&lt;/code&gt; object also knows a little about itself:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;code&gt;#path&lt;/code&gt; returns a &lt;code&gt;Pathname&lt;/code&gt; – which may or may not be an absolute path depending on the configured &lt;code&gt;dir&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;#relative_path&lt;/code&gt; returns the item’s relative path within its collection.&lt;/li&gt;
  &lt;li&gt;
&lt;code&gt;#slug&lt;/code&gt; returns the item’s “slug” (its extension-less relative path) within the collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'features/slugs'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;             &lt;span class="c1"&gt;# =&amp;gt; #&amp;lt;Pathname:content/features/slugs.md&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "/Users/dave/my-website/content/features/slugs.md"&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_path&lt;/span&gt;    &lt;span class="c1"&gt;# =&amp;gt; "features/slugs.md"&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slug&lt;/span&gt;             &lt;span class="c1"&gt;# =&amp;gt; "features/slugs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Frontmatter&lt;/h2&gt;

&lt;p&gt;Frontmatter must start with a line consisting of three dashes &lt;code&gt;---&lt;/code&gt;, then the YAML, then another line of three dashes (&lt;a href="https://jekyllrb.com/docs/front-matter/"&gt;just like Jekyll&lt;/a&gt;). The YAML should be key/value pairs and the returned &lt;code&gt;Hash&lt;/code&gt; will have &lt;code&gt;Symbol&lt;/code&gt; keys.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;---
title: Frontmatter
tags:
 - lists
 - are
 - fine
as:
  is:
    nesting: etc (it's YAML)
---
Content begins here
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above YAML frontmatter will be turned into the following Ruby data:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Frontmatter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;tags: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lists"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"are"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fine"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;is: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;nesting: &lt;/span&gt;&lt;span class="s2"&gt;"etc (it's YAML)"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;What, no Markdown?&lt;/h2&gt;

&lt;p&gt;You’re probably using Decant with collections of Markdown files – ala Jekyll and friends – so it might be a surprise to find that Decant knows nothing about Markdown and how to turn it into HTML but it’s just not a Decant concern; there are loads of different Markdown libraries out there and it’s up to you which one you choose. Besides, there’s more than just Markdown – remember &lt;a href="https://en.wikipedia.org/wiki/Textile_(markup_language)"&gt;Textile&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Anyway it’s generally simple to add Markdown support using the library of your choice configured exactly how you like it. Here I’m using &lt;a href="https://kramdown.gettalong.org"&gt;Kramdown&lt;/a&gt; with its GitHub-Flavoured Markdown support:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Decant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;dir: &lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ext: &lt;/span&gt;&lt;span class="s1"&gt;'md'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;html&lt;/span&gt;
    &lt;span class="no"&gt;Kramdown&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;input: &lt;/span&gt;&lt;span class="s1"&gt;'GFM'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_html&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'about'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "&amp;lt;h1 id=\"about\"&amp;gt;About&amp;lt;/h1&amp;gt;\n\n&amp;lt;p&amp;gt;More words.&amp;lt;/p&amp;gt;\n"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sinatra also has a builtin &lt;a href="https://sinatrarb.com/intro.html#markdown-templates"&gt;&lt;code&gt;markdown&lt;/code&gt; helper&lt;/a&gt; that can be used in a view:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= markdown @page.content %&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or directly from a route (and wrapped by a layout):&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/*slug'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vi"&gt;@page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="vi"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;layout_engine: :erb&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But maybe you can even get away without Markdown and use something like Rails’s &lt;code&gt;simple_format&lt;/code&gt; helper.&lt;/p&gt;

&lt;h2&gt;Maybe just make a website&lt;/h2&gt;

&lt;p&gt;When paired with Parklife plus the Ruby web framework of your choice Decant helps make it incredibly easy to make a content-driven static website with very little code and none of the constraints of a traditional static site framework. I’m using Decant in a few places (here for instance), the most visible is the &lt;a href="https://parklife.dev/"&gt;Parklife website&lt;/a&gt; that, thanks to Sinatra, has just &lt;a href="https://github.com/benpickles/parklife.dev/blob/main/app.rb"&gt;a handful of lines of code&lt;/a&gt; leaving me to get on with making a website.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/97</id>
    <published>2025-04-01T16:00:00Z</published>
    <updated>2025-04-02T12:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/97-find-the-longest-gap"/>
    <title>Challenge: find the longest time gap</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;Very occasionally I’m &lt;a href="/articles/66-trying-my-hand-at-code-golf"&gt;inspired by a code challenge&lt;/a&gt;, this time it’s &lt;a href="https://buttondown.com/cassidoo/archive/it-is-never-too-late-to-be-what-you-might-have/#:~:text=Interview%20question%20of%20the%20week"&gt;finding the longest gap in minutes between consecutive timestamps&lt;/a&gt; (via &lt;a href="https://ruby.social/@andycroll/114256040209256505"&gt;@andycroll&lt;/a&gt;). Here’s my approach using Ruby:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_longest_time_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;times&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;find_longest_time_gap&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'15:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10:30'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; 210&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Breaking it down.&lt;/p&gt;

&lt;p&gt;I’m using a “numbered parameter” &lt;code&gt;_1&lt;/code&gt; (which I kinda like more than the newer &lt;code&gt;it&lt;/code&gt; 😬 though I’m not sure I’d use either in “proper” code). This is split into two-and-only-two parts (it’s maybe a bit defensive but I like that it’s explicit) and turned into integers to get an array of hours/minutes tuples:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; [[14, 0], [9, 0], [15, 0], [10, 30]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;They’re converted to minutes. Here I’m expanding each two-element tuple/array block argument into two variables (I’m not sure what this is called) which is helpful in these simple cases because it turns it into a neat single line:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; [840, 540, 900, 630]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The original times may not be in order so we need to &lt;code&gt;sort&lt;/code&gt; them:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; [540, 630, 840, 900]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ruby has a wealth of useful methods – I’m thinking of &lt;code&gt;chunk_while&lt;/code&gt; – so I knew there would be &lt;em&gt;something&lt;/em&gt; to help me out. I cheated a little and peeked at Andy’s solution &lt;a href="https://ruby.social/@andycroll/114256040209256505"&gt;👀&lt;/a&gt; which revealed that this time it’s &lt;code&gt;each_cons&lt;/code&gt; (its name was bound to contain an underscore).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;each_cons&lt;/code&gt; is one of those methods I find slightly weird. It’s called “each” which suggests a side-effect rather than something being returned, but calling it without a block returns an &lt;code&gt;Enumerator&lt;/code&gt; which I think I’d want more often (&lt;code&gt;map_cons&lt;/code&gt;?).&lt;/p&gt;

&lt;p&gt;Here’s what &lt;code&gt;each_cons(n)&lt;/code&gt; does:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Original array&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# each_cons(2)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# each_cons(3)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So here’s the &lt;code&gt;each_cons(2)&lt;/code&gt; magic:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_cons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; [[540, 630], [630, 840], [840, 900]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can map over these to get the differences:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; [90, 210, 60]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then get the maximum value. If the array has fewer items than the number passed to &lt;code&gt;each_cons&lt;/code&gt; then the returned &lt;code&gt;Enumerator&lt;/code&gt; will be empty and the call to &lt;code&gt;max&lt;/code&gt; will return &lt;code&gt;nil&lt;/code&gt; so we should default to zero:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; 210&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; I realised I hadn’t read the question fully and had an incorrect assumption that the times would always be sorted. I fixed that, and the case where there’s only one time (or zero) and therefore no gap, by adding some tests:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestMe&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_cases&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;find_longest_time_gap&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'12:00'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;find_longest_time_gap&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'11:00'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;find_longest_time_gap&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'15:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10:30'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;find_longest_time_gap&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'08:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'14:00'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/94</id>
    <published>2024-05-15T09:00:00Z</published>
    <updated>2024-05-15T09:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/94-operatic-0-7"/>
    <title>Operatic 0.7</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;&lt;a href="https://github.com/benpickles/operatic"&gt;Operatic&lt;/a&gt; defines a minimal standard interface to encapsulate your Ruby operations. The job of Operatic is to receive input and make it available to the operation, and to gather output and return it via a result object. This leaves you a well-defined space to write the actual code by implementing the &lt;code&gt;#call&lt;/code&gt; method – and with so much of the ceremony taken care of it feels like writing a function but in a Ruby wrapping.&lt;/p&gt;

&lt;p&gt;Version 0.7.0 introduces concrete &lt;code&gt;Success&lt;/code&gt;/&lt;code&gt;Failure&lt;/code&gt; result classes which brings a slight change to the API and may therefore impose some tweaks to your code.&lt;/p&gt;

&lt;p&gt;In earlier versions an operation gathered data on its result object and marked the result as a success or failure. Now the operation gathers data on a separate &lt;code&gt;Operatic::Data&lt;/code&gt; object and it’s the operation that controls the success/failure status by choosing the appropriate &lt;code&gt;Success&lt;/code&gt;/&lt;code&gt;Failure&lt;/code&gt; result class and initialising it with its data.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Operatic::Data&lt;/code&gt; could have perhaps been a plain &lt;code&gt;Hash&lt;/code&gt; but I wanted to retain the ability to define a per-operation subclass with custom accessors (which remain accessible via the result thanks to the magic of &lt;code&gt;#method_missing&lt;/code&gt;). I couldn’t use the &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2 because it’s frozen at the point of creation whereas in an operation data is gathered throughout execution and frozen on completion.&lt;/p&gt;

&lt;p&gt;Having a concrete success/failure result class also makes pattern matching clearer by replacing an anonymous boolean (&lt;code&gt;[true, { message: }]&lt;/code&gt;) with the self-documenting result class itself:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Dave'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Operatic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="c1"&gt;# Result is a success, do something with the `message` variable.&lt;/span&gt;
&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Operatic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="c1"&gt;# Result is a failure, ignore any data and do something else.&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Altogether I’m pleased with the overall refactor, I think it’s resulted in a better API by more clearly defining the roles and responsibilities of each of the component parts.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/93</id>
    <published>2024-02-05T09:00:00Z</published>
    <updated>2024-02-05T09:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/93-streaming-phlex-from-sinatra"/>
    <title>Streaming Phlex from Sinatra</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;Sinatra supports streaming, Phlex supports streaming, but until recently I hadn’t put the two together in &lt;a href="https://github.com/benpickles/phlex-sinatra"&gt;phlex-sinatra&lt;/a&gt;. Well now I have and from version 0.3 you can pass &lt;code&gt;stream: true&lt;/code&gt; like so:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/foo'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;phlex&lt;/span&gt; &lt;span class="no"&gt;MyView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stream: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When streaming is enabled Phlex will automatically flush to the response after a closing &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt; which means that the browser can start processing external resources as quickly as possible while the rest of the view is being generated. Even with no further intervention this small change should improve your &lt;a href="https://web.dev/articles/ttfb"&gt;Time to First Byte&lt;/a&gt; and could improve your &lt;a href="https://web.dev/articles/fcp"&gt;First Contentful Paint&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It took very little code to integrate Phlex with Sinatra’s streaming but it did require a lot of learning and understanding, some of which will be useful to you dear reader. The first thing to note is that streaming with Sinatra requires a compatible server like Puma (it appears that WEBrick isn’t compatible) but the main thing to be aware of is the assortment of buffers between your code and the receiving client.&lt;/p&gt;

&lt;h2&gt;Buffers buffers buffers&lt;/h2&gt;

&lt;p&gt;It’s normal for a web framework to wait until a page has been fully generated (to buffer) before sending the response – so that the &lt;code&gt;Content-Length&lt;/code&gt; can be determined, among other things. Sinatra (et al) provides a way to bypass its buffer and &lt;a href="https://sinatrarb.com/intro.html#streaming-responses"&gt;write directly to the response&lt;/a&gt;, and it’s this object that’s passed to Phlex.&lt;/p&gt;

&lt;p&gt;I was using curl to inspect the response and although I could now see the immediately-streamed HTTP headers (pass &lt;a href="https://curl.se/docs/manpage.html#-i"&gt;&lt;code&gt;-i&lt;/code&gt;/&lt;code&gt;--include&lt;/code&gt;&lt;/a&gt; to curl to include the headers in its output) the rest of the response was only shown once complete. I spent an age investigating both Sinatra and Phlex’s internals only to realise that everything was working correctly and the culprit was curl itself which has its own buffer. Curl behaves like other command-line tools and outputs &lt;em&gt;lines&lt;/em&gt; but Phlex “uglifies by default” (it doesn’t add extra whitespace to generate pretty HTML) and this lack of newlines exacerbated the apparent problem – the trick is to pass &lt;a href="https://curl.se/docs/manpage.html#-N"&gt;&lt;code&gt;-N&lt;/code&gt;/&lt;code&gt;--no-buffer&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But streaming &lt;em&gt;still&lt;/em&gt; wasn’t working as expected. It was then I discovered that Phlex has a buffer that’s automatically flushed after a closing &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt; tag, but it’s otherwise left to the developer to choose if and when to call the provided &lt;a href="https://github.com/phlex-ruby/phlex/blob/c88b1bd9ade55bea0b600dcf0d1c1d95601fd47c/lib/phlex/sgml.rb#L191-L197"&gt;&lt;code&gt;#flush&lt;/code&gt;&lt;/a&gt; method (I’m guessing Phlex doesn’t write directly to the buffer due to some sort of performance penalty).&lt;/p&gt;

&lt;p&gt;One more thing that I didn’t encounter while testing but that it’s worth being aware of is that other intermediaries can buffer the response without your knowledge, for example the reason for adding a &lt;code&gt;X-Accel-Buffering=no&lt;/code&gt; header is &lt;a href="https://piotrmurach.com/articles/streaming-large-zip-files-in-rails/#:~:text=Web%20servers%20like%20Nginx%20perform%20buffering"&gt;explained here&lt;/a&gt; (the whole article is well worth a read).&lt;/p&gt;

&lt;h2&gt;When to use streaming&lt;/h2&gt;

&lt;p&gt;So… I think ideally you shouldn’t &lt;em&gt;need&lt;/em&gt; to use streaming at all but it’s easy to find yourself with a particularly problematic page (I’m thinking of a very long list or if it depends on some slow external service) where streaming can help. Adding pagination or completely rethinking the approach is quite probably the proper solution but enabling streaming combined with some judicious calls to &lt;code&gt;#flush&lt;/code&gt; could be enough to patch over the issue and give some breathing room for the real solution.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/92</id>
    <published>2023-06-01T09:00:00Z</published>
    <updated>2023-06-01T09:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/92-phlex-sinatra"/>
    <title>Using Phlex in Sinatra with phlex-sinatra</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;Phlex already works with Sinatra (and everything else) but its normal usage leaves you without access to Sinatra’s standard helper methods. That’s why I created &lt;a href="https://github.com/benpickles/phlex-sinatra"&gt;phlex-sinatra&lt;/a&gt; which lets you use Sinatra’s &lt;code&gt;url()&lt;/code&gt; helper from within Phlex (along with the rest of the usual helper methods available in a Sinatra action).&lt;/p&gt;

&lt;p&gt;To enable the integration use the &lt;code&gt;phlex&lt;/code&gt; method in your Sinatra action and pass an &lt;em&gt;instance&lt;/em&gt; of the Phlex view (instead of using &lt;code&gt;.call&lt;/code&gt; to get its output):&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/foo'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;phlex&lt;/span&gt; &lt;span class="no"&gt;MyView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can now use Sinatra’s &lt;code&gt;url()&lt;/code&gt; helper method directly and its other methods (&lt;code&gt;params&lt;/code&gt;, &lt;code&gt;request&lt;/code&gt;, etc) via the &lt;code&gt;helpers&lt;/code&gt; proxy:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyView&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Phlex&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;template&lt;/span&gt;
    &lt;span class="n"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'Phlex / Sinatra integration'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/foo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'link to foo'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;pre&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Why?&lt;/h2&gt;

&lt;p&gt;It might not seem obvious at first why you’d use &lt;code&gt;url()&lt;/code&gt; at all given that you mostly just pass the string you want to output 🤷🏻‍♂️ but I hit the issue immediately when I switched to Phlex in my &lt;a href="/articles/91-my-wordle-results"&gt;Wordle results Sinatra/Parklife microsite&lt;/a&gt; hosted on GitHub Pages.&lt;/p&gt;

&lt;p&gt;One of the main features of using Parklife is that your development flow remains completely unchanged. In development you start the server as usual which means the app is almost certainly served from the root &lt;code&gt;/&lt;/code&gt;, but if the static site is hosted as a GitHub Pages repository site it’ll be served from &lt;code&gt;/my-repository-name&lt;/code&gt; – which means &lt;em&gt;all your links will be broken in production&lt;/em&gt;! It’s incredibly frustrating but luckily easily fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; is to use Sinatra’s &lt;code&gt;url()&lt;/code&gt; helper method wherever you need a URL (the &lt;code&gt;false&lt;/code&gt; second argument means the scheme/host isn’t included):&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/app.css'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;rel: &lt;/span&gt;&lt;span class="s1"&gt;'stylesheet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'text/css'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;, configure a Parklife &lt;code&gt;base&lt;/code&gt;:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Parklife&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/wordle'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s also possible to pass &lt;code&gt;--base&lt;/code&gt; at build-time, in fact if you used Parklife to generate a GitHub Actions workflow (&lt;code&gt;parklife init --github-pages&lt;/code&gt;) then it’s already configured to fetch your GitHub Pages site URL – whether it’s a custom domain or a standard repository site – and pass it to the build script so you won’t need to manually configure it as above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; (profit?). The result is that when Parklife generates the static build Sinatra will know to serve the site from the &lt;code&gt;/wordle&lt;/code&gt; subpath and will include the prefix on all &lt;code&gt;url()&lt;/code&gt;-generated URLs:&lt;/p&gt;

&lt;pre class="highlight" data-lang="html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/wordle/app.css"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Another main reason to use the &lt;code&gt;url()&lt;/code&gt; helper is to generate a full URL – for instance from within a feed or for an &lt;code&gt;og:image&lt;/code&gt; social media preview link. In this case don’t pass the &lt;code&gt;false&lt;/code&gt; second argument (it defaults to &lt;code&gt;true&lt;/code&gt;) and the full URL will be generated. Once again you’ll need to configure Parklife with the correct base but once again it’s already taken care of if you generated the GitHub Actions workflow with &lt;code&gt;parklife init --github-pages&lt;/code&gt;.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/91</id>
    <published>2023-05-25T12:00:00Z</published>
    <updated>2023-05-25T12:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/91-my-wordle-results"/>
    <title>My Wordle results, a Sinatra/Parklife app</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;div&gt;&lt;img src="https://www.benpickles.com/parklife/blobs/9e29047c73ef2462eb1591de91446c33/my-wordle-results.png" /&gt;&lt;/div&gt;&lt;p&gt;Like many others last year I fell into a daily Wordle routine – I also collected the results. It’s taken a while but I’ve turned the data into a microsite of &lt;a href="https://www.benpickles.com/wordle/"&gt;my Wordle results&lt;/a&gt;. It’s a Sinatra/Parklife app and a good (albeit tiny) example of using &lt;a href="/articles/90-introducing-parklife"&gt;Parklife&lt;/a&gt; in the wild showing just how incredibly easy it is to take a standard Sinatra app and turn it into a static production build.&lt;/p&gt;

&lt;p&gt;Whilst Ruby and its wealth of web libraries make it easy to create a web site the question of deployment isn’t as simple as it used to be. Back when Heroku had a free plan it would have been a prime choice for a little app like this and even though the content is essentially static it would still be a good match, but $60/year is enough of a nudge to reconsider whether &lt;em&gt;essentially&lt;/em&gt; static should be &lt;em&gt;actually&lt;/em&gt; static. I suspect this is a common situation.&lt;/p&gt;

&lt;p&gt;The site itself is custom content and code that wouldn’t easily be squeezed into a more traditional blogging engine which is one of the reasons I think it’s a good example of why you should use &lt;a href="https://github.com/benpickles/parklife"&gt;Parklife&lt;/a&gt; and demonstrates the space that Parklife fills in the Ruby world of static sites.&lt;/p&gt;

&lt;p&gt;In the past I’ve generated a number of custom static sites with Ruby, each time getting to know ERB and each one resulting in its own unique build process using Rake, Make, or some other bunch of scripts to generate HTML. They all worked fine and were satisfying to build but it’s not a sustainable, repeatable approach and it isn’t suitable for everyone. Parklife completely changes the landscape in this regard because it allows you to use any of the great existing frameworks you already know and lets you follow the same familiar approach as all your other Ruby apps – did I mention your preferred development flow remains completely unchanged.&lt;/p&gt;

&lt;p&gt;My Wordle results are stored in a text file (currently 467 results, 2924 lines) that’s parsed into Result objects and served by a &lt;a href="https://sinatrarb.com"&gt;Sinatra&lt;/a&gt; app (a single route). At first I was surprised just how large the HTML was (its file size) and made a scattering of changes in an attempt to reduce it before finally switching to &lt;a href="https://www.phlex.fun"&gt;Phlex&lt;/a&gt; which doesn’t output additional whitespace (you could say it uglifies by default…). The final piece was to add Parklife and run &lt;code&gt;parklife init --sinatra --github-pages&lt;/code&gt; to generate everything required to turn the app into a static site including a GitHub Actions workflow to build and deploy to GitHub Pages. Oh and the repo is &lt;a href="https://github.com/benpickles/wordle"&gt;public on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hopefully this example will encourage you to avoid dependency hell and use Ruby for your next static site.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/90</id>
    <published>2023-03-22T10:00:00Z</published>
    <updated>2023-03-22T10:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/90-introducing-parklife"/>
    <title>Introducing Parklife</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;&lt;a href="https://github.com/benpickles/parklife"&gt;Parklife&lt;/a&gt; turns any Rack app (Rails, Sinatra, Hanami, Roda, Camping) into a static HTML build ready to be hosted on GitHub Pages, Netlify, Now, S3, or any other web server.&lt;/p&gt;

&lt;p&gt;The great thing is that your development flow remains completely unchanged – keep working with the frameworks you already know and love and all those other gems you favour, keep running the usual development server, and keep writing unit and browser tests. Then when it comes to production, instead of deploying and managing a server-side Ruby app you generate a static build that can live indefinitely with no maintenance cost.&lt;/p&gt;

&lt;p&gt;While Parklife works great with Rails – this site is a 15+ year old Rails app using SQLite/Paperclip/Sprockets+Sass/Parklife and hosted on Netlify – where I’ve really felt its impact is with Sinatra. Now you can use Sinatra’s renowned rapid and minimal server-side development flow to build a static production site – I feel like it’s fully unlocked Sinatra’s microbenefits and installed it in its rightful place as monarch of the microframeworks. So now there’s no reason not to use Ruby for those little ad hoc microsites that might otherwise have ended up using some other complicated technology – or maybe even not be created at all.&lt;/p&gt;

&lt;p&gt;Parklife doesn’t start an actual web server and instead interfaces directly with your app via Rack by sending mock HTTP requests and writing the response body to disk – this is what allows it to work with any Rack-compatible framework. It can also detect and follow links to crawl an entire site so you can often get away with only configuring a starting root route.&lt;/p&gt;

&lt;p&gt;It’s easy to add Parklife to your app, start by &lt;a href="https://rubygems.org/gems/parklife"&gt;installing the gem&lt;/a&gt; and then run &lt;code&gt;parklife init --sinatra --github-pages&lt;/code&gt; to generate a starter &lt;code&gt;Parkfile&lt;/code&gt; configuration file (tailored for a Sinatra app), a build script, and a full GitHub Actions workflow to generate and deploy your site to GitHub Pages whenever you push to the main branch. It’s also not a one-way trip because at any point in the future you can choose to set sail and return to a life on the server-side seas – you can’t lose.&lt;/p&gt;

&lt;p&gt;For me Parklife has somehow managed to roll back the decades to bring simplicity back to making websites. I’ve once again felt that je ne sais quoi immediacy of writing HTML in Dreamweaver and syncing it over FTP… Only this time I get to use today’s Ruby, today’s frameworks, and today’s CI and hosting providers.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/89</id>
    <published>2023-01-24T18:00:00Z</published>
    <updated>2023-01-24T18:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/89-git-aliases-that-make-my-life-easier"/>
    <title>Git aliases that make my life easier</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;Before thinking about aliases the best thing you can do to make git nicer to use is configure tab completion. If you’re using Homebrew follow their &lt;a href="https://docs.brew.sh/Shell-Completion"&gt;shell completion docs&lt;/a&gt; otherwise you’re on your own – but do it! The great thing about git aliases is that they’re also tab completable.&lt;/p&gt;

&lt;p&gt;Here are the git aliases I use &lt;strong&gt;all the time&lt;/strong&gt;, add them to your &lt;code&gt;~/.gitconfig&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[alias]
  browse = !gh browse
  co = checkout
  recent = branch --sort=-committerdate
  st = status
  touch = commit --amend --date=now --no-edit
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;First of all &lt;code&gt;git co&lt;/code&gt; and &lt;code&gt;git st&lt;/code&gt; are a throwback to my SVN transition days but they really should come as default – on unfamiliar systems I can’t believe how much of an impediment it is having to type the whole of &lt;code&gt;git status&lt;/code&gt; each time.&lt;/p&gt;

&lt;p&gt;I’ve only just switched from &lt;a href="https://github.com/github/hub"&gt;&lt;code&gt;hub&lt;/code&gt;&lt;/a&gt; (which seems to be very nearly deprecated) to GitHub’s &lt;a href="https://github.com/cli/cli"&gt;“official” command line tool&lt;/a&gt;. Hub’s &lt;code&gt;browse&lt;/code&gt; command was pretty much the only thing I used and although &lt;code&gt;gh&lt;/code&gt; also comes with its own &lt;code&gt;browse&lt;/code&gt; command my muscle memory is just too ingrained to be able to switch. Fortunately &lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-alias"&gt;adding a preceding &lt;code&gt;!&lt;/code&gt; will execute the alias as an external shell command&lt;/a&gt; so now I can type &lt;code&gt;git browse&lt;/code&gt; and it will run &lt;code&gt;gh browse&lt;/code&gt; for me. It’s not me it’s you.&lt;/p&gt;

&lt;p&gt;Focus and deep work are an imperative to achieving quality and continual progress but sometimes reality hits and you find yourself working across many branches at the same time. Now what was the name of that branch I was working on the other day 🤔 This is an example of a feature that’s already part of git but only if you know the magic incantation – it’s &lt;code&gt;git branch --sort=-committerdate&lt;/code&gt; or &lt;code&gt;git recent&lt;/code&gt; with this alias.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;git touch&lt;/code&gt; more than I should admit but something in me needs the commit’s date to represent the real date I finished the commit (even though squash and merge will have its way eventually). FYI you can &lt;a href="https://git-scm.com/docs/git-commit#_date_formats"&gt;pass any date&lt;/a&gt; and use &lt;code&gt;--amend&lt;/code&gt; for evil 😈 “Yes I really was working late last night” &lt;code&gt;git commit --amend --date=2023-01-23T23:34:51 --no-edit&lt;/code&gt; 😴&lt;/p&gt;

&lt;p&gt;One more bonus alias that’s useful but I rarely use:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[alias]
  graph = log --oneline --graph
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is less of a graph nowadays when everything’s squashed and merged 😞 but I find being able to visually navigate the git tree really useful. Again this is already a part of git if you can remember where to find it but with an alias it’s a single tab-completable command away. The nice thing with all of these is that you can pass further options and they’re appended to the alias, so with this you can include all local branches with &lt;code&gt;git graph --all&lt;/code&gt; or all remote branches with &lt;code&gt;git graph --remotes&lt;/code&gt; (again tab completion came to my rescue – even through the alias – because I didn’t remember if it was &lt;code&gt;--remote&lt;/code&gt; singular).&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/88</id>
    <published>2022-09-07T11:00:00Z</published>
    <updated>2022-09-07T11:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/88-vim-syntax-highlight-markdown-code-blocks"/>
    <title>Syntax highlighting Markdown fenced code blocks in Vim</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;I’ve only just learned that Vim can &lt;a href="https://github.com/tpope/vim-markdown"&gt;syntax highlight Markdown fenced code blocks by their language&lt;/a&gt; with the addition of a setting in your &lt;code&gt;.vimrc&lt;/code&gt;:&lt;/p&gt;

&lt;pre class="highlight" data-lang="vim"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;g:markdown_fenced_languages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'js=javascript'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ruby'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;js=javascript&lt;/code&gt; syntax above means that a fenced code block with the language &lt;code&gt;js&lt;/code&gt; will load the &lt;code&gt;javascript&lt;/code&gt; Vim syntax file.&lt;/p&gt;

&lt;p&gt;It’s worth noting that this &lt;a href="https://thoughtbot.com/blog/profiling-vim"&gt;can slow down opening a Markdown file&lt;/a&gt; so it sounds like it’s best not to stuff the list with every known language.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
  <entry>
    <id>tag:www.benpickles.com:,2005:Article/87</id>
    <published>2022-08-23T09:00:00Z</published>
    <updated>2022-08-23T09:00:00Z</updated>
    <link rel="alternate" type="text/html" href="https://www.benpickles.com/articles/87-announcing-operatic"/>
    <title>Announcing Operatic</title>
    <content type="html" xml:base="https://www.benpickles.com">&lt;p&gt;&lt;a href="https://github.com/benpickles/operatic"&gt;Operatic&lt;/a&gt; defines a minimal standard interface to encapsulate your Ruby operations (often referred to as “service objects”). The job of Operatic is to receive input and make it available to the operation, and to gather output and return it via a result object, this leaves you a well-defined space to write the actual code by implementing the &lt;code&gt;#call&lt;/code&gt; method. And with so much of the ceremony taken care of it feels like writing a function but in a Ruby wrapping.&lt;/p&gt;

&lt;aside&gt;
  Version 0.7.0 of Operatic &lt;a href="/articles/94-operatic-0-7"&gt;introduced some small API changes&lt;/a&gt; which have been reflected here.
&lt;/aside&gt;

&lt;h2&gt;Creating an Operatic operation&lt;/h2&gt;

&lt;p&gt;To define an operation include &lt;code&gt;Operatic&lt;/code&gt; and implement the instance method &lt;code&gt;#call&lt;/code&gt;, data can be attached to the operation’s &lt;code&gt;#data&lt;/code&gt; object:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SayHello&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Operatic&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Hello world'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The operation is called with the class method &lt;code&gt;.call&lt;/code&gt; and a &lt;code&gt;Success&lt;/code&gt;/&lt;code&gt;Failure&lt;/code&gt; result object is returned with the operation’s data:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; Operatic::Success&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Hello world"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Data can be passed to the operation via keyword arguments that are set as instance variables and can be made available as methods with a standard Ruby &lt;code&gt;attr_reader&lt;/code&gt;:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SayHello&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Operatic&lt;/span&gt;

  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hello &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Dave'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; Operatic::Success&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Hello Dave"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;An operation can be specifically marked as a success or failure using &lt;code&gt;#success!&lt;/code&gt;/&lt;code&gt;#failure!&lt;/code&gt; and data can be attached at the same time:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SayHello&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Operatic&lt;/span&gt;

  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;failure!&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;

    &lt;span class="n"&gt;success!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Hello &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Dave'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; Operatic::Success&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Hello Dave"&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;     &lt;span class="c1"&gt;# =&amp;gt; Operatic::Failure&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once the operation has completed or its status specifically declared using &lt;code&gt;#success!&lt;/code&gt;/&lt;code&gt;#failure!&lt;/code&gt; the operation, its result, and its data are frozen and any attempt at modification will raise a &lt;code&gt;FrozenError&lt;/code&gt;. This helps to ensure that even a complicated operation with many code paths can only finalise its result once:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SayHello&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Operatic&lt;/span&gt;

  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;success!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Hello &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;success!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Goodbye &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Dave'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; FrozenError (can't modify frozen SayHello...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Using Operatic in Rails&lt;/h2&gt;

&lt;p&gt;A Rails controller might use Operatic like this:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HellosController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;plain: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;A pattern matching future&lt;/h2&gt;

&lt;p&gt;Where I think Operatic gets interesting is in being able to pattern match against an &lt;code&gt;Operatic::Result&lt;/code&gt; (in Ruby 2.7+) allowing you to think of it as a standardised tuple – an array of its result class and data – instead of an object with methods.&lt;/p&gt;

&lt;p&gt;Here’s the Rails example rewritten to use pattern matching, I think it’d be fair to describe it as dense (particularly if you’re unfamiliar with pattern matching) though I’d prefer to call it succinct. But it’s doing quite a lot by allowing you to declare both the expected result type and the shape of the data, and to extract certain keys, all at the same time:&lt;/p&gt;

&lt;pre class="highlight" data-lang="ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HellosController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;SayHello&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Operatic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;plain: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Operatic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;In summary&lt;/h2&gt;

&lt;p&gt;Operatic defines a standard interface for your operations and takes care of input and output leaving you to concentrate on writing code.&lt;/p&gt;

&lt;p&gt;You can find the &lt;a href="https://github.com/benpickles/operatic"&gt;source on GitHub&lt;/a&gt;.&lt;/p&gt;</content>
    <author>
      <name>Ben Pickles</name>
    </author>
  </entry>
</feed>
