Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Powerful Rails Features You Might Not Know

excid3
October 10, 2023

Powerful Rails Features You Might Not Know

This talk was given at Rails World 2023.

If all you've got is a hammer, everything looks like a nail. In tech, there is a constant stream of new features being added every day. Keeping up with the latest Ruby on Rails functionality can help you and your team be far more productive than you might normally be.

In this talk, we walk through a bunch of lesser known or easy to miss features in Ruby on Rails that you can use to improve your skills.

excid3

October 10, 2023
Tweet

Other Decks in Programming

Transcript

  1. Chris Oliver
    Powerful TypeScript Rails
    Features You Might Not know

    View Slide

  2. Learn Your Tools

    View Slide

  3. ActiveRecord Excluding
    User.where.not(id: users.map(&:id))
    # SELECT "users".* FROM "users"
    WHERE "users"."id" NOT IN (1,2)
    User.all.excluding(users)
    # SELECT "users".* FROM "users"
    WHERE "users"."id" NOT IN (1,2)

    View Slide

  4. ActiveRecord Strict Loading
    class Project < ApplicationRecord
    has_many :comments, strict_loading: true
    end
    project = Project.first
    project.comments
    ActiveRecord::StrictLoadingViolationError
    `Project` is marked as strict_loading. The Comment
    association named `:comments` cannot be lazily loaded.

    View Slide

  5. ActiveRecord Strict Loading
    project = Project.includes(:comments).first
    Project Load (0.3ms) SELECT "projects".* FROM
    "projects" ORDER BY "projects"."id" ASC LIMIT ?
    [["LIMIT", 1]]
    Comment Load (0.1ms) SELECT "comments".* FROM
    "comments" WHERE "comments"."project_id" = ?
    [["project_id", 1]]
    => #
    project.comments
    => [#body: "Hello RailsWorld">]

    View Slide

  6. Generated Columns
    class AddNameVirtualColumnToUsers < ActiveRecord::Migration[7.0]
    def change
    add_column :users, :full_name, :virtual,
    type: :string,
    as: "first_name || ' ' || last_name",
    stored: true
    end
    end

    View Slide

  7. attr_readonly
    class User < ApplicationRecord
    attr_readonly :super_admin
    end

    View Slide

  8. with_options
    class Account < ActiveRecord::Base
    with_options dependent: :destroy do
    has_many :customers
    has_many :products
    has_many :invoices
    has_many :expenses
    end
    end

    View Slide

  9. with_options
    I18n.with_options locale: user.locale, scope: 'newsletter' do |i18n|
    subject i18n.t :subject
    body i18n.t :body, user_name: user.name
    end

    View Slide

  10. Try
    method_name if respond_to?(:method_name)
    try(:method_name)
    try(:method_name) || default
    (method_name if respond_to?(:method_name)) || default

    View Slide

  11. ActionText Embeds

    View Slide

  12. Searching Users
    json.array! @users do |user|
    json.sgid user.attachable_sgid
    json.content render(
    partial: "users/user",
    locals: {user: user},
    formats: [:html]
    )
    end

    View Slide

  13. Signed GlobalIDs
    User.first.attachable_sgid.to_s
    "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDluYVdRNkx5OX
    FkVzF3YzNSaGNuUXRZWEJ3TDFWelpYSXZNUVk2QmtWVSIsImV4c
    CI6IjIwMjMtMTAtMjhUMTY6MDY6MTEuMjEyWiIsInB1ciI6ImRl
    ZmF1bHQifX0=--217284b31bc4e28f6e2cf2890ccee87ca7d3e
    a2d"

    View Slide

  14. ActionText Embeds

    View Slide

  15. ActionText Embeds
    import Trix from "trix"
    let attachment = new Trix.Attachment({
    content: " Chris Oliver",
    sgid: "BAh7CEkiCG…"
    })
    element.editor.insertAttachment(attachment)

    View Slide

  16. ActionText Embeds
    Hey
    !

    View Slide

  17. Signed GlobalIDs
    Base64.decode64("eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaDluYVdRNkx5
    OXFkVzF3YzNSaGNuUXRZWEJ3TDFWelpYSXZNUVk2QmtWVSIsImV4cCI6IjIwMjMtM
    TAtMjhUMTY6MD
    Y6MTEuMjEyWiIsInB1ciI6ImRlZmF1bHQifX0=“)
    => “{\"_rails\":{\"message\":
    \"BAhJIh9naWQ6Ly9qdW1wc3RhcnQtYXBwL1VzZXIvMQY6BkVU\",\"exp\":
    \"2023-10-28T16:06:11.212Z\",\"pur\":\"attachable\"}}"

    View Slide

  18. Signed GlobalIDs
    Base64.decode64(“BAhJIh9naWQ6Ly9qdW1wc3RhcnQtYXBwL1VzZXI
    vMQY6BkVU")
    => “\x04\bI\"\x1Fgid://jumpstart-pro/User/1\x06:\x06ET"

    View Slide

  19. ActionText Embeds

    View Slide

  20. Serialize Coders
    module ActionText
    class RichText < Record
    serialize :body, coder: ActionText::Content
    end
    end

    View Slide

  21. Serialize Coders
    class ActionText::Content
    def self.load(content)
    new(content) if content
    end
    def self.dump(content)
    case content
    when nil
    nil
    when self
    content.to_html
    end
    end
    end

    View Slide

  22. ActionMailbox Inbound Emails

    View Slide

  23. ActionMailbox Inbound Emails
    # app/mailboxes/application_mailbox.rb
    class ApplicationMailbox < ActionMailbox::Base
    routing /@replies\./i => :replies
    end

    View Slide

  24. class RepliesMailbox < ApplicationMailbox
    MATCHER = /^reply-(\d+)@replies\./
    def process
    conversation.posts.create!(
    author: author,
    body: body,
    message_id: mail.message_id
    )
    end
    private
    def conversation
    Conversation.find(conversation_id)
    end
    def conversation_id
    mail.recipients.find { |recipient| MATCHER.match?(recipient) }[MATCHER, 1]
    end
    end
    ActionMailbox Inbound Emails

    View Slide

  25. Routing constraints
    constraints subdomain: :app do
    root "dashboard#show"
    end
    root "homepage#show"

    View Slide

  26. Routing constraints
    authenticated :user, -> { _1.admin? } do
    resource :admin
    end
    authenticated :user do
    root "dashboard#show"
    end
    root "homepage#show"

    View Slide

  27. Routing constraints

    View Slide

  28. Draw Routes
    Rails.application.routes.draw do
    draw :api
    # ...
    end
    # config/routes/api.rb
    namespace :api, defaults: {format: :json} do
    namespace :v1 do
    resources :accounts
    end
    end

    View Slide

  29. Custom Generators
    $ bin/rails g api_client OpenAI
    --base-url https://api.openai.com

    View Slide

  30. Custom Generators
    $ bin/rails generate generator ApiClient
    creates an ApiClient generator:
    lib/generators/api_client/
    lib/generators/api_client/api_client_generator.rb
    lib/generators/api_client/USAGE
    lib/generators/api_client/templates/
    test/lib/generators/api_client_generator_test.rb

    View Slide

  31. Custom Turbo Stream Actions

    View Slide

  32. Custom Turbo Stream Actions


    View Slide

  33. Custom Turbo Stream Actions
    # create.turbo_stream.erb
    <%= turbo_stream_action_tag “notification”,
    title: “Hello world” %>

    View Slide

  34. Custom Turbo Stream Actions
    import "@hotwired/turbo-rails"
    Turbo.StreamActions.notification = function() {
    Notification.requestPermission(function(status) {
    if (status == "granted") {
    new Notification(this.getAttribute("title"))
    }
    })
    }

    View Slide

  35. Custom Turbo Stream Actions

    View Slide

  36. truncate_words
    content = 'And they found that many people were sleeping better.'
    content.truncate_words(5, omission: '... (continued)')
    # => "And they found that many... (continued)"

    View Slide

  37. starts_at > Time.current
    starts_at.after?(Time.current)
    starts_at.future?
    Time Helpers
    starts_at < Time.current
    starts_at.before?(Time.current)
    starts_at.past?

    View Slide

  38. Time.current.all_day
    #=> Fri, 06 Oct 2023 00:00:00 UTC +00:00..
    Fri, 06 Oct 2023 23:59:59 UTC +00:00
    Time.current.all_week
    #=> Mon, 02 Oct 2023 00:00:00 UTC +00:00..
    Sun, 08 Oct 2023 23:59:59 UTC +00:00
    Time.current.all_month
    #=> Sun, 01 Oct 2023 00:00:00 UTC +00:00..
    Tue, 31 Oct 2023 23:59:59 UTC +00:00
    Time Helpers

    View Slide

  39. 123 -> 123
    1234 -> 1,234
    10512 -> 10.5K
    2300123 -> 2.3M
    Abbreviated numbers

    View Slide

  40. Abbreviated numbers
    number_to_human(123) # => "123"
    number_to_human(12345) # => "12.3 Thousand"
    number_to_human(1234567) # => "1.23 Million"
    number_to_human(489939, precision: 2) # => "490 Thousand"
    number_to_human(489939, precision: 4) # => "489.9 Thousand"
    number_to_human(1234567, precision: 4,
    significant: false)# => "1.2346 Million"

    View Slide

  41. def number_to_social(number)
    return number_with_delimiter(number) if number < 10_000
    number_to_human(number,
    precision: 1,
    round_mode: :down,
    significant: false,
    format: "%n%u",
    units: {thousand: "K", million: "M", billion: "B"}
    )
    end
    Abbreviated numbers

    View Slide

  42. Abbreviated numbers
    number_to_social(123) #=> 123
    number_to_social(1_234) #=> 1,234
    number_to_social(10_512) #=> 10.5K
    number_to_social(2_300_123) #=> 2.3M

    View Slide

  43. Rails 7.1

    View Slide

  44. Rails.env.local?
    Rails.env.development? || Rails.env.test?
    Rails.env.local?

    View Slide

  45. ActiveSupport Inquiry
    "production".inquiry.production? # => true
    "active".inquiry.inactive? # => false

    View Slide

  46. Unused routes
    $ rails routes --unused
    Found 4 unused routes:
    Prefix Verb URI Pattern Controller#Action
    edit_comment GET /comments/:id/edit(.:format) comments#edit
    PATCH /comments/:id(.:format) comments#update
    PUT /comments/:id(.:format) comments#update
    DELETE /comments/:id(.:format) comments#destroy

    View Slide

  47. Template strict locals
    <%# locals: (message:) -%>
    <%= tag.div id: dom_id(message) do %>
    <%= message %>
    <% end %>
    <%= render partial: “message” %>
    ArgumentError: missing local: :message
    <%= render partial: “message”,
    locals: {message: @message} %>

    View Slide

  48. Template strict locals
    <%# locals: (message: “Hello”) -%>

    <%= message %>

    <%= render partial: “message” %>
    <%= render partial: “message”,
    locals: {message: “Hey”} %>

    View Slide

  49. Template strict locals
    <%# locals: () -%>

    Hello RailsWorld!

    <%= render partial: “message” %>

    View Slide

  50. normalizes
    class User < ApplicationRecord
    end
    def email=(value)
    super value&.strip&.downcase
    end

    View Slide

  51. normalizes
    class User < ApplicationRecord
    end
    normalizes :email, with: ->(email) { email.strip.downcase }
    normalizes :email, with: ->{ _1.strip.downcase }

    View Slide

  52. has_secure_password
    authenticate_by & password_challenge

    View Slide

  53. authenticate_by
    class User < ApplicationRecord
    has_secure_password
    end
    User.find_by(email: "[email protected]")
    &.authenticate(“railsworld2023")
    #=> false or user
    User.authenticate_by(
    email: "[email protected]",
    password: "railsworld2023"
    ) #=> user or nil

    View Slide

  54. password_challenge
    Current.user.update(password_params)
    def password_params
    params.require(:user).permit(
    :password,
    :password_confirmation,
    :password_challenge
    ).with_defaults(password_challenge: "")
    end

    View Slide

  55. generates_token_for

    View Slide

  56. generates_token_for
    class User < ApplicationRecord
    generates_token_for :password_reset, expires_in: 15.minutes do
    # BCrypt salt changes when password is updated
    BCrypt::Password.new(password_digest).salt[-10..]
    end
    end

    View Slide

  57. generates_token_for
    User.find_by_token_for(:password_reset, params[:token])
    #=> User or nil
    user.generate_token_for(:password_reset)
    “eyJfcmFpbHMiOnsiZGF0YSI6WzkwMjU0MTYzNSwiaC9oTkRJck9uL…”

    View Slide

  58. ActiveStorage Variants

    View Slide

  59. Named variants
    class User < ApplicationRecord
    has_one_attached :avatar do |attachable|
    attachable.variant :thumbnail,
    resize_to_limit: [200, 200]
    end
    end

    View Slide

  60. Preprocessed variants
    class User < ApplicationRecord
    has_one_attached :avatar do |attachable|
    attachable.variant :thumbnail,
    resize_to_limit: [200, 200],
    preprocessed: true
    end
    end

    View Slide

  61. if @project.file.previewable?
    @project.file.preview(:thumbnail)
    elsif @project.file.variable?
    @project.file.variant(:thumbnail)
    end
    # Render a preview or variant
    @project.file.represenation(:thumbnail)
    Representations

    View Slide

  62. Fact
    L
    Learn

    View Slide