How to enable pretty IRB inspection for your Ruby class

When Ruby objects are inspected in any modern IRB, some objects (like ActiveRecord instances) are rendered with neat colors and line breaks.
You will not get that for custom classes by default -- which can be annoying if your inspection contains lots of meaningful information.

Here is what you need to do if you want your objects to be inspected nicely.

Implement a pretty_print method

As an example, consider the following class.

class MyClass

  # ...

  def inspect
    "#<#{self.class} attr1: #{attr1.inspect}, attr2: #{attr2...

Generating an Entity Relationship Diagram for your Rails application

This card explains how to generate an entity relationship diagram for your Rails application.
We also show how to limit your ERD to a subset of models, e.g. models inside a namespace.

Generating a full ERD

Option A: RubyMine

  1. Right-click anywhere in your project tree
  2. In the context menu, find the "Diagrams" menu item at/near the bottom
  3. Inside, choose "Show diagram" → "Rails Model Dependency Diagram"
  4. A new tab will open with the diagram inside. You can modify it there, and export it as an image.

Option B: Use rails-e...

Custom error messages in RSpec or Cucumber steps

Sometimes you have a test expectation but actually want a better error message in case of a failure. Here is how to do that.

Background

Consider this test:

expect(User.last).to be_present

In case of an error, it will fail with a not-so-helpful error message:

expected present? to return true, got false (Spec::Expectations::ExpectationNotMetError)

Solution

That can be fixed easily. RSpec expectations allow you to pass an error message like this:

expect(User.last).to be_present, 'Could not find a user!'

...

Heads up: counting may be slow in PostgreSQL

The linked article points out that COUNT queries might be unexpectedly slow in PostgreSQL.

If you just need to know "are there any records" use any?. This uses SELECT 1 AS one FROM ... LIMIT 1 under the hood.
If you just need to know "are there no records" use empty? or none?. This uses SELECT 1 AS one FROM ... LIMIT 1 under the hood.

In short: Replace foo.count > 0 with foo.any? or foo.count == 0 with foo.none?

Debugging Capistrano

Capistrano 3 has a doctor task that will print information about

  • Environment: Ruby, Rubygems and Bundler versions
  • List of Capistrano gems and whether an update is available
  • All config variables and their values
  • Capistrano server config
$ bundle exec cap staging doctor

Rails 7 adds #caching? and #uncacheable!

Rails' fragment caching caches subtrees of an HTML document tree. While constructing that tree though, it can be really hard to keep track of whether some code is run in a caching context. Fortunately, Rails 7 brings two helpers that simplify this.

Note that these helpers are all about Rails' fragment caching and not about downstream caching (i.e. Cache-Control).

uncacheable!

Invoke this helper in a partial or another helper that should never be cached. Used outside of fragment caches, the helper does just nothing. But should it ...

Enabling YJIT

YJIT is Ruby's default just-in-time compiler. It is considered production-ready since Ruby 3.2 (source).

To activate YJIT you need two steps:

  • Your ruby binary needs to be compiled with YJIT support.
  • You need to enable YJIT.

Getting a Ruby with YJIT support

We usually install Ruby with tools like rbenv or asdf. This compiles the ruby binary from the source code. Support for YJIT will be automatically added during this compilation...

Running Rubocop as a pre-push hook

Git has the concept of hooks: bash scripts that are invoked at certain points in the Git lifecycle. One handy use is a pre-push hook that runs Rubocop. It will prevent pushing code that Rubocop finds fault with.

Configuring the hook

Git hooks are normally stored locally with a repository. They are not committed.

  1. Store this snippet in .git/hooks/pre-push:
if [ -f ./.rubocop.yml ]; then                                                  
  echo 'Running Rubocop ...'                                                    
  bundle exe...

DB enums are ordered

A lesser known fact about PG enums is that they are ordered. This can be really handy when values have an implicit ordering.

Let's imagine a record Issue(criticality: string). criticality should have the following three possible values: critical, medium, low.

Sorting with Issue.all.order(criticality: :desc) will return the following order:

  1. The 'medium' issue
  2. The 'low' issue
  3. The 'cricital' issue

This happens, because as the database column backing the the criticality attribute is a string and PG will use a [col...

Geordi 11.2.0 released

  • Add support for default branches other than "master" (e.g. "main"). Will read this information from origin.
  • geordi branch will not fail if it can't determine local branches.
  • Improved Linear issue menu: now includes the issue id, truncates long issue titles and puts metadata last.

Switching the package manager from yarn to npm

We recently migrated a Rails application from yarn to npm. We decided to go this step instead of upgrading to > Yarn 2.0 to reduce the number of dependencies in our project.

Migration

  • Remove the yarn.lock file
  • Remove the node_modules folder
  • Run npm install
  • Replace all occurrences of yarn with npm in your project

Notes

  • With npm vendored packages with dependencies create their own node_modules folder within the vendor path. We...

How to eager load a single directory with Zeitwerk

Zeitwerk is the new autoloader of Rails. It is mandatory starting with Rails 7.0.

Sometimes, a model needs to know all its descendants. They might be organized in a subdirectory:

# Example
app/models/design.rb
app/models/design/light.rb
app/models/design/dark.rb
...

Now imagine that some external code needs to iterate all design subclasses.

To eager load all designs, use this line:

Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/design")

Make sure that app/models/design.rb is not required manually be...

jeremyevans/ruby-warning: Add custom processing for warnings

ruby-warning adds custom processing for warnings, including the ability to ignore specific warning messages, ignore warnings in specific files/directories, include backtraces with warnings, treat warnings as errors, deduplicate warnings, and add custom handling for all warnings in specific files/directories.

This tool can precisely silence deprecation warnings.

While you should fix deprecations in your application, ruby-warning is handy to silence those warnings that you cannot fix. E.g. when they originate from libraries that you ca...

CSS: Matching against attributes and their values (or parts of them)

You probably know that you can use CSS selectors to match against elements and their attributes, such as:

a[title] { /* any <a> that has a "title" */ }
a[data-fancy="true"] { /* any <a> that has their "data-fancy" attribute set to "true" */ }

But there is more: You do not need to match against "full" attribute values but can match against parts of them.

They work in all somewhat modern browsers, and IE9 or later.

Match variants

Exact match (CSS2)

[foo="bar"] (matches `<div foo="b...

JavaScript: Calling a function with a variable number of arguments

This card describes how to pass an array with multiple element to a JavaScript function, so that the first array element becomes the first function argument, the second element becomes the second argument, etc.

Note how this is different from passing the entire array as the first argument. Compare these two different ways of calling fun() in Ruby:

# Ruby
array = [1, 2, 3]
fun(array)  # same as fun([1, 2, 3]) (1 argument)
fun(*array) # same as fun(1, 2, 3)   (3 arguments)

Depending on your culture the spreading of array eleme...

Rails: Using normalizes without copying code

Rails 7.1 added the normalizes method which can be used to normalize user input.

It lets you define the fields you want to normalize and how to normalize them. In the example below, the Movie#title attribute is stripped from leading and trailing whitespace automatically:

class Movie < ApplicationRecord
  normalizes :title, with: -> {  _1.strip }
end

Tip

Normalization lambdas are not called for nil values by default. To normalize nil values, pa...

Carrierwave: How to remove container directories when deleting a record

When deleting a record in your Rails app, Carrierwave automatically takes care of removing all associated files.
However, the file's container directory will not be removed automatically. If you delete records regularly, this may be an annoyance.

Here is a solution which was adapted from the Carrierwave GitHub wiki and cleans up any empty parent directories it can find.

class ExampleUploader < CarrierWave...

SameSite cookies

TL;DR Most web applications do not require action on this. SameSite=None (old browser default) will continue to work, and SameSite=Lax (new Chrome default, gradually rolled out) is an even better default for cookies. Set SameSite=Strict only for extra security in special cases (see below). If your application is rendered in an iframe (e.g. a video player or some news stream), you need to configure its relevant cookies as SameSite=None.


The SameSite cookie attribute targets **c...

How to update the bundler version in a Gemfile.lock

  1. Install the latest bundler version:

    gem install bundler
    Fetching bundler-2.3.5.gem
    Successfully installed bundler-2.3.5
    1 gem installed
    
  2. Update the bundler version in Gemfile.lock:

    bundle update --bundler  
    
  3. Confirm it worked:

    $ tail -n2 Gemfile.lock 
    BUNDLED WITH
      2.3.5
    

Notes:

  • Bundler should automatically detect the latest installed version. If it does not, you can specify your preferred version like so:

    b...
    

Designing HTML emails

The 90s are calling: they want their tables back. Unfortunately, you need them all for laying out your HTML emails.

Email client HTML rendering is way more scattered than browser HTML. While you might have a pretty good understanding of what features and patterns you can use to support all major browsers, I doubt anyone masters this craft for HTML email clients.

The only way to ensure your email looks good (acceptable, at least) in all mail clients, is to check it. Litmus is your go-to solution for this (see below). W...

Project management best practices: Working with clients in person

When working on a bigger project, the easiest way to improve your work relation with a client or an external product manager, is to make sure you see them in person once in a while.

It makes sense to meet each other when you start working together to establish a relationship and find out what makes them tick.

If you need to discuss a larger package of work, use the opportunity and meet up and discuss it in person.

When you have to discuss something in your daily work, prefer talking to writing, and consider using a webcam.

It's OK to put block elements inside an <a> tag

In general, you should not put a block element inside an inline element. So don't do this:

<span>
  <div>text</div>
</span>

The browser will think you wrote invalid HTML by accident, and will sometimes reorder elements silently.

There is one notable exception: It's OK to wrap block elements in a <a> tag in HTML5 (not 4). The spec says:

The a element may be wrapped around entire paragraphs, lists, tables, and so forth, even entire sections, so long ...

tig: install a more recent version

I noticed that tig 2.5.1 that is provided by Ubuntu 22.04 repositories has inferior bash completion than older versions after a complete rewrite. Newer versions, however, received some fixes. This inspired me to upgrade tig.

The official debian repositories have more recent versions of tig than Ubuntu does.