Saying stuff about stuff.

Announcing Operatic

Operatic 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 #call method. And with so much of the ceremony taken care of it feels like writing a function but in a Ruby wrapping.

Creating an Operatic operation

To define an operation include Operatic and implement the instance method #call, data can be attached to the operation’s Operatic::Result object:

class SayHello
  include Operatic

  def call
    result[:message] = 'Hello world'
  end
end

The operation is called with the class method .call and the result object is returned with its attached data, the result is either a #success? or a #failure?:

result = SayHello.call
result.success?  # => true
result.failure?  # => false
result[:message] # => "Hello world"

Data can be passed via keyword arguments that are set as instance variables on the operation and can be made available as methods with a standard Ruby attr_reader:

class SayHello
  include Operatic

  attr_reader :name

  def call
    result[:message] = "Hello #{name}"
  end
end

result = SayHello.call(name: 'Dave')
result.success?  # => true
result.failure?  # => false
result[:message] # => "Hello Dave"

An operation can specifically mark its result as a success or failure using #success!/#failure! (which are convenience delegates to the Operatic::Result’s counterparts) and attach data at the same time:

class SayHello
  include Operatic

  attr_reader :name

  def call
    return failure! unless name

    success!(message: "Hello #{name}")
  end
end

result = SayHello.call(name: 'Dave')
result.success?  # => true
result.failure?  # => false
result[:message] # => "Hello Dave"

result = SayHello.call
result.success?  # => false
result.failure?  # => true
result[:message] # => nil

Once the result is returned or has been marked as a success/failure it’s frozen and any further attempt at modification will raise a FrozenError. This helps to ensure that even a complicated operation with many code paths can only finalise its result once:

result = Operatic::Result.new
result.success!
result.success!
# => #<FrozenError: can't modify frozen Operatic::Result: #<Operatic::Result:0x00000001130c0710 @data={}, @success=true>>

Using Operatic in Rails

A Rails controller might use Operatic like this:

class HellosController < ApplicationController
  def create
    result = SayHello.call(name: params[:name])

    if result.success?
      render plain: result[:message]
    else
      render :new
    end
  end
end

A pattern matching future

Where I think Operatic gets interesting is in being able to pattern match against an Operatic::Result (in Ruby 2.7+) allowing you to think of it as a standardised tuple – an array of its success status and data – instead of an object with methods.

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 status and the shape of the data, and to extract certain keys, all at the same time:

class HellosController < ApplicationController
  def create
    case SayHello.call(name: params[:name])
    in [true, { message: }]
      render plain: message
    in [false, _]
      render :new
    end
  end
end

In summary

Operatic defines a standard interface for your operations and takes care of input and output leaving you to concentrate on writing code.

You can find the source on GitHub.

The Return of Screamshot

Screamshot is an HTTP screenshot service I first released back in 2016. Although light on features it served its purpose before suffering from the usual bitrot, but I’ve finally resurrected the project and updated it to meet my current needs.

The biggest change is that Screamshot now uses Chrome. Back in 2016 I found PhantomJS to be the most convenient headless browser to run — especially on Heroku — but headless browsers have moved on greatly over the intervening years and it was deprecated soon after the release of headless Chrome in mid 2017.

I’ve added some options that were missing from the previous version like being able to specify the size of the window/viewport, the scale of the image to capture (retina/devicePixelRatio), and whether to capture the full page. But my favourite addition is prefers-reduced-motion=reduce which can be used as a standards-based hint to the page to disable animations that might otherwise be visually problematic in a static screenshot. Screenshot services often include a delay option that can be used (or abused) to wait for animations to finish but if you support prefers-reduced-motion=reduce in your pages then you can prevent animations from running in the first place which not only makes taking a screenshot easier but can also aid accessibility — win win.

Another big difference is that I’ve added a much more comprehensive test suite which will make the project easier to maintain and help prevent further bitrot. The app now includes a set of tests that exercise the entire stack and verify that everything is working as expected by checking a few key pixels in the captured images.

As ever the source is on GitHub and pull requests are welcome. It’s a surprisingly small amount of code for the functionality it provides but that’s because it rests on a heap of great open source libraries like ferrum, rack, sinatra, and capybara to name just a few.

Supporting fractions in Ruby/Rails

Unicode comes with a selection of common single-character fractions like ½, ¼, and even ⅐ (see en.wikipedia.org/wiki/Number_Forms for more), but I need to store and display somewhat arbitrary fractions in a Rails app and it isn’t something I’ve had to do before so I’m documenting my workings.

Ruby has the Rational class that makes it easy to work with fractions and can perform a specific calculation I need (½ + ⅐ = ⁹⁄₁₄):

a = Rational(1, 2)
# => (1/2)

b = Rational(1, 7)
# => (1/7)

a + b
# => (9/14)

My numbers are stored in a PostgreSQL database which doesn’t support rationals but fortunately in Ruby any number can be turned into a Rational with #to_r:

0.5.to_r
# => (1/2)

Unfortunately you don’t often get a sensible fraction:

(9.0 / 14).to_r
# => (5790342378047781/9007199254740992)

A Rational can be simplified with Rational#rationalize and with a bit of trial and error I determined that to arrive at ⁹⁄₁₄ I need to store the number with 3 decimal places and pass a magic number of ~0.005 to Rational#rationalize:

0.643.to_r
# => (2895814560399229/4503599627370496)

0.643.to_r.rationalize(0.1)
# => (2/3)

0.643.to_r.rationalize(0.01)
# => (7/11)

0.643.to_r.rationalize(0.001)
# => (9/14) ✅

# Can I store the number with two decimal places? No.
0.64.to_r.rationalize(0.001)
# => (16/25)

# What magic number do I need?
0.643.to_r.rationalize(0.005)
# => (9/14) ✅

Next, I can never remember how to use decimals with Rails/PostgreSQL. The following Rails migration adds a decimal type column that can store numbers up to 999.999 — precision: 6 means the number can have total of 6 digits, scale: 3 means 3 of those digits come after the decimal point (surely it should be the other way round, scale for how big the number is and precision for its decimal places?):

add_column :standings, :points, :decimal, precision: 6, scale: 3

Now that I’m storing the number with an appropriate fidelity and can turn it into a sensible fraction I want it to look nice. The following Rails helper method determines if a number is a fraction and formats it with <sup>/<sub> tags:

module ApplicationHelper
  def fraction(numerator, denominator)
    capture do
      concat tag.sup(numerator)
      concat '⁄' # Unicode fraction slash.
      concat tag.sub(denominator)
    end
  end

  def points(number)
    return '0' if number.zero?

    rational = number.to_r.rationalize(0.005)
    whole, remainder = rational.numerator.divmod(rational.denominator)

    capture do
      concat whole.to_s unless whole.zero?
      concat fraction(remainder, rational.denominator) unless remainder.zero?
    end
  end
end

Storing approximate values in the database means you might have to be careful about losing precision, for example ⅐ × 49 = 7 but 0.143 × 49 ≠ 7:

BigDecimal('0.143').to_r.rationalize(0.005) * 49
# => (7/1)

BigDecimal('0.143') * 49
# => 0.7007e1

(BigDecimal('0.143') * 49).to_r.rationalize(0.005)
# => (589/84)

Luckily I perform limited calculations on my data so this isn’t something I have to worry about.

Bonus: Unicode fractions

I happened upon a site that demonstrates how to construct fractions from Unicode superscript and subscript characters. Even better is that the source is on GitHub so I borrowed the approach and implemented it in Ruby/Rails:

module ApplicationHelper
  SUB = %w(₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉)
  SUP = %w(⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹)

  def fraction(numerator, denominator)
    numerator.digits.reverse.map { |char| SUP.fetch(char) }.join \
      + '⁄' \ # Unicode fraction slash.
      + denominator.digits.reverse.map { |char| SUB.fetch(char) }.join
  end
end

Caching dependencies on GitHub Actions

I previously wrote about caching gems on CircleCI and although this is even easier to achieve with GitHub Actions there’s still a useful approach worth applying that will minimise the overall time spent installing dependencies when a workflow contains many jobs.

The thing I want to prevent is each job having to download and install its dependencies, when this occurs one of the jobs will finish first and write to the cache and then we’ll see something like Cache hit occurred on the primary key BIG-LONG-KEY, not saving cache from the Post Run actions/cache@v2 output in other jobs. Nothing will blow up, but all of those separate jobs installing dependencies are wasting billable time.

The simple fix is to declare an initial job whose sole purpose is to do the work of installing dependencies and to make them available for the other jobs. Rather than store the installed dependencies as a workflow artifact I prefer to treat this step as warming a cache — which may be a little more verbose but I think is simpler overall (also, with this approach, the cache is treated as a performance optimisation and each dependant job is still able to run if it isn’t present for for some reason).

Caching gem dependencies

Here’s an example of installing gems, note the job cache_gems and how the other jobs declare it as a dependency by specifying needs: cache_gems:

on: [push]

jobs:
  cache_gems:
    name: Cache gems
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: ruby/setup-ruby@v1
      with:
        # We always need to tell ruby/setup-ruby to cache the gems for us.
        bundler-cache: true

  brakeman:
    name: Brakeman
    runs-on: ubuntu-latest

    # Tell this job to wait for the `cache_gems` job to successfully complete
    # before it runs.
    needs: cache_gems

    steps:
    - uses: actions/checkout@v3

    # Each dependant job still need to install Ruby and ensure that it reads
    # from the cache before installing the gems (which is as simple as
    # specifying `bundler-cache: true` when using ruby/setup-ruby).
    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true

    # Now run your actual CI command.
    - run: bundle exec brakeman --quiet --run-all-checks

  rspec:
    name: RSpec
    runs-on: ubuntu-latest
    needs: cache_gems
    steps:
    - uses: actions/checkout@v3
    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true
    - run: bundle exec rspec

  rubocop:
    name: Rubocop
    runs-on: ubuntu-latest
    needs: cache_gems
    steps:
    - uses: actions/checkout@v3
    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true
    - run: bundle exec rubocop --parallel

Caching JavaScript dependencies

I use the same technique for installing JavaScript dependencies but it’s a tiny bit more involved because while ruby/setup-ruby takes care of caching and installing for us actions/setup-node only does the caching so we have to do the install ourselves.

  cache_node_modules:
    name: Cache node_modules
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    # I'm using Yarn so I select the yarn caching strategy so setup-node will
    # read/write from Yarn's global install location.
    - uses: actions/setup-node@v3
      with:
        cache: 'yarn'

    # Now actually install the dependencies (which actually reads from Yarn's
    # global install location).
    - run: yarn install --frozen-lockfile

  jest:
    name: Jest
    runs-on: ubuntu-latest
    needs: cache_node_modules
    steps:
    - uses: actions/checkout@v3

    # Mirror the cache job's setup-node steps.
    - uses: actions/setup-node@v3
      with:
        cache: 'yarn'
    - run: yarn install --frozen-lockfile

    # And here's your CI command.
    - run: yarn test

  prettier:
    name: Prettier
    runs-on: ubuntu-latest
    needs: cache_node_modules
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v1
      with:
        cache: 'yarn'
    - run: yarn install --frozen-lockfile
    - run: yarn prettier --check app/javascript

Running system tests

Running system tests may require both Ruby and JavaScript dependencies which can be achieved with the following changes:

   rspec:
     name: RSpec
     runs-on: ubuntu-latest
-    needs: cache_gems
+    needs: [cache_gems, cache_node_modules]
     steps:
     - uses: actions/checkout@v3
     - uses: ruby/setup-ruby@v1
       with:
         bundler-cache: true
+    - uses: actions/setup-node@v3
+      with:
+        cache: 'yarn'
+    - run: yarn install --frozen-lockfile
     - run: bundle exec rspec

Updated 2022-09-05: Updated Node examples to use actions/setup-node v3 and take advantage of its inbuilt caching.

My YAML reference

I find myself having to write ever more YAML nowadays and whilst it seems pretty simple at first (isn’t it just key/value?) having a bit more knowledge can be helpful. So here are a bunch of YAML things that I find useful — and that I continue to forget and have to look up again.

Strings

It’s not always necessary to “quote” a string and because of this there are lots of subtle ways that things can go awry — The Norway Problem being a classic example — but there are also lots of features that can help with formatting strings.

Formatting with |

The | treats its following lines as a block of multiline text:

text: |
  # Markdown Heading

  These separate lines
  will remain separate lines
  with no extra indentation.
"# Markdown Heading\n\nThese separate lines\nwill remain separate lines\nwith no extra indentation.\n"

There’s also |- which removes the final trailing newline.

Formatting with >

The > joins its following lines with a space:

text: >
  These separate lines
  will become one long line
  joined with spaces and
  with no indentation.
"These separate lines will become one long line joined with spaces and with no indentation.\n"

Using > can aid readability by splitting a single long command over many lines:

step:
  run: >
    NODE_ENV=production
    SOME=more
    ENV=vars
    npm build
"NODE_ENV=production SOME=more ENV=vars npm build\n"

There’s also >- which joins its following lines with a space and removes the final trailing newline.

Note that lines will only continue to be joined while the indentation level remains the same (this has caught me out in the past). So the following will join the first two lines with a space but the rest with newlines:

text: >
  These separate lines
  will NOT become one long line
    joined with spaces and
  with no indentation.
"These separate lines will NOT become one long line\n  joined with spaces and\nwith no indentation.\n"

Here’s what the spec says:

each line break is folded to a space unless it ends an empty or a more-indented line

Multiline with no extra formatting

The behaviour of >- appears to be similar to the default behaviour for a multiline string in that the lines are joined with a space and there’s no trailing newline — the big difference seems to be that indentation changes are ignored:

text:
  These separate lines
    will become one long line
      joined with spaces and
  with no indentation.
"These separate lines will become one long line joined with spaces and with no indentation."

This, again, can aid readability of a long command by splitting it over many lines with different indentation:

step:
  run:
    ./run_a_command
      --with=a
      --big=list
      --of=arguments
      -- and/file/paths
"./run_a_command --with=a --big=list --of=arguments -- and/file/paths"

Maps

Key/value pairs are “simple” can be nested:

key: value
nested:
  key: value
{
  "key": "value",
  "nested": {
    "key": "value"
  }
}

But this is exactly the sort of thing that caused The Norway Problem because the values can be anything and may not result in what you expected:

a: true
b: false
c: null
d: YES
e: NO
f: hello
g: 1.234
h: a long unquoted string
{
  "a": true,
  "b": false,
  "c": null,
  "d": true,
  "e": false,
  "f": "hello",
  "g": 1.234,
  "h": "a long unquoted string"
}

Collections

I think of arrays as Markdown bullet lists — they can also be nested:

- one
- two
- three
-
  - nested
  - array
["one", "two", "three", ["nested", "array"]]

And can be written “inline”:

- one
- two
- three
- [nested, array]

Anchors (&)

Anchors act as variables and can be used to reduce repetition. Here’s an example from the spec where & declares the named anchor SS and * is used to reference it further on through the document:

---
hr:
  - Mark McGwire
  # Following node labeled SS
  - &SS Sammy Sosa
rbi:
  - *SS # Subsequent occurrence
  - Ken Griffey

Anchors can be used to DRY up CI config (although they can’t be used in the GitHub Actions YAML) or a Rails database.yml:

default: &default
  adapter: postgresql
  encoding: unicode
  pool: 5

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

Comments

A comment starts with a #:

# Commented line.
- hello
# Interleaved comment.
- there # Another comment.
["hello", "there"]

But, as you may have noticed from a previous example, a # can appear in a multiline string when using | or > without being interpreted as a comment:

text: |
  # Markdown Heading

  These separate lines
  will remain separate lines
  with no extra indentation.
"# Markdown Heading\n\nThese separate lines\nwill remain separate lines\nwith no extra indentation.\n"

How to quickly test a snippet of YAML with Ruby

While writing this I encountered loads of little mistakes in my YAML and often had to verify that the output was what I expected. To check I used the DATA/__END__ trick:

require 'yaml'

pp YAML.load(DATA.read)

__END__

a: true
b: false
c: null
d: YES
e: NO
f: hello
g: 1.234
h: a long unquoted string
{"a"=>true,
 "b"=>false,
 "c"=>nil,
 "d"=>true,
 "e"=>false,
 "f"=>"hello",
 "g"=>1.234,
 "h"=>"a long unquoted string"}