Today I Learned

hashrocket A Hashrocket project

306 posts about #ruby surprise

FactoryBot Traits inside Traits

Today I learned you can nest FactoryBot traits within other traits. Traitception?!

Say you have a blog post model with a deleted_at attribute, and an optional deleted_by attribute. You could have:

FactoryBot.define do
  factory :post do
    trait :deleted do
      deleted_at { Time.current }
    end

    trait :deleted_by_admin do
      deleted
      deleted_by { :admin }
    end
  end
end

There the deleted in deleted_by_admin references the deleted trait above it.

You could alternatively define a new factory that composes the two traits, but it's always nice to have options.

factory :admin_deleted_post, traits: [:deleted, :deleted_by_admin]

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

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

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)).

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)

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)

Change creation strategy in FactoryBot

I found out that's possible to change the FactoryBot strategy by invoking the to_create method inside the factory definition.

We had to do that to make factory bot linting to work on a factory that acts like an enum. So we did something like this:

FactoryBot.define do
  factory :role do
    to_create do |instance|
      instance.attributes = instance.class
        .create_with(instance.attributes)
        .find_or_create_by(slug: instance.slug)
        .attributes

      instance.instance_variable_set(:@new_record, false)
    end
  end
end

The said part here is that FactoryBot expects us to mutate that instance in order to work.

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

Remove padding from values in strftime

By default, some numbers in strftime are padded, either with 0 or ' '.

For example:

best_moment_ever = DateTime.new(1996, 2, 15, 19, 21, 0, '-05:00')
=> Thu, 15 Feb 1996 19:21:00 -0500

best_moment_ever.strftime("%m/%e/%Y at %l:%M%P")
=> "02/15/1996 at  7:21pm"

As we can see, there is a big gap between at and 7:21pm. This is because the hour is being padded with empty string. Sometimes this is fine, but if you ever wanted to remove any padding, just add a - flag to the directive:

best_moment_ever.strftime("%-m/%e/%Y at %-l:%M%P")
=> "2/15/1996 at 7:21pm"

Notice how we also removed other built in padding, like the 0 in the month

There's a few other ways you can manipulate the results. Learn more here!

Grab first N elements from an array

Have you ever wanted to grab the first n elements from an array?

You might think to do something like this:

fruit = ["apple", "banana", "blueberry", "cherry", "dragonfruit"]

# Grab the first 3 elements of the array
fruit[0...3]
=> ["apple", "banana", "blueberry"]

Well you could just use Array#take and tell it how many elements you want to take:

fruit = ["apple", "banana", "blueberry", "cherry", "dragonfruit"]

# Grab the first 3 elements of the array
fruit.take(3)
=> ["apple", "banana", "blueberry"]

Bonus: There is also Array#take_while which takes a block and passes elements until the block returns nil or false

fruit.take_while {|f| f.length < 8 }
=> ["apple", "banana"]
fruit.take_while {|f| f.length < 10 }
=> ["apple", "banana", "blueberry", "cherry"]

Map with Index

Ever wanted map_with_index, like each_with_index except for map instead of each? Turns out you can, with just a 1 character change. with_index is a method on Enumerator that lets you do just that:

['a', 'b', 'c'].map.with_index do |x, index|
  [x, index]
end
#=> [["a", 0], ["b", 1], ["c", 2]]

Docs

Ruby Scan with Index

If you want to search for a pattern in a string and get back all the matches of that pattern, you can use String#scan:

irb(main)> "..123...456...123".scan(/\d+/)
=> ["123", "456", "123"]

This is super useful. But sometimes, it would be even more useful to also know the index of where the match starts. Turns out, you can do this with $~

irb(main)> matches_with_index = []
irb(main)* "..123...456...123".scan(/\d+/).map do |x|
irb(main)*   [x, $~.offset(0)[0]]
irb(main)> end
irb(main)> matches_with_index
=> [["123", 2], ["456", 8], ["123", 14]]

$~ is a global variable that's equivalent to Regexp.last_match, which is the MatchData for the last successful pattern match - it basically lets you get some data about the last thing Regexp matched.

MatchData#offsetreturns an array with the starting and ending offsets of the match. So $~.offset(0)[0] -> the offset to the start of the match, and $~.offset(0)[1] -> the offset to the end of the match.

Ruby's Abbreviated Assignment Operators

Today I Learned ruby has a lot of abbreviated assignment operators.

The best known are += and -= to increment and decrement values:

x = 2
x += 1
x #=> 3

And of course there's ||=, to assign only if the value is nil or false:

x = nil
x ||= 4 #=> 4
x ||= 5 #=> 4

But these abbreviations can be applied to a lot more operators!

It works with all of the following: +, -, *, /, %, **, &, |, ^, <<, >>, &&, ||.

So we could use |= to union two arrays and assign the result to the variable:

x = [1, 2, 3]
x |= [2, 3, 4, 4]
x #=> [1, 2, 3, 4]

Using Symbol name in Ruby

Symbols are an integral part of Ruby… we use them everyday. However sometimes they can be used for identification where we use their stringified version for comparison.

input = “wasabi”
:wasabi.to_s == input
=> true[[]]

Every time we do this, a new string is allocated in memory as the representation of :wasabi. For a trivial example like this, it’s not a big deal but consider how often Ruby on Rails uses symbols (HashWithIndifferentAccess anyone?). Then the bloat becomes very real.

Introduced in Ruby 3.0, Symbol#name(https://github.com/ruby/ruby/pull/3514) aims to help. Utilizing this method returns a frozen string version of the symbol.

:wasabi.name
=> “wasabi”

This looks like we’re reproducing the same result and in a way we are. However due to the returned string being frozen, there is only one immutable instance of it being used in memory!

It can be used the same way as well now with less memory bloat.

input = “wasabi”
:wasabi.name == input
=> true

Endless Method Definition in Ruby

A new method definition was introduced in Ruby 3.0, the endless definition.

You're probably familiar with:

def do_something(number)
  number * 2
end

Of course, we can express this as a one-liner previously as:

def do_something(number); number * 2; end

Now you have the option to write it like this:

def do_something(number) = number * 2

Or another example:

def thing(x) = @thing = x

If you'd like to know more, here is where the spec was discussed

Yield a double to a Block in RSpec

TIL in rspec you can yield a double to a block with and_yield, similar to how you return a double with and_return.

With and_return you can write a test like this:

sftp = Net::SFTP.start(args)
sftp.upload!(content, path)

# Test
client = double
allow(Net::SFTP).to receive(:start).and_return(client)
expect(client).to receive(:upload!)

However, if your code has a block like below and_return won't work. Instead, you can use and_yield to yield the double to the block:

Net::SFTP.start(args) do |sftp|
  sftp.upload!(content, path)
end

# Test
client = double
allow(Net::SFTP).to receive(:start).and_yield(client)
expect(client).to receive(:upload!)

Docs

Using slice_after to split arrays by a value

Given you have an array of objects that you may want to split apart based on a value on one of the objects, you can use slice_after (there's also slice_before, which behaves the same way).

array = [
  {activity: "traveling", ticket: "123"},
  {activity: "working", ticket: "123"},
  {activity: "awaiting_assignment", ticket: ""},
  {activity: "traveling", ticket: "234"},
  {activity: "refueling", ticket: "234"},
  {activity: "traveling", ticket: "234"},
  {activity: "working", ticket: "234"},
  {activity: "awaiting_assignment", ticket: ""}
]

array.slice_after { |i| i.activity == "awaiting_assignment" }
# Returns:
[
  [
    {activity: "traveling", ticket: "123"},
    {activity: "working", ticket: "123"},
    {activity: "awaiting_assignment", ticket: ""}
  ],
  [
    {activity: "traveling", ticket: "234"},
    {activity: "refueling", ticket: "234"},
    {activity: "traveling", ticket: "234"},
    {activity: "working", ticket: "234"},
    {activity: "awaiting_assignment", ticket: ""}
  ]
]

Decomposing Nested Arrays

As you probably already know, in Ruby, you can decompose a nested array into variables like so:

letters_and_numbers = [["a", "b", "c", "d", "e"], [1, 2, 3, 4, 5]]
letters, numbers = letters_and_numbers

>> letters
=> ["a", "b", "c", "d", "e"]

>> numbers
=> [1, 2, 3, 4, 5]

However, did you also know that you can add parentheses () to decompose specific values from a nested array?

letters_and_numbers = [["a", "b", "c", "d", "e"], [1, 2, 3, 4, 5]]
(a, b, *other_letters), numbers = letters_and_numbers

>> a
=> "a"

>> b
=> "b"

>> other_letters
=> ["c", "d", "e"]

>> numbers
=> [1, 2, 3, 4, 5]

Note: You can also grab values from either the beginning or end of the array!

letters_and_numbers = [["a", "b", "c", "d", "e"], [1, 2, 3, 4, 5]]
(a, *other, d, e), _ = letters_and_numbers

>> a
=> "a"

>> other
=> ["b", "c"]

>> d
=> "d"

>> e
=> "e"

Ruby Rightward Assignment -> JS-like Destructuring

It's not often there's a javascript feature I wish was available in ruby, but here we are. But, it turns out ruby has the functionality as of 2.7 and I was just out of the loop.

In javascript you can use destructuring assignment to unpack a bunch of variables in a single line:

const obj = { a: 1, b: 2, c: 3, d: 4 }
const { a, b, d: newName } = obj
console.log([a, b, newName]) 
// => [1, 2, 4]

With rightward assignment you can do a similar thing with hashes, though with slightly different syntax:

hsh = { a: 1, b: 2, c: 3, d: 4 }
hsh => { a:, b:, d: new_name }
puts [a, b, new_name]
# => [1, 2, 4]

Nice!

Gem pristine <gem_name> restores installed gems

If you have been directly working or debugging within your gems and wish to revert any changes made, instead of manually undoing all of the changes in each file, you can simply run gem pristine <gem_name>. This command restores installed gems to their original, pristine state using the files stored in the gem cache.

If you want to check out what options you can pass to it, here is some documentation.
H/T Matt Polito

FactoryBot skip_create

factory_bot has an option to skip calling save! on create:

FactoryBot.define do
  factory :model_without_table do
    skip_create
    an_attribute { "An Attribute" }
  end
end

This will build the object in memory, but not persist it. Useful if you want to create a factory for a model that isn't backed by a database table, where trying to persist the record would result in an exception.

Docs

RSpec Spies 🕵️‍♀️

Most of the time when I write an RSpec test to see if a message was received, I'll write the expectation first using a mock, then exercise the subject under test:

class SomeJob
  def perform
    SomeService.call
  end

end

Rspec.describe SomeJob do
  it "makes the call" do
    expect(SomeService).to receive(:call)
    SomeJob.new.perform
  end
end

Sometimes it's useful to flip this around, with the expectation after the action was performed. We can do this with a spy - we first stub the call with allow, exercise the subject under test, then assert with have_received:

class SomeJob
  attr_reader :some_attribute

  def perform
    set_an_instance_var
    SomeService.call(some_attribute)
  end

  def set_an_instance_var
    @some_attribute = :something
  end
end

Rspec.describe SomeJob do
  subject { SomeJob.new }

  it "makes the call with an argument" do
    allow(SomeService).to receive(:call)
    subject.perform
    expect(SomeService).to have_received(:call).with(subject.some_attribute)
  end
end

This is particularly useful if you want to assert against something (say an instance variable) that doesn't get set until the subject is exercised. There's no way to test the above example with the assertion first, since we won't know what some_attribute is until we perform - a perfect use case for a spy.

h/t Matt Polito

Don't auto generate Gem documentation in Ruby

I always forget to disable generation of gem documenatation until I see it getting generated during install :-(

Do yourself a favor and create a .gemrc if you don't already have one and add:

gem: --no-document

Now all of your gem installs will be speedier and take up less space.

Some of you may remember --no-ri & --no-rdoc, however --no-document takes care of both.

View the gem documentation for more info

Create a Date object for a specific day

Say you have some date-specific functionality, and you want to test for a specific day of the week.

Date#commercial is what you're looking for! It will create a Date object for you based on the year,week, and day that you give it.

require 'date'

# Wednesday (3) of week 1 of year 2023
Date.commercial(2023, 1, 3)

In Rails we can take this a step further, for example, to get Friday of this week:

Date.commercial(Date.current.year, Date.current.cweek, 5)

In your testing you can simply make use of the Rails TimeHelpers to travel to that specific date you need:

next_friday = Date.commercial(Date.current.year, Date.current.cweek + 1, 5)

travel_to next_friday do
  # Friday specific code
end

Pretty-print JSON in Ruby

When receiving a JSON payload it can, of course, be useful to see it in a more readable way. Turns out there is a built in utility in Ruby that can help with this.

Kernal#j & Kernal#jj

Utilizing the Kernal#j method:

foo = {name: "Matt", company: "Hashrocket"}

=> j foo
{"name": "Matt", "company": "Hashrocket"}

Utilizing the Kernal#jj method:

foo = {name: "Matt", company: "Hashrocket"}

=> jj foo
{
  "name": "Matt",
  "company": "Hashrocket"
}

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

The difference between %w and %W in Ruby

%w can construct space delimited word arrays like this

%w(my cool word array)
#=> ["my", "cool", "word", "array"]

%W works similarly, however it offers ways to interpolate with variables and escape special characters in the assignment, while %w does not.

street_name = 'Sesame Street'
%W(I live on #{street_name})
#=> ["I", "live", "on", "Sesame Street"]

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

Ruby memoization with nil values

As Ruby developers, we're often looking for ways to reduce time consuming lookups in our code. A lot of times, that leads us to memoizing those lookups with the common ||= operator.

However, if our lookups return a nil or falsey value, our memo will actually keep executing the lookup:

def ticket
  @ticket ||= Ticket.find_by(owner:)
end

This code essentially boils down to:

def ticket
  @ticket = @ticket || Ticket.find_by(owner:)
end

If our find_by in the example above returns nil, the code will continue to run the find_by every time we call the ticket method.

To avoid this, we can shift our pattern a bit, and look to see if we have already set our instance variable or not:

def ticket
  return @ticket if defined?(@ticket)
  @ticket = Ticket.find_by(owner:)
end

Quickly find module inclusion in Ruby

Given I have a class like so:

class Location < ActiveEnum::Base
  include WithLabel
end

Normally I would check for inclusion of something via a declarative method like:

Location.ancestors.include?(ActiveEnum::Base)
=> true

Location.ancestors.include?(String)
=> false

Location.included_modules.include?(WithLabel)
=> true

However it never occured to me that < is defined on Class and returns true if is a subclass of the requested module.

So we can do something like this now:

Location < ActiveEnum::Base
=> true

Location < String
=> nil

Location < WithLabel
=> true

Subtle difference is that the 'falsey' case returns nil instead of false.

Also the definition of this method states that it returns true if module is a subclass of other but I've found that it returns true for methods that are included as well. Take that as you will.

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.

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

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.