Today I Learned

hashrocket A Hashrocket project

Easily Update ActiveRecord Enum Values

Today I learned about some nice helper methods for ActiveRecord Enums. You might already know the ? methods to check the value of the enum. There are also ! methods to succinctly set the value of the enum too!

class Post < ActiveRecord::Base
  enum status: {
    draft: 0,
    published: 1
  }
end

post = Post.new
post.published? # => false
post.published! # updates the status to `published`
post.published? # => true

Turn Off Spell Check in Vim

When I'm writing longer form text in vim, I like to turn on spell check. I don't always want spell check on, so I toggle it on manually when I'm in a Markdown or text file: :set spell.

While I always remember how to toggle it on, I never remember how to toggle it off:

:set nospell

It's the same syntax for any setting - if you wanted to turn off incsearch, its :set noincsearch.

Test for String Equality

In Rails, there is an nice and easy interface that can be used to check a string for equality.

To do this, you can use the ActiveSupport::StringInquirer object. Simply initialize it with a string value, like so:

string = ActiveSupport::StringInquirer.new("vanilla")

and now we can just call string#vanilla? to check for equality

string.vanilla?
=> true

string.chocolate?
=> false

Fun fact:
This is how you are able to do Rails.env.production?

H/T: Matt Polito

Time terminal commands

If you enter time before any terminal command, it will output some information about how long the task took to run.

For example, let's run it on these two, crude, simple ruby scripts to see the time difference

# ./million.rb
1_000_000.times do
  2 + 2
end
> time ruby million.rb
ruby million.rb  0.05s user 0.02s system 47% cpu 0.143 total

​

# ./billion.rb
1_000_000_000.times do
  2 + 2
end
> time ruby billion.rb
ruby billion.rb  18.41s user 0.13s system 99% cpu 18.618 total

H/T Matt Polito

Stub Environment Variables in Vitest

You can stub environment variables in Vitest using vi.stubEnv. This will stub the value on process.env and import.meta.env.

You can also reset all env vars back to their original value with vi.unstubAllEnvs.

process.env.COOL_ENV_VAR; // => "test"
import.meta.env.COOL_ENV_VAR; // => "test"

vi.stubEnv('COOL_ENV_VAR', "stubbed");

process.env.COOL_ENV_VAR; // => "stubbed"
import.meta.env.COOL_ENV_VAR; // => "stubbed"

vi.unstubAllEnvs();

process.env.COOL_ENV_VAR; // => "test"
import.meta.env.COOL_ENV_VAR; // => "test"

Docs

Delete the inner content of an HTML tag

After yesterday's TIL, fellow Hashrocketeer Jack Rosa shared with me a few other cool ways to delete things in vim.

My favorite way that he showed me that I never knew existed was using dit.

This deletes all content of the HTML tag you are inside of.

Let's use the following HTML for a couple examples:

1 |<div>
2 |  <h1>Header</h1>
3 |
4 |  <p>
5 |    This is some paragraph text that is <em>emphasized</em> for importance.
6 |  </p>
7 |</div>

If your cursor was anywhere within the <div></div> without being inside of a nested element, the result would be:

1 |<div></div>

If your cursor was anywhere within <em>emphasized</em>, the result would be:

1 |<div>
2 |  <h1>Header</h1>
3 |
4 |  <p>
5 |    This is some paragraph text that is <em></em> for importance.
6 |  </p>
7 |</div>

Conclusion: This is a nice and easy way to clear out html tags quickly.

Add conditional class names

If you want to conditionally add class names in Rails, here is a clean way to do it.

<%= link_to("Some Page", some_path, 
  class: class_names("bg-black text-black", {"text-white": text_visible?})
%>

This takes advantage of the class_names method, which is just an alias for the token_list method.

h/t to Matt Polito.
I always find myself looking up his TIL and figured if I make it myself it will help me retain it. πŸ˜…

ISO 8601 Durations

Today I learned that ISO 8601 includes a spec for durations as well as dates and times.

Normally when I think of ISO 8601 I think of dates (2024-05-30) or times (2024-05-30T19:54:14Z).

But it also includes a spec for durations! Super convenient to have a standardized way to represent this, and rails and javascript libraries are able to parse it.

Some examples:

  • 8 hours => PT8H
  • 3.5 hours => PT3H30M
  • 2 months, 1 day => P2M1D
  • 6 years, 5 months, 4 days, 3 hours, 2 minutes, 1 second => P6Y5M4DT3H2M1S

Use ri to Lookup Ruby Docs from the Command Line

Did you know you can look up ruby documentation on Classes and methods from the command line? The slightly elusive ri command does just that. You can pass it an argument of the class/method you want to look up, or you can enter interactive mode without arguments.

$ ri uniq

$ ri Array#compact

$ ri Hash

You can also use it to browse all the pre-defined ruby global variables:

$ ri ruby:globals

Check out the docs or run ri --help to see all it can do.

h/t Brian Dunn

Rerun Only Failed Tests with RSpec

Say you run your entire rspec suite and a couple of tests fail. You make a change that should fix them. How can you quickly rerun those failed tests to see if they're green? It could take minutes to run the whole suite again, and all you care about is 2 tests.

That's where the --next-failure (-n) flag comes in handy. According to the docs it is "Equivalent to --only-failures --fail-fast --order defined)". So you can rerun only your failed specs, and exit immediately if one does fail. You could of course just use --only-failures too, but sometimes it's nice to fail fast.

bundle exec rspec -n

h/t Brian Dunn

Two Types of Ranges in Ruby

Today I learned there are two ways to construct a range in ruby. Using two dots .. creates a range including the start and end values.

(2..5).include?(2) # => true
(2..5).include?(5) # => true

Using three dots ... creates a range including the start value, but not the end value.

(2...5).include?(2) # => true
(2...5).include?(5) # => false

So if we think of them in terms of intervals, (a..b) is a closed interval ([a, b]), and (a...b) is a right half-open interval ([a, b)).

Postgres comparison with null values

When Postgres is running comparisons, any null values will yield null at the end of the comparison. This is because the null is an unknown value that Postgres can't run the comparison against.

Take the following simple example of a users table and query:

create table users (name text, email text);

insert into users (name, email)
  values ('Joe', 'joe@hashrocket.com'),
  ('Rick', null);
  
select * from users where email not like '%gmail.com';
--  name |       email
-- ------+--------------------
--  Joe  | joe@hashrocket.com
-- (1 row)

You'll notice that the Rick user is not returned in the results.

If you want rows with the null value included in your results, you can coalesce the column to an empty string. This allows Postgres to run the comparison against two known values and return the rows with the null values.

select * from users where coalesce(email, '') not like '%gmail.com';
--  name |       email
-- ------+--------------------
--  Joe  | joe@hashrocket.com
--  Rick | ΓΈ
-- (2 rows)

Handling exceptions with rescue_from

ActiveSupport has a handy tool for handling exceptions globally in your Rails app. You can use it to catch specific exceptions and provide a centralized way to manage errors across your application, making your code cleaner and more maintainable.

Here is the example from the docs, showcasing how to catch specific exceptions and what method to run when they are caught.
Note: You can also pass it a block as the handler instead.

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :deny_access
  rescue_from ActiveRecord::RecordInvalid, with: :show_record_errors

  rescue_from "MyApp::BaseError" do |exception|
    redirect_to root_url, alert: exception.message
  end

  private
    def deny_access
      head :forbidden
    end

    def show_record_errors(exception)
      redirect_back_or_to root_url, alert: exception.record.errors.full_messages.to_sentence
    end
end

Delete to beginning of current word

If you are typing something in your shell and you need to delete the entire word you're on, instead of pressing backspace repeatedly, you can press ESC + Backspace and this will delete back to the start of the current word you are on.

Example; Say you wanted to delete this whole filepath, or a majority of it. Simply place your cursor at the end (or wherever you want to delete up to) and press ESC + Backspace to delete all the way back to the word vim

vim /i/accidentally/typed/something/big/that/was/incorrect

and it might leave you with something like this

vim /that/was/incorrect

ActiveRecord Reload only Associated Record

Today I learned about some focused reload methods for ActiveRecord Associations.

Let's say I have an Author class that can have one magnum opus.

class Author < ApplicationRecord
  has_one :magnum_opus
end

Now suppose I've instantiated an Author, and his magnum opus gets updated elsewhere. If I try to access that instance's magnum opus again (existing instance, no reloading), I'll get the old cached version:

author = Author.first
author.magnum_opus.title # => "The Dark Tower"

#elsewhere
mo = MagnumOpus.find_by(title: "The Dark Tower")
mo.update title: "The Shining"

author.magnum_opus.title # => "The Dark Tower"

Now to remedy this, we can reload the author:

author.reload.magnum_opus.title # => "The Shining"

But why reload the whole author when you can reload just the association? ActiveRecord has reload_* methods for each has_one and belongs_to association that does just that - it reloads only the associated record.

author.reload_magnum_opus.title # => "The Shining"

Docs for has_one and belongs_to.

h/t Matt Polito

Markdown alert style content blocks on Github

Use special markdown to emphasize content inside alert style content blocks on Github.

> [!NOTE]  
> Highlights information that users should take into account, even when skimming.

> [!TIP]
> Optional information to help a user be more successful.

> [!IMPORTANT]  
> Crucial information necessary for users to succeed.

> [!WARNING]  
> Critical content demanding immediate user attention due to potential risks.

> [!CAUTION]
> Negative potential consequences of an action.

image

Find Missing Relations Easily with .missing

It can be kind of clunky to query ActiveRecord for records where the has_manyassociation is empty.

Say I have an Author class that has many Books. One way to query for Authors without books is the following:

class Author < ApplicationRecord
  has_many :books
end

Author.left_joins(:books).where(books: {id: nil}) 

This totally works, but at least for me is difficult to reason what we're querying. However, Rails 6.1 added a .missing method that really helps clarify what the query is doing:

Author.where.missing(:books)

Much clearer - this will grab all authors that do not have any books.

Docs for further reading. Happy querying!

Set Attributes when Creating with .create_with

You can set attributes to be used when creating new records with .create_with:

Author.create_with(name: "Hashrocketeer")
Author.new.name # => "Hashrocketeer"

This can be particularly useful if you're doing a .find_or_create_by and want to set some values only when creating:

Author
  .create_with(post_count: 0)
  .find_or_create_by(name: "Hashrocketeer")

This will find an author with name "Hashrocketeer". If we find an existing Author, we won't change their post_count. If we don't find an existing Author, we'll create one with a post_count of 0.

Read more in the docs

Use comm to Compare Files

Today I learned about comm, which is used to select the common lines in two files. It's pretty neat, but has a strange output format.

Say we have two text files:

# first.txt
one
two
three

# second.txt
one
three
four

We can run the below command to find the common lines. In the output, the first column is what's only in first.txt, the second column is what's in second.txt, and the third column is what's common.

$ comm first.txt second.txt                                                                                         
                one
        three
        four
two
three

Hmm, that doesn't look right - one and three are common, not just one. The caveat with comm is that the files need to be sorted lexically. You can sort easily in bash with bird beak notation for process substitution.

$ comm <(sort first.txt) <(sort second.txt)
        four
                one
                three
two

If we only want the common lines, we can apply the flags -12 to hide the first and second columns:

$ comm -12 <(sort first.txt) <(sort second.txt)
one
three

Add metadata to Stripe webhooks using TwiML <Pay>

This is kind of a specific TIL, but hopefully it saves somebody else the time it took me to find the answer.

If you are using the TwiML <Pay> verb there is a noun on <Pay> called <Parameter> which lets you pass parameters to your action.

If your payment provider is Stripe and you want to send custom metadata, you can send a <Parameter> with a name prefaced as metadata_ and it will be added to the Stripe metadata that gets sent out in the webhook.

Here's an example:

<Pay paymentConnector="stripe"> 
   <Parameter name="metadata_testKey" value="value" /> 
</Pay>

and now the object you receive in your webhook will have the following:

"metadata": {
  "testKey": "value"
}

Modify Tables with `change_table` in Rails

Ever since I used ecto in Elixir Phoenix, I've always thought about their alter table function while working on Rails projects. Turns out, I have been living under a rock and I could have been using change_table this whole time πŸ€¦πŸ»β€β™‚οΈ

For those new to the change_table function, it allows you to make bulk alterations to a single table with one block statement. You can adjust columns(and their types), references, indexes, indexes, etc. For a full list of transformations, see the official api documentation.

If you're using Postgres or MySQL, you can also add an optional argument bulk: true and it will tell the block to generate a bulk SQL alter statement.

class AddUserIdToPosts < ActiveRecord::Migration[7.1]
  def change
    change_table(:posts, bulk: true) do |t|
      t.remove_reference :user_post, :text, null: false
      t.references :user, null: false
    end
  end
end

API Ruby on Rails - change_table

Hooray Rails! πŸ₯³

Convert objects into URL query strings

In Rails you can easily convert objects into URL query strings.

Here are a few examples:

string = "vanilla"
big_string = "extra chocolate"
integer = 5
array = ["vanilla", "chocolate"]
hash = {flavor: "vanilla", scoops: 2}

string.to_query("flavor")
=> "flavor=vanilla"

big_string.to_query("flavor")
=> "flavor=extra+chocolate"

integer.to_query("scoops")
=> "scoops=5"

array.to_query("flavors")
=> "flavors%5B%5D=vanilla&flavors%5B%5D=chocolate"

hash.to_query
=> "flavor=vanilla&scoops=2"

Note: Hashes don't require a key to be passed in, because they are inferred from the hash keys.

Change ActionCable's URL on the Client

ActionCable provides a handy escape hatch for changing the url path of the cable in your frontend. In my case, I wanted to specify a token on the cable url so that I could preserve some in place functionality while also being able to use the Turbo::StreamsChannel.

I did some source diving in turbo-rails and found that turbo calls out to createConsumer which comes from @rails/actioncable.

And that's where I found this (part of which is pasted below with a subtle change for readers) -

export function createConsumer(url = getConfig("url") || "/cable") {
  return new Consumer(url)
}

export function getConfig(name) {
  const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
  if (element) {
    return element.getAttribute("content")
  }
}

You can place a meta tag for the cable url in your views; this will be picked up by Action Cable and Turbo -

<meta name="action-cable-url" content="/cable?foo=bar">

Open New Tab with HTML Form Submit

You can open a new tab on form submit by changing the target attribute of the form element:

<form target="_blank">
 <!-- ... -->
</form>

While this is useful, it doesn't handle the scenario where you have multiple submit buttons (different actions) that need to behave differently.

For that, there's the formtarget attribute on your submit button/input. Note that by setting this attribute, you will override the form target attribute if set.

In the example below, the "Save" button will submit the form in the current window, while the Preview window will submit the form in a new tab

<form action="/liquid-templates/1">
  <!-- ... -->

  <button type="submit">Save</button>
  <button type="submit" formaction="/liquid-templates/1/preview" formtarget="_blank">Preview</button>
</form>

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#target https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formtarget

Ruby Data class to replace Struct

Ruby has a new-ish class to build "immutable" structs, check this out:

Measure = Data.define(:amount, :unit)
distance = Measure.new(100, 'km')

distance.amount
#=> 100

distance.unit
#=> "km"

And if you try to use a setter, then it will fail:

 distance.amount = 101
(irb):7:in `<main>': undefined method `amount=' for an instance of Measure (NoMethodError)

Restrict Phoenix Component Attr Values

If you want to limit the permissible values of a Phoenix Component attribute, you can use the values keyword list option when you call attr/3. Here's is an example that restricts which atoms are being passed to the component:

use MyApp.Component

attr :acceptable_color, :atom, values: [:green, :blue]

if you were to pass any atom other than :green or :blue to this component, the compiler will warn you.

Specify behavior for nulls in a unique index

Postgres 15 gave us the ability to specify how we want null values to be treated when dealing with unique indexes.

By default, nulls are considered unique values in Postgres:

create table users (name text, email text unique);
-- CREATE TABLE
insert into users values ('Joe', null), ('Jane', null);
-- INSERT 0 2

This default behavior can also be explicitly set using the nulls distinct clause:

create table users (name text, email text unique nulls distinct);
-- CREATE TABLE
insert into users values ('Joe', null), ('Jane', null);
-- INSERT 0 2

To change the default behavior and prevent nulls from being considered unique values, you can use the nulls not distinct clause:

create table users (name text, email text unique nulls not distinct);
-- CREATE TABLE
insert into users values ('Joe', null), ('Jane', null);
-- ERROR:  duplicate key value violates unique constraint "users_email_key"
-- DETAIL:  Key (email)=(null) already exists.

See this change in the Postgres 15 release notes

Return Types In PSQL Pattern Match

If you use the pattern match operators in PSQL, you'll want to mind the column types passed to these statements

If you use a string, you will get a boolean return -

select 'a' like '%b%';
?column?
----------
 f
(1 row)

select 'a' like '%a%';
?column?
----------
 t
(1 row)

But if you select a null in one of these statements, the return is null as well -

select null like '%a%';
?column?
----------
 ΓΈ
(1 row)

Moral of the story - if you're expecting a boolean, you can coalesce the column before the pattern match -

select coalesce(null, '') like '%a%';
?column?
----------
 f
(1 row)

https://www.postgresql.org/docs/current/functions-matching.html

Silence output of command in Makefile

Have you ever wanted to keep your makefile output a bit tidier? The @ symbol is your secret weapon.

Think of it like a silencer for your makefile. Every time you run a command, the makefile helpfully echos it back to you. But that echo gets silenced with the @ symbol at the beginning of a line.

This can be handy for keeping things clean, especially when you have a long list of commands in your makefile. Imagine a recipe with a million ingredients – you only care about the final dish, not every single step along the way, right?

Here's an example:

server: # Runs rails server
  @RAILS_LOG_LEVEL=debug bin/rails server

See how that works? The command executed for make server runs silently in the background.

Now, remember, this doesn't mean errors magically disappear. If something goes wrong, the error message will still show up. But for everything else, it's like a behind-the-scenes operation, keeping your makefile output focused on the important stuff.

So next time you want to streamline your makefile output, grab the @ symbol and hit the mute button on those noisy commands!

Set Env Vars with Shell Scripts

Sometimes I have limited screen real estate in my terminal, and my normal prompt of current/working/directory (git_branch) % takes up too much space. I wanted a bash script that could change my prompt to something short like & in one quick command. So I wrote a shell script:

#!/bin/bash

PS1="\[\e[32m\]& \[\e[m\]"

Nice and simple, right?

~/src/dotfiles (main) % ./shorter.sh
~/src/dotfiles (main) % 

Except, it doesn't change anything 😱. That's because changing PS1 isn't executing a command, it's setting an environment variable. So, just executing this shell script isn't enough, we need to source it to source the new PS1 in this terminal.*

~/src/dotfiles (main) % . ./shorter.sh
& 

* This is also why changing the prompt like this only affects the current terminal, and not any others that you may have open at the same time.

Regex in RSpec Argument Matchers

Today I learned you can use regular expressions in RSpec method argument expectations.

Suppose I have a method that takes an email, and a registered boolean as parameters:

def some_method(email:, registered:)
end

In a spec, it's easy enough to verify that it was called with set parameters, like test@example.com and true:

expect(subject)
  .to receive(:some_method)
  .with(email: 'test@example.com', registered: true)

But what if I want to verify that the email address just belongs to example.com? We can use a regex for that!

expect(subject)
  .to receive(:some_method)
  .with(email: /@example.com$/, registered: true)