:use_route in controller specs inside engines

If you have controller specs inside your Rails engine, you'll need to pass use_route: :my_engine when doing the request. Otherwise you'll get an UrlGenerationError since the dummy app is not running and the engine is not mounted!

describe MyEngine::MainController, type: :controller do
  it "does something" do
    get :index, params: { use_route: :my_engine }
    # ...
  end
end

Reduce your slug size on Heroku!

You don't need app/javascript and node_modules after compiling assets. Enhance the assets:precompile task and delete them so they don't get added to your Heroku slug! Small slugs allow for fast boot up times and better scaling. This also helps stay below the 500MB size limit!

Rake::Task["assets:precompile"].enhance do
  next unless Rails.env.production?

  ["#{Dir.pwd}/app/javascript", "#{Dir.pwd}/node_modules"].each do |dir_path|
    FileUtils.rm_rf(dir_path)
  end
end

Mastering extend and when you want class methods and instance methods for your class

Mastering extend and when you want class methods and instance methods for your class

I just realized something cool, which is the underlying of including same module for a class and share instance and class methods with that class.

So here is the trick

You have this simple module:

module TestModule
  def test_name
    'test_name'
  end
end

And this class

class Test
  def name
    'Test'
  end
end
Test.extend(TestModule)
irb(main):033> Test.test_name
=> "test_name"
irb(main):034> Test.new.name
=> "Test"

So that is why if you want to include both instance and class methods, with something like this

class Test
  include TestModule
end

How would you include some as class methods, and the answer is by defining those methods in a module or subgroup within the module, and it would look like this

module TestModule
  def instance_method
    'instance method'
  end

  module TheClassMethods
    def class_hello
      'class_hello'
    end
  end

  def self.included(base)
    base.extend(TheClassMethods)
  end
end

class Test
  include TestModule
end

So now you can access both of them

Test.new.instance_method # instance method
Test.class_hello # class_hello

NOTE: remember that it is included in the past

def self.included(base)
  base.extend(YouClassMethodsToShare)
end

And active_support/concern comparison

module M
  def self.included(klass)
    klass.extend ClassMethods
    klass.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  def instance_method
    'instance method'
  end

  module ClassMethods do
     def class_method_one
       'class method'
     end
  end
end

Vs

require 'active_support/concern'

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }

    def self.direct_class_method_here
      ..
    end
  end

class_methods do
    def class_method_one
      'class method one'
    end
  end
end

class YourClass
  include M
end

Setting up multiple Okta orgs with the same Omniauth Oauth2 Strategy in Rails

Hello There,

I want to share what I did in order to support a second Okta Organization in a Ruby on Rails application using omniauth_oktaoauth gem.

I started with a basic spec to make sure I dont break anything:

RSpec.describe 'Sign in with Okta', type: :request do
  describe "Using Okta" do
    let(:user) { User.find_by email: user_email }
    let(:okta_oauth) do
      {
        provider: okta_provider.to_s,
        uid: "123456789",
        info: {
          name: "John Doe",
          email: user_email,
        }
      }
    end

    before(:each) do
      OmniAuth.config.test_mode = true
      OmniAuth.config.mock_auth[okta_provider] = OmniAuth::AuthHash.new(okta_oauth)
    end

    context "When using existing configuration" do
      let(:okta_provider) { :oktaoauth }
      let(:user_email) { "john.okta@existing.domain" }

      it "keeps working" do
        post "/users/auth/oktaoauth"
        follow_redirect!
        expect(user.email).to eq(user_email)
      end
    end
  end
end

With this, I proceed to write another spec to ensure the new option would work:

context "When using second okta org" do
  let(:okta_provider) { :second_okta }
  let(:user_email) { "john.okta@new.domain" }

  it "signs the right user in" do
    post "/users/auth/second_okta"
    follow_redirect!

    expect(user.email).to eq(user_email)
  end
end

After this, I started to use the normal steps:

# user.rb
devise :omniauthable, omniauth_providers: [:oktaoauth, :second_okta]

Then, modify the initializer to tell omniauth about the new strategy

# config/initializers/okta.rb
....
config.omniauth(:second_okta,
                Rails.configuration.second_okta.client_id,
                Rails.configuration.second_okta.client_secret,
                name: "second_okta",
                request_path: "/users/auth/second_okta",
                callback_path: "/users/auth/second_okta/callback",
                scope: "openid profile email",
                fields: %w[profile email],
                client_options: {
                  site: Rails.configuration.second_okta.url,
                  authorize_url: "#{Rails.configuration.second_okta.auth_issuer}/v1/authorize",
                  token_url: "#{Rails.configuration.second_okta.auth_issuer}/v1/token"
                },
                redirect_uri: "#{Rails.configuration.app.base_domain}/users/auth/second_okta/callback",
                issuer: Rails.configuration.second_okta.auth_issuer,
                strategy_class: OmniAuth::Strategies::Oktaoauth)

At the beginning, it looked easy to add a second option using the same strategy, but after dealing with omniauth internals and omniauth_oktaoauth source code itself, I found that I needed to specify name and strategy_class to override defaults, there's something inside the source code that did not work out of the box, I had to specify request_path and callback_path explicitly (I'll dig deeper later and send a patch if it's a bug).

After making those changes, it worked just fine.

Thanks!

What is the difference between "and" and "&&" in Ruby or between "||" and "or"?

First of all, all of them are logical operators, but the assignment operator = has higher precedence than and and or, but lower precedence than && and ||.

And if not understood correctly it can lead to tricky and unexpected results, for instance:

irb(main):064* def execute_notification
irb(main):065*   puts 'execute_notification'
irb(main):066> end
=> :execute_notification
irb(main):067> execute_notification
execute_notification
=> nil
irb(main):068> result = true and execute_notification
execute_notification
=> nil
irb(main):069> result
=> true
irb(main):070> result = false and execute_notification
=> false
irb(main):071> result
=> false
irb(main):072> result = true or execute_notification
=> true
irb(main):073> result = false or execute_notification
execute_notification
=> nil
irb(main):074> result
=> false
irb(main):075> result = true && execute_notification
execute_notification
=> nil
irb(main):076> result
=> nil
irb(main):077> result = false && execute_notification
=> false
irb(main):078> result
=> false
irb(main):079> result = true || execute_notification
=> true
irb(main):080> result
=> true
irb(main):081> result = false || execute_notification
execute_notification
=> nil
irb(main):082> result
=> nil

Automatically set your Ngrok tunnel as default Rails host for development environment

Ever needed to test your Rails app's emails or background jobs in a local environment but also wanted to expose it through a public URL? Here's a handy trick to dynamically set your default URL options based on Ngrok's current public URL.

Note that you should fire up ngrok before starting your local server. Also this will just grab the first tunnel you have active.

Code: config/initializers/default_url_options.rb

if Rails.env.local?
  ngrok_results = `curl -s -X GET -H "Authorization: Bearer <NGROK_API_KEY>" -H "Ngrok-Version: 2" https://api.ngrok.com/tunnels`
  ngrok_results = JSON.parse(ngrok_results)
  public_url = ngrok_results.dig("tunnels", 0, "public_url")
  host = public_url.gsub("https://", "")
  Rails.application.routes.default_url_options = { host: host }
else
  Rails.application.routes.default_url_options[:host] = "<YOUR PRODUCTION HOST>"
end

How it Works:

  • Checks if the environment is local.
  • Fetches the current Ngrok public URL using the Ngrok API.
  • Parses the JSON response to get the public URL.
  • Updates the default_url_options for the Rails application with this public URL.

Note: Replace <NGROK_API_KEY> and <YOUR PRODUCTION HOST> with your actual keys and host.


Feel free to tweak it!

Exponenciation in Ruby <> Javascript and Ruby to_i in Javascript and prevent rounding float/decimals

Let’s begin with an actual example and conversion for both languages(Ruby and JavaScript).

Most of the time, if you want to take n number of decimals after the . from a big decimal or just fill it with zeros, you would do something like this:

Ruby

number = 4.9999999
# eg: with a precision of 6
'%.6f' % number # => "5.000000"
sprintf('%.6f', number) # => "5.000000"
format('%.6f', number) # => "5.000000"

Javascript

number = 4.9999999
number.toFixed(6) => '5.000000'

How to solve that problem?

By using a custom method!

Ruby

def truncate_float(number, precision)
  factor = 10 ** precision
  (factor * number).to_i / factor.to_f
end

result = truncate_float(4.9, 6)# => 4.9
# then
'%.6f' %  result # => "4.900000"
truncated_value = truncate_float(4.9999999, 6)  # => 4.999999
'%.6f' % truncated_value # => "4.999999"

Now in JavaScript

function truncateDecimal(decimalNumber, precision) {
  factor = Math.pow(10, precision)
  return Math.floor(factor * decimalNumber) / factor
}

truncateDecimal(4.9, 6) // 4.9
truncateDecimal(4.9999999999, 6) // 4.999999

There you have it!

Verify existence of arbitrary email addresses from the command line

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'resolv'
require 'net/smtp'

def mx_records(domain)
  Resolv::DNS.open do |dns|
    dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
  end
end

def mailbox_exist?(email)
  domain = email.split('@').last
  mx = mx_records(domain).first
  return false unless mx

  Net::SMTP.start(mx.exchange.to_s, 25) do |smtp|
    smtp.mailfrom 'info@example.com' # replace with your email address or something more realistic
    smtp.rcptto email
  end
  true
rescue Net::SMTPFatalError, Net::SMTPSyntaxError
  false
end

if ARGV.length != 1
  puts "Usage: ruby #{__FILE__} <email_address>"
  exit 1
end

email = ARGV[0]
if mailbox_exist?(email)
  puts "Mailbox exists."
else
  puts "Mailbox doesn't exist or couldn't be verified."
end

Find files with largest amount of lines in your project

Ever needed to find the file with the largest amount of lines in your project? Use the snippet below to list all files and their line count, neatly sorted from smallest to largest.

find . -type f -print0 | xargs -0 wc -l | sort -n

This translates to the following:

find . -type f -print0 # Find all regular files in this dir and pipe them into xargs with \0 as separators.
xargs -0 wc -l # For each file contents, count the amount of lines in it and...
sort -n # Sort them numerically.

Ruby partition on arrays

In Ruby, the partition is a very useful method that you can use to filter some items in an array and that you need the ones that satisfy the condition and the ones that do not, and it takes a block of code and returns two arrays: the first contains the elements for which the block of code returns true, and the second contains the elements for which the block returns false.

Let's see an actual example:

def create_fake_emails_array
  emails = []
  10.times do |i|
    emails << { email: "user#{i + 1}@mailinator.com" }
  end

  10.times do |i|
    emails << { email: "user#{i + 1}@something.com" }
  end
  emails
end

my_emails = create_fake_emails_array

class EmailContactsWhitelistCleaner
  attr_reader :email_recipients

  def initialize(email_recipients)
    @email_recipients = email_recipients
  end

  def get_white_list_collection
    valid_recipients, invalid_recipients = partition_emails

    log_black_list_email_recipients(invalid_recipients)
    valid_recipients
  end

  private

  def partition_emails
    email_recipients.partition { |recipient| valid_recipient?(recipient[:email]) }
  end

  def valid_recipient?(email)
    !email.match?('mailinator') || mailinator_white_list.include?(email)
  end

  def log_black_list_email_recipients(invalid_recipients)
    return if invalid_recipients.empty?

    email_list = invalid_recipients.map { |recipient| recipient[:email] }.join(',')
    puts "The following emails are not in the whitelist: #{email_list}"
  end

  def mailinator_white_list
    # ENV.fetch('MAILINATOR_WHITE_LIST', '').split(',')
    'user1@mailinator.com,user2@mailinator.com,user3@mailinator.com'
  end
end

service = EmailContactsWhitelistCleaner.new(my_emails)
puts service.get_white_list_collection

Use OpenTelemetry gems to track your app's performance

Instead of going with expensive services like New Relic or Datadog, trace your Rails app's performance using the OpenTelemetry gems.

First, add the gems to your Gemfile:

gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-all'

Then, add this inside config/initializers/opentelemetry.rb

require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

OpenTelemetry::SDK.configure do |c|
  c.service_name = '<YOUR_SERVICE_NAME>'
  c.use_all() # enables all instrumentation!
end

Finally, launch your application and point it to a collector, like the OpenTelemetry Collector, Grafana Agent or SigNoz. Most of them have cloud or self-hosted versions.

Enjoy your observability!