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

週刊Railsウォッチ(20201026前編)Shopifyのerb-lint gem、Form Objectを使いやすくするyaaf gem、railsrcの機能追加ほか

こんにちは、hachi8833です。

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

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

以下のコミットリストのChangelogを中心に見繕いました。6.1.0マイルストーンは変わっていませんでした。

新機能: values_atメソッドをActive Recordインスタンスで使えるようになった

# Before
person = Person.new(name: 'Francesco', age: '22')
person.attributes.values_at("name", "age")
# => ["Francesco", "22"]
# After:
person = Person.new(name: 'Francesco', age: '22')
person.values_at(:name, :age)  
# => ["Francesco", "22"]

このコンテキストではpluckvalues_atのエイリアスとも言えると思う。
同PRより大意


つっつきボイス:「values_atはあまり使ったことなかったかも」「このメソッド知りませんでした」「形としてはRubyのHash#values_at的なメソッドをActive Recordインスタンスでも使えるようになった感じみたいです」「そういえばActive RecordのインターフェイスはRubyのHash的なところがありますね」「values_atを定義してflattenしてpublic_sendしているのね↓」「短く書けるのはうれしい」

# activerecord/lib/active_record/core.rb#L605
+   def values_at(*methods)
+     methods.flatten.map! { |method| public_send(method) }
+   end
+

参考: Hash#values_at (Ruby 2.7.0 リファレンスマニュアル)

read_attribute_before_type_castが属性エイリアスを扱えるよう修正

エイリアス化された属性のnumericalityバリデーションはtypecastするまで属性の値を取得できない。理由はActive Recordが元の属性名ではなくエイリアス名で属性値を取得しようとしていたため。
以下は無効な値を渡してもパスしてしまうバリデーションの例。

class MyModel < ActiveRecord::Base
  validates :aliased_balance, numericality: { greater_than_or_equal_to: 0 }
end

MyModelを以下のようにインスタンス化したとすると、numericalityバリデーションが走るときはtypecastされるまで値を取得できないので、typecastされた値(ここでは0.0になる)を用いることでバリデーションが成功し、有効とされてしまう。

subject = MyModel.new(aliased_balance: "abcd")
subject.valid?

しかしMyModelを以下のように宣言したとする。

class MyModel < ActiveRecord::Base
  validates :balance, numericality: { greater_than_or_equal_to: 0 }
end

そしてbalanceプロパティに"abcd"という値を代入すると、バリデーションが走るときにこのモデルが無効になる(Active Recordはtypecastの前に値を取得できるようになるため)。

今回の変更によって、read_attribute_before_type_castを用いればattr_nameがalias_attributeであってもtypecast前に値を取得できるようになる。
同PRより大意


つっつきボイス:「numericalityはRailsガイドにも昔からあるバリデーションです」「上のコード例で:aliased_balance, numericality:でバリデーションを設定しているのに"abcd"みたいな文字列を渡すとvalid?がtrueを返してしまうのか」

参考: 2.8 numericality -- Active Record バリデーション - Railsガイド

「typecastする前に型をチェックすべきだったけど、『Active Recordが元の属性名ではなくエイリアス名で属性値を取得しようとしていた』ためにそうなっていなかったということですね」

# activerecord/lib/active_record/attribute_methods/before_type_cast.rb#L49
-     def read_attribute_before_type_cast(attr_name)
+       attribute_before_type_cast(attr_name.to_s)
+       name = attr_name.to_s
+       name = self.class.attribute_aliases[name] || name
+
+       attribute_before_type_cast(name)
+     end

「ちなみに文字列をto_fするとこうなるんですよ↓」「え」「こわい」

"abcd".to_f
#=> 0.0

numericalityバリデーションにnumericでない値が渡されたら比較演算子でArgumentErrorなどをraiseして欲しいですね」「Rubyで文字列と数値を比較演算すると以下のようにArgumentErrorになりますけど、validates numericalityで比較したものはArgumentErrorにならずに、本来想定されない型キャスト後の値でバリデーションが走ってしまっていたんですね」

"abcd" > 0
Traceback (most recent call last):
        5: from /Users/hachi8833/.anyenv/envs/rbenv/versions/2.7.2/bin/irb:23:in `<main>'
        4: from /Users/hachi8833/.anyenv/envs/rbenv/versions/2.7.2/bin/irb:23:in `load'
        3: from /Users/hachi8833/.config/anyenv/envs/rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
        2: from (irb):7
        1: from (irb):7:in `>'
ArgumentError (comparison of String with 0 failed)

「データベース側で型が指定されていれば、仮にここでバリデーションをすり抜けてしまってもsaveでエラーになりますが、バリデーターでエラーにならないのはたしかによくないですね」

MemCacheStoreのフォールバックに$MEMCACHE_SERVERSを使うようになった

概要

このプルリクは、アドレスが指定されていない場合にActiveSupport::Cache::MemCacheStorelocalhost:11211よりも前にENV['MEMCACHE_SERVERS']にフォールバックするようにする。

詳細

デフォルトでは、Dalli::Clientでサーバーアドレスが渡されなかった場合のフォールバック先は以下の2つになってくる。

  • $MEMCACHE_SERVERS
  • "127.0.0.1:11211"

しかしMemCacheStoreはアドレスを独自にチェックし、存在しない場合は"localhost:11211"にフォールバックするようになっている。

これが原因で、(Dalliが提供している)非推奨化された:dalli_storeからmem_cache_storeに移行するときのバグにつながることがある。

-config.cache_store = :dalli_store     # could be implicitly relying on $MEMCACHE_SERVERS
+config.cache_store = :mem_cache_store # ignores $MEMCACHE_SERVERS

自分たち独自のフォールバックを削除して単にDalli::Clientnilを渡すことでこのフォールバックロジックがただで手に入る。テストが追加されたので変更があれば検出できる。

動機

Dalliで:dalli_storeが非推奨化されて:mem_cache_storeが推奨になった(削除はgemのバージョン3の予定)。移行では、新しいアダプターが$MEMCACHE_SERVERSもチェックしてくれると仮定できれば楽なのだが、この仮定は正しくなく、使うと事実上キャッシュが無効になる

$MEMCACHE_SERVERSが別の値に設定されているシステムではlocalhost:11211へのフォールバックに頼るとは考えにくいしそこに接続したいということもないだろうから、この変更は安全なはず。さらにこの変更によって使いやすさが向上し凡ミスも防げる。🦶 🔫 🙅

その他

このプルリクから#40419を切り出した。これによって自分がMemCacheStoreのテストをローカルで正しく実行できた。
#40420より大意


つっつきボイス:「先週にもあったShopifyのMemcached関連の新たな改修か(ウォッチ20201020)」「MemCacheStoreが使うMemcachedサーバーのアドレスは、環境変数があればデフォルトのフォールバックよりも環境変数を参照するようになったんですね」「既に環境変数MEMCACHE_SERVERSが設定されている環境で現在のlocalhost:11211へのフォールバックを利用していることはないだろうと考えられるのでこの変更は安全なはず、とプルリクに書かれているのも納得」


「そういえば来月の銀座Railsの出張RailsウォッチでRailsのコンフィグ周りの話をしようかなと思っていたところだったんですけど、ちょうどこのような話です」「おぉ」「Railsのコンフィグって、デフォルト値があったり環境変数で渡したりyamlで渡したり引数で渡したりと、作法や優先順位にいろんな仮定というか暗黙の期待値があるので、自分でも整理しつつそういう話をしようかなと考えています」「自分もこの間Qiitaにそういう記事を書きました」

「最初からこの改修後のような設計になっていればよかったのにと思われそうですけど、最初からこういうものを見通して設計するのは難しいんだなと改めて思いますね」「人間はエスパーにはなれないか...」「自分が作ったものが将来どう使われるようになるかは本当にわからない」

「Railsでも当初はコンフィグで設定できたものが後で環境変数でも設定できるようになることがありますけど、かといって何でも環境変数でできるようにするのがいいかというとそうではないんですよ」「もしやったらたぶんえらいことになりそうですね」「どの設定は環境変数で上書きできるとうれしくて、どの設定だとよくないかというのは、世の中のニーズによって変わってくるところもあるんじゃないかと思ったりします: 実際今回の改修もこれまで問題になっていなかったぐらいなので」

railsrcファイルの置き場所をXDGに沿ってリファクタリングした

概要

XDG Base Directory Specification(現在はGitやTmuxやPryやRspecなどのFOSSプロジェクトで用いられている)は、config/rcなどのさまざまなファイル形式のデフォルトの置き場所を提供している。

このコミットはapp_generator.rbをリファクタリングし、XDG_CONFIG_HOMEが設定されていて、かつXDG_CONFIG_HOMEの場所にrails/railrcファイルが存在する場合はそこからrailsrcを読み出す。

後方互換性維持のため、XDG_CONFIG_HOMEが未設定またはrails/railsrcファイルが存在しない場合はデフォルトを~/.railsrcにする。
同PRより大意

参考: XDG ユーザーディレクトリ - ArchWiki


つっつきボイス:「XDG_CONFIG_HOMEがなければこれまでどおりホームディレクトリの`/.railsrcを参照するからこれまで使っている人には影響しませんね」

「railsrcって最近追加されたのかと思ったらだいぶ前からあったみたい↓(Rails 3.2〜)」「rails newのオプションを書ける設定ファイルなんですね」「railsrc、使ったことなかった」「名前からしてLinuxのいわゆるrcファイルと同じような扱いなんでしょうね」

参考: .railsrcにrails newするときのオプションを書いておける - かみぽわーる

rails newをしょっちゅう実行する人には便利そう」「自分はそんなにしょっちゅうはやらないかな」「こうやって既存の仕様に沿って動くようになったのはいいことだと思います」「そういえばrailsrcの話題を最近Twitterかどこかで見かけた気がする🤔」

「今回はrailsrcをXDGに合わせて、XDG_CONFIG_HOMEが設定されていたらそれに対応するディレクトリからrailsrcを読み出すようになったんですね」「XDGって何だろう」「POSIX的な規格の一種のようですね」「XDGはX Desktop Groupの略で今はfreedesktop.orgなのか」

参考: freedesktop.org - Wikipedia

かつてX Desktop Groupと名乗っていたため、"XDG" という省略形も、ディレクトリ名などあちこちにいまだによく使われている。
Wikipediaより


つっつき後に見つけたツイートを念のため貼っておきます。

Rails

Railsアプリのサイズを見積もる


つっつきボイス:「アプリのサイズというのは依存関係やコードサイズなどのことか」「Railsの標準機能としてrails statsでコードの統計情報を表示できます↓」「開発案件の事前調査段階での現行アプリケーションの状況をヒアリングするときに、ソースコードがまだ閲覧できない状況だとrails statsの結果をもらって多少の参考にすることはありますね」「なるほど」

参考: 1.15 その他のタスク -- Rails のコマンドラインツール - Railsガイド

rails statsで取れる情報はあくまで目安なので当てにしすぎない方がいいでしょうね: たとえば行数が多い場合でも、コピペコードのせいで多いのか、ちゃんと書いたコードが多いのかは実際にコードを見ないとわからないので」「テストコードが存在するかどうかをさっと見たいときなどには便利だと思います」


同記事では以下のgemに触れていました。

jmmastey/bundler-stats - GitHub

bleonard/rails_stats - GitHub

fastruby/skunk - GitHub

whitesmith/rubycritic - GitHub

yaaf: RailsのForm Objectを使いやすくするシンプルなgem

rootstrap/yaaf - GitHub

# 同リポジトリより
# app/forms/registration_form.rb

class RegistrationForm < YAAF::Form
  validates :phone, presence: true
  validate :a_custom_validation

  # ...

  def a_custom_validation
    # ...
  end
end

以下の記事で知りました。Form Objectでバリデーションやコールバックも使えるようです。


つっつきボイス:「Yet Another Active Formの略でyaafだそうです」「Form Objectの書き方は人それぞれ好みがありますね」「この人の好きなForm Objectをgem化したのかな」「READMEを眺めた限りではシンプルで、トリッキーなことはしていなさそう」

「記事を見ると、同じアプリの中でForm Objectのインターフェイスがまちまちになるのを何とかしたくて作ったようです」「そういうアプリはそもそも書き方がよくないので修正しておくべきでしょうね」「自分もそう思います」


「話がちょっとそれますけど、記事の見出しに『Bob Ross of form objects』とあったのを調べると、30分ぐらいですごい絵を描く『ボブの絵画教室』という米国のTV番組のことみたいです」「ああ、そんな番組ありましたね」「ほら簡単でしょって言いながら描くけど簡単じゃないという番組😆」「このgemでForm Objectを簡単にやれるというニュアンスかなと思いました」

参考: ボブ・ロス - Wikipedia

Form Objectよもやま話

「ところで、最近Form Objectという用語を以前ほど見かけなくなった印象がちょっとあります」「Form Objectという呼び方に『Form』という単語が使われているんですけど、その単語に引きずられて、HTMLビューのフォームを抽象化するものだという印象を与えている気がするんですよ」「Form ObjectがビューのHTML UIに寄っているというか依存している感じがするということですね」「本来Form Objectは単体で呼び出しても構わないものなんですが、Form ObjectがビューのUIに一対一で結び付けられるものだと見なされてしまうと、クラスの自由度が下がると思うんですよ」「あ〜たしかに」

「自分はForm Objectを主にビジネスロジックの置き場所に使っていますしTechRachoにForm Objectの記事を書いたこともありますけど、Form Objectという名前で呼ぶと、書かれるコードもフォーム寄りになりやすいところがあるかなと思います」「たしかにそうなると汎用性が落ちそうですね」

「Form Objectという呼び方は、そういう誤解を招きそうなところがもしかするとあまりよくないのかなと最近思っているところです」「かといってこれをService Objectと呼ぶのも違うんですよね...」「う〜む」

「いわゆるビジネスモデルオブジェクトのような感じの名前がいいのかな?🤔」「UIのフォームに相当するモデルオブジェクトではあるけど、ビジネスモデルという言葉を使うことで、コントローラでの利用に限定せずにrakeタスクやバッチなどのCLIで使ってもいいんだよ、というニュアンスを出せたらいいなと思うんですけどね」「そういう雰囲気を伝えたいということですね、ちょっと腑に落ちました」

「たとえばBusiness Objectのような用語で呼ぶ方がよいのかもしれないとちょっと思いました」「あとForm Objectと呼ぶとフロントエンド側と共同作業するときにも相性が悪そうな気がしますね」「ああたしかに」「Business Objectと呼ぶことにしてUIの差異はコントローラで吸収するということにすれば、フロントエンド側に受け入れてもらいやすそう」「ともあれ、呼び出し元からの値を設定したりバリデーションしてエラーを出したりsaveしたりできるクラスが欲しくなるのはたしかで、それがBusiness Objectなのかもしれないと思いました」

Ruby on Rails Discussionsより


つっつきボイス:「Dockerなどに関するRailsの公式なデプロイ方法が欲しいとありました」「たしかにRailsガイドにはデプロイに関する記述がありませんよね」「Capistoranoの情報ぐらいは公式にあってもいいのかなと思いましたけど」「今ならCapistoranoよりDockerでしょう」


「ところでこのディスカッションにrails.github.io↓というサイトへのリンクがあるけど、Rails 4.2.0とか書かれていて相当古くありません?」「え、このサイト初めて見ました!」「rails.github.ioは更新するか消すなど何か対応してもよさそうな気がしますね」

erb-lint gem

Shopify/erb-lint - GitHub

# 同リポジトリより
linters:
  AllowedScriptType:
    enabled: true
    allowed_types:
      - 'application/json'
      - 'text/javascript'
      - 'text/html'
    allow_blank: false
    disallow_inline_scripts: false

つっつきボイス:「Shopifyがforkしたgemで、fork元より★が多いです」「ERBのlinterってあるんですね」

# 同リポジトリより
Not allowed ❌
<a onclick="alert(<%= some_data %>)">

Allowed ✅
<a onclick="alert(<%= some_data.to_json %>)">

Not allowed ❌
<script>var myData = <%= some_data %>;</script>

Allowed ✅
<script>var myData = <%= some_data.to_json %>;</script>

「こういうふうにERBで暗黙のto_jsonを禁止したりできるのか↑」「この書き方やっちゃいそう...」「erb-lintはRuboCopとも連携できるのね」「設定ファイルのallowed_typesでホワイトリストも指定できる」「このgem入れてみようかな」「ERBでlintをかけたい場合によさそう: 取りあえず入れてみてもいいんじゃないかと思います👍」

「ところでこのチェックちょっと面白い↓」「あ、小文字のiがïになってる😳」

# 同リポジトリより
Bad ❌
<script type="text/javacsrïpt"></script>
Good ✅
<script type="text/javascript"></script>

その他Rails


つっつきボイス:「factory_botなど多くのgemでおなじみのthoughtbotが、Railsアプリの多言語化に関するイベントを開催するそうです」「多言語対応には独自のつらみがいろいろありますね」「多言語化はデザインにも影響するのが大変」「アラビア語みたいなRTLの言語だとメニューの配置も変えないといけなくなったりしますね」

参考: RTL (右書き) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN


前編は以上です。

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

週刊Railsウォッチ(20201021後編)webpack 5リリースでWebpacker対応開始、AWS Lambda Extensions発表、Pythonにマクロ構文追加提案ほか

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

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

Rails公式ニュース

Ruby on Rails Discussions

Hacklines

Hacklines


CONTACT

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