Today I Learned

hashrocket A Hashrocket project

Rails Environment Task

In a Rails task if you need to load in your rails application (to get access to your models, etc.), you have to call :environment in the task:

task task_name: :environment do 
  ...
end

I kind of always took this for granted, and never thought much about it. Then, in Xavier Noria's 2024 Rails World talk he mentioned that :environment itself is a task - and that syntax is actually saying your task depends upon :environment and run that task before your task runs. 🀯

So I decided to look up what the environment task actually does:

task :environment do
  ActiveSupport.on_load(:before_initialize) { config.eager_load = config.rake_eager_load }

  require_environment!
end

require_environment! ... requires your environment, specifically it requires your config/environment.rb which runs Rails.application.initialize! - which is what actually starts your rails app.

Cool!

RSpec expect not_to change from

I use the rspec change matcher a lot to check the before and after values of something while the subject under test executes. A convoluted example:

RSpec.describe "expect change from to" do
  it do
    x = 1
    expect{ x = x + 1 }.to change { x }.from(1).to(2)
  end
end

And sometimes I use to it to verify something doesn't change.

RSpec.describe "expect not to change" do
  it do
    x = 1
    expect{ nil }.not_to change { x }
  end
end

Which is great - but sometimes I want to make sure my understanding of the initial state is correct and want to verify it didn't change from its initial value - in this case 1.

RSpec.describe "expect not to change from" do
  it do
    x = 1
    expect{ nil }.not_to change { x }.from(1)
  end
end

And if that from value is wrong, I'll get a nice message explaining what's wrong.

RSpec.describe "expect not to change from" do
  it do
    x = 1
    expect{ nil }.not_to change { x }.from(0)
  end
end
# => expected `x` to have initially been 0, but was 1

Case Regex Matching with Capture Groups

I ran into a problem where I wanted to use a case statement to match against a regex, but also capture some values from the matched data.

Say I want to match on a pair of numbers wrapped in parens like (2,4) and add them together. If I match without capture groups I have to grab the numbers again in the when block:

x = "(2,4)"
case x
when /\(\d+,\d+\)/
  a,b = x.scan(/\d+/)
  a.to_i + b.to_i
end
# => 6

Seems a shame to have perform another op to get a and b. What if we used capturing groups like /\((\d+),\(d+)\)/? How do we refer to those captured values inside the when? We can use some of ruby's special variables, namely $1 and $2 to refer to the captured groups of the last regexp.

x = "(2,4)"
case x
when /\((\d+),(\d+)\)/
  $1.to_i + $2.to_i
end
# => 6

$1 and $2 feel a little magic, so we can name our capture groups group1 and group2, and access those from $~ (the MatchData of the last regexp).

x = "(2,4)"
case x
when /\((?<group1>\d+),(?<group2>\d+)\)/
  $~[:group1].to_i + $~[:group2].to_i
end
# => 6

Count Occurrences of Elements in List

The Enum Module in Elixir has the frequencies/1 function, which is useful for counting the occurrences of each element in a list.

> Enum.frequencies(["dog", "cat", "dog", "bird", "dog", "cat"])
%{"bird" => 1, "cat" => 2, "dog" => 3}

There's also frequencies_by/2, where the 2nd argument is a function that normalizes the element/key before counting.

> Enum.frequencies_by(["a", "b", "a", "C", "A"], fn key ->
  String.upcase(key)
end)
%{"A" => 3, "B" => 1, "C" => 1}

https://devdocs.io/elixir~1.17/enum#frequencies/1

Break words in Tailwind.

In Tailwind you can use certain classes to wrap text within an element.
You could use something like text-wrap or whitespace-pre-wrap, or any of the other classes.

However, if a single word is too long it will continue on past the element it is contained in.

Kind of like this:

<div class="text-wrap">
<!-- really long text -->
</div>

HEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGword

Well, just add the Tailwind class break-words and it will break up a word when it reaches the end of the element, and you'll get something more like this:

<div class="text-wrap break-words">
<!-- really long text -->
</div>

HEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGword HEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGwordHEREisAreallyLONGword

Generate Rails Models with Namespaces

You can use the rails model generator to generate namespaced models. I always have a hard time remembering the syntax for namespaces, but it's pretty straightforward.

Say I want to create a model Blog::Post. The generator for this is:

rails generate model blog/post title:string ...

This will generate the following files (assuming you're using rspec and FactoryBot)

invoke  active_record
create    db/migrate/20241028193321_create_blog_posts.rb
create    app/models/blog/post.rb
create    app/models/blog.rb
invoke    rspec
create      spec/models/blog/post_spec.rb
invoke      factory_bot
create        spec/factories/blog/posts.rb

If the namespace already exists, you'll be prompted to either overwrite or keep the existing app/models/blog.rb file (you'll probably want to keep the existing one).

Happy generating!

Expo Reactive Native Apps on IOS Simulator

With Expo's CLI, the command npx expo run:ios will build and run your application in xcode's IOS simulator. You will need to have Xcode installed in order for the command to work. If you receive an error stating Xcode must be fully installed before you can continue, you may need to navigate to Xcode's settings and set a version of command line tools under the locations tab. image

Create index with migration shorthand in Rails

Using the Ruby on Rails migration generator is great to get up and running but always seems to require just a little bit more effort once the migration file is created.

There is a shorthand syntax for the cli interface of the generator but I can never remember it past a column name and type... so I'm putting this here to remember it for all of us!

To create an index just continue adding a bit more information to your column declaration. Let's start with this as a base so we're on the same page:

rails generate add_name_to_widgets name

This will create this migration:

class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
  end
end

Now by making a minor adjustment to the original cli call we can generate the index too:

rails generate add_name_to_widgets name:string:index
class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
    add_index :widgets, :name
  end
end

And you know what? We can do unique indexes as well:

rails generate add_name_to_widgets name:string:uniq
class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
    add_index :widgets, :name, unique: true
  end
end

Generate non-null field migration in Rails 8

I'm sure many of us are familiar with the Ruby on Rails migration generator...

rails generate migration add_name_to_widgets name

which generates a migration that looks like so:

class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
  end
end

Then to make the name column have a non-null constraint an addition of null: false would be needed:

class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string, null: false
  end
end

However, now in Rails 8 an adjustment was made to allow this to be done from the generator shorthand

rails generate migration add_name_to_widgets name:string!

By adding the exclamation point after the column type a migration will now be generated as we wanted before but without the additional manual edit:

class AddNameToWidgets < ActiveRecord::Migration[8.0]
  def change
    add_column :widgets, :name, :string, null: false
  end
end

h/t Akshay Khot

PG Cluster Commands in Ubuntu

The postgresql package for Ubuntu includes some useful commands for managing your database cluster. I learned about these today when we upgraded postgres versions on a digital ocean droplet.

The ones that I learned of were -

  • pg_lsclusters for listing the available clusters on the machine and their status
  • pg_upgradecluster OLD_VERSION CLUSTER_NAME for upgrading to a new cluster. If you don't specify the optional -v (new version), it's assumed you want the most recent version you have installed. This handles copying your old conf files to the new version and setting it up on the originally specified port.
  • pg_dropcluster VERSION CLUSTER_NAME for removing a cluster. Optionally you can specify --stop to force a shutdown and delete.
  • pg_createcluster VERSION CLUSTER_NAME which will create a new cluster given the version and name param. The default is to not start this new cluster on creation, but you can optionally specify the --start flag to immediately start the cluster.

https://manpages.ubuntu.com/manpages/trusty/man8/pg_createcluster.8.html https://manpages.ubuntu.com/manpages/trusty/en/man8/pg_dropcluster.8.html https://manpages.ubuntu.com/manpages/trusty/man8/pg_upgradecluster.8.html https://manpages.ubuntu.com/manpages/xenial/en/man1/pg_lsclusters.1.html

String Replace in Javascript

I'm very used to ruby's gsub(pattern, replacement) that does a [g]lobal [sub]stitution on a string - that is, it replaces all occurrences of the pattern in the string. If you wanted to just replace the first occurrence of the pattern you could use sub, but I so rarely need to do that I forgot it existed.

Javascript's equivalent, replace, handles things a little bit differently. So much so I was surprised by it's behavior.

replace(pattern, replacement)

pattern can be a string or a RegExp, but you get different replacement behavior depending on which you use.

  • If pattern is a string, only the first occurrence of pattern will be replaced.
  • If pattern is a RegExp, by default it will replace the first occurrence, unless you pass the global flag to the pattern. So if I wanted to replace asdf in with hjkl in a string, replace(/asdf/, "hjkl") will replace just the first occurrence. To replace all occurrences, it needs to be replace(/adsf/g, "hjkl) (note the global g flag in the regex).

So maybe the moral of the story is to always use a regex (and remember your flags!).

Docs: String.prototype.replace() - JavaScript | MDN

Sigils in Elixir

In Elixir, there's strings "hi" and charlists 'hi'.

You can also use the ~c sigil to denote a charlist -

~c"hi there"

There's also a word list sigil ~w which behaves similarly to %w in Ruby. It constructs a list of words -

~w(blue red green)
=> ["blue", "red", "green"]

Also worth mentioning, the ~r sigil for regexes -

"foo" =~ ~r"hashrocket"
=> false

"hashrocket" =~ ~r/hashrocket/
=> true

There's many other sigils in Phoenix and Elixir. Be sure to check out the docs!

h/t to Jack Rosa

https://hexdocs.pm/elixir/sigils.html

Alias Method on a GraphQL Field

A lot of times I find I have to alias a method on a model when I'm exposing it on a GraphQL type. Most often I have to do this with ? methods:

class Post < ApplicationRecord
  def draft?
    ...
  end
end

class PostType < GraphQL::Schema::Object
  field :draft, Boolean, null: false

  def draft
    @object.draft?
  end
end

I want the field to be draft (without the ?), but on it's own the GraphQL Ruby gem can't resolve it to draft?, so I have to add an additional def in the Type class. This works, but it feels clunky.

Turns out you can use the method: option on the field to alias it inline, which I think streamlines things:

class PostType < GraphQL::Schema::Object
  field :draft, Boolean, null: false, method: :draft?
end

Docs

Refetch Query with Apollo GraphQL Client

The Apollo GraphQL client allows you to easily refetch query results, which can come in super handy if you need to refresh data after some user action on the page. You can do this with the refetch function from useQuery:

const { data, loading, refetch } = useQuery(QUERY);

Then if you want to refetch data after a button click, you can:

<button onClick={() => refetch()}>
  Refetch the data!!
</button>

If you defined any variables in the initial useQuery, refetching will re-use those variables, but you can override them as well in the call to refetch:

refetch({ param: "new value" });

If you need to refetch data after a mutation, you can do that too.

Docs

Change Text of Blank Option in a Rails Select Tag

The form helper select tag (ex - f.select) accepts a boolean option for include_blank, which informs the select tag to have an extra blank option. Setting include_blank: true will look like the below - image

But you can also pass a string to include_blank and this will change the text content of the blank option -

<%= f.select :state, options_for_select(state_options), {include_blank: 'Select a State'} %>

image

https://devdocs.io/rails~7.1/actionview/helpers/formtaghelper#method-i-select_tag

casecmp to Compare Strings

Today I learned about casecmp and casecmp? to compare strings in ruby.

casecmp compares the downcase of both strings and returns 1 if the compared string is smaller, -1 if it's larger, and 0 if they are equal (and nil if they can't be compared).

"hashrocket".casecmp("hashrocket") # => 0
"hashrocket".casecmp("hAsHrOcKeT") # => 0
"hashrocket".casecmp("hashrocket123") # => -1
"hashrocket".casecmp("hashrock") # => 1
"hashrocket".casecmp(123) # => nil

casecmp? does the same comparison but just returns a boolean.

"hashrocket".casecmp("hAsHrOcKeT") # => true
"hashrocket".casecmp("hashrock") # => false
"hashrocket".casecmp(123) # => nil

h/t Brian Dunn

Enum Reject vs Filter

If you're anything like me, you regularly forget if Enum.filter/2 keeps elements that return true or filters them out.

Let existence of Enum.reject/2 be your reminder.

reject/2 does the same thing as filter/2 but discards truthy elements instead of keeping them. I wish that filter/2 was renamed to keep/2, then the 2 functions would feel more like logical opposites and perhaps more readable.

Ruby GraphQL Generators

Today I Learned the GraphQL ruby gem includes generators for types, mutations and other fun things.

rails g graphql:object
rails g graphql:input
rails g graphql:interface
rails g graphql:union
rails g graphql:enum
rails g graphql:scalar
rails g graphql:mutation

What's even neater is if the name of thing things you're generating matches an existing ActiveRecord model, it will scaffold all the database columns as fields!

If you have a model Book like:

class Book < ApplicationRecord
  attribute :id
  attribute :author_id
  attribute :title
  attribute :published_at
end

Running the object generator rails g graphql:object Book will produce:

module Types
  class BookType < Types::BaseObject
    field :id, ID
    field :author_id, ID
    field :title, String
    field :published_at, GraphQL::Types::ISO8601DateTime
  end
end

Using Enum Sort with multiple keys

You can sort an enum by multiple keys at once. For example, If we wanted to sort a list of 'Vehicle' structs by type and model at the same time, we could do this:

vehicle_list = [
  %Vehicle{type: "van", model: "Odyssey"},
  %Vehicle{type: "truck", model: "Avalanche"},
  %Vehicle{type: "van", model: "Pacifica"},
  %Vehicle{type: "truck", model: "Bronco"}
]

Enum.sort_by(vehicle_list, &{&1.type, &1.model})
#=>
[
  %Vehicle{type: "truck", model: "Avalanche"},
  %Vehicle{type: "truck", model: "Bronco"},
  %Vehicle{type: "van", model: "Odyssey"},
  %Vehicle{type: "van", model: "Pacifica"},
]

sort_by is first sorting the vehicles by the :type key and then sorting by the :model key.

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