Today I Learned

A Hashrocket project

Ready to join Hashrocket? Find Openings here and apply today.

Limiting object counts in rails associations

Let’s say you have a model owner and a model pet. Every owner has_many pets, but you want to limit the number of pets an owner can have. You can use the following validation in your model to make sure these owners don’t get greedy with their number of pets.

has_many :pets
validates :pets, length: { maximum: 5 } 

The length helper is telling rails to only allow an owner to have a maximum of 5 pets. This is a little awkward because the length helper usually pertains to enforcing a minimum or maximum length on a string attribute, but it still works on the ActiveRecord Association Collection of :pets in a similar way where it is basically validating the maximum size of the collection.

Endless Range

If you have a Range that you want to extend infinitely in either direction, just simply leave it blank.

Here’s a simple example:

def age_category(age)
  case age
  when (...0)
    "unborn"
  when (0..12)
    "youngling"
  when (13..17)
    "teenager"
  when (18..64)
    "adult"
  when (65..)
    "old"
  end
end
>> age_category    0 => "youngling"
>> age_category   13 => "teenager"
>> age_category   18 => "adult"
>> age_category   65 => "old"
>> age_category  999 => "old"
>> age_category -999 => "unborn"

In a situation like this it’s nice to extend infinitely, rather than having to come up with some kind of arbitrary cutoff age like 100, that could in rare cases cause problems.

H/T Matt Polito for showing me this.

Expo Go over VS Code Live Share shared servers

VS Code Live Share is a pretty sweet way to remote pair. It supports Shared Servers, a fancy way to forward ports without exchanging ssh keys. Unlike ssh port forwarding, however, there isn’t a way for the joiner to choose which of their local ports will be used. The port selection is random.

That doesn’t cut it with Expo Go (and maybe vanilla react-native?) which demands the port be the same for the server and the client. So I looked for a way to forward one port on my local machine, the random one chosen by Live Share, to another port on my machine - the expo default of 19000.

Thanks to socat that was easy:

 socat tcp-listen:19000,reuseaddr,fork tcp:localhost:<random liveshare port>

I opened exp://localhost:19000 in Expo Go in the simulator. 🤘 Sweet.

Rails TimeHelpers

Rails ActiveSupport testing library includes some really helpful methods for manipulating time.

Here’s a cool one:

travel_to Time.new(2022, 9, 14) do  
    #everything inside this block is now happening as if it is 9-14-2022

end
# afterwards we return to the present

This way you can test all sorts of time-based features by jumping back and forth through time.

Trippy 🔮

Nil is actually NilClass

If for whatever reason you wanted to modify something inside nil, the class name is NilClass

🤯 Yes it’s very mind blowing, I know 🤯

Example of bad code I couldn’t figure out why it wasn’t working

class Nil
  def to_i
    -1
  end
end

The fix needed to make it behave as expected

class NilClass
  def to_i
    -1
  end
end

CSS Filter Property

There is a CSS property filter which allows you to manipulate properties of an element such as their blur, brightness, contrast, grayscale, opacity, and more. This is most often applied to images, but does work on any element.

.single-filter {
  filter: opacity(50%);
}
.multiple-filters {
  filter: blur(5px) grayscale(100%);
}

Here comes our friendly neighborhood Pikachu to help show us what this looks like: image

Here is a good reference for what filter can do.

Duplicating tabs in iTerm

If you want to open a new tab in iTerm in your same working directory, you can use the following steps:

  1. Navigate to preferences or use (⌘,).
  2. Click on the Keys settings.
  3. Hit + in the bottom left corner to add a new shortcut.
  4. Record a shortcut of your choosing
  5. Using the dropdown menu for Action: select ‘Duplicate Tab”

Now your new shortcut will open a new iTerm tab in whatever directory you have currently open.

Rename Git Remote

If the need arises to change the name of a git remote, in the past, I’ve normally done one of these two things:

  • delete and recreate the remote
  • manually edit the remote info in .git/config

Turns out there is a git command for this and I completely missed it!

git remote rename OLD_NAME NEW_NAME

As you can probably imagine, this handles the name change as well as the reference change in the config.

Clone a specific git branch

After cloning a repo, git will set your branch to whatever the value of HEAD is in the source repo:

$ ssh git@example.com
$ cat my-repo.git/HEAD
ref: refs/heads/master

If there is a different branch you want to clone, use the --branch flag:

$ git clone --branch my-feature git@example.com/my-repo.git
$ git branch
* my-feature

Returning to normal mode

When my keyboard randomly lost functionality of the ESC key, I was left unable to escape out of insert mode, back into normal mode.

If you ever find yourself in this conundrum, you can use CTRL-C to escape back to normal mode.

The caveat to this is that CTRL-C does not trigger abbreviations or the InsertLeave event.

Pathnames using division in Ruby

This will probably seem like a common pattern to you:

Rails.root.join("spec/support")

or

Rails.root.join("spec", "support")

Did you know that the division operator on Pathname is aliased to the addition operator? So you can do this:

Rails.root / "app" / "views"

I know it will probably frazzle a bunch of people, but I kinda love it.

📧 Change the delivery method of a mailer

You can change the delivery method of a mailer with the default method:

Rails.application.config.action_mailer.delivery_method
=> :smtp

class LocalMailer < ApplicationMailer
  default delivery_method: :sendmail

  def notify_root
    mail({to: "root@localhost", subject: "Alert"})
  end
end

LocalMailer.notify_root.deliver_now

Then check the inbox:

sudo su root
mail
Mail version 8.1 6/6/93.  Type ? for help.
"/var/mail/root": 1 message 1 new
>N  1 no-reply@rails-app.example.com     Fri Aug 19 17:29 494/19390 "Alert"
?

Batch Active Record Operations with `find_each`

Iterating over a collection of ActiveRecord models can incur a performance hit to your application, especially when the result set is large.

A basic query like all or where could return tens of thousand of records. When that query returns, it will instantiate your records all at once, loading them into memory, and potentially leading to instability. Luckily if we need to operate on a large result set, we can use the find_each method to work in batches on our query.

find_each works by using the find_in_batches method under the hood, with a default batch size of 1000.

# This is bad ❌
Customer.where(active: true).each do |customer|
  # ....
end

# This is good ☑️
Customer.where(active: true).find_each do |customer|
  # ....
end

If you need to, you can also adjust the batch size by passing the batch_size kwarg to find_each

Customer.where(active: true).find_each(batch_size: 2000) do |customer|
  # ...
end

https://devdocs.io/rails~6.1/activerecord/batches#method-i-find_each

H/T https://rails.rubystyle.guide/

Reverse proxy tcp/udp with nginx

Nginx has the ability to reverse proxy tcp and udp with the stream directive, similar to the http directive:

# Reverse proxy postgres server
stream {
  server {
    listen 5432;
    proxy_pass 172.18.65.97:5432;
  }
}

http {
  server { 
    ...
  }
}

This can be useful to load balance tcp streams like a database connection.

If you built nginx with --with-stream=dynamic (you can check with nginx -V) you will need to manually load a shared object:

# nginx.conf
load_module /usr/lib/nginx/modules/ngx_stream_module.so

Override a form's action url with `formaction`

Recently, I found myself with a form that needed to be able to submit to multiple urls. You can definitely use some javascript to get around this but that seemed like overkill for my situation. That’s when I learned about the HTML formaction attribute.

You can use this attribute on a submit tag and it will override the action specified by the form. Here’s an example with a Ruby on Rails form, but you just as easily do something similar with plain HTML -

<% form_with url: false do |f| %>
  <% users.each do |user| %>
    <%= check_box_tag "user_ids[]", user.id, false %>
  <% end %>

  <%= f.submit "Assign to Users", formaction: "/users/assign" %>
  <%= f.submit "Print for Users", formaction: "/users/print" %>
<% end %>

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-formaction

Saving screenshots to clipboard on Mac

Have you ever needed to take a screenshot just to send it to somebody, but then you are left with the cumbersome chore of having to actually delete the screenshot before they pile up on your desktop like a pile of dirty laundry?

Well, I am here to save you those 5 seconds you’ve been missing out on so you can enjoy that next sip of coffee—guilt free.

When taking a screenshot with CMD + shift + 3, hold down control as well.

When taking a screenshot with CMD + shift + 4, hold down control while dragging the crosshairs to select a section of screen to capture.

Now, instead of saving to your default screenshot location, it will just copy it to your clipboard. You can now just paste the image into your message with no cleanup necessary.

TL;DR
Hold down control when taking a screenshot to copy it to the clipboard.

Alternate ways to quit out of Vim

In an effort to increase my Vim knowledge, I stumbled across a blog post by rocketeer alumnus Josh Branchaud. His post covers a few ways to quit out of vim and it is worth the read.

In there I found these two useful ways for quitting out of Vim from Normal mode, as opposed to Command mode.

ZZ in normal mode is equivalent to :x   in command mode.
ZQ in normal mode is equivalent to :q! in command mode

Logging data as a table in the console

I’m sure you’re already familiar with console.log() to debug, but did you know that there is a similar console.table() that is great for displaying arrays and objects?

Here are some examples,

Assuming you had an array of names

img

Assuming you had a person object

img

Assuming you had an array of person objects

img

There is also an optional columns parameter, which takes an array containing the names of columns you want to include in the output.

console.table(data)
console.table(data, columns)

Understanding Query I/O in Postgres with BUFFERS

The EXPLAIN command in Postgres can help you understand the query plan for a given query. Furthermore, you can use EXPLAIN ANALYZE to see the estimated query plan and cost vs the actual time and rows.

To take it a step further, you can use EXPLAIN (ANALYZE, BUFFERS) to include a number that represents the I/O disk usage of certain parts of your query.

explain (analyze, buffers)
  select
    *
  from floor_plans
  order by created_at desc
;

                                                  QUERY PLAN
---------------------------------------------------------------------------------------------------------------
 Sort  (cost=2.56..2.60 rows=18 width=159) (actual time=0.062..0.065 rows=16 loops=1)
   Sort Key: created_at DESC
   Sort Method: quicksort  Memory: 28kB
   Buffers: shared hit=2
   ->  Seq Scan on floor_plans  (cost=0.00..2.18 rows=18 width=159) (actual time=0.018..0.032 rows=16 loops=1)
         Buffers: shared hit=2
 Planning Time: 0.111 ms
 Execution Time: 0.106 ms
(8 rows)

Make sure that if you run this with a query that writes, that you wrap it in a BEGIN...ROLLBACK statement.

https://www.postgresql.org/docs/current/using-explain.html

Nested table querying with ActiveRecord

Sometimes you need to query a joined table in ActiveRecord. This can be easily done via the hash syntax:

User.where(greetings: {text: "HOWDY"}).to_sql

=> SELECT "users".* FROM "users" WHERE ("greetings"."text" = 'HOWDY')

Note: the key here (greetings) is pluralized because we’re referencing a table name and not a potential relationship that ActiveRecord knows about.

Note: I’ve ommited a join to the greetings table that would have this query make sense

Cool: so we can do that but there’s even an alternative way to handle this using dot notation

User.where("greetings.text": "HOWDY"}).to_sql

=> SELECT "users".* FROM "users" WHERE ("greetings"."text" = 'HOWDY')

Under the hood, ActiveRecord will convert dots from the string key and interpret them as table and column names.

Match strings with regular expressions

In Ruby you can use String#=~ to compare a string with regexp, returning the first index where it is found. For example let’s search for the first ? in this string:

"www.example.com/search?meatloaf" =~ /\?/ 
=> 22

If there is no match, it returns nil

"www.example.com" =~ /\?/ 
=> nil

BONUS: When using Regexp#=~, which functions very similarly, you can use a regexp with named captures to store them in local variables.

/(?<search_params>\?.+)/ =~ "www.example.com/search?lasagna"
=> 22
search_params
=> "?lasagna"

Receive Javascript messages from iFrame

Due to security reasons what can be received from an iFrame can be very limited. However there is a utility built for the purpose of safely communicating with things like an iFrame, pop-up, etc.

An iframe can utilize the Window.postMessage() function to post info that may be relevant to someone embedding the iFrame.

Then in the page where the embedded iFrame is located, the message event can be watched.

window.addEventListener("message", (event) => {
  if (event.origin !== "IFRAME URL")
    return;

  // ...
}, false);

By utilizing the event’s origin you can be even safer. Above we’re declaring that if the origin of tghe event did not come from the expected iframe’s url… just stop.

If you’re good with the origin check then looks at the event’s data property. It will provide whatever was sent via postMessage().

Write a warning message to $stderr

While debugging, if you ever need to write to $stderr you might use $stderr#puts, but you can use Warning#warn which is better called from Kernel, because Kernel appends newlines and respects the -W0 flag:

$stderr.puts "you have been warned"
Warning.warn "you have been warned"
Kernel.warn "you have been warned"
warn "you have been warned"

Output:

you have been warned
you have been warnedyou have been warned
you have been warned

Listen for onfocus events on document

If you want to set an event listener on document for an input’s onFocus event, you’ll find there is no “onFocus” event for document. Instead you can use the focusin event listener:

document.addEventListener("focusin", function(e) {
  console.log("an input received focus");
});

So if you had dynamic events you can achieve this:

const on = (eventName, elementSelector, handler) => {
  document.addEventListener(eventName, function (e) {
    for (var target = e.target; target && target != this; target = target.parentNode) {
      if (target.matches(elementSelector)) {
        handler.call(target, e);
        break;
      }
    }
  }, false);
}

on("focusin", "#email_address", function(e) {
  const currentTarget = this;
  console.log("Email address just received focus");
});

Set the id of the root document fragment

<template id="my-template">
  <div>
    <input type="hidden" value="1" name="amount" />
  </div>
</template>

You can set the id by querying for the div:

const deep = true;
const template = document.querySelector("#my-template");
const div = document.importNode(template.content, deep);
div.querySelector("div").id = "my-id";
document.body.appendChild(div);

Output:

  <div id="my-id">
    <input type="hidden" value="1" name="amount" />
  </div>

Start multiple processes in dev env with bin/dev

Rails comes with bin/dev to start multiple processes from a Procfile.dev

# Procfile.dev
web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch
jobs: bin/good_job
firebase: firebase emulators:start --import=./emulator-data

So that you can run everything at once:

$ bin/dev
08:36:10 web.1  | started with pid 40038
08:36:10 css.1  | started with pid 40039
08:36:10 firebase.1 | started with pid 40040
08:36:11 firebase.1 | i  emulators: Starting emulators: auth, firestore, storage
08:36:11 firebase.1 | i  firestore: Firestore Emulator logging to firestore-debug.log
08:36:11 web.1  | => Booting Puma
08:36:11 web.1  | => Rails 7.0.2.3 application starting in development
08:36:11 web.1  | => Run `bin/rails server --help` for more startup options
08:36:12 web.1  | Puma starting in single mode...
08:36:12 web.1  | * Puma version: 5.6.4 (ruby 3.1.2-p20) ("Birdie's Version")
08:36:12 web.1  | *  Min threads: 5
08:36:12 web.1  | *  Max threads: 5
08:36:12 web.1  | *  Environment: development
08:36:12 web.1  | *          PID: 40038
08:36:12 web.1  | * Listening on http://127.0.0.1:3000
08:36:12 web.1  | * Listening on http://[::1]:3000
08:36:12 web.1  | Use Ctrl-C to stop
08:36:12 css.1  |
08:36:12 css.1  | Rebuilding...
08:36:12 css.1  | Done in 300ms.

Avoid time discrepancies when benchmarking

You can avoid time discrepancies due to gc and allocs by using Benchmark#bmbm:

require 'benchmark'
array = (1..1000000).map { rand }
Benchmark.bmbm do |x|
  x.report("sort!") { array.dup.sort! }
  x.report("sort")  { array.dup.sort  }
end
Rehearsal -----------------------------------------
sort!   1.490000   0.010000   1.500000 (  1.490520)
sort    1.460000   0.000000   1.460000 (  1.463025)
-------------------------------- total: 2.960000sec
            user     system      total        real
sort!   1.460000   0.000000   1.460000 (  1.460465)
sort    1.450000   0.010000   1.460000 (  1.448327)

Make numbers more readable in a Rails view

Rails has a number of handy helper methods.

Specifically, in ActionView::Helpers::NumberHelper there is the method number_with_delimiter

If you want to display the number 100000 and have people know right away whether it is 1 million or 100 thousand, just pass it to number_with_delimiter and it will return "100,000".

Some useful optional keyword parameters:
delimiter:"🍔" replaces the default , delimiter. (100000 becomes 100🍔000)
separator:"🍕" replaces the default . separator. (100.56 becomes 100🍕56)

Add primary key to table

You can add a primary key to a table with alter an alter table statement:

Table with no primary key:

create table no_pks (id int generated by default as identity not null);
insert into no_pks select from generate_series(0,999);
[local] dillon@dillon=# \d no_pks
                           Table "public.no_pks"
 Column |  Type   | Collation | Nullable |             Default
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | generated by default as identity

You can add it later:

alter table no_pks add primary key (id);
[local] dillon@dillon=# \d no_pks
                           Table "public.no_pks"
 Column |  Type   | Collation | Nullable |             Default
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | generated by default as identity
Indexes:
    "no_pks_pkey" PRIMARY KEY, btree (id)