Rails: Using PostgreSQL full-text search without a gem

PostgreSQL can cosplay as a full-text search engine. It doesn't have the features or fidelity of ElasticSearch or Algolia, but it's good enough if you just need to search and rank large volumes of text.

This card will teach you how to index, search and rank your Rails models in a PostgreSQL full-text index. We will do this without using any gems aside from ActiveRecord. While there are gems like pg_search or pg_fulltext, manual integration requires very...

RSpec: Executing specs by example id (or "nesting index")

There are several ways to run a single spec. I usually copy the spec file path with the line number of the example and pass it to the RSpec binary: bin/rspec spec/models/user_spec.rb:30 (multiple line numbers work as well: :30:36:68). Another is to tag the example with focus: true or to run the example by matching its name.

In this card I'd like to ...

Using Low-Level Prompts for High-Accuracy AI Coding

The key to unlocking the full potential of LLMs in coding lies in crafting precise prompts. The main challenge is learning how to structure prompts effectively to guide the model toward accurate results. Further evidence supporting this is the fact that Aider already writes ~70% of its own code (as of 02/2025). However, when starting out, your results may fall short of efficiently generating large portions of your code with the...

How to use your favorite font in Slack

In Slack, the settings dialog only offers a fixed selection of fonts. You can use any font you like using the /slackfont command.

Fonts need to be installed on your machine. Webfonts beyond those provided by Slack won't magically work unless you install them locally.
Only the chat font can be changed. The monospaced font used for code blocks isn't easily customizable.

Example usage

  • /slackfont Comic Neue to use "Comic Neue" (if installed)
  • /slackfont system-ui to use your desktop's system font in Slack.
  • /slackfont (withou...

Rails: Accessing strong parameters

Rails wraps your parameters into an interface called StrongParameters. In most cases, your form submits your data in a nested structure which goes hand in hand with the strong parameters interface.

Example:

curl -X POST -d "user[name]=bob" https://example.com/users
class UsersController
  def create
    User.create!(params.expect(user: [:name])) # Or User.create!(params.require(:user).permit(:name))
  end
end

This works well most of the time...

Rails 8 introduces `params.expect`

The new params.expect method in Rails 8 improves parameter filtering, addressing issues with malformed input and enhancing security. It provides a cleaner, more explicit way to enforce the structure and types of incoming parameters.

What changed

  • Replaces require and permit: Combines both methods for concise parameter validation.
  • Explicit Array Handling: Requires double array syntax to define arrays of hashes, improving clarity.
  • Enhanced Validation: Ensures expected parameter structure, rejecting malformed input wi...

Specify Gemfile for bundle

Bundler allows you to specify the name of the Gemfile you want to bundle with the BUNDLE_GEMFILE environment variable.

BUNDLE_GEMFILE=Gemfile.rails.7.2 bundle

By default, bundler will look for a file called Gemfile in your project, but there may be cases where you want to have multiple Gemfiles in your project, which cannot all be named Gemfile. Let's say for example, you maintain a gem and want to run automated tests against multiple rails versions. When you need to bundle one of your secondary Gemfiles, the solution above ...

Rails: Reverse Lookup a Fixture Name by it's id and table name

To reverse lookup a fixture by its table name and id, use the following approach on ActiveRecord::FixtureSet:

table = 'users'       # Specify the fixture table name
id = 123122           # Specify the ID to look for

# Find the fixture that matches the given ID
ActiveRecord::FixtureSet.all_loaded_fixtures[table].fixtures.find { |key, value| value['id'] == id }

Result Example:

[
  "one", # Fixture name
  #<ActiveRecord::Fixture:0x00007e79990234c8>, # ActiveRecord::Fixture object
  @fixture= { ... }, # The raw fixtu...

Rails: Looking up constants by their name string

TL;DR: Rails ships two methods to convert strings to constants, constantize and safe_constantize. Neither is safe for untrusted user input. Before you call either method you must validate the input string against an allowlist. The only difference between the two methods is that unresolvable constants raise an error with constantize, but return nil with safe_constantize. If you validate the input string against an allowlist, an error should never happen.

Preventing Dangerous Lookups

Suppose an application uses eit...

How to disable logging for ActiveStorage's Disk Service routes

In development, we store files using ActiveStorage's disk service. This means that stored files are served by your Rails application, and every request to a file results in (at least!) one non-trivial log entry which can be annoying. Here is how to disable those log entries.

Example

Here is an example of what loading a single <img> in an example application writes to the Rails log.

Started GET "/rails/active_storage/blobs/redirect/..." for ::1 at ...
Processing by ActiveStorage::Blobs::RedirectController#show as SVG
  Parameter...

How the Date Header Affects Cookie Expiration and Caching

tl;dr

When a cookie includes an Expires attribute or an HTTP response includes caching headers like Expires or Cache-Control, their validity depends on the server's Date header if present. Otherwise, the browser uses its local time. This can lead to issues in tests with mocked time or inconsistent cache behavior.

Cookie Expires depends on the Date header or browser time

When a cookie includes an Expires attribute, the browser evaluates the expiration date relative to a reference time:

  1. If the HTTP response ...

How to enable Rails' file_fixture helper in FactoryBot

In FactoryBot factories, Rails' file_fixture is not available by default. To enable it, include a support module from rspec-rails:

FactoryBot::SyntaxRunner.include(RSpec::Rails::FileFixtureSupport)

That includes ActiveSupport::Testing::FileFixtures, where file_fixture is defined, but also configures the file_fixture_path so that you can actually use file_fixture.

TestProf II: Factory therapy for your Ruby tests—Martian Chronicles, Evil Martians’ team blog

Some key highlights and points from the linked article TestProf II: Factory therapy for your Ruby tests.

The Problem with Factories in Ruby Tests

  • Factories are used to easily generate test data.
  • However, they can unintentionally slow down test suites by creating unnecessary or excessive associated data (factory cascades).

Understanding Factory-Induced Slowdowns

  • Factories often create additional data (e.g., associated records) th...

Text fragments in the browser URI fragment

Text fragments allow linking directly to a specific portion of text in a web document, without requiring the author to annotate it with an ID, using particular syntax in the URL fragment. Supporting browsers are free to choose how to draw attention to the linked text, e.g. with a color highlight and/or scrolling to the content on the page. This is useful because it allows web content authors to deep-link to other content they don't control, without relying on the presence of IDs to make that possible. Building on top of that, it could be u...

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...

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.join 'app/models/design')

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

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...

A different testing approach with Minitest and Fixtures

Slow test suites are a major pain point in projects, often due to RSpec and FactoryBot. Although minitest and fixtures are sometimes viewed as outdated, they can greatly improve test speed.

We adopted a project using minitest and fixtures, and while it required some initial refactoring and establishing good practices, the faster test suite was well worth it! Stick with me to explore how these tools might actually be a good practice.

So, why is this setup faster? Partially, it's because minitest is more lightweight than RSpec, which...

Timeouts for long-running SQL queries

While the main goal always is to prevent long-running queries in the first place, automatic timeouts can serve as a safety net to terminate problematic queries automatically if a set time limit is exceeded. This prevents single queries from taking up all of your database’s resources and reduces the need for manual intervention that might destabilize or even crash the application.

As Rails does not set a timeout on database statements by default, the following query will run for an entire day:

ActiveRecord::Base.connection.execute("S...

RSpec: Increase readability with super_diff

When handling nested hashes the RSpec output is often hard to read. Here the gem super_diff could help.

Add super_diff to your project

  1. Add super_diff to your Gemfile:
gem 'super_diff'
  1. Require it in your spec_helper.rb
require 'super_diff/rspec' # For Rails applications you can replace this with 'super_diff/rspec-rails'
  1. Customize colors in spec/support/super_diff.rb
SuperDiff.configure do |config|
  config.ac...

Learn how to use ruby/debug

This talk shows simple and advanced usages of the ruby/debug debugger. It goes through a step by step debugging workflow.

Here are some command examples:

(rdbg) step 2 # step twice
(rdbg) info # show current scope, including self
(rdbg) bt # show backtrace
(rdbg) frame 3 # go directly to frame 3
(rdbg) break User#email # add a breakpoint in the email instance method
(rdbg) catch SomeException # break when SomeException is raised

Some advanced exam...

Using the ActiveSupport::BroadcastLogger

The ActiveSupport::BroadcastLogger allows you to log to multiple sinks. You know this behavior from from the rails server command, that both logs to standard out and the log/development.log file.

Here is an example from the ActiveSupport::BroadcastLogger API:

stdout_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("development.log")
broadcast = ActiveSupport::BroadcastLogger.new(stdout_logger, file_logger)

broadcast.i...

How to allow testing beforeunload confirmation dialogs with modern ChromeDrivers

Starting with ChromeDriver 127, if your application displays a beforeunload confirmation dialog, ChromeDriver will immediately close it. In consequence, any automated tests which try to interact with unload prompts will fail.

This is because ChromeDriver now follows the W3C WebDriver spec which states that any unload prompts should be closed automatically.
However, this applies only to "HTTP" test sessions, i.e. what you're using by default. The spec also defines that bi-directional test se...

High-level data types with "composed_of"

I recently stumbled upon the Rails feature composed_of. One of our applications dealt with a lot of addresses and they were implemented as 7 separate columns in the DB and Rails models. This seemed like a perfect use case to try out this feature.

TLDR

The feature is still a VERY leaky abstraction. I ran into a lot of ugly edge cases.

It also doesn't solve the question of UI. We like to use simple_form. It's currently not possible to simply write `f...