- Ruby / Rails関連
週刊Railsウォッチ(20191028前編)RailsにSTI用メソッドsti_class_forとpolymorphic_class_forが追加、RuboCopを変更箇所だけにかけるgem、strftime書式生成サイトほか
こんにちは、hachi8833です。Blawn言語が盛り上がってますね。
- 元記事: 15歳が独自プログラミング言語「Blawn」を発表 わずか数週間で開発 - ライブドアニュース
- リポジトリ: Naotonosato/Blawn: Pleasant Programming Language.
U22プロコン経済産業大臣賞のBlawnが大変話題で、実にできのよい言語ですが、実は事前審査通過40作品のうち、自作言語が3つあり、最終審査まで進めなかった残り2言語(
BoneとEmelio)も大変優秀な作品であったことは(自称言語担当)審査員として申し添えておきます。— Yukihiro Matz (@yukihiro_matz) October 23, 2019
なんていうんだろう。Blawnを巡るあれこれをみていると、プログラミング言語処理系というものが如何に、普通のエンジニアに理解されてないのかがよくわかる気がする。めっちゃ難しく考えてる人と、めっちゃ簡単に考えてる人と。Pythonに食わせてるとか、文字列置き換えしてるだけとか、その他、色々。
— Kota Mizushima (on a diet) (@kmizu) October 23, 2019
つっつきボイス:「15歳でプログラミング言語を作ったという話題で、早速Qiitaにもやってみた記事が出てました」「しかもC++で😳」「少し追ってみたところでは、生まれたての言語らしく小さなバグがちょこちょこある様子で、今後を見守っていきたい感じですね☺️」「言語を作る人が増えてきっとMatzは内心とても嬉しかったと思います😋」
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください
⚓お知らせ: 週刊Railsウォッチ「第16回公開つっつき会」(無料)
第16回目公開つっつき会は、11月14日(木)19:30〜にBPS会議スペースにて開催されます。今回は第一木曜日ではありませんのでご注意ください。
週刊Railsウォッチの記事にいち早く触れられるチャンスです!発言も自由です。皆さまのお気軽なご参加をお待ちしております🙇。
⚓Rails: 先週の改修(Rails公式ニュースより)
今回はmasterのコミットリストから見繕いました。
⚓ActiveSupport::SafeBuffer
が6.0で動かない問題を修正
# activesupport/lib/active_support/core_ext/string/output_safety.rb#L298
def set_block_back_references(block, match_data)
block.binding.eval("proc { |m| $~ = m }").call(match_data)
+ rescue ArgumentError
+ # Can't create binding from C level Proc
end
#34405が
with_index
メソッドと互換性がないらしい。
37422より大意
# #37422より
ActiveSupport::SafeBuffer.new('aaa').gsub!(/a/).with_index {|m,i| i }
# 期待する出力: 012
# 実際の出力: ArgumentError
つっつきボイス:「このwith_index
は、each_with_index
とまた別のメソッドでしたね」「こんなメソッドがあったんですね😳」「indexのオフセットも指定できるけど指定しないとゼロから始まるとかそんな感じで☺️」「Enumeratorでインデックスを使いたいときに便利そうですね☺️」「修正はrescue ArgumentError
を追加して終了」
参考: instance method Enumerator#with_index
(Ruby 2.6.0)
参考: with_indexが便利だという話とstable_sort_by - Qiita
⚓新機能: STI用メソッドをActiveRecord::Inheritance
のpublic APIに追加
追加したメソッドを使ってSTIやポリモーフィック関連付けをextendできる。
クラスをリネームしてクラス名がデータベース内のデータとマッチしなくなったときに有用。
自分のモデルに以下のメソッドを実装すると、既に存在しなくなったクラスの名前でレコードを読み込めるようになる。以下はシンプルな実装例。
同PRより
# 同PRより
class Animal < ActiveRecord::Base
@@old_names = {
"Lion" => "BigCat"
}
def self.sti_name
name = super
@@old_names[name] || name
end
def self.sti_class_for(type_name)
@@old_names.inverse[type_name]&.constantize || super
end
end
つっつきボイス:「STI関連の機能追加だそうです」「extendできるとはエグい😆」「STIでクラス名が変わっても読み込めると: テーブルは変わらない前提ですねなるほど ☺️」
参考: 5 シングルテーブル継承 (STI)-- Active Record の関連付け - Rails ガイド
「上の実装例を見るとsti_class_for
というAPIが追加されたようなので、例でやっているように継承して使うという意図でしょうね☺️」「self.sti_name
もオーバーライドしておく必要があるのか、へ〜」「sti_name
は前からあったみたい」「これが追加されたメソッドのコード↓」
# activerecord/lib/active_record/inheritance.rb#173
+ # 指定されたtype_nameに対応するクラスを返す
+ #
+ # 継承カラムに保存された値に対応するクラスを探索するのに使われる
+ def sti_class_for(type_name)
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ compute_type(type_name)
+ end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " \
+ "or overwrite #{name}.inheritance_column to use another column for that information."
+ end
「実装例のLion
とBigCat
、どっちがoldでどっちがnewだろう?🤔」「これだけだとわかりにくいけど、たぶんLion
がnewでBigCat
がoldなのかな〜」「constantize
は文字列からクラスやモジュールを生成するメソッドでしたね」
参考: constantize
-- ActiveSupport::Inflector
「なるほど、こう書けばクラス名が変わった場合にデータベースに入っている前のクラス名をアップデートしなくてもよくなると」「さすがに変えてからそのまま放置はしないでしょうね😆」「それやったらカオス確定😆: リネーム前のクラスが存在しなくなってるから後でコード読んだ人がパニクるし😇」「たとえば、いったんこの書き方でしばらく運用して、うまくいくようなら本格的にクラス名を移行するという使い方ができそうですね☺️」「なるほど!」
「あるいは、クラスからクラス名を取れるAPIが前からあるなら、その逆にクラス名からクラスを取れるAPIもあった方がいいよね、という発想で作られたのかもしれないと今思いました☺️」「そういう見方もあるのか😳」「コードのコメントに『このconstantize
はZeitwerkとコンパチなの?』『大丈夫、decorateしてある』というやり取りもありますね」
「これが2つ目に追加されたAPI↓: polymorphic_class_for
」
# activerecord/lib/active_record/inheritance.rb#L192
+ # 指定されたnameに対応するクラスを返す
+ #
+ # ポリモーフィックtypeカラムに保存された値に対応するクラスを探索するのに使われる
+ def polymorphic_class_for(name)
+ name.constantize
+ end
「そして既存のfind_sti_class
もsti_class_for
を使う形に変わっている↓」「つかsti_class_for
に切り出して委譲した形か」「たしかにfind_sti_class
はオーバーライドしたくないし😆」「切り出したことで柔軟になった感じですね😋」
# activerecord/lib/active_record/inheritance.rb#L251
def find_sti_class(type_name)
type_name = base_class.type_for_attribute(inheritance_column).cast(type_name)
- subclass = begin
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
- end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " \
- "or overwrite #{name}.inheritance_column to use another column for that information."
- end
+ subclass = sti_class_for(type_name)
+
unless subclass == self || descendants.include?(subclass)
raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
end
+
subclass
end
⚓Active Storageのコンパイル済みJSをソースと同期
# activestorage/test/javascript_package_test.rb
+# frozen_string_literal: true
+
+require "test_helper"
+
+class JavascriptPackageTest < ActiveSupport::TestCase
+ def test_compiled_code_is_in_sync_with_source_code
+ compiled_file = File.expand_path("../app/assets/javascripts/activestorage.js", __dir__)
+
+ assert_no_changes -> { File.read(compiled_file) } do
+ system "yarn build"
+ end
+ end
+end
Active Storageのコンパイル済みJSがソースコードと同期しない問題を追った。
これを修正するためにActive Storageのコンパイル済みJSバンドルとソースを強制的に同期するようにした。同期していればテストはパスし、していなければfailする。
同PRより大意
つっつきボイス:「これはテストコードの割と小さな修正ですね☺️」
⚓ActiveModel::Error
訳文参照でインデックス付き属性探索をサポート
- PR: Support ActiveModel::Error translation lookup on indexed attributes. by jonathankwok · Pull Request #37447 · rails/rails
- PR: Fix i18n of attributes with multi-digit indexes by jonathanhefner · Pull Request #37486 · rails/rails
このPRの意図は、
ActiveModel::Error
のインスタンスでattribute
がインデックス付きattributeの場合にインデックス付きsuffixなしでメッセージを探索できるようにすること。これはindex_errors: true
を使うActiveRecord
モデルによくある。
#37447より大意
# 同PRより
class Manager < ActiveRecord::Base
has_many :reports, index_errors: true
end
class Report < ActiveRecord::Base
belongs_to :manager
validates_presence_of :name
end
manager = Manager.new
invalid_report = Report.new
manager.reports = [invalid_report]
manager.save
error = manager.errors.first
error.attribute
# => :"reports[0].name"
error.type
# => :presence
上のようにattributeがインデックス化されているとする。これは
Report
レコードの場合はよいが、カスタム訳文メッセージの場合にはインデックスがあるためにうまくいかない。このPRでは探索キーからインデックスを削除する探索エントリを追加することで、インデックスなしで訳文を探索できるようになる。
#37447より大意
つっつきボイス:「エラーのI18n関連の修正だそうで、2つPRがあります」「Reportだとreports[0]
みたいなメッセージ参照が生成されるけど、カスタムメッセージのときにインデックスが邪魔だったのね」「1つめのPRの修正は以下のように[インデックス]
を削除しただけでした↓」「お〜、attributeからインデックス指定を削るだけでやれちゃうのか😳」「これでインデックスを気にせず書けるようになる😋」
attribute = attribute.to_s.remove(/\[\d\]/)
「で2つ目のPRではさらに上の修正の不足部分が修正されていました↓」「おっと+
が漏れてた😆」「あやうく1桁インデックスしか処理できないところだった〜😆」「プルリクが2つ並んでたので気づきました☺️」
# activemodel/lib/active_model/error.rb#L20
- attribute = attribute.remove(/\[\d\]/)
+ attribute = attribute.remove(/\[\d+\]/)
⚓関連付けリレーションでのインスタンス作成でunscopeが効くように修正
#35868の意図は、関連付けの読み込みを一貫させることにある。関連付けのリレーションでのインスタンス作成は副作用を伴うべきではない。
同PRより大意
上の#35868はウォッチ20191015でも話題にしました。
# activerecord/lib/active_record/association_relation.rb#L18
- def build(*args, &block)
+ def build(attributes = nil, &block)
block = _deprecated_scope_block("new", &block)
- scoping { @association.build(*args, &block) }
+ @association.enable_scoping do
+ scoping { @association.build(attributes, &block) }
+ end
+ end
alias new build
- def create(*args, &block)
+ def create(attributes = nil, &block)
block = _deprecated_scope_block("create", &block)
- scoping { @association.create(*args, &block) }
+ @association.enable_scoping do
+ scoping { @association.create(attributes, &block) }
+ end
end
- def create!(*args, &block)
+ def create!(attributes = nil, &block)
block = _deprecated_scope_block("create!", &block)
- scoping { @association.create!(*args, &block) }
+ @association.enable_scoping do
+ scoping { @association.create!(attributes, &block) }
+ end
end
つっつきボイス:「@kamipoさんによる修正です」「これだけだとわかりにくいので、issue #37138を見てみると、unscopeしたはずの関連付けでunscopeが無視されるという現象が起きてたそうです」「うひゃぁ😱」「そもそもunscope使うなと言いたいけど😆」
期待される動作:
new_comment = post.comments.unscope(where: :visible).new
でデフォルトスコープがunscopeされるので、new_comment.visible
はfalseになる(そのフィールドのデータベースデフォルト値)
実際の動作:new_comment = post.comments.unscope(where: :visible).new
のunscopeが効かず、default_scope
によってnew_comment.visible
がtrueになる
「以前訳した記事↓でも『default_scope
は使うな』というのがありました」「まあdefaultのに限らずunscope
したくなることはまれになくもないんですけど、unscope
するぐらいならもう一回ビルドし直す方がいいんじゃね?って思いますし😆」「😆」
「再現コードでdefault_scope
は...あるか↓😇」「出たな😆」「visible
をVisibleCommentみたいにラップしちゃうなら、そっちの方でdefault_scope
を使うのは自分は別にいいと思いますし」「ふぅむ🤔」
# #37138より
class Comment < ActiveRecord::Base
belongs_to :post
default_scope -> { where(visible: true) }
end
「そして修正↓はというと、attributes = nil
...?」「従来の*args
は実はattributesで、*
は付いてたけどarrayじゃなくてhashを取るという前提だったらしい🤔」
# activerecord/lib/active_record/association_relation.rb#L18
- def build(*args, &block)
+ def build(attributes = nil, &block)
block = _deprecated_scope_block("new", &block)
- scoping { @association.build(*args, &block) }
+ @association.enable_scoping do
+ scoping { @association.build(attributes, &block) }
+ end
+ end
alias new build
「元の*args
という引数名はふわっとしてて少々雑な感じではありますね😆」「引数名をattributes
に変えつつ、何も渡されなかった場合に明示的にnil
が渡るようにしたのか」「ここは意味は変わらないはずだからリファクタリングですね☺️」「修正後はscoping { @association.create!(*args, &block) }
をenable_scoping
↓のブロックで囲んでいて、これがキモかな」「難しいコードや😭」「さすが@kamipoさん」
# activerecord/lib/active_record/associations/association.rb#L46
+ @enable_scoping = false
...
+ def enable_scoping
+ @enable_scoping = true
+ yield
+ ensure
+ @enable_scoping = false
+ end
「以下がテストコード↓」「unscope
してbuild
したらbulb.name
がnilになるのが正しいと」
# activerecord/test/cases/associations/has_many_associations_test.rb#241
+ def test_build_and_create_from_association_should_respect_unscope_over_default_scope
+ car = Car.create(name: "honda")
+
+ bulb = car.bulbs.unscope(where: :name).build
+ assert_nil bulb.name
+
+ bulb = car.bulbs.unscope(where: :name).create
+ assert_nil bulb.name
+
+ bulb = car.bulbs.unscope(where: :name).create!
+ assert_nil bulb.name
end
「引数に*args
とか書くとレビューでツッコまれます?」「そうとも限らなくて、昔のRailsでは割と見かける書き方でしたし、引数を触らずに委譲したいコードだと*args
って書いたりしてましたね😋」「おぉ」
⚓Rails
⚓Rails 6のAction Cableテスト機能
# 同記事より
require "test_helper"
class PublishCommentaryJobTest < ActionCable::Channel::TestCase
include ActiveJob::TestHelper
# `assert_broadcast_on` asserts exact message sent on a channel stream.
test "publishes commentary" do
perform_enqueued_jobs do
assert_broadcast_on(CommentaryChannel.broadcasting_for('match_1'), comment: "Hello and welcome everyone!!") do
PublishCommentaryJob.perform_later(1, "Hello and welcome everyone!!")
end
end
end
# `assert_broadcasts` asserts the number of messages sent to stream
test "asserts number of messages" do
perform_enqueued_jobs do
PublishCommentaryJob.perform_later(1, "Hello and welcome everyone!!")
assert_broadcasts CommentaryChannel.broadcasting_for('match_1'), 1
end
end
# `assert_no_broadcasts` asserts no messages sent to stream
test "no comment published if invalid match id" do
perform_enqueued_jobs do
PublishCommentaryJob.perform_later(-1, "Hello and welcome everyone!!")
assert_no_broadcasts CommentaryChannel.broadcasting_for('match_1')
end
end
end
つっつきボイス:「上の記事に出てくるaction-cable-testingはたしかEvil Martiansの人が作ったgemでした」「ほほぅ☺️」「このgemはRails 5で入りそこなったけどRails 6でマージされたと以下の翻訳記事にあります😋」
「Action Cableのテスト書いたことないんで、そもそも書けるのか?ってちょっと思いましたけど😆」「テストを自力で書こうとすると難しそうですね🤔」
- 接続テスト
- チャネルのテスト
- ブロードキャストのテスト
参考: ActionCable::Connection::TestCase
⚓レガシープロジェクトでRuboCopを使う
抜粋:
- .rubocop_todo.ymlを使わない方針で進める
- 過去のコードよりも今後のコードチェックを重視
- 古いコードベースをずっとそのままにするということではない
- 古いコードベースで当面わずらわされないようにするgemの紹介
つっつきボイス:「レガシーコードになるべく触らずにコードチェックする系の記事で、以下のgemも紹介されています」
⚓Pront
「その中でprontoというgemは変更部分だけをRuboCopとかでチェックできるようにするそうです↓」「GitHubにレビューコメント付けてくれる😋」「つまり修正のプルリク作るところまでやってくれるってことですよね😋」「prontoは自体はランナーを呼ぶようですが、RuboCopやさまざまなlintのランナーがずらりとありますね😍」
「GitLabでも動くのかな?」「お、GitLabCIもやれるとあります❤️: 割と試しやすそうですし社内のGitLabで使ってみません?」
# GitLabCI用config例
lint:
image: ruby
variables:
PRONTO_GITLAB_API_ENDPOINT: "https://gitlab.com/api/v4"
PRONTO_GITLAB_API_PRIVATE_TOKEN: token
only:
- merge_requests
script:
- bundle install
- bundle exec pronto run -f gitlab_mr -c origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
参考: Prontoでソースレビュー自動化 - アクトインディ開発者ブログ
⚓Overcommit
「overcommit gemはコミット時にローカルでGit Hooksを起動して必須の処理を回すんだったかな」「社内でも誰か使ってた覚えあります」「設定を欲張り過ぎるとコミットのたびにうるさく鳴き出しそう😆」
overcommitでmasterへのプッシュを禁止するとかもできるそうです↓。
参考: git hooksをovercommitで管理して作業効率の底上げを狙う - LCL Engineers' Blog
⚓rubocop_lineup
「rubocop_lineupは新しいせいか日本語記事まだないですね」「ブランチでmasterとの差分行だけをRuboCopでチェックする拡張だそうです」
rubocop_lineupになぜこの写真↓?と思って調べると、lineupには『面通しのために並ばせた容疑者の列』という意味もあるんだそうです(米国のみ)。毎度邪魔になるメッセージを常習犯に見立てた感じですね👮🏼♀️。
⚓active_merchant: 支払いサービスの抽象化ライブラリ(Ruby Weeklyより)
- 元記事: activemerchant/active_merchant: Active Merchant is a simple payment abstraction library extracted from Shopify. The aim of the project is to feel natural to Ruby users and to abstract as many parts as possible away from the user to offer a consistent interface across all supported gateways.
- サイト: Active Merchant
# 同サイトより
# ゲートウェイのテストサーバーにリクエスト送信
ActiveMerchant::Billing::Base.mode = :test
# クレジットカードオブジェクトの作成
credit_card = ActiveMerchant::Billing::CreditCard.new(
:number => '4111111111111111',
:month => '8',
:year => '2009',
:first_name => 'Tobias',
:last_name => 'Luetke',
:verification_value => '123'
)
if credit_card.valid?
# TrustCommerceへのゲートウェイオブジェクトを作成
gateway = ActiveMerchant::Billing::TrustCommerceGateway.new(
:login => 'TestMerchant',
:password => 'password'
)
# 10ドル(1000セント)を認証
response = gateway.authorize(1000, credit_card)
if response.success?
# 金額をキャプチャ
gateway.capture(1000, response.authorization)
else
raise StandardError, response.message
end
end
Active MerchantはeコマースシステムShopifyの抜粋です。このライブラリの主な設計原則は、ShopifyのシンプルAPIや統合APIの要件を用いて、内部APIが大きく異るさまざまな支払いゲートウェイにアクセスすることです。
同リポジトリより
TechRachoにも記事がありました↓。
つっつきボイス:「支払いを抽象化するという触れ込みのactive_merchantはかなり前からあるみたいで、今も熱心にメンテされているようです」「使ったことなかった😆」「対応している支払いサービスもたくさんあって、当然のようにStripeにも対応してますね😋↓」「StripeにJP含まれてませんけど😆」「なかなか使い所が難しそう: 多言語展開するような全世界向けシステムで、国によって決済業者が違う、みたいなケースでマッチするんだろうけど🤔」
⚓foragoodstrftime.com: strftime
の書式生成サイト
つっつきボイス:「strftime
の書式をさっと調べられるサイトです」「これは覚えられないヤツ😆」「要素をドロップして並べられるとか、随分凝ったサイトですね😆」「まあそこまでせんでも😆」「作ってみたかったんですよきっと☺️」
なお、以下は以前もウォッチで紹介した同様の趣旨のサイトです。
前編は以上です。
おたより発掘
STIとテーブルのリネーム、ダウンタイムなしで変更するの本当にしんどいので嬉しい😆
週刊Railsウォッチ(20191028前編)RailsにSTI用メソッドsti_class_forとpolymorphic_class_forが追加、RuboCopを変更箇所だけにかけるgem、strftime書式生成サイトほか https://t.co/4P4vMbhVR7
— Jaga Apple (@jagaapple_tech) October 29, 2019
バックナンバー(2019年度第4四半期)
週刊Railsウォッチ(20191021)Rails 6でhas_many関連の修正やSprockets 4.0対応、Shrine 3.0がリリース、Minitestスタイルガイドほか
- 20191015 スライド「Rails Performance issues and Solutions」を見る、dirtyに*_previously_was が追加、Sidekiq 6.0.1ほか
- 20191008後編 Ruby 2.7のInteger#[]でバイナリチェック、rubyzip gemは強力、13KBのJavaScriptゲームほか
- 20191001後編 RedisとRubyをつなぐredis-object gem、Fullstaq Rubyの新バージョン、COUNT(*)とCOUNT(1)の速度ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。