Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか(20211101前編)

こんにちは、hachi8833です。直近ですが、明日11/2(火)19:30より「大江戸Ruby会議09 出前Edition」がオンライン開催されます。Rails界隈で知られた「あの人」や「あの人」も登壇するそうです。皆さんもぜひ!


また、Kaigi on Rails 2021の全動画がYouTubeチャンネル↓で公開されました 🎉

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

以前の公式更新情報で拾いきれなかったものから見繕いました。

🔗 Action MailテンプレートにDOM idを追加

自分のチームでは、CapybaraとSeleniumの代わりにCypressですべてのエンドツーエンドテストをやっている。その中で、すべてのメイラーでMailer Previewのアクションがエラーなしに開いてレンダリングできることをテストしている。メイラーやメイラーのアクションがたくさんあるのでバグの早期発見に役立っている。
自分たちが直面した問題は、メールのtofromsubjectなどにある重要なdd要素のほとんどに一意のセレクタがなくDOMで追いかけづらいというもの。このプルリクはこの問題を修正する。
私たちのCypressによるテストは以下のような感じになっている。

context('Mailer Preview', () => {
  it('works for all mailer actions', () => {
    cy.visit('/rails/mailers')

    cy.get('li a').each($a => {
      const href = $a.attr('href');
      cy.log(href)
      cy.visit(href)

      cy.get('#from').then($dd => {
        cy.log('FROM :' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('from@example.com')
      })

      cy.get('#to').then($dd => {
        cy.log('TO: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('foo@bar.com')
      })

      cy.get('#subject').then($dd => {
        cy.log('SUBJECT: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('Test subject')
      })

      cy.get('#mime_type').then($dd => {
        cy.log('MIME TYPE: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('HTML email')
      })

      cy.get('[download="icon.png"]').should('exist')
    })
  })
})

同PRより


つっつきボイス:「Action MailテンプレートのDOM idを増やしたそうです」「そうそう、idがあるとシステムテストを書いているときにとても取りやすいんですよ」「たしかに」「順序で指定したりすると後で仕様が変わったときにハマりやすいので、これはありがたい修正👍」「自分もテストのためにこんな感じでフィールドにidを振ったりしたことがあります↓」

<!-- railties/lib/rails/templates/rails/mailers/email.html.erb#56 -->
<dl>
    <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
      <dt>SMTP-From:</dt>
-     <dd><%= @email.smtp_envelope_from %></dd>
+     <dd id="smtp_from"><%= @email.smtp_envelope_from %></dd>
    <% end %>
    <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
      <dt>SMTP-To:</dt>
-     <dd><%= @email.smtp_envelope_to %></dd>
+     <dd id="smtp_to"><%= @email.smtp_envelope_to %></dd>
    <% end %>

    <dt>From:</dt>
-   <dd><%= @email.header['from'] %></dd>
+   <dd id="from"><%= @email.header['from'] %></dd>

    <% if @email.reply_to %>
      <dt>Reply-To:</dt>
-     <dd><%= @email.header['reply-to'] %></dd>
+     <dd id="reply_to"><%= @email.header['reply-to'] %></dd>
    <% end %>

    <dt>To:</dt>
-   <dd><%= @email.header['to'] %></dd>
+   <dd id="to"><%= @email.header['to'] %></dd>

    <% if @email.cc %>
      <dt>CC:</dt>
-     <dd><%= @email.header['cc'] %></dd>
+     <dd id="cc"><%= @email.header['cc'] %></dd>
    <% end %>

    <dt>Date:</dt>
-   <dd><%= Time.current.rfc2822 %></dd>
+   <dd id="date"><%= Time.current.rfc2822 %></dd>

    <dt>Subject:</dt>
-   <dd><strong><%= @email.subject %></strong></dd>
+   <dd><strong id="subject"><%= @email.subject %></strong></dd>

    <% unless @email.attachments.nil? || @email.attachments.empty? %>
      <dt>Attachments:</dt>

🔗 BASIC認証に無効な認証文字列が渡されたときの挙動を修正

BASIC認証で保護されているコントローラに、コロンが抜け落ちている誤った認証情報でリクエストを送信すると、以下のようにNoMethodError: undefined method 'bytesize' for nil:NilClassエラーが発生する。

class UsersController < ApplicationController
   http_basic_authenticate_with name: "king", password: "secret"

   def index
     render plain: "Something"
   end
end
credentials=$(echo -n king secret | base64)
curl 'http://localhost:3000/users' -H "Authorization: Basic $credentials"

同PRより


つっつきボイス:「BASIC認証の認証情報が不備だった場合のエラーがこれまでNoMethodErrorだったのを、has_basic_credentials?がfalseを返す形に修正したようです」「たしかにNoMethodErrorだと違いますよね」「今まではNoMethodErrorがhas_basic_credentials?を突き抜けてしまっていたのか」

# actionpack/test/controller/http_basic_authentication_test.rb#115
  test "has_basic_credentials? should fail with credentials without colon" do
    @request.env["HTTP_AUTHORIZATION"] = "Basic #{::Base64.encode64("David Goliath")}"
    assert_not ActionController::HttpAuthentication::Basic.has_basic_credentials?(@request)
  end

🔗 inflectorに登録した略語を削除できるようにした

ActiveSupport::Inflectorの略語(acronym)を削除しようとすると実装が壊れ、別の略語を登録しようとするとTypeErrorが発生する。

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "activesupport", require: "active_support/all"
end

ActiveSupport::Inflector.inflections do |inflect|
  inflect.clear :acronyms
  inflect.acronym "HTML" # => '[]=': no implicit conversion of String into Integer (TypeError)
end

これは#clearinstance_variable_set "@#{scope}", []のように@acronymsに新しいArrayを設定していたのが原因。デフォルトの初期値はHashになっている↓。

# activesupport/lib/active_support/inflector/inflections.rb#L78-L79
def initialize
  @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {} 

このプルリクでは、ActiveSupport::Inflector::Inflectionsの#clear#clear(:all)`を拡張して、従来できなかった略語の削除をできるようにもしておいた。
同PRより


つっつきボイス:「Inflectorは単数・複数形単語の登録や独自の固有名詞・略語の登録を行える機能ですね↓: #clearなどで削除できない問題と、その後で略語を再登録できない問題が修正された」「日本語だとあまり使わない機能だと思いますが、たしかに登録できるなら削除もできて欲しいですよね」

参考: Rails API ActiveSupport::Inflector::Inflections

# api.rubyonrails.orgより
acronym 'RESTful'
underscore 'RESTful'           # => 'restful'
underscore 'RESTfulController' # => 'restful_controller'
titleize 'RESTfulController'   # => 'RESTful Controller'
camelize 'restful'             # => 'RESTful'
camelize 'restful_controller'  # => 'RESTfulController'

acronym 'McDonald'
underscore 'McDonald' # => 'mcdonald'
camelize 'mcdonald'   # => 'McDonald'

🔗 ジェネレータのCSSプロセッサリストにBootstrapとBulmaを追加

# railties/lib/rails/generators/rails/app/app_generator.rb#L265
-     class_option :css, type: :string, desc: "Choose CSS processor [options: tailwind, postcss, sass]"
+     class_option :css, type: :string, desc: "Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass... check https://github.com/rails/cssbundling-rails]"

つっつきボイス:「ci skipとあるのはだいたいドキュメント関連の改修」「そういえばCSSフレームワークのBulmaは、以前#43110でsass-railsへのデフォルト依存が削除されたときに見かけました(ウォッチ20210921)」「Bulmaは使ったことないな〜」

参考: Bulma: Free, open source, and modern CSS framework based on Flexbox

🔗 Railsガイドのスタイル改修


つっつきボイス:「Railsガイドのスタイルにいくつか細かな修正が入っていました」「地道な修正大事ですね」

「ガイドの目次ドロップダウンを開いたらEscキーで閉じられるようにする、なるほど」「edgeガイドに反映されていました」

// guides/assets/javascripts/guides.js
    document.addEventListener("keyup", function(e) {
      if (e.key === "Escape" && guides.classList.contains("visible")) {
        guides.classList.remove("visible");
      }
    });

#43250の修正はどこだろう?」「モバイル表示の左右マージンが調整されて、フッターのmargin-bottomがわずかに小さくなっていた↓」「diffを見る方が早いかも」


同PRより(編集部で横並びに変更)

#42989はダークモードのdiffが見づらかったのを修正」「お〜なるほど」「個人的にダークモードってどうも不要な文明感がありますけど😆」「同じく」



同PRより

🔗Rails

🔗 Rails 7.0のアセットパイプライン周り解説記事


つっつきボイス:「これはいい記事でしたね👍」「歴史と現状と見通しのまとめが凄いですね」「ふわっとさせずに詳細を解説しきっていて、Simpackerにも言及しているのがさすが」

hokaccha/simpacker - GitHub

参考: Simpacker: Rails と webpack をもっとシンプルにインテグレーションしたいのです - クックパッド開発者ブログ

「この間話題にしたPropshaft(ウォッチ20211018)も、記事によるとRails 7で主要な選択肢のひとつになりそうで、Propshaftは思っていたより進んでいるんですね」「Rails 7は今はAlpha2ですが、次のAlpha3あたりでPropshaftが入るかもしれませんね」「お〜」

後で調べると、Propshaftはv7.0.0.alpha2ブランチではまだジェネレータのapp_base.rbには取り込まれていませんでしたが、masterブランチapp_base.rbには取り込まれていました。

Rails 7: import-map-rails gem README(翻訳)

🔗 ビジネスロジックをapp/operatorsで整理(Ruby Weeklyより)


つっつきボイス:「記事ではTrailbrazer↓を使ったりしてみたけど自分に合わなかったのでapp/operatorsで整理したらしい」「Trailbrazerにも名前の似たOperationsという概念があるんですね」

trailblazer/trailblazer - GitHub

「app/operatorsは前回取り上げたapp/contextsに似ている感じでしょうか?(ウォッチ20211025)」「記事冒頭でも前回のcontext記事↓を引用しているので意識はしているでしょうね」

参考: Organizing business logic in Rails with contexts

「前回のcontextsもそうでしたけど、この記事のコードに出てくる何とかOperatorもまさにGoF本で言うFacade(ファサード)ですね↓」「なるほど、名前が違う感じですか」

# 元記事より
# app/operators/invoice_operator.rb

class InvoiceOperator < BaseOperator
  def update(params:)
    @record.assign_attributes(params)

    # do things before updating the invoice eg:
    # update/create related records (like InvoiceItems)
    # a few more examples:
    calculate_total 
    calculate_vat

    state = @record.save

    # do your other business eg.:
    # send emails,
    # call external services and so on

    Result.new(state: state, record: @record)
  end

  def create(params:)
    @record.assign_attributes(params)

    # do things before creating the invoice eg:
    # create related records (like InvoiceItems)
    # a few more examples:
    calculate_total
    calculate_vat

    state = @record.save

    # do your other business eg.:
    # send emails,
    # call external services and so on

    Result.new(state: state, record: @record)
  end

  private

  def new_record
    Invoice.new
  end

  def calculate_total
    # you can write the logic here, 
    # or call a class that handles the calculation
  end

  def calculate_vat
    # you can write the logic here, 
    # or call a class that handles the calculation
  end
end

参考: Facade パターン - Wikipedia

「よりシンプルなパターンを作る試みは常にありますけど、実際に使ってみたときにシンプルになるとは限らないのが悩ましいところなんですよ」「もしかするとService Objectも出た当初はシンプルだと言われてたのかも」

「コントローラにActive Recordのメソッドチェーンがたくさんあるのは確かに気持ちよくないので、operatorsやcontextsでFacadeにするのもわかる: 個人的にはRailsでActive Recordが使えるのが嬉しい点だと思うので、そこまでしてActive Recordを直接触らせない形にしなくてもいいかなという気持ちが少しあります」「たしかに」「規模が大きくなればまた違ってくると思いますが」

「元記事末尾によると、このoperatorsはまだ長期のバトルテストは経てないそうです」「アプリが育ってきたときの設計は一概にどれがいいとは言えませんが、大きくする予定のないアプリなら別にoperatorsでやってもいいんじゃないかな」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

🔗 RailsアプリのJSテストコードのカバレッジ(RubyFlowより)


つっつきボイス:「RailsプロジェクトにおけるJSテストのコードカバレッジの記事のようですね」「記事に出てくるIstanbulはJS製のカバレッジツールなのか↓」「simplecov(Ruby製のカバレッジツール)も使ってる」

istanbuljs/istanbuljs - GitHub

simplecov-ruby/simplecov - GitHub

「記事ではIstanbulで集計したJSテストカバレッジのダンプをRSpecからトリガーしているようですね↓」

# 同記事より
# spec/rails_helper.rb

RSpec.shared_context "dump JS coverage" do
  after { dump_js_coverage }
end

RSpec.configure do |config|
  config.include_context "dump JS coverage", type: :system

  ...

「JSのテストコードが多いRailsプロジェクトで使いそうなテクニックですね: もっともフロントエンドとバックエンドでリポジトリが分かれているプロジェクトだと少し工夫が必要そうですが」「あ、たしかに」

🔗 GitLab 14.4リリース

つっつきボイス:「14.4が出たのでBPS社内のGitLabもアップデートしておくかな」「DASTって何だろうと思ったらDynamic Application Security Testingなんですね」「静的なセキュリティスキャン機能ですね: 最近のGitLabはこういった機能をよく追加していますね」「なるほど」「よく見たらDASTはGitLabのUltimate版のみなのでFree版では使えないことが判明」「う、残念」

参考: GitLab Pricing | GitLab

「他の機能はというと、VSCodeからのGitLabリモートリポジトリ参照」「おぉ?」「ローカルにcloneしていないプロジェクトをVSCodeから読み取り専用で参照できるようですね: おそらく動画↓で動いているVSCode拡張用のインターフェイスをGitLab側に用意したということかな」「リモートリポジトリをちょっと見たいときにローカルにcloneしなくてもVSCodeで見られるのはよさそうですね😋」

「GitLabは機能が着々と増えていますし、セキュリティパッチもちゃんと出し続けているので、出たらとりあえず当てることにしています」

🔗 その他Rails


つっつきボイス:「この間のruby/debug + Chrome Devtoolsの記事にアンサーが付いてたので拾いました」「そうそう、この方は明日10/29(注: つっつきは前日10/28夜でした)の銀座Rails#38に登壇されるosyoさんです」

以下は登壇後の資料ツイートです。

「そしてその後もrdbgについてTwitter上でosyoさんとやり取りしました↓」「あ、続きがあったんですね」「とりあえずの結論としてはbundle exec経由だとGemfileに追加しないとrdbgが動かないけど、binstub経由ならGemfileに追加しなくてもrdbgが動くということになりました」「お〜そうだったんですね」「Ruby界隈はTwitterでつぶやくと即誰かがコメントしてくれるから便利😊」「そうそう😊」

binstubをしっかり理解する: RubyGems、rbenv、bundlerの挙動(翻訳+解説)


前編は以上です。

バックナンバー(2021年度第4四半期)

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。