Today I Learned

hashrocket A Hashrocket project

103 posts by andrewvogel twitter @calllmehvogel

Run Explain (and Anaylze) in Rails Console

If you're troubleshooting some slow SQL queries, chances are you've used EXPLAIN or even EXPLAIN ANALYZE. But TIL, that you can actually run these in the Rails console on your Rails models (Active Model Relations). It looks something like this -

> User.active.explain

Or if you're on a more recent version of Rails, you can also pass arguments to .explain, such as :analyze -

> User.active.explain(:analyze)

You can also call it on other methods like pluck, etc. -

> User.active.explain.pluck

Be warned that calling .explain will execute the query.

https://devdocs.io/rails~7.2/activerecord/relation#method-i-explain

https://guides.rubyonrails.org/active_record_querying.html#running-explain

Normalization Attributes in Rails

In Rails 7.1, attribute normalization was introduced. This gives us an easy way to clean up data before persistence, or other actions.

class Contact < ApplicationRecord
  normalizes :phone, with: -> (phone) { phone.gsub(/\D/, "") } # remove non-number chars
end

The above ensures that before save, a phone number like 904-123-4567 gets converted to 9041234567.

In Rails 8, attribute normalization was backported to ActiveModel. So you can also use these methods in ActiveModels (re: Form objects) -

class ContactForm
  include ActiveModel::Attributes
  include ActiveModel::Attributes::Normalization

  attribute :phone
  
  normalizes :phone, with: -> (phone) { phone.gsub(/\D/, "") }
end

contact = ContactForm.new
contact.phone = "904-123-4567"
contact.phone # 9041234567

Authenticate Req Requests with Basic Auth

Req has a built-in for handling some auth mechanisms, such as Basic Auth, Bearer, and netrc.

You can send the auth as a tuple, where the first element is the authentication type and the second is the credential -

Req.post("https://example.com", json %{foo: "bar"}, auth: {:basic, "user:password"})

https://hexdocs.pm/req/Req.Steps.html#auth/1

Extract Secrets from 1Password for Kamal Deploy

Kamal, the default deployment tool for Rails, has some really great features. One that I just discovered today is kamal secrets.

You can use this utility to expand sensitive credentials from external sources during deployment. Out of the box, it supports 1Password, Bitwarden (and Secrets Manager), LastPass, AWS Secrets Manager, Doppler, and GCP.

You can run the command for SECRETS from the .kamal/secrets file manually to test everything out.

A pre-req for using 1Password is that you will need to install the OP CLI and login to your vault:

brew install 1password-cli

op signin

Next you'll need your account id. You can get that with -

op whoami

Then verify you can read your secrets. The output of the command inside the $(...) is a stringified JSON -

SECRETS=$(kamal secrets fetch --adapter 1password --account op_account_id --from "op://Example/ApiKeys" KAMAL_REGISTRY_PASSWORD)

The output will look something like this -

\{\"Example/ApiKeys/KAMAL_REGISTRY_PASSWORD\":\"xxxxxxxxxxxxx\"\}

The last part is expanding this. You can pass this JSON string to kamal secrets extract to extract the value from the key in the JSON.

kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}

Handy Commands for Raspberry Pi Sensors

I recently got an Enviro hat for my Raspberry Pi Zero W. And in tinkering with it, I found there's a ruby driver for interacting with the BME280 sensor.

You can use it like this -

require "i2c/device/bme280"
require "i2c/driver/i2c-dev"

I2CDevice::Bme280.new(driver: I2CDevice::Driver::I2CDev.new("/dev/i2c-1")).read_raw

In debugging, I learned a few useful commands to check the i2c port. Basically, I just needed to figure out what i2c port to use for the ruby driver.

Check if the i2c port is enabled (raspberry pi os)

sudo raspi-config nonint get_i2c # 0 if enabled, 1 if disabled

List all the i2c interfaces by lsing the dev node.

ls /dev/i2c*

Check if the i2c board interface is in the list of kernel modules

lsmod | grep i2c

Run Script on Login/Start in Windows

You can automatically run a script on Windows during login by placing files in your users "Programs\Start Up" folder. It's useful for running *.bat scripts.

You can easily access this location by opening the "Run" dialog - Windows + r and typing shell:startup.

If you're an admin, you can also set this for all users by typing shell:common startup, which will open the shared Start Up folder.

If this is too much, you can also accomplish a similar thing by creating a "Scheduled Task". In the Windows search bar, type "Task Scheduler". Open this then click "Actions" -> "Create Task"

Managing Return Value of ActiveRecord Transactions

I recently ran into a situation where I wanted to know the return state of a transaction in ActiveRecord. Basically, I wanted to know if it succeeded or failed, then return true/false.

There's 3 scenarios to keep in mind to when trying to use this approach:

  1. Transaction succeeds - the last line of the transaction is the return
    class TestModel
     def save
       result =
         ActiveRecord::Base.transaction do
           SomeModel.last.touch
         end
    
       result
     end
    end
    
    > TestModel.new.save
    => true
  2. Transaction rollbacks - transactions handle ActiveRecord::Rollback by default, so the transaction block returns nil
    class TestModel
     def save
       result =
         ActiveRecord::Base.transaction do
           raise ActiveRecord::Rollback
         end
    
       result
     end
    end
    
    > TestModel.new.save
    => nil
  3. Other Errors - any errors raised inside the transaction will be raised by the block
    class TestModel
     def save
       result =
         ActiveRecord::Base.transaction do
           raise ActiveRecord::Rollback
         end
    
       result
     end
    end
    
    > TestModel.new.save
    StandardError: StandardError from .....

Putting it all together now, we can return a true/false for all scenarios -

class TestModel
    def save
      result =
        ActiveRecord::Base.transaction do
          SomeModel.new(attrs).save!
          OtherModel.new(other_attrs).save!
        end

      result

      did_save = !!result
      did_save
    rescue => e
       false
    end
end

Rails 8 Generate One Off Scripts

Rails 8 added a new generator command for creating 1 script files. The docs encourage the use of this folder for one off tasks, data migrations, etc. Previously, you could have used rake tasks to accomplish a similar thing.

Running this generator command will create the script dir if one does not already exist.

❯ rails g script migration_script
      create  script/migration_script.rb

Ruby 3.4 Warn on Method Unused Blocks

A change to Ruby 3.4 will print a warning while using the verbose flag -w when a method is passed a block and the block is not used by that method.

Here's our example file scratch.rb -

def print_message(message) = puts message

print_message("hello_world") { puts "hello anonymous" }
> ruby scratch.rb
hello world

> ruby -w scratch.rb
scratch.rb:3: warning: the block passed to 'Object#print_message' defined at scratch.rb:1 may be ignored
hello world

https://www.ruby-lang.org/en/news/2024/12/25/ruby-3-4-0-released/

Start a Rails Console in the Browser

When you generate a new Rails app, it comes with the web-console gem, a handy helper for embedding a rails console in the browser.

This can be useful in development for debugging things in the same process as your webserver. In my case, I used it to test out some action cable related things.

In your controller, just add console to the action -

class AppointmentsController < ApplicationController
  def index
    console
  end
end

image

https://github.com/rails/web-console

Convert a Keyword List to a Struct

You can use Kernel.struct/2 to convert a keyword list (or Enumerable) into a struct.

I found this useful when working with a new social media integration. I read some key-values out of the config environment and needed to convert these into a named struct.


> creds = Application.get_env(:tilex, __MODULE__)
[username: "foo", password: "bar"]

> struct(BlueskyEx.Client.Credentials, creds)
%BlueskyEx.Client.Credentials{
  username: "foo",
  password: "bar"
 }

When you pass keys that are not part of the struct, they are discarded for you. If you want to raise an error, you can use the related struct!/2 function

https://devdocs.io/elixir~1.17/kernel#struct/2

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

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

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

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

Run Expo Build Locally

As a follow up to my previous TIL about building expo, I just learned that you can run your build locally! Just use the --local flag.

It's important to note, you will need Ruby installed locally, as well as the fastlane and cocoapods gems

gem install fastlane cocoapods # only if you haven't already

eas build --platform ios --profile dev --local

https://docs.expo.dev/build-reference/local-builds/

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! 🥳

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="/https/til.hashrocket.com/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

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

Create Accessors for JSONB column data in Rails

I was recently reading about the ActiveRecord::Store API and just today, I got the chance to use it. It's a really great API for working with serialized data in Rails.

Let's consider the following model Geolocation, with a jsonb column data. The contents of the data column look something like this -

{
  "coordinates": {
    "latitude": "5.887692972524717",
    "longitude": "-162.08234016158394"
  }
}

The store_accessor creates getters and settings for coordinates.

class Geolocation < ApplicationRecord
  store_accessor :data, :coordinates
end

Geolocation.last.coordinates
# => {"latitude" => "5.887692972524717", "longitude" => "-162.08234016158394"}

In my case, I have nested data that I'd like to create accessors for - latitude and longitude. From what I could find, this API doesn't support nested data yet, so we have to bring in a helper from ActiveModel::Attributes. We declare the coordinates portion as a jsonb attribute.

class Geolocation < ApplicationRecord
  store_accessor :data, :coordinates
  store_accessor :coordinates, :latitude, :longitude
  attribute :coordinates, :jsonb
end

geo = Geolocation.new
geo.coordinates
# => nil
geo.latitude
# => nil
geo.longitude
# => nil

geo.latitude = 5.887692972524717
# => 5.887692972524717
geo.coordinates
# => {"latitude"=>5.887692972524717}
geo.longitude = -162.08234016158394
# => -162.08234016158394
geo.coordinates
# => {"latitude"=>5.887692972524717, "longitude"=>-162.08234016158394}

I_Ran_Out_Of_Words_Hopefully_You_Get_My_Point :)

S/O Vlad & LayeredDesignForRailsApplications

https://devdocs.io/rails~7.1/activerecord/store

Handle Julian Dates in Ruby

I recently had to deal with parsing Julian Dates on a project. This was my first time seeing this in the wild and to my surprise, Ruby has some handy utilities on Date for converting to/from Julian Date Numbers.

Parse a Julian Date Number into a Date

Date.jd(2459936)
#=> #<Date: 2022-12-22 ((2459936j,0s,0n),+0s,2299161j)>

Convert a Date Object to a Julian Date Number

Date.new(2024, 02, 29).jd
#=> 2460370

https://en.wikipedia.org/wiki/Julian_day

Disable database management tasks in Rails

If you wish to not have Rails manage migrations, seeds, schema changes, etc, you can add the database_tasks: false to your database.yml. This may be useful if you are "inheriting", sharing a database managed with some other system, or if you are connecting to a readonly database.

production:
  database: hashrocket
  adapter: postgresql
  encoding: unicode
  pool: 5
  database_tasks: false

https://guides.rubyonrails.org/active_record_multiple_databases.html#connecting-to-databases-without-managing-schema-and-migrations

Unsubscribe from `watchPositionAsync` in Expo

After seeing some very unexpected behavior, I went down the rabbit hole of StackOverflow, Reddit, and finally the Expo documentation for the function watchPositionAsync.

As it turns out, I had overlooked that this function returns a promise that resolves to a LocationSubscription. Meaning the following code does NOT work for obvious reasons.

Bad ❌

import { Accuracy, watchPositionAsync } from 'expo-location';

function Actions() {
   useEffect(() => {
      const subscription = watchPositionAsync(
        { accuracy: Accuracy.Balanced },
        coords => console.log(coords)
      );
      return subscription.remove();
    }, []);
  // ...
}

In order to get the subscription object, I had to let the promise resolve. This now looks like below.

Good ✅

import { Accuracy, watchPositionAsync } from 'expo-location';

function Actions() {
   useEffect(() => {
      let subscription;
      async function watchLocation() {
        subscription = await watchPositionAsync(
          { accuracy: Accuracy.Balanced },
          coords => console.log(coords)
        );
      }
      watchLocation();
      return subscription?.remove();
    }, [])
  // ...
}

Hooray documentation! 😅

https://docs.expo.dev/versions/latest/sdk/location/#locationsubscription

Create Environment Specific Rails Credentials

If you've worked with the rails encrypted credentials system before, you're probably familiar with the command to edit them

bin/rails credentials:edit

This will create(if it doesn't exist config/credentials.yml.enc by way of the RAILS_MASTER_KEY). If you only have this credentials file, the items in this file will be available to all environments of your application.

But you can also create/edit environment specific credentials by specifying an environment. This will similarly create 2 files - config/development.key and config/development.yml.enc.

bin/rails credentials:edit --environment development

Credentials specified in this file will only be available in the development environment

https://edgeguides.rubyonrails.org/security.html#custom-credentials

Get Screen Dimensions in React Native

Before today, I was familiar with the older Dimension API. However, you can use the hook useWindowDimensions to get screen size updates in a React Native component. The values returned by the hook will change as the screen size changes.

import { useWindowDimensions, Text, View } from "react-native"

function App() {
  const { width, height } = useWindowDimensions();
  
  return (
    <View>
      <Text>{width}</Text>
      <Text>{height}</Text>
    </View>
  )
}

https://reactnative.dev/docs/dimensions

https://reactnative.dev/docs/usewindowdimensions

Files Have Extended Attributes in OSX

I was looking into OSX's quarantine mechanism, and learned how downloaded executables are prevented from running. OSX uses extended attributes to prevent these from running, aka quarantine.

For example, if you install chromedriver with homebrew, then try to open it, you'll see that all too familiar alert that "the developer cannot be verified". How does OSX know to do this? Well, when the file is downloaded, it writes an extended attribute (com.apple.quarantine) key-value to the file (see Gatekeeper). These extended attributes hold additional metadata about each file.

In the terminal, you can view these attributes on a file with the xattr command.

> xattr -l /usr/local/bin/chromedriver
com.apple.quarantine: xxxxxxxxxxxxxxxxxxxxxxxx

You can manually remove an attribute with this utility using the -d flag -

> xattr -d com.apple.quarantine

https://ss64.com/osx/xattr.html

ActiveRecord's attribute_in_database method

Wanted to share a method that I learned about today.

ActiveRecord has a handy utility for checking the value of an attribute in the database. These methods are actually on the Dirty module, and as such, are intended to be used when checking during validations or callbacks before saving.

These particular methods will read from the database (as their name implies), instead of using the value currently in memory.

There's 2 different methods to be aware of that you can use like below -

class Invoice < ActiveRecord::Base
  validates :amount, presence: true
end

invoice = Invoice.last
invoice.amount
=> 10.0

invoice.amount = 80.0

invoice.attribute_in_database("amount")
=> 10.0

invoice.amount_in_database
=> 10.0

ActiveRecord::AttributeMethods::Dirty Docs

Understanding the 'WWW-Authenticate' Header

If you've ever made an HTTP request and got a 401 response status, the response headers most likely included 1 or more entries for WWW-Authenticate. This flow is called "challenge and response" and it's part of the framework for doing HTTP Authentication.

This header is used to tell the client how it can authenticate in order to gain access to the requested resource.

For example, a response might include the following headers:

WWW-Authenticate: Basic
WWW-Authenticate: NTLM

Which means that the requested resource supports both Basic and NTLM authentication schemes.

It's also possible for these headers to come back with other metadata about their authentication schemes like token68 and realm.

Next time you get a 401, check out the response headers to see what Auth schemes are supported!

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate

Import Github User Public SSH Keys

You can use this command to add keys to the current user authorized_keys file. This command works for public keyservers, but in my case, I used it for Github. Handy when setting up a new machine or adding a new user's keys to a system.

# Example - ssh-import-id gh:GITHUB_USERNAME

ssh-import-id gh:avogel3

If the PROTOCOL (gh above) portion is absent, it defaults to lp. Acceptable values are lp = launchpad and gh = github. You can also configure this command to handle a specific URL when not specifying a protocol.

https://manpages.ubuntu.com/manpages/xenial/man1/ssh-import-id.1.html

Quote a SQL Value in Rails

If you saw my last post about Geocoding, you'll notice that the value passed to the geocode sql function is an address. To properly pass that value, we need to make sure that we quote it for SQL land.

❌ Bad

ActiveRecord::Base.connection.execute(<<~SQL)
  select
    rating
  from geocode('#{address}', 1)
SQL

Passing a mundane address like 100 O'Connel Ave will cause the above to throw an error in the database

But if we use the quote function from ActiveRecord, we properly quote our strings for SQL:

✅ Better

quoted_address = ActiveRecord::Base.connection.quote(address)

ActiveRecord::Base.connection.execute(<<~SQL)
  select
    rating
  from geocode(#{quoted_address}, 1)
SQL

Doing this ensures that we mitigate SQL Injection attacks and we properly account for things like single quotes in our values.

https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Quoting.html#method-i-quote

Geocode an Address in PostgreSQL with PostGIS

I recently learned that you can use PostGIS alongside the Tiger Geocoder extension to geocode an address in Postgres. This is especially handy if you have a specific locale (US or state level) that you need to Geocode. In my case, I need lat/long coordinates for addresses in Florida and Illinois.

Another reason I like this is because it is free - no need to pay for an additional service.

Here's what the API looks like:

select
  result.rating,
  ST_X(result.geomout) as longitude,
  ST_Y(result.geomout) as latitude
from geocode('320 1st St N, Jacksonville Beach FL', 1) as result;

 rating |     longitude      |      latitude
--------+--------------------+--------------------
      1 | -81.39021163774713 | 30.291481272126084
(1 row)

https://postgis.net/docs/manual-3.3/postgis_installation.html#loading_extras_tiger_geocoder

h/t Mary Lee

Download and Run an Expo Build on Simulator

If you're using EAS to build your Expo app, you can build then install it with the following command:

eas build --platform ios

Of course, the caveat here is that you must wait for the build to finish.

But, if you already have a build in EAS and you just want to install it on your local simulator, you can use:

eas build:run --platform ios

https://docs.expo.dev/build-reference/simulators/

Bulk Delete Records in Rails with `delete_by`

I'm sure you're familiar with the Active Record method delete_all . But I recently learned of another method that can used to shorthand this method-call on a where statement.

You see - delete_by(...) is the shorthand for Model.where(...).delete_all

Model.where(foo: true, bar: false).delete_all

# vs.

Model.delete_by(foo: true, bar: false)

Similarly to delete_all, you will need to be careful with delete_by as it creates a single delete statement for the database and callbacks are not called.

Bonus round -> destroy_by is a similar shorthand for Model.where(...).destroy

https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by

Upload Active Storage Attachments to Specific Path

If you've ever wanted to organize your bucket while using Active Storage, it looks like this is now possible as of Rails 6.1

By passing the key argument to #attach, you can specify the location of the file within the bucket -

class Invoice < ApplicationRecord
  has_one_attached :document
end

invoice = Invoice.create
invoice.document.attach(
  key: "invoices/invoice_1_20230505.pdf",
  io: File.read("invoice_1.pdf")
)

https://github.com/rails/rails/commit/4dba136c83cc808282625c0d5b195ce5e0bbaa68 https://github.com/rails/rails/issues/32790

Custom Flash Messages in Rails

Why Aren't My Flash Messages Working?

Turns out, there's 2 keys that Rails supports by default for flash messages. Those are alert and notice; you can use them like this in your controller:

redirect_to users_path, notice: "User created successfully"
# - or -
render :new, alert: "An error prevented the user from being saved"

But if your flash rendering code is generic enough, you might notice that explicitly setting a key/message on flash will work for values other than the defaults:

flash[:success] = "User created successfully"
redirect_to users_path

TIL Rails has a helper that will allow us to add our own custom flash messages - add_flash_type. You can use this an a controller (re: ApplicationController) to enable new flash message types. This will allow you to do the one liners in render and redirect calls:

class ApplicationController < ActionController::Base
  add_flash_type :my_flash_type
end

# ...

redirect_to users_path, my_flash_type: "User created successfully"
# - or -
render :new, my_flash_type: "An error prevented the user from being saved"

In addition, it will also add a variable in our views to retrieve this message:

<%= my_flash_type %>

https://api.rubyonrails.org/classes/ActionController/Flash/ClassMethods.html

Convert Changeset Into Schema With `apply_changes`

Often times, it's necessary to perform some intermediate operations on a Ecto changeset before saving it to the database. It can be easier to deal with the underlying schema. And you might need the entire schema rather than only the changes in the current changeset.

You can use the apply_changes/1 function to apply that changeset and receive schema with which you can perform operations. It's worth noting that the data is not validated when changes are applied, so care needs to be taken to ensure validity before an attempt to save that record.

iex> lead_schema = Lead.changeset(%{name: "Andrew", email: "Andrew.Vogel@hashrocket.com"})
                   |> apply_changes() 
%Lead{name: "Andrew", email: "Andrew.Vogel@hashrocket.com"}

iex> lead_schema |> downcase_email() |> do_some_other_processing()

https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_changes/1

Curl `-T/--upload-file` in Faraday

I had a bit of trouble trying to find docs on how to do a curl --upload-file request with ruby. This flag is a special flag that tells curl to generate a PUT request with the body being the file(s) to upload to the remote server.

In my case, I wanted to upload a single file, and I accomplished this with the faraday and faraday-multipart gem:

require 'faraday'
require 'faraday-multipart'

conn = Faraday.new("https://example.com") do |f|
  f.request :multipart
end

upload_file = File.open("./path/to/image.jpg")

conn.put("/file-upload") do |req|
  req['Content-Type'] = 'image/jpeg'
  req['Content-Length'] = upload_file.size.to_s
  req.body = Faraday::Multipart::FilePart.new(
    upload_file,
    'image/jpeg'
  )
end

Part of the magic here is that you need to explicitly set the Content-Type and the Content-Length header.

https://github.com/lostisland/faraday-multipart

Remove Padding from Postgres Formatting Functions

Earlier today, I was trying to join a table on a formatted string row, but wasn't getting the results I expected. Turns out that my formatting string had blank padding and I discovered "fill mode".

When using postgres formatting functions, like to_char, some of the formatting options include padding in the result. For example, the day format string will be blank padded to 9 chars.

select to_char(current_date, 'day');

  to_char
-----------
 sunday

You can use the "fill mode" (FM) option to remove any leading zeros or blank padding characters by prepending it to your format option:

select to_char(current_date, 'FMday')

 to_char
---------
 sunday

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

Find Unused Cucumber Step Definitions

One of the challenges of using cucumber is properly managing your step definitions. Left unchecked, you will eventually have many unused steps. It's extremely cumbersome to prune these manually. Luckily, you can use cucumber's -f / --format flag to get feedback on unused step_definitions and their locations:

bundle exec cucumber --dry-run --format=stepdefs

If your step definition is unused, it will be annotated with a line under that says NOT MATCHED BY ANY STEPS. See the example -

/^I submit the proposal request form$/     # features/step_definitions/contact_steps.rb:39
  NOT MATCHED BY ANY STEPS

Pattern Match Keyword List in Function Def

TIL that you can pattern match a keyword list in a function definition.

Sometimes you'll receive the last argument as an empty keyword list; in this case, we're calling it opts. You can pattern match key-values by defining the shape in a function.

  defmodule Example do
    def hello(opts \\ [])
 
   # will match when message is the only keyword included in opts
    def hello([message: message]) do
      IO.puts("Hello #{message}")
    end

    # will match when there are multiple keywords but message is the first
    def hello([{:message, message} | _rest] = opts) do
      IO.puts("Hello #{message}")
    end
  end

> Example.hello(message: "World")
Hello World
:ok

# could also call in the more verbose way
> Example.hello([message: "World"])
Hello World
:ok

# :message but with other args after
> Example.hello(message: "World", something_else: "hi")
Hello World
:ok

Note: As a TIL reader pointed out, pattern matching Keywords will make your function args order dependent. The following would not work:

> Example.hello(foo: "bar", message: "World")
** (FunctionClauseError) no function clause matching in Example.hello/1 

If you need them to be order independent, use a map or just match on single argument, then check for each option appropriately with the Keyword module.

https://elixir-lang.org/getting-started/keywords-and-maps.html

How to Change Password of SSH key

It's possible to change the password of your current ssh key if you have the current password or it is not currently password protected. You can use the command:

ssh-keygen -p

From the man pages -

Requests changing the passphrase of a private key file instead of
creating a new private key.  The program will prompt for the file
containing the private key, for the old passphrase, and twice for
the new passphrase.

https://man.openbsd.org/ssh-keygen.1#p