Code & Clay – Notes to self. Mainly Ruby/Rails.

Monkey-patching locally

In Ruby, classes are open. They can be modified at any time. You can add new methods to existing classes and even re-define methods. Rubyists call this monkey-patching.

In the example below, I’ve added String#word_count to return the number of words in a string.

class String
  def word_count
    self.split.count
  end
end
"Don't put your blues where your shoes should be.".word_count # => 9

However, this change is global. What happens if someone else defines their own version of String#word_count based on different rules? Say, they might want to count the individual parts of a contraction, or ignore digits and any non-word characters.

Refinements allow us to monkey-patch classes and modules locally.

Here, I’ve created a new module and used Module#refine to add word_count to String.

module MyStringUtilities
  refine String do
    def word_count
      self.split.count
    end
  end
end

The refinement isn’t available in the global scope:

"Take my shoes off and throw them in the lake.".word_count # => NoMethodError: undefined method `word_count' for…

To activate the refinment, I need to use using:

using MyStringUtilities

"Take my shoes off and throw them in the lake.".word_count # => 10
"A pseudonym to fool him".word_count # => 5

If I activate the refinement within a class or module, the refinement is available until the end of the class or module definition.

Below, the refinement is not available at the top level because it is scoped to MyClass.

module MyStringUtilities
  refine String do
    def word_count
      self.split.count
    end
  end
end

class MyClass
  using MyStringUtilities

  def self.number_of_words_in(string)
    string.word_count
  end
end

"Out on the wily, windy moors".word_count #=> NoMethodError

MyClass.number_of_words_in("Out on the wily, windy moors") #=> 6

Audiobook streaming platform on AWS stack and Ruby on Rails

Piotr discusses how he implemented an audiobook streaming site in Rails, along with the challenges he faced.

Audiobooks tend to take up a lot of disk space so often streaming is the only practical method of delivery. Piotr writes about using AWS to convert MP3s and using Cloudfront to handle the streaming.

How to rename database columns in Rails

Often when I begin a project, I find myself rolling back migrations, dropping the database, renaming tables and columns whilst I figure out the shape of the model. If I need to rename a column, I’ll just update or delete the corresponding migration.

However, it is possible to change a column’s name in a migration.

Say, I have a table tasks and I want to change the name of the title column to name. First I ask Rails to generate the migration:

rails generate migration rename_title_to_name_in_tasks

The generated migration looks like this:

class RenameTitleToNameInTasks < ActiveRecord::Migration[6.0]
  def change
  end
end

I then remove change and add the up and down migration like so:

class RenameTitleToNameInTasks < ActiveRecord::Migration[6.0]
  def up
    rename_column :tasks, :title, :name
  end

  def down
    rename_column :tasks, :name, :title
  end
end

rename_column takes three arguments. The first is the table name. The second is the existing column name. The third is the new name.

Above, I have defined the up and down migration. That is, the up migration does the renaming bit when we migrate the database. The down migration runs when we roll back the migration – in this instance, it reverts the column’s name back to the original.

Before I run the migration, I give the schema a quick check.

  create_table "tasks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "title", null: false
    t.text "notes", default: ""
    t.uuid "task_list_id"
    t.uuid "fulfiller_id"
    t.datetime "completed_at", precision: 6
    t.index ["fulfiller_id"], name: "index_tasks_on_fulfiller_id"
    t.index ["task_list_id"], name: "index_tasks_on_task_list_id"
  end

Then, after running rails db:migrate, I check it again to see if the migration has worked:

  create_table "tasks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "name", null: false
    t.text "notes", default: ""
    t.uuid "task_list_id"
    t.uuid "fulfiller_id"
    t.datetime "completed_at", precision: 6
    t.index ["fulfiller_id"], name: "index_tasks_on_fulfiller_id"
    t.index ["task_list_id"], name: "index_tasks_on_task_list_id"
  end

All done!

Of course, five minutes later, I might decide that the new name probably isn’t the best and I can run rails db:rollback. On doing so, I can see that the column name has reverted to title.

Use inverse_of when creating associations with non-standard naming

I have two models ‘Customer’ and ‘GreenPencil’.

A customer can have many green pencils. But, as far as the customer is concerned, these are just pencils. It makes sense for me to call the relationship that.

I first establish that a green pencil belongs to a customer.

class GreenPencil < ApplicationRecord
  belongs_to :customer, required: true
  #...

Now, when I establish the other side of the relationship (remember, each GreenPencil has a foreign key pointing to its Customer) I can do this:

class Customer < ApplicationRecord
  has_many :pencils,
           class_name: 'GreenPencil',
           inverse_of: :customer

I’m telling Rails that a Customer has many pencils, the associated class is GreenPencil and that this relationship is the other side of GreenPencil’s relationship with a customer that I established at the start.

Rails usually sets inverse_of on all associations itself. However, because the class name differs from the association name, we need to specifiy it explicitly.

Rails will infer the inverse relationship if we set foreign_key but I think inverse_of is clearer to the reader.

Nested attributes

Sometimes it’s desirable to update one model’s attributes through an associated parent. Say, I might provide a form that asks a user for their name and email address but the user’s name is stored in the users table and their email address is recorded in another.

Rails provides the accepts_nested_attributes_for macro.

class User < ApplicationRecord
    has_one :contact_information, dependent: :destroy, required: true
    accepts_nested_attributes_for :contact_information

This allows me to update a user’s contact information like so:

user.update_attributes( contact_information_attributes: {
    phone_number: '123',
    email_address: 'hey@you.com
})

Instead of the headache of multiple forms and multiple controllers, I can now nest the contact information fields within the user form.

<%= bootstrap_form_for(@user) do |f| %>
  <%= f.text_field :name, required: true, autocomplete: "name" %>
  # ...
  <%= f.fields_for :contact_information do |ff| %>
      <%= ff.text_field :email_address, %>
      # ...

Finally, I need to specify the associated model’s attributes as allowable parameters in the parent resource’s controller:

def strong_params
    params.require(:user).permit(
      :name,
      contact_information_attributes: %i[
        email_address
      ]
    )
end