Rails

Rails is a very cool framework for web-applications and IMO the best for mvp-prototyping and beyond.

Curriculum

Learning from some youtube guys:

Articles

Useful gems for your rails project

  • active_link_to - “Helpful method when you need to add some logic that figures out if the link (or more often navigation item) is selected based on the current page or other arbitrary condition”
  • avo - “Avo is a very custom Content Management System for Ruby on Rails that saves engineers and teams months of development time by building user interfaces and logic using configuration rather than traditional coding; When configuration is not enough, you can fallback to familiar Ruby on Rails code.”
  • brakeman - “Brakeman detects security vulnerabilities in Ruby on Rails applications via static analysis.”
  • bundler-audit - “bundler-audit provides patch-level verification for Bundled apps.”
  • database_consistency - “Provide an easy way to check the consistency of the database constraints with the application validations.”
  • devise - “Flexible authentication solution for Rails with Warden”
  • interactor - “Interactor provides a common interface for performing complex user interactions.”
  • local_time - “Rails engine for cache-friendly, client-side local time”
  • meta-tags - “Search Engine Optimization (SEO) plugin for Ruby on Rails applications.”
  • minitest - “minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking”
  • nocheckout - “Rails controllers for Stripe Checkout Sessions and Webhooks”
  • nopassword - “NoPassword is a toolkit that makes it easy to implement temporary, secure login codes initiated from peoples’ web browsers so they can login to Rails applications via email, SMS, CLI, QR Codes, or any other side-channel.”
  • og - “Object Graph (Og) is a state of the art ORM system. Og serializes standard Ruby objects to Mysql, Postgres, Sqlite, KirbyBase, Filesystem and more.”
  • omniauth-oauth2 - “An abstract OAuth2 strategy for OmniAuth.”
  • overmind - “Overmind is a process manager for Procfile-based applications and tmux.”
  • pagy - “Agnostic pagination in plain ruby. It does it all. Better.”
  • phlex - “A high-performance view framework optimised for fun.”
  • pundit - “Object oriented authorization for Rails applications”
  • ransack - “Ransack is the successor to the MetaSearch gem. It improves and expands upon MetaSearch’s functionality, but does not have a 100%-compatible API.”
  • strong_migrations - “Catch unsafe migrations in development”
  • stylecheck - “Runs code style check on Ruby and SCSS files.”
  • superform - “A better way to customize and build forms for your Rails application”
  • superview - “Build Rails applications entirely out of Phlex components.”
  • view_component - _“A framework for building reusable, testable & encapsulated view components in Ruby on Rails.”

Source: x/pkayokay / shortruby#119

Pattern matching

def extract(**data)
  case data
    in name: {first:}
      puts first
    in tags: [first_tag, *_]
      puts first_tag
  end
end

> extract(name: { first: "Brad", last: "Gessler" })
"Brad"
> extract(tags: ["person", "earthling"] })
"person"
def matcher(**data)
  if first = data.fetch(:first)
    puts first
  elsif data.key?(:tags)
    tags = data.fetch(:tags)
    if tags.is_a? Array
      puts tags.first
    end
  end
end

Source: fly.io

Colorize logger

module ColorizedLogger
  COLOR_CODES = {
    debug:   "\e[36m",  # Cyan
    info:    "\e[32m",  # Green
    warn:    "\e[33m",  # Yellow
    error:   "\e[31m",  # Red
    fatal:   "\e[35m",  # Magenta
    unknown: "\e[37m"   # White (or terminal default)
  }.freeze

  RESET = "\e[0m"

  def debug(progname = nil, &block)
    super(colorize(:debug, progname, &block))
  end

  def info(progname = nil, &block)
    super(colorize(:info, progname, &block))
  end

  def warn(progname = nil, &block)
    super(colorize(:warn, progname, &block))
  end

  def error(progname = nil, &block)
    super(colorize(:error, progname, &block))
  end

  def fatal(progname = nil, &block)
    super(colorize(:fatal, progname, &block))
  end

  def unknown(progname = nil, &block)
    super(colorize(:unknown, progname, &block))
  end

  private

  def colorize(level, message, &block)
    "#{COLOR_CODES[level]}#{message || (block && block.call)}#{RESET}"
  end
end

Rails.logger.extend(ColorizedLogger)

Test coverage pre-commit hook

To get an positive exit code for pre-commit hook integration you have to add this to your spec config:

# spec/spec_helper.rb
RSpec.configure do |config|
  if ENV["TEST_COVERAGE"]
    SimpleCov.start "rails" do
      spec_paths = ARGV.grep %r{(spec)/\w+}
      if spec_paths.any?
        file_paths = spec_paths.map { |spec_path| spec_path.gsub(%r{spec/|_spec}, "") }
        add_filter do |file|
          file_paths.none? do |file_path|
            if file.filename.include? "/app/"
              file.filename.match?(%r{/app/#{file_path}})
            else
              file.filename.include?(file_path)
            end
          end
        end
      end
      minimum_coverage 98.9
      minimum_coverage_by_file 81.4
    end
  else
    SimpleCov.start "rails"
  end
  [...]

Pre-commit hook:

files=$(git --no-pager diff --name-only --cached --diff-filter=AM)
erbfiles=$(echo "$files" | grep -e '\.html.erb$')


[[ -n "$specfiles" ]] && (TEST_COVERAGE=true bundle exec rspec "$specfiles" || exit 1)

before_action wrapper

class User::LikesController < ApplicationController
  abort_without_feature :like
  must_have_feature :like

  requires_feature :like
end

# app/controllers/concerns/requires_feature.rb
module RequiresFeature
  def requires_feature(name, from: :user, **)
    before_action(-> { head :bad_request unless Flipper.enabled?(name, Current.public_send(from)) }, **)
  end
end

Source: buttondown

Rails ERD

For creating an erd diagram of your db schema, you could create a pdf with: rails-erd with this command:

bundle exec rails erd attributes=foreign_keys,primary_keys,timestamps,content notation=bachman

Race conditions

Avoid race conditions with ActiveRecord::Base.transaction do and Model.lock.find(model_id) or my_model_object.lock!

Source: fastruby.io

Migration from sidekiq to solid_queue

Step 1: Update Gemfile
# Remove Sidekiq
# gem 'sidekiq'

# Add Solid Queue
gem 'solid_ queue'

Step 2: Run bundle install
# $ bundle install

Step 3: Generate Solid Queue installation files
# $ rails generate solid_queue:install

Step 4: Run migrations
# $ rails db:migrate

Step 5: Update config/application.rb
# Rails.application.configure do 
#	config.active_job.queue_adapter = :solid_queue
# end

Step 6: Remove Sidekiq initializer
# Delete or comment out config/initializers/sidekiq.rb

Step 7: Update worker process command in Profile or deployment scripts
# Old: worker: bundle exec sidekiq
# New: worker: bundle exec rails solid_queue: start

Step 8: Remove Redis configuration related to Sidekiq
# Check config/redis.yml or any Redis initializers

Step 9: Update any Sidekiq-specific code in your jobs

Before:
# class MyJob
# include Sidekiq: :Worker
# def perform(args)
#  job logic
# end
# end

After:
# class MyJob < ApplicationJob
# queue_as: default def perform(args)
# job logic
# end

Step 10: Update any Sidekiq-specific API calls

# Before: Sidekiq:: Client.push( 'class' => MyJob,
# After: MyJob. perform_later (1, 2, 3)

Step 11: Set up Mission Control (optional)

# In Gemfile:
gem 'mission_control-jobs'

# In config/routes.rb:
Rails.application.routes.draw do
	mount MissionControl::Jobs::Engine, at: "/jobs" 
end

Step 12: Remove any Sidekiq web UI routes

# Delete or comment out in config/routes.rb:

# require 'sidekiq/web'
# mount Sidekiq:: Web => '/sidekiq'

Responsible monkeypatch

Here’s the list of rules I try to follow:

  1. Wrap the patch in a module with an obvious name and use Module#prepend to apply it
  2. Make sure you’re patching the right thing
  3. Limit the patch’s surface area
  4. Give yourself escape hatches
  5. Over-communicate
# ActionView's date_select helper provides the option to "discard" certain
# fields. Discarded fields are (confusingly) still rendered to the page
# using hidden inputs, i.e. <input type="hidden" />. This patch adds an
# additional option to the date_select helper that allows the caller to
# skip rendering the chosen fields altogether. For example, to render all
# but the year field, you might have this in one of your views:
#
# date_select(:date_of_birth, order: [:month, :day])
#
# or, equivalently:
#
# date_select(:date_of_birth, discard_year: true)
#
# To avoid rendering the year field altogether, set :render_discarded to
# false:
#
# date_select(:date_of_birth, discard_year: true, render_discarded: false)
#
# This patch assumes the #build_hidden method exists on
# ActionView::Helpers::DateTimeSelector and accepts two arguments.
#
module RenderDiscardedMonkeypatch
  class << self
    EXPIRATION_DATE = Date.new(2021, 8, 15)

    def apply_patch
      if Date.today > EXPIRATION_DATE
        puts "WARNING: Please re-evaluate whether or not the ActionView "\
          "date_select patch present in #{__FILE__} is still necessary."
      end

      const = find_const
      mtd = find_method(const)

      # make sure the class we want to patch exists;
      # make sure the #build_hidden method exists and accepts exactly
      # two arguments
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
          "ActionView's date_select helper. Please investigate."
      end

      # if rails has been upgraded, make sure this patch is still
      # necessary
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
          "ActionView's date_select helper was monkeypatched in "\
          "#{__FILE__}. Please re-evaluate the patch."
      end

      # actually apply the patch
      const.prepend(InstanceMethods)
    end

    private

    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
      # return nil if the constant doesn't exist
    end

    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
      # return nil if the method doesn't exist
    end

    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end

  module InstanceMethods
    # :render_discarded is an additional option you can pass to the
    # date_select helper in your views. Use it to avoid rendering
    # "discarded" fields, i.e. fields marked as discarded or simply
    # not included in date_select's :order array. For example,
    # specifying order: [:day, :month] will cause the helper to
    # "discard" the :year field. Discarding a field renders it as a
    # hidden input. Set :render_discarded to false to avoid rendering
    # it altogether.
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end

RenderDiscardedMonkeypatch.apply_patch

Source: appsignal/blog

RSpec

Bisect flaky tests

with rspec --bisect <file> you could find flaky test setting to re-run it.

hint

If rspec exits with 1 if suite passes, try untilpass() { until "$@"; do :; done } and run it with untilpass rspec ...

Testing an array with attributes

expect(items[0].id).to eql(1)
expect(items[0].name).to eql('One')
expect(items[1].id).to eql(2)
expect(items[1].name).to eql('Two')

expect(items[0]).to have_attributes(id: 1, name: 'One')
expect(items[1]).to have_attributes(id: 2, name: 'Two')

expect(items).to match_array([
  have_attributes(id: 1, name: 'One'),
  have_attributes(id: 2, name: 'Two'),
])

Source: benpickles

Custom matcher

RSpec::Matchers.define :have_errors_on do |attribute|
  chain :with_message do |message|
    @message = message
  end

  match do |model|
    model.valid?

    @has_errors = model.errors.key?(attribute)

    if @message
      @has_errors && model.errors[attribute].include?(@message)
    else
      @has_errors
    end
  end

 failure_message_for_should do |model|
     if @message
       "Validation errors #{model.errors[attribute].inspect} should include #{@message.inspect}"
     else
       "#{model.class} should have errors on attribute #{attribute.inspect}"
     end
   end

   failure_message_for_should_not do |model|
     "#{model.class} should not have an error on attribute #{attribute.inspect}"
   end
 end


# usage
describe User do
  before { subject.email = "foobar" }

  it { should have_errors_on(:email).with_message("Email has an invalid format") }

end

Data class

Similiar to the value-alike objects in ruby(3.2), here an example for rails:

class DataClass
  ATTRIBUTES = %i[first_name last_name city zipcode phone_number].freeze

  include ActiveModel::Model

  attr_accessor *ATTRIBUTES

  def ==(object)
    ATTRIBUTES.all? { |attribute| public_send(attribute) == object.public_send(attribute) }
  end
end

ConsumerClass.new(DataClass({first_name: "Bill", last_name: "...", ...}))

Links: Polyfill - Data gem

RSpec factory trait & transient

FactoryBot.define do
  factory :user, class: User do
    trait :with_book do
      transient do
        # 🦄1. default value when you use :with_book trait
        # 🦄2. Dont't assign just 'Agile'. see also: https://thoughtbot.com/blog/deprecating-static-attributes-in-factory_bot-4-11
        title { 'Agile' }
      end
      after(:build) do |user, evaluator|
        user.book = FactoryBot.create(:book, title: evaluator.title)
      end
    end
  end

  factory :book, class Book do
    sequence(:title) { |n| "book no.#{n}" } # 🦄 default value
  end

end

# usage
let!(:user) { create(:user, :with_book, title: 'Ruby') }

Source: dev.to/n350071

RailsWorld2023

Some notes on talks:

  • Turbo morphing
    • new feature for keep scrolling position on whole dom body change
  • no-build / bun
    • key goal to eliminate build time for frontend stuff, ship as code
  • if assignment block
    def test
      42
    end
    if a = test
    	puts "Assign #{a}"
    else
      puts "Nil is return"
    end
    
  • Class.method(:name).source_location
    • get code location of a method
  • Model attribute strict_loading prohibit loading relations
  • Migration add_column :virtual
create_table :users do |t|
  t.numeric :height_cm
  t.virtual :height_in, type: :numeric, as: 'height_cm / 2.54', stored: true
end
  • Model with_options relations
class User
  with_options dependent: :destroy do |options|
    options.has_many :tasks, class_name: "UserTask"
    options.has_many :addresses
  end
end
  • try(:method_name) || default_method
  • Routing constraints
    • subdomain
    • authenticated
  • Routes draw split into files
  • rails generate generator ApiClient
  • String truncate_words with omission
  • DateTime before? past? future?
  • Time.current.all_day .all_week …
  • Abbreviation number_to_human() round_mode significant format units

Professional Ruby on Rails Developer with Rails 5

Some notes while doing the udamy course and some ruby notes:

Basics

Render plain text:

def hello
  render plain: "hello world!"
end

Create a controller with a method and use a object in view:

# controller
class TodosController < ApplicationController

  def new
   @todo = Todo.new
  end

end

# view
<%= form_for @todo do |f| %>

<% end %>

Use flash for notifications:

#controller
@todo.save
flash[:notice] = "Todo was created successfully"

# view
<% flash.each do |name, msg| %>
  <ul>
    <li><%= msg %></li>
  </ul>
<% end %>

A bootstrap styled flash messages:

<div class="row">
  <div class="col-md-10 col-md-offset-1">
    <% flash.each do |name, msg| %>
      <div class="alert alert-<%= name %>">
        <a href="#" class="close" data-dismiss="alert">×</a>
        <%= msg %>
      </div>
    <% end %>
  </div>
</div>

Render partials in view ie a file views/layouts/_messages.html.erb:

<%= render 'layouts/messages' %>

Before run a method do action:

# controller
before_action :set_todo, only: [:edit, :update, :show, :destroy]

private

  def set_todo
    @todo = Todo.find(params[:id])
  end

  def todo_params
    params.require(:todo).permit(:name, :description)
  end

  # permit has_many list
  def recipe_params
    params.require(:recipe).permit(:name, :description, ingredient_ids: [])
  end

end

Add pg to production for ie. heroku deployment:

group :production do
  gem 'pg'
end

bundle install --without production

Some simple validations:

validates :name, presence: true
validates :description, presence: true, length: { minimum: 5, maximum: 500 }

Application helpers:

helper_method :current_chef, :logged_in?

def current_chef
  @current_chef ||= Chef.find(session[:chef_id]) if session[:chef_id]
end

def logged_in?
  !!current_chef
end

def require_user
  if !logged_in?
    flash[:danger] = "You must be logged in to perform that action"
    redirect_to root_path
    # also possible
    redirect_to :back
  end
end

Render partial for model:

# View
<% if recipe.ingredients.any? %>
  <p>Ingredients: <%= render recipe.ingredients %></p>
<% end %>

# Now create a new partial _ingredient.html.erb under the app/views/ingredients folder for this to work

<span class="badge"><%= link_to ingredient.name, 
                      ingredient_path(ingredient) %>   </span>

Model

Order models by column:

# order by updated_at -> top of model
default_scope -> { order(updated_at: :desc) }

Get just the last 20 entries of a model

def self.most_recent
  order(:created_at).last(20)
end

Routes

Set root route:

# router.rb
root "pages#home"

Specific route to controller#method:

get '/about', to: 'pages#about'

Nested routes:

resources :recipes do
  resources :comments, only: [:create]
end

# Form in view
<%= form\_for(\[@recipe, @comment\], :html => {class: "form-horizontal", 
                                                role: "form"}) do |f| %>

Create links in views:

<%= link_to "Edit this todo", edit_todo_path(@todo) %>
<%= link_to "Back to todos listing", todos_path %>
<td><%= link_to 'delete', todo_path(todo), method: :delete, data: { confirm: "Are you sure?"} %></td>
<%= link_to "MyRecipes", root_path, class: "navbar-brand", id: "logo" %>
<%= link_to "Sign up or log in", "#" class: "btn btn-danger btn-lg" %>

Tests

Create integration test for model:

rails generate integration_test recipes

Simple test root path:

test "should get home" do
  get pages_home_url
  assert_response :success
end

test "should get root" do
  get root_url
  assert_response :success
end

to fix this tests, do:

root "pages#home"
get 'pages/home', to: 'pages#home'

# controller
class PagesController < ApplicationController
  def home
  end
end

Test a simple validation:

require 'test_helper'

class RecipeTest < ActiveSupport::TestCase

  def setup
    @recipe = Recipe.new(name: "vegetable", description: "great vegetable recipe")
  end

  test "recipe should be valid" do
    assert @recipe.valid?
  end

  test "name should be present" do
    @recipe.name = " "
    assert_not @recipe.valid?
  end

  test "description should be present" do
    @recipe.description = " "
    assert_not @recipe.valid?
  end

  test "description shouldn't be less than 5 characters" do
    @recipe.description = "a" * 3
    assert_not @recipe.valid?
  end

  test "description shouldn't be more than 500 characters" do
    @recipe.description = "a" * 501
    assert_not @recipe.valid?
  end

  test "should get recipes show" do
    get recipe_path(@recipe)
    assert_template 'recipes/show'
    assert_match @recipe.name, response.body
    assert_match @recipe.description, response.body
    assert_match @chef.chefname, response.body
  end

  test "should get recipes listing" do
    get recipes_path
    assert_template 'recipes/index'
    assert_select "a[href=?]", recipe_path(@recipe), text: @recipe.name
    assert_select "a[href=?]", recipe_path(@recipe2), text: @recipe2.name
  end
end

Test a email validation:

require 'test_helper'

class ChefTest < ActiveSupport::TestCase

  def setup
    @chef = Chef.new(chefname: "mashrur", email: "mashrur@example.com")
  end

  test "should be valid" do
    assert @chef.valid?
  end

  test "name should be present" do
    @chef.chefname = " "
    assert_not @chef.valid?
  end

  test "name should be less than 30 characters" do
    @chef.chefname = "a" * 31
    assert_not @chef.valid?
  end

  test "email should be present" do
    @chef.email = " "
    assert_not @chef.valid?
  end

  test "email should not be too long" do
    @chef.email = "a" * 245 + "@example.com"
    assert_not @chef.valid?
  end

  test "email should accept correct format" do
    valid_emails = %w[user@example.com MASHRUR@gmail.com M.first@yahoo.ca john+smith@co.uk.org]
    valid_emails.each do |valids|
      @chef.email = valids
      assert @chef.valid?, "#{valids.inspect} should be valid"
    end
  end

  test "should reject invalid addresses" do
    invalid_emails = %w[mashrur@example mashrur@example,com mashrur.name@gmail. joe@bar+foo.com]
    invalid_emails.each do |invalids|
      @chef.email = invalids
      assert_not @chef.valid?, "#{invalids.inspect} should be invalid"
    end
  end

  test "email should be unique and case insensitive" do
    duplicate_chef = @chef.dup
    duplicate_chef.email = @chef.email.upcase
    @chef.save
    assert_not duplicate_chef.valid?
  end

  test "reject an invalid signup" do
    get signup_path
    assert_no_difference "Chef.count" do
      post chefs_path, params: { chef: { chefname: " ",
                                 email: " ", password: "password",
                                    password_confirmation: " " } }
    end
    assert_template 'chefs/new'
    assert_select 'h2.panel-title'
    assert_select 'div.panel-body'
  end

  test "accept valid signup" do
    get signup_path
    assert_difference "Chef.count", 1 do
      post chefs_path, params: { chef: { chefname: "mashrur",
                email: "mashrur@example.com", password: "password",
                              password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'chefs/show'
    assert_not flash.empty?
  end

end

# controller
class Chef < ApplicationRecord
  validates :chefname, presence: true, length: { maximum: 30 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }

end

Downcase before save with test:

before_save { self.email = email.downcase }

# And then test it with:

test "email should be lower case before hitting db" do
  mixed_email = "JohN@ExampLe.com"
  @chef.email = mixed_email
  @chef.save
  assert_equal mixed_email.downcase, @chef.reload.email
end

Create a association and test it:

Association

# chef model
validates :chef_id, presence: true
has_many :recipes

# recipe model
belongs_to :chef

# destroy with dependent
has_many :recipes, dependent: :destroy

Test

def setup
  @chef = Chef.create!(chefname: "mashrur", email: "mashrur@example.com")
  @recipe = @chef.recipes.build(name: "vegetable", description: "great vegetable recipe")
end

test "recipe without chef should be invalid" do
  @recipe.chef_id = nil
  assert_not @recipe.valid?
end

Styling

Add bootstrap to project (good html/css tutorial link):

gem 'bootstrap-sass', '~> 3.3.7'
gem 'jquery-rails'

# app/assets/javascripts/application.js
//= require bootstrap-sprockets

# app/assets/stylesheets/custom.css.scss
@import "bootstrap-sprockets";
@import "bootstrap";

Views

Render html for each data:

<%= f.collection_check_boxes :ingredient_ids, 
                                    Ingredient.all, :id, :name do |cb| %>
<% cb.label(class: "checkbox-inline input_checkbox") {cb.check_box(class: "checkbox") + cb.text} %>
      <% end %>

Database

Create migration:

rails generate migration create_recipes

# modify the migration file
rails db:migrate

Rename column:

rename_column :recipes, :email, :description
rails db:migrate

Many to many association:

# Model
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
Last change: 2025-03-07, commit: e0fd894