Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1823 articles
Browse latest View live

Rails 7.0.0がリリースされました

$
0
0

Ruby on Rails 7.0.0がついにリリースされました。

同時にRuby on Rails公式サイトのデザインもリニューアルされました。


rubyonrails.orgより

英語版Changelogをまとめて見るにはGItHubのリリースタグ↓が便利です。v7.0.0タグの日付は日本時間の2021/12/16 9:26でした。

コミットリストは1977ファイルが更新されています。

🔗 更新の概要

Rails 7.0.0の更新の概要は、以下の7.0.0.rc1リリース記事で紹介した内容と基本的に同じです。

Rails 7.0.0.rc1がリリースされました

また、以下の6.0.4.4/6.1.4.4と同じセキュリティ修正もRails 7.0.0に含まれています。

Railsセキュリティ修正6.0.4.4/6.1.4.4がリリースされました

参考: Rails 7で生成したアプリのGemfile

以下はRails 7.0.0でrails new .を実行して生成されたGemfileです。sprocket-rails、importmap-rails、turbo-rails、stimulus-railsがデフォルトでインストールされています。

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.1.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.0"

# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"

# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", "~> 5.0"

# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Use Sass to process CSS
# gem "sassc-rails"

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri mingw x64_mingw ]
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"

  # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
  # gem "rack-mini-profiler"

  # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
  # gem "spring"
end

group :test do
  # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
  gem "capybara"
  gem "selenium-webdriver"
  gem "webdrivers"
end

rails sで起動すると、Welcome画面も公式サイトと同様にリニューアルされていました。

なお、rails new . -c tailwindでアプリを生成するとtailwindcss-rails gemがインストールされます(-c tailwindの場合はjsbundling-railsやcssbundling-railsはインストールされず、node_modulesディレクトリも作成されません)。さらに、scaffold(rails g scaffold post regex:string comment:text)で生成したビューに最初からtailwindのスタイルが追加されます。

  <div class="my-5">
    <label for="post_regex">Regex</label>
    <input class="block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" type="text" name="post[regex]" id="post_regex" />
  </div>

  <div class="my-5">
    <label for="post_comment">Comment</label>
    <textarea rows="4" class="block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" name="post[comment]" id="post_comment">
</textarea>
  </div>

  <div class="inline">
    <input type="submit" name="commit" value="Create Post" class="rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" data-disable-with="Create Post" />
  </div>

参考 Rails 7とRuby 3.1.0-preview1+YJITを試すdocker-compose

YJITを有効にしたRuby 3.1.0-preview1上でRails 7.0.0のrails newを手軽に試すためのdocker-compose環境を以下のリポジトリに取り急ぎ作りました。

hachi8833/rails7_ruby310p1 - GitHub


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

関連記事

Railsセキュリティ修正6.0.4.4/6.1.4.4がリリースされました

Rails 7.0.0.rc1がリリースされました

The post Rails 7.0.0がリリースされました first appeared on TechRacho.


週刊Railsウォッチ: Rails 7リリース、5.2系と6.0.x系のサポート終了時期決定、localhost gemで自己署名証明書生成ほか(20211221前編)

$
0
0

こんにちは、hachi8833です。今年最後の週刊Railsウォッチ前編をお送りします。

週刊Railsウォッチについて

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

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

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

今回はRails 7に最近マージされたプルリクから見繕いました。

🔗 URL安全でないCSRFトークンを非推奨化

このプルリクでは、urlsafe_csrf_tokensをtrueにする設定も非推奨化されるので、Rails 7.1ではこの設定を完全に除去可能。
概要
#39076の続き。Rails 7.0でこの設定オプションを非推奨化する。次のマイナーリリースでこのオプションを削除できるようにする。
その他の情報
これはRails 7.0.0で出す必要はないが、7.1で設定を削除できるようマイナーリリースの間に非推奨にしておきたいので、7-0-stableで出すべき。mainブランチへのプルリクは7.1用になるはずなので、7-0-stableに対して行うことにする。
同PRより


つっつきボイス:「これまでデフォルトでfalseだったurlsafe_csrf_tokensを、デフォルトでtrueにしつつ、今後オフにできなくなるwarningを出すようになった」「こんな設定があるんですね」「URL-safeは、URLとして利用してよい文字だけで構成されているという意味か」「この設定はfalseにできなくなるようですね: あえてURL安全でないCSRFトークンを使うことはないと思うので大丈夫そう」

# actionpack/lib/action_controller/metal/request_forgery_protection.rb#L95
-     self.urlsafe_csrf_tokens = false
+     self.urlsafe_csrf_tokens = true
+
+     singleton_class.redefine_method(:urlsafe_csrf_tokens=) do |urlsafe_csrf_tokens|
+       if urlsafe_csrf_tokens
+         ActiveSupport::Deprecation.warn("URL-safe CSRF tokens are now the default. Use 6.1 defaults or above.")
+       else
+         ActiveSupport::Deprecation.warn("Non-URL-safe CSRF tokens are deprecated. Use 6.1 defaults or above.")
+       end
+       config.urlsafe_csrf_tokens = urlsafe_csrf_tokens
+     end

🔗 エンコーディングが無効なSQLクエリの扱いを改善

  • エンコーディングが無効なSQLクエリの扱いを改善
Post.create(name: "broken \xC8 UTF-8")

上は、書き込みクエリを検出するコードですべてのアダプタが制御不能な方法で失敗する。
このクエリがデータベースコネクションに適切に渡されるようになり、データベースコネクションで処理可能かどうかにかかわらず、より適切な方法で成功または失敗するようになる。
Jean Boussier
同PRより


つっつきボイス:「\xC8はいかにも無効なUTF-8コードですね」「そういう文字がクエリに混入したときに適切に扱われるようになったのね」

参考: UTF-8 - Wikipedia

「改修で正規表現に追加されたnオプションって何だろう?」「Rubyのドキュメントを見るとASCII-8bitエンコーディングにするオプションとありますね」「理屈はよくわからないけど、無効なUTF-8文字の検出用にここだけエンコーディングを変えてるのかな?」「通常のコードでは影響はなさそうですね」

# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L68
      def self.build_read_query_regexp(*parts) # :nodoc:
        parts += DEFAULT_READ_QUERY
        parts = parts.map { |part| /#{part}/i }
-       /\A(?:[(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/
+       /\A(?:[(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/n
      end

参考: 正規表現 (Ruby 3.0.0 リファレンスマニュアル)

🔗 Rails 7.0でbutton_toヘルパーの挙動変更を明記


つっつきボイス:「お、これは先週話題にしたbutton_to周りの変更の話ですね(ウォッチ20211214)」「リリースノートとアップグレードガイドで明記したそうです」「お〜、button_to("Do a POST", [:do_post_action, Workshop.find(1)])のようにbutton_toの第2引数にオブジェクトを配列で渡すとPATCHメソッドとして扱ってくれるようになっていたんですね(#43413)、これは賢い👍

# guides/source/7_0_release_notes.md#L91
+*  `button_to` infers HTTP verb [method] from an Active Record object if object is used to build URL
+
+   ```ruby
+   button_to("Do a POST", [:do_post_action, Workshop.find(1)])
+   # Before
+   #=>   <input type="hidden" name="_method" value="post" autocomplete="off" />
+   # After
+   #=>   <input type="hidden" name="_method" value="patch" autocomplete="off" />

なお、Railsアップグレードガイド↓には反映済みです。

参考: 2.1 ActionView::Helpers::UrlHelper#button_toの振る舞いが変更された
— Rails アップグレードガイド - Railsガイド

Rails 7.0以降のbutton_toは、ボタンURLをビルドするのに使われるActive Recordオブジェクトが永続化されている場合は、patch HTTP verbを用いるformタグをレンダリングします。現在の振る舞いを維持するには、以下のように明示的にmethod:オプションを渡します。

-button_to("Do a POST", [:my_custom_post_action_on_workshop, Workshop.find(1)])
+button_to("Do a POST", [:my_custom_post_action_on_workshop, Workshop.find(1)], method: :post)

または、以下のようにURLをビルドするヘルパーを使います。

-button_to("Do a POST", [:my_custom_post_action_on_workshop, Workshop.find(1)])
+button_to("Do a POST", my_custom_post_action_on_workshop_workshop_path(Workshop.find(1)))

Railsアップグレードガイドより

🔗 Railsメンテナンスポリシーの変更


つっつきボイス:「ガイドのメンテナンスポリシーも更新されました」「お、これは大事なヤツ: Slackにメモしておこう」「Rails 5.2系は来年2022年6月1日でサポート終了なのか〜」「公式なサポート終了時期を事前に宣言してくれるのはありがたい👍

# guides/source/maintenance_policy.md#L67
-**Currently included series:** `7.0.Z`, `6.1.Z`, `5.2.Z`.
+**Currently included series:** `7.0.Z`, `6.1.Z`.
...
+ NOTE: Rails 5.2.Z is included in the list of supported series until June 1st 2022.
+ NOTE: Rails 6.0.Z is included in the list of supported series until June 1st 2023.

🔗 ファビコンから”shortcut”リンク種別を削除


つっつきボイス:「実はこのプルリクは私が投げたものが7.0に滑り込みでマージされました😊」「お〜ついにRailsコントリビュータじゃないですか」「ファビコンに”shortcut”リンク種別をつける仕様がこんな昔になくなっていたとはね〜」「以下の翻訳記事↓を読み返していて、ふとRails 7ではどうなっているんだろうと思って調べたら、rails newで生成されるfavicon_link_tagで”shortcut”リンク種別が付けられていて、類似のプルリクもなかったので、ドキドキしながらプルリク投げました」

2021年のファビコンを極める: 本当に必要なファイルはほぼ6つ(翻訳)

icon より以前はリンク種別 shortcut がよく使用されていましたが、これは非準拠で無視されますのでウェブ作者は今後使用してはいけません。
リンク種別 - HTML: HyperText Markup Language | MDNより

「根拠がはっきりしているプルリクなのでマージしやすいですね👍」「Rails 7リリース直前の慌ただしいときのプルリクでしたが、押し流されなくてよかった😂

🔗Rails

🔗 Rails 7.0.0

Rails 7.0.0がリリースされました


つっつきボイス:「Rails 7出ましたね🎉」「予想より早く出た🎊」「記事のRails 7 Gemfileを見ると、propshaftが入るかと思ったらsprocketsがデフォルトなんですね」「propshaftがデフォルトになる説もありましたがsprocketsになりました」「rails new-a propshaftを指定すれば入れられるのね」

rails/propshaft - GitHub

「その代わりimportmap-railsがデフォルトになって、yarnはデフォルトでは入らなくなった」「これを待っていました: rails newも速くなりました」「つまりnodejsもnode_modulesディレクトリもデフォルトでは不要になった、なるほどこれは脱Webpackerせねば」「rails newするときのnodeのバージョンを気にしなくてよくなったのは嬉しい👍

rails/importmap-rails - GitHub

🔗 Rails 7リリース直前のセキュリティ修正

Railsセキュリティ修正6.0.4.4/6.1.4.4がリリースされました


つっつきボイス:「記事の注にも書きましたが↓、セキュリティ修正でdev環境のアクセスに問題が起きて修正のために再々リリースされたという流れでした」「最終的に修正されてよかった😊」「最初のセキュリティ修正と次の修正を手元で反映したら動かなかったので、issueプルリクをチェックして再々リリースを待ってから記事を出しました」「config.hosts << '.EXAMPLE.com'を環境変数で設定していたりすると見落としそう」

当初は6.0.4.2/6.1.4.2/7.0.0.rc2がリリースされましたが、localhost:3000でアクセスできない問題(#43865)があったので6.0.4.3/6.1.4.3/7.0.0.rc3がリリースされました。しかしlvh.me:3000のようにポート番号を指定できない問題(#43864)があったので、最終的に6.0.4.4/6.1.4.4がリリースされました。
同記事より

🔗 wicked: Railsでウィザードを作るgem(Ruby Weeklyより)

zombocom/wicked - GitHub

参考: Wizard-ify Your Rails Controllers with Wicked


つっつきボイス:「Railsでいわゆるウィザード的なインターフェイスを構築するのに使うgemだそうです」「ウィザード風のUIを構築するgemは他にもありますね」「欲しい人は多いでしょうね」「ウィザード風に作ろうとするとRails wayから外れてしまいがちなんですよ」

参考: ウィザード (ソフトウェア) - Wikipedia

「こうやってステップを書いてビューを切り替えていく感じなのはなかなかよさそう↓」

# 同リポジトリより
class AfterSignupController < ApplicationController
  include Wicked::Wizard

  steps :confirm_password, :confirm_profile, :find_friends

  def update
    @user = current_user
    case step
    when :confirm_password
      @user.update_attributes(user_params)
    end
    sign_in(@user, bypass: true) # needed for devise
    render_wizard @user
  end

  private
  def user_params
    params.require(:user)
          .permit(:email, :current_password) # ...
  end
end
app/
  views/
    controller_name/
      first.html.erb
      second.html.erb
      # ...

「途中で画像ファイルを添付したりしてもうまく扱えるかな?」「それ面倒そうですね」「ステップバイステップのウィザードの実装はそういう部分が大変なんですよ: テキストならhiddenフィールドに埋めたりできますけど、途中にファイル添付が入ると急に難易度が上がるし、途中でパスワードをhiddenフィールドに入れ直すのも避けたい」

「しかしwicked(邪悪な)とはすごい名前」「魔法使いを形容するのによく使われる言葉だからでしょうね」


オズの魔法使いに登場する「西の悪い魔女」がWicked Witch of the Westだったり、レイ・ブラッドベリのファンタジー小説「何かが道をやってくる」の原題が「Something Wicked This Way Comes」だったのを思い出します。

参考: オズの魔法使い - Wikipedia
参考: Something Wicked This Way Comes (novel) - Wikipedia

🔗 spyke: her gemの改良版(Ruby Weeklyより)

balvig/spyke - GitHub


つっつきボイス:「spykeは、ウォッチで何度か話題になったher gemのオルタナだそうです(ウォッチ20211108)」「たしかに、このコンフィグでアダプタやパーサーを指定しているあたりがherっぽい↓」

# 同リポジトリより
# config/initializers/spyke.rb

class JSONParser < Faraday::Response::Middleware
  def parse(body)
    json = MultiJson.load(body, symbolize_keys: true)
    {
      data: json[:result],
      metadata: json[:extra],
      errors: json[:errors]
    }
  end
end

Spyke::Base.connection = Faraday.new(url: 'http://api.com') do |c|
  c.request   :json
  c.use       JSONParser
  c.adapter   Faraday.default_adapter
end

remi/her - GitHub

「spykeのREADMEによると、herはいいgemだけど新しくないのとパフォーマンスに難があるのでspykeを作ったそうです」「あとherはプログラムの構成上なのか、サイズが割と大きいですね」「herの最初のリリースを見てみたら2013年頃からあるみたい」「その頃からherを使ってたな〜」

🔗 localhost: pumaですぐ使える自己署名証明書gem

socketry/localhost - GitHub


つっつきボイス:「今日のWebチーム内発表で話題になっていたgemです」「これは何ですか?」「このgemだけでローカル開発環境にSSL/TLSアクセスを簡単に組み込めます」「へ〜!」

参考: 自己署名証明書 - Wikipedia

「最近はローカル環境でもhttpsでリッスンしないと動かせないようなコードが増えてきていて、特にブラウザのAPIによってはGPSの現在地取得のようにhttpだとアクセスできないものもあったりするので、そういうのをローカルで検証するときに便利」「なるほど」

参考: Webブラウザで現在地情報を正しく取得できない場合の原因と対策:Tech TIPS - @IT

「localhost gemの実装はシンプルですが、pumaでも公式にサポートされているのがポイント高い↓」「手順までちゃんとREADMEに書かれてるんですね」

puma/puma - GitHub

「ローカルでhttps接続するときに大変なのは自己署名証明書を作る部分: 証明書に署名するために自分用の認証局を作らないといけないし、CSR(Certificate Signing Request)を埋めるためにcommon nameなども入力する必要があるとか、作業が多いんですよ」「あ〜たしかに」「こういう自動化ができるとありがたい」

「そういう作業をopensslコマンド手打ちでやるのは大変すぎるので、一般にはubuntuなどのパッケージにも入っているmkcertというツールを使ってやることが多いですね↓」「mkcert使ったことあったかも」「mkcertは証明書をデフォルトでユーザーのホームディレクトリの下に保存するといった細かいところがよくできている」「そういうの大事ですよね」

FiloSottile/mkcert - GitHub

「ちなみにlocalhost gemは鍵ファイルの作成が不要で、証明書を実行時に動的に作ってくれるのでさらに便利👍」「お〜」


前編は以上です。

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

週刊Railsウォッチ: RailsでGDPRに対応する、stateful_enum gem、rubyzip 3.0ほか(20211214前編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Rails 7リリース、5.2系と6.0.x系のサポート終了時期決定、localhost gemで自己署名証明書生成ほか(20211221前編) first appeared on TechRacho.

週刊Railsウォッチ: Ruby 3.1で多重代入の評価順が変更、Stimulusコンポーネントとベストプラクティスほか(20211222後編)

$
0
0

こんにちは、hachi8833です。今年最後の週刊Railsウォッチ後編をお送りします。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 Ruby 3.1で多重代入の評価順が変更


つっつきボイス:「Ruby 3.1のリリースノートをチェックしていて、この変更を見つけました」


#4443より

「お、obj, obj.foo = obj.foo, objみたいなコードはめったに書かないと思いますが、Rubyの仕様↓に書かれているとおりの挙動にするなら改修が必要ということになるんでしょうね」「左から右に評価される方がたしかに自然」

参考: JISX3017:2013 プログラム言語Ruby

「この#4443はよく見ると11年前に立てられたものでした」「そんなに昔だったとは」「それをJeremy Evansさんが拾って実装したという流れでした↓」

参考: Evaluate multiple assignment left hand side before right hand side by jeremyevans · Pull Request #4390 · ruby/ruby

「Ruby内部のスタックの動きも書かれている↓」「Jeremyさんスゴい」「修正によって多重代入のパフォーマンスが落ちるらしいけど、こういう多重代入はめったに行われないだろうから大丈夫そうかな」「多重代入の評価順で挙動が変わるようなコードはめったに書かないでしょうね」「issueを上げたmameさんなら書きそう」

self                                      # putself
abc                                       # send
abc, self                                 # putself
abc, foo                                  # send
abc, foo, 0                               # putobject 0
abc, foo, 0, [bar, baz]                   # evaluate RHS
abc, foo, 0, [bar, baz], baz, bar         # expandarray
abc, foo, 0, [bar, baz], baz, bar, abc    # topn 5
abc, foo, 0, [bar, baz], baz, abc, bar    # swap
abc, foo, 0, [bar, baz], baz, def=        # send
abc, foo, 0, [bar, baz], baz              # pop
abc, foo, 0, [bar, baz], baz, foo         # topn 3
abc, foo, 0, [bar, baz], baz, foo, 0      # topn 3
abc, foo, 0, [bar, baz], baz, foo, 0, baz # topn 2
abc, foo, 0, [bar, baz], baz, []=         # send
abc, foo, 0, [bar, baz], baz              # pop
abc, foo, 0, [bar, baz]                   # pop
[bar, baz], foo, 0, [bar, baz]            # setn 3
[bar, baz], foo, 0                        # pop
[bar, baz], foo                           # pop
[bar, baz]                                # pop

🔗 Qeweney: 機能豊富なRuby向けHTTPリクエスト/レスポンスAPI(Ruby Weeklyより)


つっつきボイス:「Rackを改善したqeweneyというgemを作ってみたという記事だそうです」

digital-fabric/qeweney - GitHub

「qeweneyの読み方がわからない😅」「この図に表わされているものを改良した感じかな↓: 図を見る限りでは、Webアプリが受けるリクエストとHTTP/1やHTTP/2のアダプタを切り離しているんでしょうね」「ふむふむ」「Rackの内部はよくわからないけど、Rackでは切り離せないんじゃないかな」


同記事より

「インターフェイスは、respondで書くあたりとかがRackと違ってますね↓」

# 同記事より
# Rack
app = ->(env) do
  [
    200,
    {'Content-Type' => 'text/plain'},
    ['Hello, world!']
  ]
}

# Qeweney
app = ->(req) do
  req.respond('Hello, world!', 'Content-Type' => 'text/plain')
end

「こちらを見るとリクエストをRackに渡すこともできるみたい↓」「Qeweneyはまだ開発途中のようですが、Rackよりもパフォーマンスを優先して作っているらしい」

# redirect to another URL
req.redirect(alternative_url)

# redirect to HTTPS
req.redirect_to_https

# serve a static file
req.serve_file(file_path)

# serve from an IO
req.serve_io(io)

# serve from a Rack app
req.serve_rack(app)

# Upgrade to arbitrary protocol
req.upgrade(protocol)

🔗 regexp_parser: 正規表現パーサー(Ruby Weeklyより)

ammar/regexp_parser - GitHub


つっつきボイス:「正規表現を解析するRuby製パーサーだそうです」「どんなふうに使うんだろう?」「こじらせて読むのがつらくなった正規表現を解読するのに使うとかかな」「あるある」

# 同リポジトリより
require 'regexp_parser'

Regexp::Scanner.scan /(ab?(cd)*[e-h]+)/  do |type, token, text, ts, te|
  puts "type: #{type}, token: #{token}, text: '#{text}' [#{ts}..#{te}]"
end

# output
# type: group, token: capture, text: '(' [0..1]
# type: literal, token: literal, text: 'ab' [1..3]
# type: quantifier, token: zero_or_one, text: '?' [3..4]
# type: group, token: capture, text: '(' [4..5]
# type: literal, token: literal, text: 'cd' [5..7]
# type: group, token: close, text: ')' [7..8]
# type: quantifier, token: zero_or_more, text: '*' [8..9]
# type: set, token: open, text: '[' [9..10]
# type: set, token: range, text: 'e-h' [10..13]
# type: set, token: close, text: ']' [13..14]
# type: quantifier, token: one_or_more, text: '+' [14..15]
# type: group, token: close, text: ')' [15..16]

「そういえば正規表現を解析してくれるregex101というWebサービスがありますね↓」「これめちゃ愛用しています」「こんなのがあるんですね」

参考: regex101: build, test, and debug regex

「regex101に正規表現を入れるといい感じに解析して、リファレンスも表示してくれる」「正規表現をステップ実行できるのもありがたいです🙏」「regex101はRubyの正規表現(Onigmo)には対応していないのか…」「RubularならRubyの正規表現をチェックできますよ↓」

参考: Rubular: a Ruby regular expression editor

k-takata/Onigmo - GitHub

はじめての正規表現とベストプラクティス1: 基本となる8つの正規表現

その他Ruby

つっつきボイス:「_ko1さんのこのツイートがとても面白かった」

「JSONで表現可能なハッシュをRubyでシリアライズしてからデシリアライズしてハッシュとして取り込むのがどれが一番速かったかをテストしてみたら、最速はmsgpackでしたが、JSONモジュールが意外に速かったという話↓」「へ〜、JSONやるな〜」「自分はMarshalが最速だと思っていたけどそうでもないんだなと気付かされました」

# ruby 3.1.0dev (2021-12-13T05:04:19Z fix_local_tp_memor.. 7ad3e2a982) [x86_64-linux]

                 user     system      total        real
JSON         4.942976   0.059088   5.002064 (  5.002097)
msgpack      4.146010   0.039891   4.185901 (  4.185935)
eval        13.473455   0.080030  13.553485 ( 13.553678)
Marshal      7.512279   0.110032   7.622311 (  7.622328)

msgpack/msgpack-ruby - GitHub

参考: module JSON (Ruby 3.0.0 リファレンスマニュアル)
参考: module Marshal (Ruby 3.0.0 リファレンスマニュアル)

「追加されたコメントを見ると、ファイルサイズはMarshalのが一番小さいのね↓」

$ ls h* -hl
-rw-rw-r-- 1 ko1 ko1 207K Dec 15 16:00 h.iseq
-rw-rw-r-- 1 ko1 ko1 249K Dec 15 16:00 h.json
-rw-rw-r-- 1 ko1 ko1 202K Dec 15 16:00 h.marshal
-rw-rw-r-- 1 ko1 ko1 204K Dec 15 16:00 h.msgpack
-rw-rw-r-- 1 ko1 ko1 254K Dec 15 16:00 h.rson

「お、I/OをなくすとMarshalがJSONに近づいてる↓」「iseq.dumpは遅いけどiseq.evalは速いのも面白い」

                 user     system      total        real
JSON         4.603379   0.008869   4.612248 (  4.612306)
msgpack      4.016187   0.009970   4.026157 (  4.026212)
eval        13.732086   0.050009  13.782095 ( 13.782156)
iseq.eval    2.844302   0.010015   2.854317 (  2.854358)
Marshal      5.147343   0.009985   5.157328 (  5.157333)

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Google Cloud SpannerがRailsのActive Recordに対応(Publickeyより)


つっつきボイス:「お〜、このアダプターはGoogle公式なのがいいですね👍」「ほんとだ」「公式だとそれだけで導入しやすいんですよ」

googleapis/ruby-spanner-activerecord - GitHub

# 同リポジトリより
development:
  adapter: "spanner"
  project: "<google project name>"
  instance: "<google instance name>"
  credentials: "<google credentails file path>"
  database: "app-dev"

参考: Cloud Spanner  |  Google Cloud

「Active RecordからSpannerにアクセスできるということなんですね」「制約を見るとカラムでDEFAULTが使えないとあるけど、SpannerにそもそもDEFAULTがないのね」「テーブルは主キーが必須か」「Railsのadd_indexでフィールド長を指定できないらしい」「この辺はCloud Spannerからくる制約なんでしょうね」

参考: Cloud Spannerの主キーの設計. Spannerがパフォーマンスを発揮するには主キーの設計が非常に重要です。そこで… | by Toyohito Murooka | google-cloud-jp | Medium
参考: add_indexActiveRecord::ConnectionAdapters::SchemaStatements

「公式なら使ってみようかなという気持ちになりますね: ちなみにoracle-enhancedアダプタ↓はよく使われていて実績もあるけど公式ではないんですよ」

rsim/oracle-enhanced - GitHub

🔗JavaScript

🔗 Stiumulus関連のサイト2つ

stimulus-components/stimulus-components - GitHub


つっつきボイス:「Rails 7でStimulusが正式に導入されたので、Stimulusの情報を掲載しているサイトをいくつか見つけました」「Stimulusのコンポーネントリストと、よいStimulusの書き方ですね」「コンポーネントによってはデモサイトもあって↓、Bootstrapのコンポーネントリスト的な感じなのがなかなかよさそう👍

参考: Stimulus Animated Number
参考: Stimulus Chartjs

参考: Components · Bootstrap

StimulusのChartjsは普通に本家Chartjsを取り入れているけどバージョンは2.9.32か: ちょうどさっき調べていたんですが、Chartjsの最新は3.6.2で、2系で書かれたチャートがAPIレベルで互換性がなくて動かなかったんですよ」「なるほど、3.xは別物な感じですね」「チャートを作り込んでからチャートライブラリで非互換が発生するとつらい😢

参考: Chart.js | Open source HTML5 Charts for your website


「もうひとつのBetter StimulusJSは↓、Stimulusのベストプラクティスを紹介しています」「こちらは個人っぽいですね」「上のStimulusコンポーネントサイトも公式っぽく見えたけどどうやら個人っぽいかも」

julianrubisch/better-stimulus - GitHub


ちなみに本家Stimulusのリポジトリは以下です↓。

hotwired/stimulus - GitHub


後編は以上です。来年もTechRachoと週刊Railsウォッチをどうぞよろしくお願いします。

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

週刊Railsウォッチ: Rails 7リリース、5.2系と6.0.x系のサポート終了時期決定、localhost gemで自己署名証明書生成ほか(20211221前編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: Ruby 3.1で多重代入の評価順が変更、Stimulusコンポーネントとベストプラクティスほか(20211222後編) first appeared on TechRacho.

Rails 7 API: ActiveRecord::FixtureSet(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。


  • 2019/07/11: 初版公開
  • 2021/12/17: 5.2.3->7.0.0に更新

以下も参考にどうぞ。

週刊Railsウォッチ(20190708-1/2前編)ActiveRecord::FixtureSetがめちゃ強くなってた、MacだとRubyが遅い理由、Puma 4登場ほか

Rails API: ActiveRecord::FixtureSet(翻訳)

fixtureとは、テストの対象としたいデータ(つまりサンプルデータ)を組み立てる手法のひとつです。

fixtureはYAMLファイルに保存されます。1つのYAMLファイルが1つのモデルに対応し、ActiveSupport::TestCase.fixture_path=(パス)で指定されたディレクトリに保存されます(このディレクトリはRailsで自動設定されますので、自分のRailsアプリ/test/fixtures/に保存できます)。fixtureファイル名の末尾には.yml拡張子が付きます(例: 自分のRailsアプリ/test/fixtures/web_sites.yml)。

fixtureファイルのフォーマットは次のような感じになります。

rubyonrails:
  id: 1
  name: Ruby on Rails
  url: http://www.rubyonrails.org

google:
  id: 2
  name: Google
  url: http://www.google.com

上のfixtureファイルにはfixtureが2件含まれています。各YAML fixture(レコードなどを表す)に名前が与えられ、続く行をインデントして、キーバリューペアを「キー: 値」の形式で記述します。レコードとレコードの間には見やすくするための空行が入ります。

注意: fixture同士の間には順序関係がありません。fixtureを順序付けしたい場合は、omapというYAMLをお使いください。omapの仕様についてはyaml.org/type/omap.htmlをどうぞ。同じテーブルのキーに外部キー制約を付ける場合は、順序ありのfixtureが必要になります。これは主に次のようなツリー構造で必要となります。

--- !omap
- parent:
    id:         1
    parent_id:  NULL
    title:      Parent
- child:
    id:         2
    parent_id:  1
    title:      Child

テストケースでfixtureを使う

fixtureはテストの構成要素なので、fixtureは単体テストや機能テストで用いられます。fixtureの利用法は2とおりありますが、まずは単体テストのサンプルを見てみましょう。

require "test_helper"

class WebSiteTest < ActiveSupport::TestCase
  test "web_site_count" do
    assert_equal 2, WebSite.count
  end
end

test_helper.rbはデフォルトですべてのfixtureをテストデータベースに読み込みます。したがって、このテストは成功します。

test環境では、各テストの実行前にすべてのfixtureが自動的にデータベースに読み込まれます。データの一貫性を保つために、fixtureは読み込みが実行される前に環境によって削除されます。

fixtureはデータベースでも利用できますが、その他にモデルと同じ名前の特殊な動的メソッドを介してアクセスすることもできます。

この動的メソッドにfixture名を渡すと、その名前と一致するfixtureを返します。

test "find one" do
  assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
end

fixture名を複数渡すと、それらの名前に一致するfixtureをすべて返します。

test "find all by name" do
  assert_equal 2, web_sites(:rubyonrails, :google).length
end

引数を渡さない場合、すべてのfixtureを返します。

test "find all" do
  assert_equal 2, web_sites.length
end

存在しないfixture名を渡すとStandardErrorが発生します。

test "find by name that does not exist" do
  assert_raise(StandardError) { web_sites(:reddit) }
end

別の方法として、fixtureデータの自動インスタンス化を有効にすることもできます。たとえば次のテストがあるとします。

test "find_alt_method_1" do
  assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
end

test "find_alt_method_2" do
  assert_equal "Ruby on Rails", @rubyonrails.name
end

テストケース内でこれらの手法を用いてfixtureデータにアクセスするには、ActiveSupport::TestCaseの派生クラスで以下のいずれか1つを指定しなければなりません。

  • インスタンス化されたfixtureをすべて有効にする(上のalt_method_1とalt_method_2を有効にする)には以下のようにします。
self.use_instantiated_fixtures = true
  • fixtureのハッシュだけを作成し、各インスタンスをfindしない(alt_method_1のみを有効にする)ようにするには以下のようにします。
self.use_instantiated_fixtures = :no_instances

これらの別の方法のいずれかを使うと、パフォーマンスに悪影響が生じます。理由は、fixtureのハッシュやインスタンス変数を作成するために、データベース内のfixture化されたデータをフルスキャンしなければならないためです。そのため、fixture化されたデータセットが大規模になるとコストがかさみます。

ERBで動的fixtureを使う

fixtureの量が重要だが、fixtureの中身はさほど重要ではない場合があります。そのような場合はYAMLのfixtureでERBを併用することで、以下のようにテストの読み込みで大量のfixtureを作成できます。

<% 1.upto(1000) do |i| %>
fix_<%= i %>:
  id: <%= i %>
  name: guy_<%= i %>
<% end %>

上はきわめてシンプルなfixtureを1000件作成します。

ERBを用いることで、<%= Date.today.strftime("%Y-%m-%d") %>などをのような動的な値をfixtureに注入できます。ただしこの機能には若干の注意点があります。fixtureは、それがfixtureの形であれば予測可能なサンプルデータの安定的な単位となりますが、そこに動的な値を注入する必要があると思えてきた場合は、おそらくアプリケーションが正しくテスト可能な状態であるかどうかを再点検する必要があるでしょう。つまり、fixtureの中で動的な値を使うことはすなわち「コードの匂い」と考えるべきです。

あるfixture内で定義されたヘルパーメソッドは、他のfixtureでは利用できません。これは、テスト同士をまたがる依存関係を意図に反して作り出さないためです。複数のfixtureで用いるメソッドは、ActiveRecord::FixtureSet.context_classincludeされる何らかのモジュール内で定義すべきです。

  • test_helper.rbでヘルパーメソッドを定義する
module FixtureFileHelpers
  def file_sha(path)
    OpenSSL::Digest::SHA256.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
  end
end
ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
  • fixtureの中でヘルパーメソッドを定義する
photo:
  name: kitten.png
  sha: <%= file_sha 'files/kitten.png' %>

トランザクショナルテスト

テストケースでは、テストケースごとにdeleteとinsertを使う代わりに、beginとrollbackを用いてデータベース変更を分離できます。

class FooTest < ActiveSupport::TestCase
  self.use_transactional_tests = true

  test "godzilla" do
    assert_not_empty Foo.all
    Foo.destroy_all
    assert_empty Foo.all
  end

  test "godzilla aftermath" do
    assert_not_empty Foo.all
  end
end

(おそらくbin/rails db:fixtures:loadを実行して)すべてのfixtureデータをテストデータベースにプリロードしてトランザクショナルテスト(transactional test)を行うと、テストケース内のすべてのfixture宣言を省略できることがあります。理由は、全データが既に存在し、すべてのケースの変更がロールバックされるからです。

インスタンス化されたfixtureでプリロード済みデータを用いるには、self.pre_loaded_fixturesをtrueに設定してください。これにより、fixture経由で読み込まれたすべてのテーブルのfixtureデータにアクセスできるようになります(この挙動はuse_instantiated_fixturesの値によって変わります)。

以下の場合はトランザクショナルテストを使いません

  1. トランザクションが正しく動いているかどうかをテストする場合。ネストしたトランザクションは、すべての親トランザクションがコミットされるまでコミットされません(特にfixtureのトランザクションがセットアップで開始され、ティアダウン(teardown)でロールバックする場合)。つまり、Active Recordがネステッドトランザクションまたはsavepointsをサポートするまではトランザクションの結果を検証できません(これについては作業中)。
  2. データベースがトランザクションをサポートしていない場合。MySQLのMyISAMをのぞくあらゆるActive Recordデータベースはトランザクションをサポートしています。その場合はInnoDBかMaxDBかNDBをお使いください。

アドバンストfixture

IDを指定しないfixtureにはいくつかの機能を利用できます。

  • 安定な自動生成ID
  • 関連付けをラベルで参照する(belongs_to、has_one、has_many)
  • HABTM関連付けをインラインリストとして用いる

訳注: 現在のRailsでHABTM(has_and_belongs_to_many)を使ったリレーションは一般に悪手とされています。現在のRailsでは代わりにhas_many :through関連付けを使うのが一般的です。
* 参考: HABTMリレーションシップは悪であるという論争 | A-Listers
* 参考: has_and_belongs_to_many関連付け — Active Record の関連付け - Rails ガイド
* 参考: has_many :through関連付け — Active Record の関連付け - Rails ガイド

IDを指定した場合でも利用できる機能がいくつかあります。

  • Timestampカラム値の自動入力
  • fixtureラベルの式展開(interpolation)
  • YAML defaultsのサポート

安定な自動生成ID

以下のおさるfixtureがあるとします。

george:
  id: 1
  name: George the Monkey

reginald:
  id: 2
  name: Reginald the Pirate

それぞれのfixtureには一意のIDが2種類あります。2つのIDのうち、1つはデータベース用で、もう1つは人間用です。そうする代わりに、主キーを生成できるとよいでしょう。それぞれのfixtureのラベルがハッシュ化されて一貫したIDが生成されます。

george: # 生成されたID: 503576764
  name: George the Monkey

reginald: # 生成されたID: 324201669
  name: Reginald the Pirate

Active Recordはこのfixtureのモデルクラスを参照して正しい主キーを見つけ、fixtureがデータベースに挿入される直前に主キーを生成します。

指定したラベルで生成されるIDは定数なので、ラベルがわかっていれば、読み込みなしに任意のfixture IDを見つけられます。

関連付けをラベルで参照する(belongs_tohas_onehas_many

fixtureで外部キーを指定すると非常にもろくなる可能性があり、しかも読みにくくなります(言うまでもありませんが)。Active Recordは任意のfixtureのIDをラベルで特定できるので、外部キーを(IDではなく)ラベルで指定できます。

belongs_to

おさると海賊でもう少しやってみましょう。

### pirates.yml

reginald:
  id: 1
  name: Reginald the Pirate
  monkey_id: 1

### monkeys.yml

george:
  id: 1
  name: George the Monkey
  pirate_id: 1

この調子でおさると海賊を増やして複数のファイルに分割すると、追いかけるのがつらくなってきます。IDをやめてラベルを使ってみましょう。

### pirates.yml

reginald:
  name: Reginald the Pirate
  monkey: george

### monkeys.yml

george:
  name: George the Monkey
  pirate: reginald

見事にすっきりしました。このfixtureのモデルクラスはActive Recordに反映されてすべてのbelongs_to関連付けが検索されるようになり、(外部キーのターゲットIDではなく)関連付けmonkey: george)のターゲットラベルを指定できるようになります。

ポリモーフィックbelongs_to

ポリモーフィックなリレーションシップのサポートは少し複雑です。理由は、関連付けが指す先の型をActive Recordが認識する必要があるためです。次のような例ならわかりやすいでしょう。

### fruit.rb

belongs_to :eater, polymorphic: true
### fruits.yml

apple:
  id: 1
  name: apple
  eater_id: 1
  eater_type: Monkey

もっとよくできそうですね。

apple:
  eater: george (Monkey)

ポリモーフィック関連付けのターゲットの型を指定するだけで、後はActive Recordが面倒を見てくれます。

has_and_belongs_to_manyまたはhas_many :through

おさるにフルーツをあげる時間になりました。

### monkeys.yml

george:
  id: 1
  name: George the Monkey

### fruits.yml

apple:
  id: 1
  name: apple

orange:
  id: 2
  name: orange

grape:
  id: 3
  name: grape

### fruits_monkeys.yml

apple_george:
  fruit_id: 1
  monkey_id: 1

orange_george:
  fruit_id: 2
  monkey_id: 1

grape_george:
  fruit_id: 3
  monkey_id: 1

このHABTM fixtureを消し去りましょう。

### monkeys.yml

george:
  id: 1
  name: George the Monkey
  fruits: apple, orange, grape

### fruits.yml

apple:
  name: apple

orange:
  name: orange

grape:
  name: grape

fruits_monkeys.ymlファイルが不要になりました!ここではジョージのfixtureでフルーツのリストを指定しましたが、フルーツごとにおさるのリストを指定することも簡単です。fixtureのモデルクラスがbelongs_toによってActive Recordに反映され、has_and_belongs_to_many関連付けが見つかるようになります。

Timestampカラム値の自動入力

Active Recordの標準的なタイムスタンプカラム(created_atcreated_onupdated_atupdated_on)のいずれかがテーブルやモデルで指定される場合は、自動的にTime.nowで設定されます。

特定の値が設定済みの場合は何もしません。

fixtureラベルの式展開

現在のfixtureのラベルは、カラムの値としていつでも参照できます。

geeksomnia:
  name: Geeksomnia's Account
  subdomain: $LABEL
  email: $LABEL@email.com

また、指定のラベルでIDを保持できるようにする必要が生じることがあります(古い結合テーブルfixtureを移植する場合など)。そんなときはERBが役に立ちます。

george_reginald:
  monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
  pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>

YAML defaultsのサポート

fixtureのYAMLファイルでデフォルト値を設定して再利用できます。これはdatabase.ymlでデフォルトを指定するのに使われる手法と同じです。

訳注: この場合DEFAULTSはすべて大文字にする必要があります。

DEFAULTS: &DEFAULTS
  created_on: <%= 3.weeks.ago.to_formatted_s(:db) %>

first:
  name: Smurf
  <<: *DEFAULTS

second:
  name: Fraggle
  <<: *DEFAULTS

ラベルが「DEFAULTS」のfixtureはすべて安全に無視されます。

DEFAULTS」を使う他に、_fixtureセクションにignoreを設定して、どのフィクスチャを無視するかを指定することも可能です。

# users.yml
_fixture:
  ignore:
    - base
  # or use "ignore: base" when there is only one fixture that needs to be ignored.

base: &base
  admin: false
  introduction: "This is a default description"

admin:
  <<: *base
  admin: true

visitor:
  <<: *base

In the above example, ‘base’ will be ignored when creating fixtures. This can be used for common attributes inheriting.

fixtureのモデルクラスを設定する

fixtureのモデルクラスをYAMLファイルで直接指定できます。これは、fixtureがテストの外部で読み込まれ、かつset_fixture_classが利用できない場合(bin/rails db:fixtures:loadの実行時など)に有用です。

_fixture:
  model_class: User
david:
  name: David

ラベルが「_fixture」のfixtureはすべて安全に無視されます。

関連記事

Rails 5.1〜: ‘form_with’ APIドキュメント完全翻訳

The post Rails 7 API: ActiveRecord::FixtureSet(翻訳) first appeared on TechRacho.

ウイングドア社の社内制度のひとつをご紹介

$
0
0

ウイングドア社の社内制度のひとつをご紹介

TechRachoをご覧の皆様、こんにちは。ウイングドアの福原です。Advent Calendarも終盤ですね。私自身は純粋なBPSメンバーではないのですが、BPSの仲間?という事で枠を拝借しました。TechRacho運営の皆様、渡辺社長、貴重な場をありがとうございます!

ウイングドアは、福岡でwebアプリ・スマホアプリの受託開発を行ってます。BPSさんがRailsに特化されている中、私達はPHPでの開発案件を受け持つことが多いです。最近はLaravel、Vue.js、Nuxt.jsなんかのワードが良く聞こえてきますよ。

こんな私達、以前はTechRachoに定期的に投稿させて頂いてました。今年に入り、私達自身のコーポレートサイトをリニューアルしたことをきっかけに、独り立ちをして自身のサイト上で技術ブログの展開を始めました。まだまだTechRachoクオリティには追い付かないのですが、こんな事で躓いたよ、些細な事だけど便利だよ、というエンジニアが現場で実感できる、軽めのネタを提供しています。月に2本ほど記事を投稿してますので、TechRachoと共にウイングドアの技術ブログもご愛顧頂けると嬉しいです。

縁の下の力持ちに日の目を!

さて本題。ウイングドアではスタッフ同士が感謝しあう事で、それがちょっとしたインセンティブとして還元される仕組みがあります。ウインターシーズンにホッとするネタにでもなればと、この仕組みについてご紹介してみます。

きっかけは、別案件のスタッフ同士で助け合っているシーンを何気なく目にしたことから始まります。案件数・スタッフ数が増えてきて、私自身がスタッフ一人一人の小さな行動まで把握ができなくなっていました。そんな中、私(上司)が直接目にする成果やスキルや振る舞いに対して評価するだけでなく、スタッフ同士がそれぞれ感謝して評価し合う仕組みがあると良いなと思ったんです。福利厚生の制度として、”サンクスカード”なるものがありますが、それをもうちょっとお気軽にして、かつ、何かしらインセンティブとして還元できたら、というイメージです。

サンクスカード含め、世の中にはこの手のサービスは既にいくつもあります。(ピアボーナスとか)それらを参考にさせて頂きつつ、ウイングドアなりの仕組みを実現してみました。制度に名称はないのですが、Slack上で「#ありがとう」というチャネルを作り、そこで1年ほど運用してますので、ここでは”ありがとうチャネル”という制度名でご紹介します。

“ありがとうチャネル”って?

オフィスワークでもリモートワークでも全社員がリアルタイムで確認し、コメント・スタンプも気軽にできる弊社のコミュニケーションの基盤、”Slack”での実現を目指しました。Slackに「#ありがとう」というチャネルを作り、そこで感謝したい事や褒めてあげたい事等を本人宛にメンションつけてメッセージしてもらうようにしました。また”ありがとうチャネル”に限らず、素敵な振る舞いやありがたい行動に対して、お礼や感謝を意味する特殊スタンプをつけてもらいました。こんなかんじ。賞賛の嵐です!

感謝された方・褒められた方はどうなるの?

“ありがとうチャネル”でのメッセージ数や特殊スタンプの数を毎月集計し、沢山感謝された方を発表・表彰する事にしました。そして表彰されたスタッフにはちょっとしたインセンティブが付与されます。毎月こんなメッセージと共に朝礼で表彰してお祝いしてます!

褒められて終わるよりも、やって良かった・もっと頑張ろう、というモチベーションになればとインセンティブは提供したいと考え、表彰者にはAmazonやiTunes等のギフト券を贈呈しています。
ちなみに、感謝のメッセージ数やスタンプ数を月次集計するSlackアプリは、弊社エンジニア陣が自前で組んでくれました!SlackのAPIを叩いてスタンプ数等を取得し、スプレッドシートに集計結果を吐き出すスクリプトをGASで実現したようです。

“ありがとうチャネル”を運用してみて

今まであまり日の目をみなかった縁の下の力持ち的な振る舞いを可視化する事ができました。また今まで以上に、部署や案件に関係なく、助け合おう・協力し合おう、という意識が芽生えたのではと感じてます。一方で、そんな行動をちゃんと見てるよ、と表現してあげられることは経営者目線では大きいです。最初は照れ臭さもありましたが、今では随分慣れて気軽に「ありがとう」と言えるようになったようです。些細な事でも感謝しあう・褒め合う文化は、わたしが客観的に見ても嬉しく感じてます。

“ありがとうチャネル”の今後

もちろん人の価値観は様々で、逆に、褒め合う事を不快に感じる人もいるかもしれません。「人は厳しく育てるべきだ、怒ってなんぼ」という考えの方もいらっしゃるかもしれませんし。(個人的には好きではありませんが)ですので、”ありがとうチャネル”を強制せずに、自己の判断で活用してもらってます。またインセンティブが特定のスタッフに偏る可能性もあり得ます。全員が満遍なく褒めてもらえる・感謝してもらえると、会社としては円満になるので、制度のチューニングは模索している所です。後はマンネリ化しないようにも考えたいですね。
とにかく、楽しく仕事ができる環境のひとつになればと期待をしてます。

いかがだったでしょうか?ウイングドアではこのように感謝し合って・褒め合って、協力しながらお仕事してます。仲間に加わってみたい方、是非、お問い合わせ下さい。



株式会社ウイングドアでは、Ruby on RailsやPHPを活用したwebサービス、webサイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。

The post ウイングドア社の社内制度のひとつをご紹介 first appeared on TechRacho.

Ruby 3.1.0がリリースされました

$
0
0

Ruby 3.1.0がリリースされました。

リリースは日本時間の2021年12月25日21:23でした。

詳しくはリリース情報をご覧ください。

以下の関連記事もどうぞ。

YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳)

Ruby 3.1: ES6風のハッシュリテラル省略記法が追加(翻訳)

以下はハッシュ値の省略記法についてのブログ記事です。

追加情報

ruby-buildもリリース直後に更新されたので、rbenvでRuby 3.1.0をインストールできます。

rbenv/ruby-build - GitHub

起動時に--yjitを指定するか、環境変数でRUBY_YJIT_ENABLE=1を設定しておくとYJITが有効になります。

なお、DockerhubのRuby公式コンテナは執筆時点では3.1.0がマージ待ちです。

🔗 Ruby 3.1 release event

本日夜20:00よりRuby 3.1 release eventがオンライン開催されます。

関連記事

Ruby 3.0.3/2.7.5/2.6.9セキュリティ修正がリリースされました

The post Ruby 3.1.0がリリースされました first appeared on TechRacho.

Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた

$
0
0

参考: Rails 7.0でアセットパイプラインはどう変わるか | Wantedly Engineer Blog

上の記事にインスパイアされて、Rails 7 でrails newするときのフロントエンド関連オプションの組み合わせを確かめてみました。結果は今後変わる可能性がありますのでご了承ください。

rails newするときの環境は以下のテンプレートを元にしました。

hachi8833/rails7_ruby310p1 - GitHub

  • Rails: 7.0.0
  • Ruby: 3.1.0preview1
    • YJITは有効

結果

長過ぎるので以下のgistに置きました。当初はRails 7 RC1で試し、Rails 7リリース後に再度試しました。

まとめ

以下はすべてRails 7.0.0の時点のものです。まずは原則的なものから。

  • デフォルトではsprockets-railsがインストールされる
    • propshaftは、-a propshaftを指定しない限りインストールされない
    • propshaftとsprocketsはいずれか一方しかインストールできない
    • sprocketsが必要な他のオプションを指定すると、-a propshaftは指定しても無効になる

rails/propshaft - GitHub

  • importmap-railsは指定なしでも「基本的に」インストールされる(後述)
    • es-module-shmisもインストールされるので、FirefoxやSafariでもimportmap-railsの恩恵を受けられるはず

guybedford/es-module-shims - GitHub

  • turbo-railsとstimulus-railsは常にインストールされる
    • スキップするオプションは見当たらない

オプションの組み合わせ

  • importmapは、完全にデフォルトというわけではない
    • -cオプションにtailwind以外のCSSフレームワークを指定すると、たとえ-j importmapを指定してもデフォルトでimportmapではなくesbuildがjsbundling-rails経由で使われる
    • つまり-cでCSSフレームワークを指定するときはesbuildを使うか、-jでrollupかwebpackのいずれかを指定することになる
  • tailwindは例外的にgemで提供される
    • DHHとしては、nodeを使わずにやれる見本としてtailwindcss-railsを考えているらしい(#43531のDHHコメント
    • 他のCSSフレームワークはcssbundling-rails経由でインストールされる
      • -c tailwindを指定する場合はimportmap-railsが使われる(jsbundling-railsもcssbundling-railsもインストールされない)
      • ただし-c tailwindに加えて、-jでesbuild/rollup/webpackを指定する場合は、tailwindはgemではなくcssbundling-rails経由でインストールされる

importmapを使う場合は、node_modulesディレクトリは生成されません。それ以外のesbuild/rollup/webpackを使う場合はnode_modulesディレクトリが生成されます。

  • -a propshaft-c tailwindは両方指定可能

Rails 7 RC1の時点では、-a propshaft-c tailwindを両方指定するとインストール中にエラーになりましたが、Rails 7リリース後には正常になりました。

これはtailwindcss-rails側で認識されていたようです。rails/tailwindcss-railsの以前のREADME↓では、いずれtailwindcss-railsもpropshaftから使えるようにするとありましたが、その後正常に動くようになったためか、現在のREADMEではこの文が変更され、SprocketsとPropshaftの記述はなくなっています。

This gem gives access to the standard Tailwind CSS framework configured for dark mode, forms, aspect-ratio, typography, and the Inter font via the asset pipeline using Sprockets (and soon Propshaft).
同READMEより

自分がRails 7でアプリを作るときは、DHHが使って欲しそうな組み合わせでrails newしようと考え中です。

tailwindcss-railsではビューテンプレートにスタイルが付く

tailwindcss-railsにはジェネレータも含まれているので、以下のようにscaffoldで生成されるビューのテンプレートにtailwindのスタイルが追加されます。

  <div class="my-5">
    <%= form.label :comment %>
    <%= form.text_area :comment, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

また、tailwindはimportmapかnode経由でもインストール可能です(この場合tailwindのscaffoldジェネレータは入らないと思いますが)。

rails-ujsは追加インストール可能

Rails 7ではrails-ujsが標準では入らなくなっていますが、npmパッケージやCDNにはあるので、./bin/importmap pin rails-ujsを実行すると以下のようにrails-ujsをインストールできることまでは確認しました。

# config/importmap.rb
pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "rails-ujs", to: "https://ga.jspm.io/npm:rails-ujs@5.2.6/lib/assets/compiled/rails-ujs.js"

なお、npmでインストールする場合は以下のように@railsを追加する必要があるそうです(Rails 7アップグレードガイド

actioncable   → @rails/actioncable
activestorage → @rails/activestorage
rails-ujs     → @rails/ujs

Rails 7でActive Jobはスキップできないらしい

Turbo Streamは実はActive Jobに依存している部分があるようです。

--skip-active-jobでActive Jobをスキップすると、developmentモードだとエラーになりませんでしたがproductionでエラーになりました。

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
# require "active_job/railtie"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
# require "action_mailer/railtie"
# require "action_mailbox/engine"
# require "action_text/engine"
require "action_view/railtie"
# require "action_cable/engine"
require "rails/test_unit/railtie"
RAILS_ENV=production dip rails s
Creating rails7_ruby310p1_rails_run ... done
=> Booting Puma
=> Rails 7.0.0 application starting in production
=> Run `bin/rails server --help` for more startup options
Exiting
/bundle/ruby/3.1.0/gems/turbo-rails-1.0.0/app/jobs/turbo/streams/broadcast_job.rb:3:in `<main>': uninitialized constant ActiveJob (NameError)

class Turbo::Streams::BroadcastJob < ActiveJob::Base
                                     ^^^^^^^^^
Did you mean?  ActiveModel
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/kernel.rb:27:in `require'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader/helpers.rb:95:in `const_get'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader/helpers.rb:95:in `cget'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader.rb:232:in `block (2 levels) in eager_load'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader/helpers.rb:26:in `block in ls'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader/helpers.rb:18:in `each_child'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader/helpers.rb:18:in `ls'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader.rb:227:in `block in eager_load'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader.rb:212:in `synchronize'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader.rb:212:in `eager_load'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader.rb:312:in `each'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/loader.rb:312:in `eager_load_all'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/application/finisher.rb:79:in `block in <module:Finisher>'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/initializable.rb:32:in `instance_exec'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/initializable.rb:32:in `run'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/initializable.rb:61:in `block in run_initializers'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:228:in `block in tsort_each'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:431:in `each_strongly_connected_component_from'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:349:in `block in each_strongly_connected_component'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:347:in `each'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:347:in `call'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:347:in `each_strongly_connected_component'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:226:in `tsort_each'
    from /usr/local/lib/ruby/3.1.0/tsort.rb:205:in `tsort_each'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/initializable.rb:60:in `run_initializers'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/application.rb:369:in `initialize!'
    from /app/config/environment.rb:5:in `<main>'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
    from /bundle/ruby/3.1.0/gems/zeitwerk-2.5.1/lib/zeitwerk/kernel.rb:35:in `require'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:54:in `require_relative'
    from config.ru:3:in `block in <main>'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/builder.rb:116:in `eval'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/builder.rb:116:in `new_from_string'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/builder.rb:105:in `load_file'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/builder.rb:66:in `parse_file'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/server.rb:349:in `build_app_and_options_from_config'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/server.rb:249:in `app'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/server.rb:422:in `wrapped_app'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/server.rb:312:in `block in start'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/server.rb:379:in `handle_profiling'
    from /bundle/ruby/3.1.0/gems/rack-2.2.3/lib/rack/server.rb:311:in `start'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/server/server_command.rb:38:in `start'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/server/server_command.rb:143:in `block in perform'
    from <internal:kernel>:90:in `tap'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/server/server_command.rb:134:in `perform'
    from /bundle/ruby/3.1.0/gems/thor-1.1.0/lib/thor/command.rb:27:in `run'
    from /bundle/ruby/3.1.0/gems/thor-1.1.0/lib/thor/invocation.rb:127:in `invoke_command'
    from /bundle/ruby/3.1.0/gems/thor-1.1.0/lib/thor.rb:392:in `dispatch'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/command/base.rb:87:in `perform'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/command.rb:48:in `invoke'
    from /bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands.rb:18:in `<main>'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
    from /bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
    from bin/rails:4:in `<main>'
ERROR: 1

Active Jobを有効にするとproductionで正常に起動できました。Rails 7ではTurboが標準で入るので、Active Jobは省略できないようです(Turboをひっぺがせば可能なのかもしれませんが)。

関連記事

Rails 7.0.0.rc1がリリースされました

The post Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた first appeared on TechRacho.

Propshaft gem README(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。

propshaft README(翻訳)

Propshaftは、Railsのアセットパイプライン用ライブラリです。Propshaftは、以下のような時代に合わせて構築されました。

  • HTTPコネクションを節約するためのアセットバンドルがもはや切実ではなくなった
  • JavaScriptやCSSを専用のNode.jsバンドルでコンパイルするかブラウザに直接提供するようになった
  • 帯域幅が増えたことでアセットの最小化(minify)の必要性が低くなった

これらの要因によって、Sprocketsのような従来のオプションと比べて劇的にシンプルかつ高速なアセットパイプラインを実現できるようになりました。

すなわち、Propshaftでは上記のような処理は行いません。Propshaftが提供するのは以下の機能です。

  1. コンフィグ可能な読み込みパス: アプリやgem内のさまざまな場所にあるディレクトリを登録して、それらがあたかも1つのパス上に置かれているかのように、すべてのパスのアセットを参照できるようになります。
  2. ダイジェストの付与: production環境では読み込みパスにあるすべてのアセットがコピー(またはコンパイル)され、そのすべてのファイル名にダイジェストハッシュが付与されます。すなわち、有効期限の長いキャッシュヘッダーを用いてパフォーマンスを向上させることが可能です。この処理中に変換方法を提供するマニフェストファイルが生成されるので、ダイジェストを付与されたアセットを論理パス経由で参照できるようになります。
  3. 開発用サーバー: developmentモードではアセットのプリコンパイルは不要です。同じasset_pathヘルパーでこれらを参照すれば、developmentモードのサーバーでアセットを表示できます。
  4. 基本的なコンパイラ: Propshaftの設計では、すべてのトランスパイラ機能を提供するわけではないことが明示されています。よりよいトランスパイラは他にもあります。しかし、Propshaftはシンプルな入力->出力コンパイラセットアップを提供し、デフォルトではCSSでのasset-path関数呼び出しをurl(digested-asset)に変換するために使われます。

インストール

Rails 7以降では、rails new myapp -a propshaftでPropshaftを有効にした新しいアプリケーションの開発を始められます。

利用法

Propshaftは、config.assets.pathsで設定されたすべてのパスにあるアセットを提供可能にし、プリコンパイル時にはすべてのアセットをpublic/assetsにコピーします。この振る舞いは、バンドル済みアセットに明示的に含まれていないアセットを明示的にコピーしないSprocketの振る舞いとは異なります。

これらのアセットは、asset_pathimage_tagjavascript_include_tagといった通常のヘルパータグで論理パスから参照可能です。これらの論理参照は、rails assets:precompileが実行されると自動的にダイジェストを考慮したパスに変換されます(public/assets/.manifest.jsonにあるJSONマッピングファイルを経由)。

また、Propshaftにはasset-path("image.svg")というCSS関数が同梱され、コンパイル時にはurl("/assets/image-f2e1ec14d6856e1958083094170ca6119c529a73.svg")に変換されます。この関数は、あらゆる.cssファイルに適用されます。

ダイジェスト追加をスキップするには

JavaScriptファイルとそのsource mapで行っているように、複数のファイルをPropshaft経由で相互参照する必要がある場合は、ファイル名を安定させるために、それらのファイルを事前にダイジェスト化しておかなければなりません。Propshaftは、アセットファイル名の末尾が-[ダイジェスト].digested.jsというパターンかどうかを探します(このパターンはファイルがダイジェスト済みであることを示します)。

SprocketsからPropshaftへの移行

PropshaftはSprocketsよりも機能がかなり少ないので、移行が望ましい場合でもそれなりの作業量が必要になるかもしれません。特に、アプリケーションがCoffeeScriptやSassなどのトランスパイラ機能を提供するためにSprocketsに依存している場合や、トランスパイラを提供するgemに依存している場合は、作業量が増える傾向にあります。トランスパイルをやめるか、jsbundling-railscssbundling-railsなどのNodeベースのトランスパイラを利用する必要があります。

一方、JavaScriptやCSSを既にNodeベースのセットアップでバンドルしていれば、Propshaftを手軽に導入できます。この場合、バンドルやトランスパイスに別のツールを使う必要はなく、単にダイジェスト化して提供するだけで完了します。

しかしPropshaftは、新規アプリケーションでデフォルトのimport-mapアプローチを用いる場合にもうまく機能します(vanilla CSSを扱う用意ができていれば)。

Propshaftは今後RailsのデフォルトとしてSprocketを置き換えますか?

その可能性は高いのですが、Sprocketsについても今後長期間のサポートが必要です。Sprocketsの機能の上に構築されたアプリやgemがたくさんあり、それらはすぐには移行しないでしょう。互換性については現在検討中です。現時点のPropshaftは、ごく初期状態のソフトウェアです。

ライセンス

Propshaft is released under the MIT License.

関連記事

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

The post Propshaft gem README(翻訳) first appeared on TechRacho.


Rails 7.0.0+Ruby 3.1.0のGemfileは7-0-stableブランチの指定が必要

$
0
0

現象

Ruby 3.1.0がリリースされた直後にRails 7.0.0をRuby 3.1.0で動かそうとすると、以下のRails::Engine is abstract, you cannot instantiate it directly.エラーが出て動きませんでした。これはrails newの場合ですが、他の操作でも出ました。

$ dip rails new . --skip-git
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
       exist
    conflict  README.md
Overwrite /app/README.md? (enter "h" for help) [Ynaqdhm] n
        skip  README.md
      create  Rakefile
    conflict  .ruby-version
Overwrite /app/.ruby-version? (enter "h" for help) [Ynaqdhm] n
        skip  .ruby-version
      create  config.ru
    conflict  Gemfile
Overwrite /app/Gemfile? (enter "h" for help) [Ynaqdhm] y
       force  Gemfile
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/cable.yml
      create  config/puma.rb
      create  config/storage.yml
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/assets.rb
      create  config/initializers/content_security_policy.rb
      create  config/initializers/cors.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/new_framework_defaults_7_0.rb
      create  config/initializers/permissions_policy.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/master.key
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  lib/assets
      create  lib/assets/.keep
      create  log
      create  log/.keep
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/apple-touch-icon-precomposed.png
      create  public/apple-touch-icon.png
      create  public/favicon.ico
      create  public/robots.txt
       exist  tmp
      create  tmp/.keep
      create  tmp/pids
      create  tmp/pids/.keep
       exist  tmp/cache
      create  tmp/cache/assets
      create  vendor
      create  vendor/.keep
      create  test/fixtures/files
      create  test/fixtures/files/.keep
      create  test/controllers
      create  test/controllers/.keep
      create  test/mailers
      create  test/mailers/.keep
      create  test/models
      create  test/models/.keep
      create  test/helpers
      create  test/helpers/.keep
      create  test/integration
      create  test/integration/.keep
      create  test/channels/application_cable/connection_test.rb
      create  test/test_helper.rb
      create  test/system
      create  test/system/.keep
      create  test/application_system_test_case.rb
      create  storage
      create  storage/.keep
      create  tmp/storage
      create  tmp/storage/.keep
      remove  config/initializers/cors.rb
      remove  config/initializers/new_framework_defaults_7_0.rb
         run  bundle install
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Using rake 13.0.6
Using concurrent-ruby 1.1.9
Using minitest 5.15.0
Using builder 3.2.4
Using erubi 1.10.0
Using crass 1.0.6
Using rack 2.2.3
Using mini_portile2 2.6.1
Using nio4r 2.5.8
Using marcel 1.0.2
Using mini_mime 1.1.2
Fetching public_suffix 4.0.6
Fetching bindex 0.8.1
Using websocket-extensions 0.1.5
Using racc 1.6.0
Fetching msgpack 1.4.2
Using bundler 2.3.3
Fetching matrix 0.4.2
Installing bindex 0.8.1 with native extensions
Installing matrix 0.4.2
Installing msgpack 1.4.2 with native extensions
Installing public_suffix 4.0.6
Fetching regexp_parser 2.2.0
Installing regexp_parser 2.2.0
Fetching childprocess 4.1.0
Fetching io-console 0.5.11
Installing io-console 0.5.11 with native extensions
Installing childprocess 4.1.0
Using method_source 1.0.0
Using thor 1.1.0
Using zeitwerk 2.5.3
Fetching rexml 3.2.5
Installing rexml 3.2.5
Fetching rubyzip 2.3.2
Fetching sqlite3 1.4.2
Installing rubyzip 2.3.2
Installing sqlite3 1.4.2 with native extensions
Using i18n 1.8.11
Using tzinfo 2.0.4
Using rack-test 1.1.0
Using mail 2.7.1
Fetching puma 5.5.2
Installing puma 5.5.2 with native extensions
Fetching sprockets 4.0.2
Installing sprockets 4.0.2
Using websocket-driver 0.7.5
Using nokogiri 1.12.5
Fetching addressable 2.8.0
Fetching selenium-webdriver 4.1.0
Installing addressable 2.8.0
Using activesupport 7.0.0
Fetching reline 0.3.1
Installing selenium-webdriver 4.1.0
Using loofah 2.13.0
Fetching xpath 3.2.0
Installing reline 0.3.1
Using rails-dom-testing 2.0.3
Using globalid 1.0.0
Using activemodel 7.0.0
Fetching webdrivers 5.0.0
Installing xpath 3.2.0
Using rails-html-sanitizer 1.4.2
Using irb 1.4.1
Using activejob 7.0.0
Using activerecord 7.0.0
Fetching capybara 3.36.0
Installing webdrivers 5.0.0
Using actionview 7.0.0
Fetching debug 1.4.0
Installing capybara 3.36.0
Installing debug 1.4.0 with native extensions
Using actionpack 7.0.0
Fetching jbuilder 2.11.5
Installing jbuilder 2.11.5
Using actioncable 7.0.0
Using activestorage 7.0.0
Using actionmailer 7.0.0
Using railties 7.0.0
Fetching sprockets-rails 3.4.2
Installing sprockets-rails 3.4.2
Using actionmailbox 7.0.0
Using actiontext 7.0.0
Fetching importmap-rails 1.0.1
Installing importmap-rails 1.0.1
Fetching stimulus-rails 1.0.2
Installing stimulus-rails 1.0.2
Fetching turbo-rails 1.0.0
Fetching web-console 4.2.0
Installing web-console 4.2.0
Installing turbo-rails 1.0.0
Using rails 7.0.0
Fetching bootsnap 1.9.3
Installing bootsnap 1.9.3 with native extensions
Bundle complete! 15 Gemfile dependencies, 67 gems now installed.
Bundled gems are installed into `/bundle`
         run  bundle binstubs bundler
Skipped bundle since it already exists.
If you want to overwrite skipped stubs, use --force.
       rails  importmap:install
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
rails aborted!
Rails::Engine is abstract, you cannot instantiate it directly.
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:246:in `initialize'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:184:in `new'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:184:in `instance'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:223:in `method_missing'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/descendants_tracker.rb:90:in `descendants'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:923:in `block in define_callbacks'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:920:in `each'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:920:in `define_callbacks'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/engine.rb:427:in `<class:Engine>'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/engine.rb:349:in `<module:Rails>'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/engine.rb:11:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/application.rb:11:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails.rb:13:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/all.rb:5:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/app/config/application.rb:3:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:54:in `require_relative'
/app/rakefile:4:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:60:in `load'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:60:in `load'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/rake/rake_command.rb:20:in `block in perform'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/rake/rake_command.rb:18:in `perform'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/command.rb:51:in `invoke'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands.rb:18:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
bin/rails:4:in `<main>'
(See full trace by running task with --trace)
       rails  turbo:install stimulus:install
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
rails aborted!
Rails::Engine is abstract, you cannot instantiate it directly.
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:246:in `initialize'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:184:in `new'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:184:in `instance'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/railtie.rb:223:in `method_missing'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/descendants_tracker.rb:90:in `descendants'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:923:in `block in define_callbacks'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:920:in `each'
/bundle/ruby/3.1.0/gems/activesupport-7.0.0/lib/active_support/callbacks.rb:920:in `define_callbacks'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/engine.rb:427:in `<class:Engine>'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/engine.rb:349:in `<module:Rails>'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/engine.rb:11:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/application.rb:11:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails.rb:13:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/all.rb:5:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/app/config/application.rb:3:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:54:in `require_relative'
/app/rakefile:4:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:60:in `load'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:60:in `load'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/rake/rake_command.rb:20:in `block in perform'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands/rake/rake_command.rb:18:in `perform'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/command.rb:51:in `invoke'
/bundle/ruby/3.1.0/gems/railties-7.0.0/lib/rails/commands.rb:18:in `<main>'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:100:in `register'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
/bundle/ruby/3.1.0/gems/bootsnap-1.9.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
bin/rails:4:in `<main>'
(See full trace by running task with --trace)

原因

2021年12月はRails 7.0.0が先にリリースされ、その後でRuby 3.1がリリースされました。Rails 7.0.0ではRuby 3.1.0の新機能であるClass#descendantsをいち早く取り入れていました。しかしClass#descendantsはRuby 3.1リリース直前にMatzの意向で仕様再検討のためいったん削除された(#5309)、という流れのようです。それによってRails 7のClass#descendantsに依存している部分がエラーになったようです。

上のプルリクはClass#descendantsへの依存をRails 7から削除します。これはmainブランチと7-0-stableブランチにはマージ済みで、今後Rails 7.0.1に含まれる見込みです。

現時点の解決方法

既に@yahondaさんが解決方法や詳しい解説をgistにまとめてくださっています🙏。正直、ここを見れば必要なことはわかります。

同Gistにもあるように、Rails 7.0.0をRuby 3.1で動かすには、Gemfileで7-0-stableブランチを指定する必要があります(この場合以下のようにリポジトリ名も指定する必要があります)。今後はRails 7.0.1がリリースされれば解消されるはずです。

# Gemfile
gem "rails", github: "rails/rails", branch: "7-0-stable"

なお、rails newの場合は、事前にbundle initで生成したGemfileで上のように7-0-stableブランチを指定しておいても、Gemfileが更新されるときに以下のようにデフォルトの7.0.0で上書きされてしまうので、上の現象が再発しました。

# Gemfile
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.1.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.0"
# ...

私の場合は、このGemfileだけいったん残して他の生成ファイルを削除し、Gemfileを以下のようにgem "rails", github: "rails/rails", branch: "7-0-stable"に変更してbundle installしてから、改めてrails newするという方法で無理やり回避しました。

# Gemfile
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.1.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", github: "rails/rails", branch: "7-0-stable"
# ...

参考: Ruby 3.1.0向けの設定

Ruby 3.1.0のYJITを有効にするには、環境変数RUBY_YJIT_ENABLE=1を設定するか、起動時に--yjitオプションを追加します。

また、Rails 7.0.0ではconfig/application.rbでconfig.active_support.disable_to_s_conversion
= true
を設定すると、Ruby 3.1.0で最適化された式展開を有効にするためにto_sを上書きしなくなります。

参考: thorのwarningについて

上の問題とは別に、以下のdeprecation warningがthor gemで発生していました。

Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.

Ruby 3.1環境でyamlを扱うさまざまな場所でこのwarningが表示されます。thorはRailsリポジトリにあるgemですが、Rails以外でも使われていることがあります。

私の場合は、以下のdipツールで上のwarningが出るようになりました。影響があるわけではありませんが、ちょっと目障りですね。

docker-composeを便利にするツール「dip」を使ってみた

昨年末に以下のプルリクが上がっていましたが、つい今朝がたマージされました🎉gem updateでthorを更新するとdipでもwarningが発生しなくなりました。

関連記事

Rails 7.0.0がリリースされました

Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた

Ruby 3.1.0がリリースされました

The post Rails 7.0.0+Ruby 3.1.0のGemfileは7-0-stableブランチの指定が必要 first appeared on TechRacho.

Ruby: 文字列マッチは正規表現より先に専用メソッドを使おう

$
0
0

正規表現は文字列メソッドより「遅い」

新しい話ではなくて恐縮です。
Rubyに限らず、一般に正規表現は言語の文字列マッチメソッドより低速になります。

複雑なパターンを調べたい場合は正規表現を使うことになりますが、特に「開始文字列」「終了文字列」とのマッチを単純にチェックするだけなら、String#start_with?String#end_with?でマッチを取る方が可読性の上でも速度面からもおすすめです。

本記事ではtrue/falseを返す文字列マッチメソッドについてのみ言及していますが、文字列の取り出しや置換といった操作についても、専用メソッドの方が正規表現よりも一般に高速なので、「正規表現は次なる手段」と考えるようにしています。

正規表現が前提の文字列マッチメソッド(高機能、低速)

正規表現を使う場合も、=~よりmatch?の方が高速かつ可読性が高まりますので、マッチするかどうかをチェックするだけならmatch?にしましょう。自分も、=~~が前だったか後だったか毎回忘れてしまいます😅

match?は、以下の記事にあるRubyの特殊変数を更新しません。おそらくその分速いと思われます。

[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる

文字列マッチ専用メソッド(単機能、高速)

これらの文字列マッチ専用メソッドには、一応正規表現を引数として与えることもできますが、速度面では文字列を引数として与える方が有利です。

さらに、String#start_with?String#end_with?の引数には文字列を複数与えられます。

string = 'test_some_kind_of_long_file_name.rb'
string.start_with?('empty', 'void', 'test_') #=> true
string.end_with?('useless', 'missing', 'rb') #=> true

配列に入れた文字列もsplat演算子*を使えば渡せます。

array = %w(empty void test_)
string = 'test_some_kind_of_long_file_name.rb'

string.start_with?(*array)    #=> true

なおString#include?は引数を1つしか渡せません😢。速度的にもString#match?と大差ないようです。

ベンチマーク

Ruby 2.6.5を使いました。なおRubocop 0.75.0にperformance copも併用してかけてみましたが、特に何も言われませんでした。

参考: fast-ruby

# frozen_string_literal: true

require 'benchmark/ips'

SLUG = 'test_some_kind_of_long_file_name.rb'

def slower
  SLUG =~ /^test_/
end

def slow
  SLUG.match?(/^test_/)
end

def fast_start
  SLUG.start_with?('test_')
end

def fast_end
  SLUG.end_with?('rb')
end

def fast_include
  SLUG.include?('_long_')
end

Benchmark.ips do |x|
  x.report('String#=~')          { slower }
  x.report('String#match?')      { slow } if RUBY_VERSION >= '2.4.0'
  x.report('String#start_with?') { fast_start }
  x.report('String#end_with?')   { fast_end }
  x.report('String#include?')    { fast_include }
  x.compare!
end

結果

$ ruby start_string_checking_match_vs_start_with.rb
Warming up --------------------------------------
           String#=~   245.277k i/100ms
       String#match?   404.641k i/100ms
  String#start_with?   501.627k i/100ms
    String#end_with?   490.709k i/100ms
     String#include?   414.337k i/100ms
Calculating -------------------------------------
           String#=~      3.736M (± 2.4%) i/s -     18.886M in   5.057521s
       String#match?      8.825M (± 3.0%) i/s -     44.106M in   5.003011s
  String#start_with?     15.313M (± 2.1%) i/s -     76.749M in   5.014165s
    String#end_with?     14.849M (± 3.7%) i/s -     74.588M in   5.031067s
     String#include?      9.559M (± 3.4%) i/s -     48.063M in   5.034701s

Comparison:
  String#start_with?: 15313477.8 i/s
    String#end_with?: 14848814.2 i/s - same-ish: difference falls within error
     String#include?:  9558764.0 i/s - 1.60x  slower
       String#match?:  8824832.6 i/s - 1.74x  slower
           String#=~:  3736485.1 i/s - 4.10x  slower

追記(2021/12/28)

ちなみにRuby 3.1にはMatchData#matchMatchData#match_lengthが新たに追加されました。正規表現マッチ後にキャプチャグループの文字列や長さを取り出せます。

参考: class MatchData (Ruby 3.1 リファレンスマニュアル)
参考: Ruby 3.1 adds MatchData#match and MatchData#match_length | Saeloun Blog

更新情報

  • 2019/10/18: 初版公開
  • 2021/12/23: 更新

関連記事

はじめての正規表現とベストプラクティス1: 基本となる8つの正規表現

The post Ruby: 文字列マッチは正規表現より先に専用メソッドを使おう first appeared on TechRacho.

Ruby: JSON.parseのあまり知られていない機能(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

参考: 週刊Railsウォッチ20211110

Ruby: JSON.parseのあまり知られていない機能(翻訳)

RubyのJSON.parseが文字列キーのハッシュを返すことにうんざりしたことのある方、シンボルキーのハッシュが欲しい方、本記事は皆さんのためにあります。

こんなときは、Rails開発者ならおそらくご存知のHashdeep_symbolize_keysメソッドが使えます。特に理想の世界では、データ構造が以下のようなハッシュになっています。

require 'json'

json = <<~JSON
{
  foo: {
    bar:
      "baz"
  }
}
JSON

> JSON.parse(json)
=> { "foo" => { "bar" => "baz" } }

> JSON.parse(json).deep_symbolize_keys
=> { foo: { bar: "baz" } }

おそらくこれで足りると思いますが、私たちはRailsの世界だけで生きているわけではないので、いつもActive Supportが助けに来てくれるとは限りません。さらに、JSONのペイロードが単純なハッシュ的構造になっているとも限りません。有効なJSONフォーマットにはこんなものもあるのです。まずは1つ目。

> JSON.dump("")
#=> "\"\""

> JSON.dump(nil)
#=> "null"

> JSON.dump([{ foo: { bar: "baz" } }])
#=> "[{\"foo\":{\"bar\":\"baz\"}}]"

deep_symoblize_keysの技は、そのままでは上のどれにも通用しないという事実を思い知らされます。型を再帰的にチェックするトリッキーなアルゴリズムを書いて、可能な場合にはsymbolize_keysdeep_symbolize_keysを適用するなら別ですが。

それでは、Ruby自身が提供するJSONクラスのドキュメントをみっちり読んでみましょう。

json = <<~JSON
{
  foo: {
    bar:
      "baz"
  }
}
JSON

> JSON.parse(json, symbolize_names: true)
=> { foo: { bar: "baz" } }

ハッシュのコレクションを持つArrayではどうなるかも調べてみましょう。

> JSON.parse("[{\"foo\":{\"bar\":\"baz\"}}]", symbolize_names: true)
=> [{ foo: { bar: "baz" } }]

完璧です。

私がこの機能をどうやって見つけたかですか?少し前に、PostgreSQLのjsonカラムにデータを保存する読み取り用モデルで作業していたときのことです。ご存知のようにこのデータは自動的にシリアライズ/デシリアライズされます。つまりjsonカラムから読み込んだ結果は、以下のように文字列キーを持つデータ構造になります。

# 以前の実装
class FancyModel < ActiveRecord::Base
end

> FancyModel.last.my_json_column
=> [{"foo" => { "bar" => "baz" } }]

これは自分にとって相当不便でしたので、シンボルで値にアクセスできる信頼性の高い方法が欲しくなりました。とくにこの場合は、ハッシュを要素に含む配列だったのです。ドキュメントを少し読み進めた結果、以下のようなカスタムのシリアライザを書くことに成功しました。

class FancyModel < ActiveRecord::Base
  class SymbolizedSerializer
    def self.load(json)
      JSON.parse(json, symbolize_names: true)
    end

    def self.dump(data)
      JSON.dump(data)
    end
  end

  serialize :my_json_column, SymbolizedSerializer
end

> FancyModel.last.my_json_column
#=> [{foo: { bar: "baz" } }]

RubyのJSONクラスに隠れているこの機能はあまり知られていないように思います。本記事がお役に立ちましたら、お気軽にシェアしてください。

お知らせ

ARKADEMY.DEVに参加してArkencyのトップクラス教育プログラムコースにアクセスしましょう!「Railsアーキテクトマスタークラス」「アンチ”IF”コース」「忙しいプログラマーのためのブログ執筆コース」「Async Remoteコース」「TDD動画クラス」「ドメイン駆動Rails動画コース」以外にもさまざまなコースが新設中です。

関連記事

Rails: Value Objectで「基本データ型への執着」と戦う(翻訳)

The post Ruby: JSON.parseのあまり知られていない機能(翻訳) first appeared on TechRacho.

Rails 7.0.1がリリースされました

$
0
0

Ruby on Rails 7.0.1がリリースされました。

英語版Changelogの概要を見るにはGItHubのリリースタグ↓が便利です。v7.0.1タグの日付は日本時間の2022年01月07日08:59でした。

詳しくは以下のコミットリストをご覧ください。

🔗 更新の概要

主な更新は、以下の記事でも報じた「Rails 7.0.0がRuby 3.1.0で動かない」問題の修正です。Rails 7.0.1はRuby 3.1.0で動くようになりました。
その他に細かなバグ修正やドキュメント更新も行われています。

Rails 7.0.0+Ruby 3.1.0のGemfileは7-0-stableブランチの指定が必要

関連記事

Rails 7.0.0がリリースされました

The post Rails 7.0.1がリリースされました first appeared on TechRacho.

Dockerfile: 非推奨化されたapt-keyを置き換える

$
0
0

docker-composeでビルド中に以下のwarningが表示されているのに気づきました。

#5 65.11 Warning: apt-key output should not be parsed (stdout is not a terminal)

warning自体はさほど気にすることはなさそうです。

参考: Reduce Docker container build warnings

ついでにapt-keyについて調べてみると、apt-keyが非推奨化されていることを知りました。

以下は私が使っているDockerfileの抜粋です。PostgreSQLとYarnのインストールで使う認証キーを取得する部分でapt-keyが使われています。

# Dockerfile(抜粋)
RUN apt-get update -qq \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    curl \
    less \
    git \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives/* \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && truncate -s 0 /var/log/*log \
  && curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list \
  && curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - \
  && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

なお、私のDockerfileやdocker-compose.ymlはEvil MartiansがRails開発用に公開しているものを元にしています。

evilmartians/terraforming-rails - GitHub

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

apt-key非推奨化関連の情報

Ubuntuのmanページを見ると、apt-keyはたしかに非推奨化されています。

以下のフォーラムで、apt-keyを置き換える必要性について詳しく解説されています。ここを見る限りでは、apt-keyの代わりになるコマンドは公式には示されていないようです。

以下のブログでは、apt-key廃止の経緯などについてさらに詳しく解説されています。

同記事によると、apt-keyは単一の/etc/apt/trusted.gpgにキーを追加するが、リスクの異なるキーを同一ファイル内で共存させるべきではないというセキュリティ上の懸念から廃止が決まったそうです。なおapt-key2022年Q2で廃止が予定されています

既存のDockerfileでは急いで修正することもなさそうですが、今後Dockerfileを作るときのためにapt-keyを別のものに置き換えることにしました。

以下の記事では、apt-keyを置き換えるコマンド手順を解説しています。

Dockerfileで使われているapt-keyを置き換える記事がなかなか見当たらなかったので、自分で調べて置き換えました。

Dockerfileのapt-keyを置き換える

主に以下を参考にしました。

私のDockerfileには、PostgreSQLクライアントとYaml用のキー取得が記述されているので、どちらも置き換える必要があります。

試行錯誤の末、以下のようにテンプレートを作りました。以下は&&などを外していったんシェルの形にしています。ここではcurlを使っていますが、wgetでも構いません。

# 置き換え用テンプレート
curl -sSL キー取得元URL | gpg --dearmor -o /usr/share/keyrings/パッケージ名-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=キー取得元URL] debパッケージ取得元URL" オプション | tee /etc/apt/sources.list.d/パッケージ名.list > /dev/null
キー取得元URL
置き換え前のapt-keyと同じでよい。次の行のsigned-by=にも同じURLを指定する必要がある。
パッケージ名
1個目は生成するファイル名が重複しないためのものなので、重複しなければ何でもよいはず。2個目はパッケージ名にしないといけないと思う。
debパッケージ取得元URL
置き換え前のdebで指定されているものと同じでよい。
オプション
パッケージ取得元URLの構成に応じたオプションを指定する。
PostgreSQLの場合はディストリビューション名-pgdg main メジャーバージョン、Yarnの場合はstable mainなどとなる。

PostgreSQL

# 置き換え前
curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list

上をテンプレートに沿って置き換えたものが以下です。

# 置き換え後
curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" bullseye-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null

本当はディストリビューション名(bullseye)を環境変数化したかったのですが、まだ方法がわかりません😢

Yarn

# 置き換え前
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

上をテンプレートに沿って置き換えたものが以下です。

# 置き換え後
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/yarn-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list > /dev/null

更新後のDockerfile

以上を反映したのが以下のDockerfileです。これでapt-keyとおさらばできました。

# 更新後のDockerfile(抜粋)
RUN apt-get update -qq \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    curl \
    less \
    git \
  && apt-get clean \
  && rm -rf /var/cache/apt/archives/* \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && truncate -s 0 /var/log/*log \
  && curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg \
  && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" bullseye-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null \
  && curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - \
  && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/yarn-archive-keyring.gpg \
  && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list > /dev/null

関連記事

docker-composeを便利にするツール「dip」を使ってみた

GitHub ActionsのイメージビルドをDockerレイヤキャッシュで高速化(翻訳)

The post Dockerfile: 非推奨化されたapt-keyを置き換える first appeared on TechRacho.

週刊Railsウォッチ: Rails 7をRuby 3.1で動かす、クックパッドのRuby 3.1解説記事、Rails 6->7更新ほか(20220112)

$
0
0

あけましておめでとうございます。新年最初の週刊Railsウォッチです。

週刊Railsウォッチについて

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

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

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

今回はRails 7のリリース後の更新情報から見繕いました。

なお年末に2021年度の更新まとめ情報も公開されています。ひととおりウォッチで扱ったはずです。

🔗 3-tierコンフィグでのrails dbconsoleの挙動を修正

3-tier(階層)構成のアプリケーションにはデータベースの”primary”エントリがない可能性がある。Railsのdbconsoleを起動するときは、primaryエントリがあることを前提とせずに、最初のデータベースを読み込むべき。
この修正でメッセージがより明確になる。環境にデータベースがないためにデータベースが提供されていない場合は「primaryが存在しない」ではなく「データベースが存在しない」というメッセージを返すべき。
この変更では、エラーメッセージ以外のアプリケーションの挙動は変更されないはずだが、primaryエントリがないアプリではうまくいかないので、マルチプルDBをサポートするバージョンにもバックポートする必要がある。
同PRより


つっつきボイス:「マルチプルデータベース用か」「これはRailsのdbconsoleのメッセージを修正したんですね」

参考: 3階層システム(三階層システム)とは – IT用語辞典 e-Words

「Railsのdbconsole(bin/rails dbconsole)ってほとんど使ったことないかも」「そもそも存在を知りませんでした」「database.ymlのコンフィグを使ってpsqlやmysqlなどのDBクライアントを呼び出すだけのコマンドですが、DBクライアントや必要なヘッダファイルなども入っていないと使えません」「たしかに自分の環境でやってみたら動かなかった😢

参考: 1.5 bin/rails dbconsole — Rails のコマンドラインツール – Railsガイド

🔗 change_tableで認識できないオプションの例外を発生するようにした

このプルリクは、change_tableブロック内のテーブルでyieldするTableが、if_existsif_not_existsキーワード引数を持つメソッドを受け取ったときに例外を発生させる。
これにより、bulk: trueオプションを指定してchange_tableブロックを呼び出すと黙ってオプションが無視され、このオプションなしで呼び出すとそのとおりになるという予期しない振る舞いを防げる。
同PR冒頭より


つっつきボイス:「change_tableブロックにサポートされていないオプションを渡すと例外を発生するようになったんですね」「以前は通っちゃったのか」

「ちなみに自分はマイグレーションでif_existsif_not_existsを指定するのは怖いのであまりやりません: 以下みたいにif_existsでインデックスがあればインデックスを消すとか」「書きたくない気持ちわかります」

# 同PRより
change_table(:table) do |t|
    t.column :new_column, if_not_exists: true
    t.remove_index :old_column, if_exists: true
end

「Railsのマイグレーションは、途中で失敗したときにマイグレーションが中途半端に残ってしまう可能性があるんですよね」「そうそう、そうなると手動で残りを反映することになって残念な気持ちになる」「Railsのマイグレーションを使いたがらない人がいるのもちょっとわかります」

参考: Rails migration から ridgepole に移行した – DEV Community 👩‍💻👨‍💻

🔗 HostAuthorizationミドルウェアでIP+ポート番号を利用できるようになった

ホストがIPアドレス+ポート番号形式の場合、IPAddrオブジェクトの比較でエラーが発生してミドルウェアがリクエストを拒否していたので、IPAddrオブジェクトを比較するときにホストからホスト名を抽出するようにした。
#43864のコメント(コメント)が修正されるはず。
同PRより


つっつきボイス:「ActionDispatch::HostAuthorizationといえば少し前にセキュリティ修正されたヤツですね↓」

Railsセキュリティ修正6.0.4.1と6.1.4.1 がリリースされました

127.0.0.1:3000みたいにIPアドレスとポート番号を指定できなかったのが修正されたのね」「へ〜、こんなのがあったとは」「これは動かないと困るヤツ」「そういえばRails 7リリース間近でこれがマージされたのを見た覚えがあります」

# actionpack/lib/action_dispatch/middleware/host_authorization.rb#L18
  class HostAuthorization
    ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
-   PORT_REGEX = /(?::\d+)?/.freeze
+   PORT_REGEX = /(?::\d+)/ # :nodoc:
+   IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
+   IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
+   IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
+   VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
+     /\A#{IPV4_HOSTNAME}\z/,
+     /\A#{IPV6_HOSTNAME}\z/,
+     /\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
+   )

IPAddrクラスをちゃんと使うようになった↓」

# actionpack/lib/action_dispatch/middleware/host_authorization.rb#L39
      def allows?(host)
        @hosts.any? do |allowed|
-         allowed === host
-       rescue
-         # IPAddr#=== raises an error if you give it a hostname instead of
-         # IP. Treat similar errors as blocked access.
-         false
+         if allowed.is_a?(IPAddr)
+           begin
+             allowed === extract_hostname(host)
+           rescue
+             # IPAddr#=== raises an error if you give it a hostname instead of
+             # IP. Treat similar errors as blocked access.
+             false
+           end
+         else
+           allowed === host
          end
        end
      end

🔗Rails

🔗「Rails 7.0 + Ruby 3.1でゼロからアプリを作ってみたときにハマったところあれこれ」


つっつきボイス:「jnchitoさんのこの記事とても参考になりました❤: TechRacho記事も引用してくださってます🙏」「rails newってなかなか1回で終わらなくて、オプションを足して何回かやり直したりしますよね」「するする: そういえば昨年業務でrails newする機会は1回しかなかった😢」「これはもうしょうがないですね」

「以下の記事にも書きましたが、thor gemが出していたwarningも修正されました↓」「そういえばRails 7.0.0でRuby 3.1がまだ動かないんだったか(編集部注: つっつきの翌日にRails 7.0.1がリリースされてRuby 3.1で動くようになりました↓)」

Rails 7.0.0+Ruby 3.1.0のGemfileは7-0-stableブランチの指定が必要

Rails 7.0.1がリリースされました

「jnchitoさんの記事で今頃知ったんですが、Rails 7ではアセットをビルドするためにbin/railsではなくbin/devでdevelopmentサーバーを起動するんですね」「お、なるほど」「ちなみに私はrails newするときに例のpropshaft↓を有効にしたんですが、その場合はbin/devは生成されませんでした: propshaftはビルド不要なので従来のbin/railsでdevelopmentサーバーを起動するみたいです」

Propshaft gem README(翻訳)

「Rails 7ではpropshaftのときもbin/devで起動するように統一してくれたら、起動コマンドを使い分けなくて済むのでよさそうですけど」「それもそうですね」「bin/devはforemanを起動するらしいので、もしかするとpropshaftでは余分なforemanをインストールしないようにしているのかもしれませんが」

「あ、自分はRails 6を7にアップグレードしたんですが(後述)、bin/devはありませんね」「binstubを生成するみたいにbin/devも生成できそうですけどね🤔」「とりあえずrails assets:precompileは引き続き使えるみたい」

後で調べてみましたがbin/devを単独で生成する方法は見つけられませんでした😢

「Rails 7のTurboはややこしそう」「このstatus: :unprocessable_entity↓は、Rails 7でscaffoldすると追加されていたので気づきました」「なるほど、Turboがこれを解釈するので必要になるのか」

# 同記事より: コントローラ
  def create
    @project = current_user.projects.new(project_params)

    respond_to do |format|
      if @project.save
        format.html { redirect_to project_url(@project), notice: "Project was successfully created." }
        format.json { render :show, status: :created, location: @project }
      else
        # status: :unprocessable_entity が必須!!
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @project.errors, status: :unprocessable_entity }
      end
    end
  end

「以下のような変更↓はrails-ujsからTurboに変わることで必要になるんだと思いますが、こういうアップグレード作業は地味に大変そう」「ビューの修正が増えそうですね」「確認ダイアログの出し方もrails-ujsのときと変わるのか」「rails-ujsは昔jQuery版があったほど歴史が長くて自分もそれに慣れているので、新しい書き方に慣れないといけなくなるかな」「この感じだと一括置換が効くとも限らなさそうですね…」「こういう部分をヘルパーメソッドに切り出すことも多いので、それにも配慮しないといけなさそう」

<!-- 同記事より: ERB -->
<!-- 従来の書き方(rails-ujsを使っている場合) -->
<%= link_to 'Delete', [task.project, task], method: :delete %>

<!-- Turboを使っている場合 -->
<%= link_to 'Delete', [task.project, task], data: { turbo_method: :delete } %>
<!-- 同記事より: ERB -->
<!-- link_toの場合 -->
<%= link_to 'Delete', [task.project, task], data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>

<!-- button_toの場合 -->
<%= button_to "Delete", [task.project, task], method: :delete, form: { data: { turbo_confirm: "Are you sure?" } } %>

「destroyアクションでTurboを使うにはこのstatus: :see_otherも必要なのね↓」「これはscaffoldでも生成されなかったので、jnchitoさんの記事を読んでなかったら見落とすところでした」

# 同記事より: コントローラ
  def destroy
    @task.destroy

    respond_to do |format|
      # status: :see_other が必須!!
      format.html { redirect_to @project, notice: "Task was successfully destroyed.", status: :see_other }
      format.json { head :no_content }
    end
  end

「今さらですがTurboって何をするんでしたっけ?」「Rails 7からはHotwireがデフォルトになって、それと一緒に入るTurboがデフォルトのフロントエンド向けライブラリになります」「なるほど、rails-ujsの代わりになる感じですか」「Turboはrails-ujsも置き換えますが、それ以外にも機能があります」「Turbolinksはどうなるんですか?」「Turboに変わったのでRails 7にはデフォルトでは入りません」「ちなみにTurbolinksはもう開発も止まっています

速報: Basecampがリリースした「Hotwire」の概要

「Turboは独立してインストールできるのかな?」「そういえばRails 6.1.1にTurboをインストールする記事↓を見かけたのでできそうです」「HotwireはTurboが必須のようですね」

参考: Rails 6.1.1にTurbo Driveをインストールして困ったこと

「なぜTurboでインターフェイスを変更したんでしょうね?」「おそらくですが、rails-ujsと共存できるようにするためじゃないかな: たしか共存はできるはずですし」「それなら最初は共存させておいて、それから少しずつ書き換えて移行することもできそう」「名前空間が衝突するとそれができなくなるからでしょうね」

「jnchitoさんの記事でもrails-ujsが非推奨になったとある」「いずれTurboに移行しないといけなくなるのか」「その代わりTurboならAjax的なことやturbo_frame_tagのような強力な機能も使えるようになりますけどね↓」「rails-ujsを昔読んでみたときは比較的シンプルだった覚えがあるんですが、Turboの方が大きいから読むのに気合が要りそう」

<!-- 同記事より: app/views/tasks/_completed.html.erb -->
<%= turbo_frame_tag task do %>
  <%= form_with model: task, url: [:toggle, task.project, task] do |f| %>
    <label class="<%= task.completed? ? 'completed' : '' %>">
      <%= f.check_box :completed, onchange: "this.form.requestSubmit()" %>
      <%= task.name %>
    </label>
  <% end %>
<% end %>

「deviseがまだTurboに対応していないとあるけど、これはいずれ対応すると思います」「deviseのような認証部分のコードはなるべく改変したくないのでぜひ対応して欲しい」「そうそう」「いずれにしろRailsは7.0.1が早々に出そうな予感がします」

つっつきの翌日に本当にリリースされました↓。

Rails 7.0.0+Ruby 3.1.0のGemfileは7-0-stableブランチの指定が必要

🔗 RailsでRuby 3.1を使う

なお、Rails 7.0.1がリリースされたことで同Gistは更新されました。


つっつきボイス:「yahondaさんのこのGistも有用な情報がいろいろあって、以下の記事を出すときに助かりました↓」「Rail 7.0.0でRuby 3.1を使うには7-0-stableブランチを指定しないといけないという情報も載っていますね(編集部注: Rails 7.0.1では不要になりました)」

Rails 7.0.0+Ruby 3.1.0のGemfileは7-0-stableブランチの指定が必要

「そうそう、Ruby 3.1のClass#descendantsがリリース直前にいったん外されたことで↓、それに依存していたRails 7.0.0がRuby 3.1で動かなくなった」「RubyのリリースとRailsのリリースが接近していたことによるハプニングでしたね」

参考: Feature #14394: Class.descendants – Ruby master – Ruby Issue Tracking System

🔗 RailsデザインパターンKPT


つっつきボイス:「こうやって社内向けの設計方針を外部向けにも記事として出すのはいいですね👍」「記事では、Service ObjectとDecoratorとForm ObjectとValue Objectを維持する選択をしたそうです」「ちなみに個人的にはDecoratorは好きではなくてForm Objectは割と好き: ただForm Objectをさらに抽象化したDomain Objectの方が柔軟性が高まるのもわかります」

参考: Ruby on Rails Tutorial => Domain Objects (No More Fat Models)

「同記事ではRepositoryパターンは今後なくすそうです」「単に不要になったので削除するのかなるほど」「Repositoryパターンはあってもいいけど、オーバーキル気味な傾向はあるかな: 自分たちはActive Recordにすっかり慣れているので、独自のRepositoryを使うのは不便そうだなという気はする」

「自分は逆にRepositoryパターン大好きで、外部APIを叩くときによく使ってます」「Active Recordみたいにチェインできるオブジェクトかと思ったら実はRepositoryパターンで、チェインしたらめちゃくちゃ重くなったなんてことにもなりそうですけどね」「まあたしかに」「もう自分たちはActive Record脳ですから」

🔗 Rails 6をRails 7にアップグレード


つっつきボイス:「ぼくの記事だ〜❤」「早速のアップグレードお疲れさまです」「でもさっきTurboの話を聞いていて、自分のアプリにTurboを入れてなかったことに気が付きました😅

「記事ではbulletとannotate gemがRails 7で動かなかったgemがありますね: 私も2つほど動かないgemがありました」「annotate gemは好みが別れますが、自分は割と好き」「私も」「このgemを入れておくと、他の人がマイグレーションファイルを書くときにちゃんとコメントも書くようになってくれるんですよ」

ctran/annotate_models - GitHub

「記事にもあるけど、Rails APIサーバーならスムーズにRails 7に移行できそうかな」「逆にビューはHotwireとTurbo周りで手間がかかりそうです」「rails-ujsをTurboに書き換えるとなるとそうなるでしょうね」

🔗Ruby

🔗 クックパッドの「プロと読み解く Ruby 3.1 NEWS」


つっつきボイス:「Ruby 3.1リリースの日に公開されたクックパッドさんの記事です」「お〜後で読もうっと」「Rubyコミッターが経緯や使い方を日本語でみっちり解説してくれるのはありがたい🙏」「よく見ると記事すごく長いですね」「休みの日にでもゆっくり読んでみるといいと思います👍

そういえばRuby 2.6は今年2022年03月31日にEOLを迎えます↓のでお忘れなく。

参考: Ruby Maintenance Branches

🔗 YJITを調べてみた


つっつきボイス:「お、うちの記事が引用されていますね↓」「はてなブックマークは検索対象を引用している記事を見つけられるんですが、その機能で浮かび上がってきた記事です」「YJITをステップバイステップで追いかけていますね: 面白そう👍

YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳)

🔗 Faraday 2.0の変更


つっつきボイス:「ruby/debugメンテナーのst0012さんに教えてもらったツイートです」「お、Faradayがつい最近メジャーアップデートされたのか」「しかもnet_http以外のアダプタをすべて外したそうです」「へ〜」「アダプタが多くてサポートがつらくなったみたいなことが書かれてました」「アダプタが多いとそうなるでしょうね」

lostisland/faraday - GitHub

「どうせひとつに絞るならnet_httpよりもhttpclientにして欲しかった↓」「httpclientはよく話題にしているgemですね(ウォッチ20211108)」「httpclientはよくできてます」

nahi/httpclient - GitHub


今回は以上です。

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

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

Rails公式ニュース

The post 週刊Railsウォッチ: Rails 7をRuby 3.1で動かす、クックパッドのRuby 3.1解説記事、Rails 6->7更新ほか(20220112) first appeared on TechRacho.

Rails: キャッシュとRead Modelの違いを例で理解する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: キャッシュとRead Modelの違いを例で理解する(翻訳)

以下のような、そこそこ複雑なビューがあるとします。カレンダー的なテーブルがあって、横軸は貸部屋、縦軸は利用可能な期間を表しています。

もちろん、このビューでは以下のような機能が欲しくなります。

  • 利用可能かどうかや場所を指定してフィルタする機能
  • ソート機能
  • 貸部屋リストのページネーション
  • 期間のページネーション(別の期間を参照する)

サーバーがJSONを配信すると、SPAフロントエンドのようなクライアントがそのJSONを消費する形になります。

以下のようなさまざまなテーブルにあるデータをjoinする必要があります。

  • apartments
  • addresses
  • bookings
  • 日付のシーケンス

すると、やがてこうなります。

  • 当初はテーブルにクエリをかけて少しばかりjoinすれば済む
  • やがてクエリを最適化するようになり、いくつかのクエリを手書きするようになる
  • ビューの拡張が何年も繰り返され、データが追加される
  • 顧客のデータセットが肥大化し、ページが遅いと不満を漏らすようになる
  • ページを高速化するソリューションを考え始めるようになる。おそらくキャッシュ?

キャッシュ

こういうときは、JSONレスポンスをキャッシュしたくなることでしょう。

  • フィルタやページやソート状態のあらゆる組み合わせについてレスポンスをキャッシュする必要がある
  • キャッシュはおそらく最初のリクエストで行われるが、まだ遅いリクエストがいくつかあり、パフォーマンスを予測できない
  • キャッシュのウォームアップは面倒で、しかも美しいと思えない
  • そしてコンピュータサイエンスにおける2番目に困難な問題、すなわち「キャッシュの無効化」問題に遭遇する(ちなみに最も困難なのは「ネーミング」です)。

キャッシュ以外に手段はないのでしょうか?

それでは次をご覧ください。

Read Model

Read Modelによるソリューションは、キャッシュといささか異なります。

  • 顧客が望むクエリに完全に最適化された新しいDBテーブルを構築する。
  • テーブルは1個にしておく(ページやソート状態やフィルタのさまざまな組み合わせをキャッシュするのと逆のアプローチです)。このテーブルには、さらにページネーションやソートやフィルタを行うためのデータをすべて含めましょう(Read Modelは一般に1個のテーブルでなくともよく、SQLテーブルでなくても一向に構いません: ディスク上のファイルをメモリオブジェクトに読み込めれば何でもよいのです)。
  • すべてのフィールドをクライアントから利用可能にしておく。部屋の住所を表示する必要がある場合、addressesテーブルをjoinするのではなく、Read Modelのテーブルに入れておけば、いつでもその行を取り出す準備が整います。DBの非正規化やデータの冗長化は容認されており、むしろ大歓迎です。
  • フィルタやソートの式を複雑にする必要が生じたら、事前に計算してフィールドに入れておくことで、クエリを可能な限り手軽に実行できるようになる。

しかしRead ModelをWrite Modelで常に最新に保つにはどうすればよいのでしょうか?

Read Modelを最新に保つ方法その1: vanilla方式

vanilla(プレーン)方式なら以下のようにかなり素直に書けます。

ApplicationRecord.transaction do
  booking = Booking.create!(params)
  CalendarReadModel.handle_booking_created(booking)
end

読み取りに関連するメソッドを元のモデルからRead Modelに移せばよいのです。快感ですね。

今後は、Read Modelに関係するものを変更するたびにRead Modelを更新する必要があります(上の例では、予約作成意外に部屋の追加や住所・価格の変更のときにも必要です)。やることが増えると思いますか?このぐらいなら容認できるかもしれません。

しかしRead Modelを更新する方法はひとつではありません。

Read Modelを最新に保つ方法その2: イベントドリブン方式

もうひとつのアプローチは、部屋を予約するときにイベントをパブリッシュする方法です。

ApplicationRecord.transaction do
  booking = Booking.create!(params)
  event_store.publish(BookingCreated.new(data: { booking_id: booking.id })
end

そのイベントを、Read Modelを更新するハンドラでサブスクライブします。

event_store.subscribe(
  -> event { CalendarReadModel.handle_booking_created(event) },
  to: [BookingCreated]
)

こちらの方が癒着が減ります。関連するモデルすべてでこの作業を行う必要はあるので作業が減るわけではありませんが、実装は明らかにクリーンかつシンプルになり、パブリッシュするイベントを他のさまざまなことにも活用できるようになります。

ご注目いただきたいのは、このハンドラは同期的であり、vanilla方式の例と同様に同じトランザクション内で実行される点です。ハンドラを非同期にすることも可能ですが、その場合は「最終的な一貫性」を考慮する必要があります。つまり、Read Modelに表示されるものとWrite Modelで許可されるものの間に少し遅れが生じる可能性があるということです。この欠点は、多くの場合に容認できます。

お知らせ

ARKADEMY.DEVに参加してArkencyのトップクラス教育プログラムコースにアクセスしましょう!「Railsアーキテクトマスタークラス」「アンチ”IF”コース」「忙しいプログラマーのためのブログ執筆コース」「Async Remoteコース」「TDD動画クラス」「ドメイン駆動Rails動画コース」以外にもさまざまなコースが新設中です。

関連記事

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

The post Rails: キャッシュとRead Modelの違いを例で理解する(翻訳) first appeared on TechRacho.


Rubyの整数オーバーフローチェックを「スタンプ」で一掃する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

本記事ではstampの仮訳を「スタンプ」としています。またmaskingを「ビットマスク」としています。

型(type)とスタンプ(stamp)は一般に互いに近い関係にある言葉です。

oracle/truffleruby - GitHub

Rubyの整数オーバーフローチェックを「スタンプ」で一掃する(翻訳)

Rubyで整数演算のオーバーフロー(桁あふれ)チェックをオフにすることは可能かという質問をRedditで見かけました。質問者は、自分たちの演算がオーバーフローしないことを保証できるので、無意味なチェックでコストを増やさないようにしたいのだそうです。

そのスレッドで他の方が回答していたように、Rubyの標準実装であるMRIでは無理ですが、TruffleRubyなら可能で、かつ安全です。さらに素晴らしいのは、演算がオーバーフローしないことをコードで証明できれば、演算で自動的にそのようになる点です。そしてさらに、コードによる証明がない場合は、標準のRubyを用いて証明を簡単に追加することも可能です。この点にもう少し踏み込めるかどうかを確かめるために、安全性はやや落ちるものの少し高速になるような新しいゼロコストの証明方法を追加してみました。

この方法が実際にパフォーマンス測定可能な形で影響を与えられることを示せます。特に、これが純粋なRubyコードで可能である点は価値があると思います。

このことを述べておく価値がある理由は、最近のコンパイラが一般には気づかれない微妙な形で最適化を行っている見事な例だからです。

オーバーフローのチェック

プロセッサ(CPU)内部では整数演算をハードウェアでサポートしていますが、そこで使える整数は32ビットまたは64ビットなどの固定長です。算術演算の結果がそれより大きくなると、期待と異なる値に丸められてしまいます。Rubyの整数は固定長ではなく、任意の大きさの整数を許容します。では、どうすれば固定長のハードウェアで任意サイズのRuby演算を実装できるでしょうか。これが可能なのは、ハードウェア算術演算の結果があふれるかどうかを検出するしくみがあるからです。これが、今回お話しする「オーバーフローチェック」です。Rubyは、オーバーフローする可能性のあるすべての算術演算の後で、オーバーフローしたかどうかをチェックします。オーバーフローした場合は、任意サイズの整数をサポートするソフトウェアライブラリに切り替えて算術演算をやり直します。

Rubyのサンプルコードをいくつか以下に示します。まずは加算です。

def add_any(a, b, c)
  a + b + c
end

TruffleRubyとGraalでは、コンパイル中のプログラムをグラフデータ構造で表現します。これについては別のところで既に述べましたが、基本的にはコードのフローチャートと思っていただければ結構です。矢印は、制御とプログラム内のデータフローを表します。ボックスは演算や計算を表します。ここでは、Shopifyが開発したSeafoamというツールを用いてグラフをビジュアル表示しています。

以下はRubyのサンプルコードです。T(7)ノード、T(8)ノード、T(9)ノードはそれぞれ abcに対応します。これらの値がIntegerAddExact演算ノード内を流れる様子がグラフからわかります。Exactは、そこでオーバーフローをチェックするという意味です。

生成されたマシン語コードを以下に示します。addjoというマシン語命令(instruction)があるのがわかります。joは「jump-on-overflow」のことで、得られた結果がハードウェア上の整数として大きすぎる場合にソフトウェアで演算を処理するという意味です。

    0x11bb1f909:    add esi, eax
    0x11bb1f90b:    jo  0x11bb1f9fc
...
    0x11bb1f91f:    add r10d, esi
    0x11bb1f922:    jo  0x11bb1f9de

算術演算のたびに余分なチェックが走っています。これがどれほどよろしくないかは想像がつくでしょう。だからこそこれを排除したいと思うわけです。ここで皆さんに良いニュースと悪いニュースがあります。良いニュースは、ほとんどの人がjo命令の実行コストは非常に低いと指摘していることです。プロセッサはjo命令をうまくパイプライン化するので、この命令自体がパフォーマンスに大きく影響することはありません。悪いニュースの方は、この命令があると演算を効率よくpackできなくなり、それ以上最適化できなくなることです。

スタンプ: コンパイラ内部のマイクロ型

次に進む前に、ここで新しいコンセプトをひとつ紹介しておく必要があります。

Rubyは(最近の静的型関連のアイデアを別にすれば)動的型付け言語ですが、優秀なコンパイラは動的型付け言語をコンパイルするときに自動的に静的型を発見することをご存知でしょうか?上のコードのabcが整数であると認識するような単純なものもありますが、世にほとんど知られていないもうひとつの型、それが「スタンプ(stamp)」です。

スタンプは、コンパイラの内部にだけ存在する一種のマイクロ型です。スタンプは他の型と同様、値に関する情報を表しますが、これまで扱ったような型と異なり、「オブジェクトがnullかどうか」「値が正か負か」「整数のどのビットがオンになっているか」といったさまざまなプロパティを私たちにもたらしてくれます。このプロパティに注目していくことにします。

Rubyコードでは、以下のようにビットAND演算子&を用いることでスタンプの例を観察できます。

def stamp(a)
  a & 0xff
end

このコードのコンパイラグラフを見ると、ノード834から880に流れる値を表すエッジでスタンプがアノテーションされていることがわかります。このvalue [0 - 255]は、「その値が指定の範囲内に確実に収まる」ことを保証するスタンプがこのエッジにあるという意味です。

スタンプは何のためにあるのでしょうか。ある演算をコンパイルするときに、その演算に流れ込む値のスタンプをチェックし、スタンプが値について保証する内容に基づいてコンパイル方法を変更できます。

ここに「ビッグアイデア」があります。「値は十分小さいのでオーバーフローしない」とスタンプが主張する2つの値を足すと、コンパイラは自動的にオーバーフローチェックを除去できます。そのためには、これらのスタンプを生成するための何らかの証明(proof)が必要です。これについては次に説明します。

ビットマスクによってスタンプを追加する

証明を与える方法のひとつは、単に&演算を用いることです。3つの値をビットマスクして1バイトの範囲に収まるようにする形でaddを変更すれば、それによって演算がオーバーフローする可能性がないことを証明できます。

def add_masks(a, b, c)
  a &= 0xff
  b &= 0xff
  c &= 0xff
  a + b + c
end

これでExactノードがグラフから消え去り、代わりにずっとシンプルな基本の+ノードになりました。これがどのようにして起きたかを観察できます。そこに流れ込むスタンプは、それらの値が十分小さく、オーバーフローする可能性がないことを示しています。2つ目の+には[0 - 510]になるスタンプが入力され、出力には[0 - 765]になるスタンプがある点にご注目ください。

対応するマシン語コードは以下のようになります。

    0x1211f4095:    and esi, 0xff
    0x1211f409b:    and r10d, 0xff
    0x1211f40a2:    and eax, 0xff
    0x1211f40a8:    add r10d, eax
    0x1211f40ab:    add r10d, esi

これこそが本来のねらいです。つまり、値の範囲を証明するビットマスクを追加して、演算がオーバーフローしないことを証明すれば、優秀なコンパイラがその情報を活用してオーバーフローチェックを除去してくれるということです。

ここで、スタンプ付きの値はローカル変数に保存されており、それを取り出したものを利用している点にご注意ください。スタンプは保存から取得までの間存在し続けますが、それは問題ではありません。

スタンプの追加を組み込む

しかしビットマスクを導入すると、マスク命令が実際に実行されてしまうのが問題です。つまり余分なand命令が生じてしまいます。私たちは(プログラマとしての推論によって)実際には値が範囲内に収まることを知っているので、このand命令は何も行っていないに等しい無意味な存在です。私たちはコンパイラにそのことを伝えていただけです。誤りである場合のリスクを承知のうえで値が範囲内に収まることを確信できるなら、もっとよい方法はないものでしょうか?

そこで、実際には何の操作も行わず、単にスタンプを「注入」する特殊な関数を書く手があります。私はこれをGraalおよびTruffleRubyでTruffle::Graal.inject_stamp(value, 0xff)として実装しました。この実装はコンパイラ組み込み(compilier intrinsic)です。つまり、コンパイラで認識され、異なる扱いを受けるメソッドということです。以下はGraalでの実装です。

r.register2("injectStamp", int.class, int.class, new InvocationPlugin() {
    @Override
    public boolean inlineOnly() {
        return true;
    }

    @Override
    public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode value, ValueNode stamp) {
        b.addPush(JavaKind.Boolean, new InjectStampNode(value, stamp));
        return true;
    }
});

これは、コンパイラがinjectStamp呼び出しを見つけたら、代わりに上のコードを実行するというものです。このコードはグラフに特殊なInjectStampNodeを追加します。このノードはグラフ上にまったく出現しませんが、自分自身を単純化する最適化を行います。

public void simplify(SimplifierTool tool) {
    if (!hasUsages()) {
        return;
    }
    if (stamp.isConstant()) {
        int stampValue = stamp.asJavaConstant().asInt();
        replaceAtUsagesAndDelete(graph().addOrUnique(
            new PiNode(value, StampFactory.forInteger(32, 0, stampValue))));
    }
}

simplify演算では、スタンプの値(ビットマスク)を取得し、スタンプを追加するためだけの「πノード」と呼ばれるノードを用いてそれを注入します。実際にスタンプを探索するのはオーバーフロー演算であることを思い出しましょう。ここでは、スタンプがオーバーフローを許可しているかどうかをチェックしていることがわかります。オーバーフローが許可されていない場合は、自分自身をブーリアン定数falseに置き換え、決してオーバーフローしないことをこの値で示します。

public ValueNode canonical(CanonicalizerTool tool, ValueNode forX, ValueNode forY) {
    if (forX.isConstant() && !forY.isConstant()) {
        return new IntegerAddExactOverflowNode(forY, forX).canonical(tool);
    }
    if (forX.isConstant() && forY.isConstant()) {
        return canonicalXYconstant(forX, forY);
    } else if (forY.isConstant()) {
        long c = forY.asJavaConstant().asLong();
        if (c == 0) {
            return LogicConstantNode.forBoolean(false);
        }
    }
    if (!IntegerStamp.addCanOverflow((IntegerStamp) forX.stamp(NodeView.DEFAULT), (IntegerStamp) forY.stamp(NodeView.DEFAULT))) {
        return LogicConstantNode.forBoolean(false);
    }
    return this;
}

この「コンパイラ組み込み」を備えたaddは以下のようになります。

def add_stamps(a, b, c)
  a = Truffle::Graal.inject_stamp(a, 0xff)
  b = Truffle::Graal.inject_stamp(b, 0xff)
  c = Truffle::Graal.inject_stamp(c, 0xff)
  a + b + c
end

これで、グラフから&演算が消え去ってさらにシンプルになったことがわかります。この動的なRubyコードは、静的なJavaコードとほぼ変わらないぐらいシンプルに見えます。

そしてマシン語コードはというと、ご覧ください!マシン語のadd命令がたった2つになり、and命令は消え去りました。途中にmov命令が残っているのは惜しい点ですが、これはレジスタ割り当ての決定に基づいたものです。ハードウェアレジスタ名の変更によって、このコード例の途中に小さなしこりが残ってしまった点を除けば、実質的に何の違いもありません。

    0x12457278d:    add eax, dword ptr [r10*8 + 0xc]
    0x124572795:    mov r10d, eax
    0x124572798:    add r10d, esi

コードの予測が外れて、誤っている可能性のあるスタンプが注入されていたとしたらどうなるでしょうか。

誤ったスタンプを注入するプログラムを意図的に書いてみれば、オーバーフローや丸めが発生することを観察できます。このプログラムでは、最初の10秒間は正しく適用可能なスタンプを注入し、以後は範囲から外れた値が渡されるようになります。しかし、マシン語は既にオーバーフローチェックを行わない形で最適化されているので、下から2行目で例外が発生します。この値は丸められており、オーバーフローチェックが存在しないので、値はマイナスになります。

def foo(a, b)
  a = Truffle::Graal.inject_stamp(a, 0xff)
  b = Truffle::Graal.inject_stamp(b, 0xff)
  a + b
end

start = Time.now

until Time.now - start > 10
  a = rand(0xff)
  b = rand(0xff)
  raise if foo(a, b) < 0
end

puts 'state change'

loop do
  a = 0x7FFFFFFF
  b = 0x7FFFFFFF
  raise if foo(a, b) < 0
end

影響を測定する

さて、これは実際に有用なのでしょうか

以下のコードは、RubyのグラフィックライブラリChunkyPNGから引用したものを少し簡略化してあります。これは透明度(transparency)の値に応じて2つの色を合成します。これらの色は「packed RGB」値として保存され、色の合成にはいくつかの整数演算が必要です。

def r(value)
  (value & 0x00ff0000) >> 16
end

def g(value)
  (value & 0x0000ff00) >> 8
end

def b(value)
  value & 0x000000ff
end

def rgb(r, g, b)
  r << 16 | g << 8 | b
end

def int8_mult(a, b)
  t = a * b + 0x80
  ((t >> 8) + t) >> 8
end

def interpolate_quick(fg, bg, alpha)
  alpha_com = 255 - alpha
  new_r = int8_mult(alpha, r(fg)) + int8_mult(alpha_com, r(bg))
  new_g = int8_mult(alpha, g(fg)) + int8_mult(alpha_com, g(bg))
  new_b = int8_mult(alpha, b(fg)) + int8_mult(alpha_com, b(bg))
  rgb(new_r, new_g, new_b)
end

このバージョンのグラフとマシン語コードを見てみると、大量のオーバーフローチェックがあるのがわかります。

...
    0x120f8329e:    sub esi, eax
    0x120f832a0:    jo  0x120f835fc
    0x120f832a6:    mov r10d, dword ptr [r11 + 0x2c]
    0x120f832aa:    mov r10d, dword ptr [r10*8 + 0xc]
    0x120f832b2:    mov edx, r10d
    0x120f832b5:    and edx, 0xff0000
    0x120f832bb:    shr edx, 0x10
    0x120f832be:    imul    edx, eax
    0x120f832c1:    jo  0x120f835b8
    0x120f832c7:    add edx, 0x80
    0x120f832cd:    jo  0x120f83612
    0x120f832d3:    mov r8d, edx
    0x120f832d6:    sar r8d, 8
    0x120f832da:    mov r9d, r8d
    0x120f832dd:    add r9d, edx
    0x120f832e0:    jo  0x120f835c5
    0x120f832e6:    mov r9d, r10d
    0x120f832e9:    and r9d, 0xff00
    0x120f832f0:    shr r9d, 8
    0x120f832f4:    mov ecx, r9d
    0x120f832f7:    imul    ecx, eax
    0x120f832fa:    nop word ptr [rax + rax]
    0x120f83300:    jo  0x120f835ad
    0x120f83306:    imul    r9d, eax
    0x120f8330a:    add r9d, 0x80
    0x120f83311:    jo  0x120f8351c
    0x120f83317:    mov ecx, r9d
    0x120f8331a:    sar ecx, 8
    0x120f8331d:    mov ebx, ecx
    0x120f8331f:    add ebx, r9d
...

しかしピクセル値が1バイトの範囲を超えないことはわかりきっているので、これらのチェックはことごとく無駄であることがわかります。ここにたった1行のコードを追加するだけでこの問題をきれいサッパリ解決できるのです!

まず、すべての値がランダムである画像をいくつか合成する作業をベンチマーク化します。

a, b, c = [], [], []

1_000.times do
  a.push rgb(rand(255), rand(255), rand(255))
  b.push rgb(rand(255), rand(255), rand(255))
  c.push rgb(rand(255), rand(255), rand(255))
end

Benchmark.ips do |ips|
  ips.report('interpolate_quick') do
    alpha = rand(255)
    a.size.times do |n|
      c[n] = interpolate_quick(a[n], b[n], alpha)
    end
  end
end

ここから1062バイトのマシン語コードが生成されます。

それではビットマスクをかけてみましょう。alphaを1バイトの範囲に収めるビットマスクをかけるコードをたった1行追加するだけです。

def interpolate_quick(fg, bg, alpha)
  alpha &= 0xff
  alpha_com = 255 - alpha
  new_r = int8_mult(alpha, r(fg)) + int8_mult(alpha_com, r(bg))
  new_g = int8_mult(alpha, g(fg)) + int8_mult(alpha_com, g(bg))
  new_b = int8_mult(alpha, b(fg)) + int8_mult(alpha_com, b(bg))
  rgb(new_r, new_g, new_b)
end

ここで生成されるグラフはずっとシンプルになり、マシン語コードは640バイトになりました。オーバーフローのノードもjo命令も消え去ったのに、ちゃんと動作します。つまり、このコードにあったオーバーフローのコストは完全に一掃されたのです。

...
    0x124f5a3b9:    and edx, 0xff0000     # the mask
...
    0x124f5a3df:    sub edx, r10d
    0x124f5a3e2:    mov r9d, esi
    0x124f5a3e5:    and r9d, 0xff0000
    0x124f5a3ec:    shr r9d, 0x10
    0x124f5a3f0:    imul    r9d, edx
    0x124f5a3f4:    lea r9d, [r9 + 0x80]
    0x124f5a3fb:    mov ecx, r9d
    0x124f5a3fe:    shr ecx, 8
    0x124f5a401:    add ecx, r9d
    0x124f5a404:    shr ecx, 8
    0x124f5a407:    add r8d, ecx
    0x124f5a40a:    shl r8d, 0x10
    0x124f5a40e:    mov r9d, eax
    0x124f5a411:    and r9d, 0xff00
    0x124f5a418:    shr r9d, 8
    0x124f5a41c:    imul    r9d, r10d
    0x124f5a420:    lea r9d, [r9 + 0x80]
    0x124f5a427:    mov ecx, r9d
    0x124f5a42a:    shr ecx, 8
    0x124f5a42d:    add ecx, r9d
...

今度は例の組み込みを試してみましょう。

def interpolate_quick(fg, bg, alpha)
  alpha = Truffle::Graal.inject_stamp(alpha, 0xff)
  alpha_com = 255 - alpha
  new_r = int8_mult(alpha, r(fg)) + int8_mult(alpha_com, r(bg))
  new_g = int8_mult(alpha, g(fg)) + int8_mult(alpha_com, g(bg))
  new_b = int8_mult(alpha, b(fg)) + int8_mult(alpha_com, b(bg))
  rgb(new_r, new_g, new_b)
end

生成されたマシン語コードは614バイトで、元のコードの60%未満です。ビットマスクand命令がなくなったので、さらにコンパクトになりました。

このマシン語コードは元のコードよりずっとコンパクトかつ素直で、大きく改善されているように見えます。しかし実際の実行時間には影響するでしょうか?

答えはイエスです。ビットマスクを使うと倍近く高速になります。しかもこれは純粋なRubyコードを1行追加しただけなのです。組み込み(inject_stamp)の場合もほとんど遅くならず、すべて誤差の範囲です。

おや、先ほどの「jo命令は大して遅くならない」という話はどうなったのでしょうか。私はこれまでずっとそう教わってきましたが、どうやらこの点をもう一度見直してもう少し測定してみるべきかもしれません。また、線形のオーバーフローチェックを除去すると一般にグラフ全体が和らぐので、おそらくスケジューリングも改善されるでしょう。これについても測定できればと思います。

純粋なお楽しみとして、洗練されたスタンプシステムを備えたTruffleRubyをMRIやその他の最適化済みRuby実装と比較してみましょう。

このベンチマークでは、ビットマスクは他のシンプルな実装では効果が目立ちませんが、TruffleRubyはYARVの200倍高速、MJITの140倍高速、JRubyの50倍高速です。

それではまとめに入りましょう。純粋なRubyで算術演算がオーバーフローしないことをコンパイラにヒントとして伝える方法はたしかに存在します。やるべきことは、コンパイラが利用できる形で証明を書くことだけです。本物のRubyコンパイラならその証明を利用できます(他のコンパイラも利用できるはずです)。これは実際にパフォーマンスに有益な影響をもたらします。

asm.jsにも関連している

値を& 0xffでビットマスクするというアイデアにどこかで見覚えがありませんか。asm.jsはJavaScriptのサブセットで、本記事でやっているのと同じ要領でより多くのハードウェアプリミティブを用いて効率を高める設計になっています。Asm.jsサブセットでは、value | 0というパターンを用いて値を強制的にマシン語整数として扱います。

関連記事

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

The post Rubyの整数オーバーフローチェックを「スタンプ」で一掃する(翻訳) first appeared on TechRacho.

Rails 5.1〜7.0: ‘form_with’ APIドキュメント(翻訳)

$
0
0

更新情報

  • 更新(2019/09/13): Rails 6.0時点のドキュメント更新を反映。
  • 更新(2020/03/09): Rails 6で削除された項目を反映。
  • 更新(2021/01/22): Rails 6.1の変更を反映(edge APIドキュメントに基づいています)。6.1 edgeドキュメントで追加または変更されたパラグラフ冒頭には「(6.1 edge)」を追加しています。
  • 更新(2022/01/06): Rails 7の更新を反映し、(6.1 edge)を削除。

こんにちは、hachi8833です。Rails 6.1の#form_withのAPIドキュメント変更を反映しました。form_withはRailsのビューでフォームを送信するためのヘルパーメソッドです。

5.1より前のform_forform_tagはその後非推奨になりました。5.1以降はこのform_withだけを使いましょう。

参考: Provide form_with as a new alternative to form_for/form_tag · Issue #25197 · rails/rails

概要

MITライセンスに基づいて翻訳・公開いたします。

原文の更新や訳文の誤りにお気づきの方は、@hachi8833までお知らせください。

Rails 5.1〜7.0: ‘form_with’ APIドキュメント(翻訳)

# API呼び出し
form_with(model: nil, scope: nil, url: nil, format: nil, **options)

URL、スコープ、モデルの組み合わせを元にformタグを作成します。

⚓ URLのみを指定する

<%= form_with url: posts_path do |form| %>
<%= form.text_field :title %>
<% end %>

# =>
<form action="/posts" method="post">
  <input type="text" name="title">
</form>

訳注

<!-- Rails 6.1まで -->
<form action="/posts" method="post" data-remote="true">

Rails 6.1までの出力結果(上)にはdata-remote="true"が含まれていますが、Rails 7の出力には含まれていません(下)。この項を含め、出力結果はRails 7のものを反映しました。

<!-- Rails 7 -->
<form action="/posts" method="post">

意図的に空URLを渡す(Rails 7

<%= form_with url: false do |form| %>
  <%= form.text_field :title %>
<% end %>
# =>
<form method="post" data-remote="true">
  <input type="text" name="title">
</form>

⚓ inputフィールド名にスコープのプレフィックスを追加する

<%= form_with scope: :post, url: posts_path do |form| %>
  <%= form.text_field :title %>
<% end %>

# =>
<form action="/posts" method="post">
  <input type="text" name="post[title]">
</form>

⚓ 渡されたモデルからURLとスコープを自動推測する

<%= form_with model: Post.new do |form| %>
  <%= form.text_field :title %>
<% end %>

# =>
<form action="/posts" method="post">
  <input type="text" name="post[title]">
</form>

⚓ 既存のモデルを更新するフォームで、モデルの値をフィールドに表示する

<%= form_with model: Post.first do |form| %>
  <%= form.text_field :title %>
<% end %>

# =>
<form action="/posts/1" method="post">
  <input type="hidden" name="_method" value="patch">
  <input type="text" name="post[title]" value="<postのtitle>">
</form>

⚓ フォームのフィールドは、必ずしもモデルの属性と対応してなくてもよい

<%= form_with model: Cat.new do |form| %>
  <%= form.text_field :cats_dont_have_gills %>
  <%= form.text_field :but_in_forms_they_can %>
<% end %>

# =>
<form action="/cats" method="post">
  <input type="text" name="cat[cats_dont_have_gills]">
  <input type="text" name="cat[but_in_forms_they_can]">
</form>

フォームのパラメータは、コントローラでパラメータのネストに沿ってアクセスできます。つまり、inputフィールドにtitlepost[title]というフィールド名がある場合、コントローラではそれぞれparams[:title]params[:post][:title]としてアクセスできます。

フォームのinputフィールド名 コントローラのparams
title params[:title]
post[title] params[:post][:title]

上述のコード例では、比較しやすさのため送信ボタンを省略しています。また、UTF-8サポートを有効にする自動生成のhiddenフィールドや、CSRF(Cross Site Request Forgery)保護に必要な認証トークンも省略しています。

⚓ リソース指向のスタイル

上述のコード例の多くでは、単にform_with:modelを渡しています。これはRESTfulなルーティングのセットに対応しており、そのほとんどはconfig/routes.rbのresourcesで定義されます。

したがって、そうしたモデルのレコードを1件渡せば、RailsがURLやメソッドを推測します。

<%= form_with model: @post do |form| %>
  ...
<% end %>

上のコードは以下のようなコードと同等になります。

<%= form_with scope: :post, url: post_path(@post), method: :patch do |form| %>
  ...
<% end %>

新規レコードについても同様です。

<%= form_with model: Post.new do |form| %>
  ...
<% end %>

上のコードは以下のようなコードと同等になります。

<%= form_with scope: :post, url: posts_path do |form| %>
  ...
<% end %>

⚓ #form_withで利用できるオプション

:url
フォームの送信先URLを指定します。
渡せる値は、url_forlink_toで渡せる値と似ています。たとえば、名前付きルートを直接渡すこともできますし、:urlなしで:scopeを渡すと、現在のURLにフォームを送信することもできます。
:method
フォーム送信時のHTTPメソッド(verb)を指定します。
通常は:get:postを指定します。
:patch:put:deleteを指定すると、隠しinput名の後ろに_methodが追加され、POST verb上でこれらのHTTP verbをシミュレートします。
:format
フォーム送信先であるルーティングのフォーマットを指定します。
:jsonなど通常と異なるリソースタイプを送信するのに便利です。
:urlがオプションに渡されている場合、このオプションはスキップされます。
:scope
inputフィールド名のプレフィックスにスコープを追加します。これにより、送信されたパラメータをコントローラでグループ化できます。
:namespace
フォームの要素でid属性を一意にする名前空間です。namespace属性で指定した名前にアンダースコアを追加したものが、生成されたHTML idの前に追加されます。
:model
:url:scopeの自動推測に使うモデルオブジェクトを指定し、inputフィールドにモデルの値を表示します。
たとえば、title属性の値が"Ahoy!"ならtitleの入力フィールドの値に"Ahoy"と表示されます。
モデルが新しいレコードの場合は作成用フォームが生成され、モデルが既存のレコードの場合は更新用フォームが生成されます。
デフォルトの動作を上書きするには、:scope:urlを渡します(params[:post]params[:article]に変更するなど)。
:authenticity_token
フォームで使う認証トークンを指定します。
カスタムの認証トークンを指定して上書きすることも、falseを渡して認証トークンのフィールドをスキップすることもできます。
有効なフィールドのみに制限されている支払用ゲートウェイへのような外部リソースにフォームを送信する場合に便利です。
config.action_view.embed_authenticity_token_in_remote_forms = falseを指定すると、埋め込み認証トークンがremoteフォームで省略されることがあります。この指定はフォームでフラグメントキャッシュを使う場合に便利です(remoteフォームがmetaタグから認証トークンを取得するようになるので、JavaScriptがオフになっているブラウザをサポートする場合を除けば認証トークンをフォームに埋め込む必要がなくなります)。
:local
Rails 7)標準のHTTPフォームを送信するかどうかを指定します。trueを指定すると、フォームは標準のHTTPで送信されます。falseを指定すると、フォームは「リモートフォーム」として送信され、Rails UJSによってXHRとして処理されます。指定のない場合の振る舞いは、config.action_view.form_with_generates_remote_forms設定から導出されますが、この設定の値は実際にはlocalの値と逆です。Rails 6.1では、この設定オプションはデフォルトでfalseになります(local: trueを渡すのと同等)。それより前のバージョンのRailsでは、この設定オプションはデフォルトでtrueになります(local: falseを渡すのと同等)。
:skip_enforcing_utf8
trueを指定すると、送信時にutf8という名前の隠しフィールドが出力されなくなります。
:builder
フォームのビルドに使うオブジェクトをオーバーライドします。
:id
HTMLのid属性を指定します(オプション)。
:class
HTMLのclass属性を指定します(オプション)。
:data
HTMLのdata属性を指定します(オプション)。
:html
上以外のHTML属性を使う場合に指定します(オプション)。

⚓

#form_withにブロックを渡さない場合は、開始formタグを生成します。

# 名前付きパスを指定する場合
<%= form_with(model: @post, url: super_posts_path) %>

# スコープを追加する場合
<%= form_with(model: @post, scope: :article) %>

# フォーマットを指定する場合
<%= form_with(model: @post, format: :json) %>

# トークンを無効にする場合
<%= form_with(model: @post, authenticity_token: false) %> 

ルーティングをadmin_post_urlのように名前空間化する場合は以下のようにします。

<%= form_with(model: [ :admin, @post ]) do |form| %>
  ...
<% end %>

たとえばリソースに関連付けが定義されているとします。ルーティングが正しく設定されているdocumentにcommentを追加したい場合は次のようにします。

<%= form_with(model: [ @document, Comment.new ]) do |form| %>
  ...
<% end %>

上のdocumentには@document = Document.find(params[:id])が既に与えられているとします。

更新(2020/03/09)以下はRails 6.0で削除されました。

⚓ 他のフォームヘルパーと組み合わせる

#form_withではFormBuilderオブジェクトが使われていますが、単独のFormHelperのメソッドやFormTagHelperのメソッドと共存させることもできます。

<%= form_with scope: :person do |form| %>
  <%= form.text_field :first_name %>
  <%= form.text_field :last_name %>

  <%= text_area :person, :biography %>
  <%= check_box_tag "person[admin]", "1", @person.company.admin? %>

  <%= form.submit %>
<% end %>

同様に、FormOptionsHelperのメソッド(FormOptionsHelper#collection_selectなど)と共存させたり、DateHelperのメソッド(ActionView::Helpers::DateHelper#datetime_selectなど)と共存させることもできます。

⚓ HTTPメソッド(verb)の指定方法

以下のHTTP verbの完全な配列をoptionsハッシュに渡すことができます。

method: (:get|:post|:patch|:put|:delete)

verbがGETPOST以外の場合(この2つはHTMLフォームでネイティブでサポートされます)、フォームそのものにはPOST verbが設定され、_methodという名前の隠しinputフィールドには指定の verbが設定され、後者がサーバーで解釈されます。

⚓ HTMLオプションの設定方法

HTMLのdata-*属性はdata:ハッシュで直接渡せますが、id:class:を含む他のすべてのHTMLオプションについては次のようにhtml:ハッシュの中に置く必要があります。

<%= form_with(model: @post,
              data: { behavior: "autosave" },
              html: { name: "go" }) do |form| %>
  ...
<% end %>

上のコードから以下のHTMLが生成されます。

<form action="/posts/123" method="post" data-behavior="autosave" name="go">
  <input name="_method" type="hidden" value="patch" />
  ...
</form>

⚓ 非表示のモデルidを出力しないようにする

#form_withメソッドを使うと、自動的にモデルidが隠しフィールドとしてフォームに含まれます。このモデルidは、フォームデータとそれに関連付けられているモデルとの関連を保つために使われます。

ORMシステムによってはネストしたモデルでこうしたidを使わないものもあるので、その場合は次のようにinclude_id: falseを指定することで隠しフィールドのモデルidを出力しないようにできます。

<%= form_with(model: @post) do |form| %>
  <%= form.fields(:comments, skip_id: true) do |fields| %>
    ...
  <% end %>
<% end %>

上の例では、NoSQLデータベースにPostというモデルがひとつと、それに一対多で関連付けられるCommentというモデルが保存されています。:commentsには主キーはありません。

⚓ フォームビルダをカスタマイズする

FormBuilderクラスをカスタマイズしてフォームをビルドすることもできます。カスタマイズするには、FormBuilderを継承してサブクラスを作り、必要なヘルパーメソッドを定義またはオーバーライドします。

次のコード例では、フォームのinputにラベルを自動追加するヘルパーを作成済みであることが前提です。

<%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %>
  <%= form.text_field :first_name %>
  <%= form.text_field :last_name %>
  <%= form.text_area :biography %>
  <%= form.check_box :admin %>
  <%= form.submit %>
<% end %>

上のようにコードを書いてから、次のコードを書きます。

<%= render form %>

これにより、people/_labelling_formというテンプレートを使ってレンダリング(=HTML生成)され、フォームビルダを参照するローカル変数の名前はlabelling_formになります。

特に指定しない限り、カスタムのFormBuilderクラスは、ネストした#fields_for呼び出しのオプションと自動的にマージされます。

上のようなコードを別のヘルパーにも含めておきたい場合は、以下のように書くこともできます。

def labelled_form_with(**options, &block)
  form_with(**options.merge(builder: LabellingFormBuilder), &block)
end

関連記事

Rails 6.1で form_withのデフォルトが「remoteなし」になった(翻訳)

The post Rails 5.1〜7.0: ‘form_with’ APIドキュメント(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: rails-ujs->Turboアップグレードガイド、RubyとWeb Componentsほか(2022017前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は以下の差分のうちChangelogが変更されているものを見繕いました。

🔗 Active Jobのシリアライズテストの失敗時にdiscard_onretry_onを利用できるよう修正

従来のperform_enqueued_jobsでは、deserialize_arguments_if_neededperform_nowを呼び出す前に呼び出されていた。レコードが既に存在せず、GlobalIDでシリアライズされている場合だと、perform_nowを呼び出す前にActiveJob::DeserializationErrorエラーが発生していた。このため、ジョブでdiscard_onretry_onのロジックがテストしにくくなっていた。
この修正では、deserialize_arguments_if_needed呼び出しがperform_now呼び出しまで延期されるようになる。

class UpdateUserJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError

  def perform(user)
      # ...
   end
end

# テストコード
User.destroy_all
assert_nothing_raised do
  perform_enqueued_jobs only: UpdateUserJob
end
assert_no_enqueued_jobs

従来は上のテストコードが失敗したが、修正によってパスするようになる。
同PRより


つっつきボイス:「Active Jobでデシリアライズ時にGlobalIDを解決できないとエラーが発生してリトライや破棄ができなかった問題が修正されたんですね」「本来こうあるべき挙動という感じですね」

# activejob/lib/active_job/test_helper.rb#L663
      def flush_enqueued_jobs(only: nil, except: nil, queue: nil, at: nil)
        enqueued_jobs_with(only: only, except: except, queue: queue, at: at) do |payload|
          queue_adapter.enqueued_jobs.delete(payload)
          queue_adapter.performed_jobs << payload
-         instantiate_job(payload).perform_now
+         instantiate_job(payload, skip_deserialize_arguments: true).perform_now
        end.count
      end
# ...
-     def instantiate_job(payload)
+     def instantiate_job(payload, skip_deserialize_arguments: false)
        job = payload[:job].deserialize(payload)
        job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
-       job.send(:deserialize_arguments_if_needed)
+       job.send(:deserialize_arguments_if_needed) unless skip_deserialize_arguments
        job
      end

rails/globalid - GitHub

🔗 update_attribute!が追加

新しいActiveRecord::Persistence#update_attribute!メソッドを追加する。update_attributeと似ているが、saveではなくsave!を呼び出す。update_attribute!メソッドはbefore_*コールバックが:abortをスローしたときにActiveRecord::RecordNotSavedエラーをraiseする。

class Topic < ActiveRecord::Base
  before_save :check_title

  def check_title
    throw(:abort) if title == "abort"
  end
end

topic = Topic.create(title: "Test Title")
# #=> #<Topic title: "Test Title">
topic.update_attribute!(:title, "Another Title")
# #=> #<Topic title: "Another Title">
topic.update_attribute!(:title, "abort")
# raises ActiveRecord::RecordNotSaved

同PRより


つっつきボイス:「!付きのActiveRecord::Persistence#update_attribute!メソッドが追加された」「!なしのupdate_attributeはエラーをraiseしないのか」「たしかそうですね」「それなら整合性の面でも!ありのメソッドはあっていいですね」「ちなみにsavesave!の違いもエラーをraiseするかどうかですね」「普段updateしか使ってなかったのでupdate_attributeの挙動を知らなかった」「そういえば私もupdateしか使ってなかった」

🔗 Relation#pretty_printのeager loadingを回避

#inspectの振る舞いにならって、レコードが読み込み済みでない場合は要素を最大11個までしかフェッチしないようにした。
同PRより


つっつきボイス:「Relation#pretty_printで出力が大量にならないように11個までしかフェッチしないようにしたみたいですね」「お〜、これはいい👍」「あ、なるほど、自分もppをよく使うので助かります」「rails consoleで不用意にppして大量の出力でコンソールが固まらずに済む」

# activerecord/lib/active_record/relation.rb#L776
-   def pretty_print(q)
-     q.pp(records)
+   def pretty_print(pp)
+     subject = loaded? ? records : annotate("loading for pp")
+     entries = subject.take([limit_value, 11].compact.min)
+
+     entries[10] = "..." if entries.size == 11
+
+     pp.pp(entries)
    end

🔗 QueryMethods#in_order_ofの挙動をEnumerableに合わせた

in_order_ofが、Enumerable版のin_order_ofの挙動に合わせて、指定された値のリストでフィルタされるようになった。
これは#43916のコメントに対応したもの。振る舞いはEnumerable版に合うようになったが、リリース後の移行パスをどうすればよいかまだわからない。バグとみなしてパッチリリースの対象にする?
同PRより


つっつきボイス:「Enumerablein_order_ofに合わせて、リストの値にない値を含めないようになったようですね」

# activerecord/lib/active_record/relation/query_methods.rb#L439
    def in_order_of(column, values)
      klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
      return spawn.none! if values.empty?

      references = column_references([column])
      self.references_values |= references unless references.empty?

-     if values.empty?
-       spawn.order!(column)
-     else
+       values = values.map { |value| type_caster.type_cast_for_database(column, value) }
+     values = values.map { |value| type_caster.type_cast_for_database(column, value) }
-     arel_column = column.is_a?(Symbol) ? order_column(column.to_s) : column

-       arel_column = column.is_a?(Symbol) ? order_column(column.to_s) : column
-       spawn.order!(connection.field_ordered_value(arel_column, values), column)
-     end
+     spawn
+       .order!(connection.field_ordered_value(arel_column, values))
+       .where!(arel_column.in(values))
    end

🔗 Rails 7.1からconfig.add_autoload_paths_to_load_pathがデフォルトで無効になる

参照: 668673f
現在のオートローダーはZeitwerkのみとなったので、オートロード済みのパスを$LOAD_PATHに追加する理由がなくなった(7.0までにやれればよかったのだけど)。
@fxn 問題がなさそうならメンションくれればマージできます。
同PRより


つっつきボイス:「config.add_autoload_paths_to_load_path、長い設定名だな〜」「Railsガイドを7に更新翻訳したときにこの設定を見かけました」「今まではオートロードしたパスを$LOAD_PATHにしこしこコピーしていたけど、Rails 7がZeitwerkのみになって不要になったので、このプルリクでそれをデフォルトで無効にしようということか、なるほど」

参考: 3.1.5 config.add_autoload_paths_to_load_path
— Rails アプリケーションを設定する – Railsガイド

$LOAD_PATHにオートロードパスを足すべきかどうかを指定します。このフラグはデフォルトでtrueですが、:zeitwerkモードでは早い段階でconfig/application.rbでfalseに設定することをおすすめします。Zeitwerkは内部で絶対パスが使われ、:zeitwerkモードで動作するアプリケーションではrequire_dependencyが不要なので、モデルやコントローラやジョブなどが$LOAD_PATHに存在する必要はありません。これをfalseに設定すると、requireの解決が相対パスで呼び出されるときにRubyがそれらのディレクトリのチェックを削減でき、それらのインデックスの構築が不要になるのでBootsnapの動作やメモリも節約できます。
Railsガイドより

「昔の記事によくあった『まずオートロードパスを追加しましょう』みたいなことはもうしなくていいんだ」「Zeitwerkになってから、appディレクトリの下に置いたものは何もしなくてもオートロードされるようになりましたよね」「そうそう」

参考: 定数の自動読み込みと再読み込み (Zeitwerk) – Railsガイド

🔗 6.1リリースノートにAction Mailboxの非推奨事項を追加

6.1リリースノートを更新して、Action Mailboxの非推奨項目を追加した。6.1のChangelogには以下が記載されているが、Railsguidesのリリースノートに含まれていなかったので。

Deprecate Rails.application.credentials.action_mailbox.api_key and MAILGUN_INGRESS_API_KEY in favor of Rails.application.credentials.action_mailbox.signing_key and MAILGUN_INGRESS_SIGNING_KEY.

同PRより


つっつきボイス:「過去のリリースノートに追記するのが珍しいと思ったので取り上げてみました」「よくぞ抜けに気づきましたよね」「もしかして自分で踏んだからプルリクあげたのかも?」

🔗 デフォルトのヘッダーからX-Download-Optionsを削除

X-Download-Optionsは、deprecation間近のInternet Explorerでフィッシング攻撃を防ぐためにしか使われていないので、デフォルトのヘッダーから削除するのが筋。
解決されるissue: #43948
新しいデフォルトヘッダーはRails 7.1からフレームワークのデフォルトになる。
同PRより


つっつきボイス:「X-Download-OptionsがIEでしか使われないヘッダーだから消しましょうということだそうです」「IEだけなら消すのが順当でしょうね」

# guides/source/security.md#L1038
config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '0',
  'X-Content-Type-Options' => 'nosniff',
- 'X-Download-Options' => 'noopen',
  'X-Permitted-Cross-Domain-Policies' => 'none',
  'Referrer-Policy' => 'strict-origin-when-cross-origin'
}

X-Download-Optionsの解説↓を見ると、アプリケーションからダウンロードしたファイルをブラウザのファイルオープンダイアログに表示しないようにするためのヘッダーなのか」

参考: HTTP headers – HTTP | MDN

X-Download-Options
The X-Download-Options HTTP header indicates that the browser (Internet Explorer) should not display the option to “Open” a file that has been downloaded from an application, to prevent phishing attacks as the file otherwise would gain access to execute in the context of the application. (Note: related MS Edge bug )
developer.mozilla.orgより

🔗Rails

🔗 RubyとWeb Componentsを連携させる(Ruby Weeklyより)


つっつきボイス:「Bridgetownというstatic site generatorにRubyコードベースのテンプレートをWeb Componentsにmappingする機能があって、記事中前半のコードはこのBridgetownのWeb Components連携機能のようですね」「なるほど」「Star Rating Componentから先がViewCompoment gemを使ったRailsでの実装の話のようです: ViewComponentを素で使うだけじゃなく、Ruby2JSを使ってRubyでWeb ComponentsのJS classを定義している感じ」

参考: Ruby Components | Bridgetown

bridgetownrb/bridgetown - GitHub

参考: Web Components | MDN
参考: Ruby2JS: an extensible Ruby to modern JavaScript transpiler

# 同記事より
# src/_components/note.rb
class Note < Bridgetown::Component
  def initialize(type: :primary, icon: nil)
    @type, @icon = type.to_sym, icon
  end

  def icon
    return @icon if @icon

    case @type
    when :primary
      "system/information"
    when :warning
      "system/alert"
    end
  end
end

「拡張子はserbなのか」「SerbeaというERBのオルタナらしい↓」「Serbea自体はテンプレートエンジンのようですね」

参考: Serbea: Similar to ERB, Except Awesomer

「記事の後半でViewComponent(ウォッチ20200330)にも触れていますが↓、ViewComponent gem自体にはWeb Componentsの統合の機能はなくて、この記事ではLit&Ruby2JSを使ってViewComponentの吐き出すHTMLをRubyで書いてRuby2JSに変換したJS classでうまいことWeb Componentsとして連携させている、という感じですね」「なるほど」

参考: Overview – ViewComponent
参考: Lit
参考: 今後Railsで話題になるかもしれないViewComponentを試した | Webuilder240.com
参考: View Componentのすすめ①

github/view_component - GitHub

# viewcomponent.orgより
# app/components/message_component.rb
class MessageComponent < ViewComponent::Base
  def initialize(name:)
    @name = name
  end

  def call
    @output_buffer.safe_append='<h1>Hello, '.freeze
    @output_buffer.append=( @name )
    @output_buffer.safe_append='!</h1>'.freeze
    @output_buffer.to_s
  end
end

🔗 Turbo関連記事

つっつきボイス:「Turboについて調べていて、いくつか目についた記事です: 1つめは上のアップグレードガイドです」「rails-ujsやTurbolinksからTurboへのアップグレードガイドか」「ドキュメントの量はそんなに多くない感じです」

「rails-ujsが担当していた機能はそんなに多くないので、考慮するものの種類は少ないんですが、rails-ujsが使われているUIの数は少なくないんですよね」「たしかに」「あって当然の機能だっただけに、完全に移行したことを確認する工数はかかりそう」


「2つめは、この間のjnchitoさんの記事でも扱われていたstatus: :unprocessable_entityを付ける話や、turbo-frameを制御する方法などの記事ですね」


「3つ目は1年前の記事ですが、今になって気づきました↓」「お、これは読んだことあったかも」「Hotwireを開発のプログレッシブエンハンスメントという観点から説明してくれているのが自分にとってありがたかったです」

参考: Progressive Enhancement (プログレッシブエンハンスメント) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

「RailsとHotwire関連はこの半年ぐらいで大きく進みましたよね」「そうそう」「記事はその前に書かれているので、変更点に注意して読めば大丈夫そう👍

🔗 DeviseのRails 7対応状況


つっつきボイス:「DeviseにRails 7のサポートが入りましたけど↓、上のChangelogではまだTurboの対応は完全ではないとありました」「自分はDeviseのデフォルトビューをほとんど使ってませんけどね」

「ちなみにDeviseに5-rcブランチがあったのを見かけたんですが、2019年からそのままでした」「Deviseの開発体制はわからないけど、とりあえず作ったものっぽいのでまた作り直すんじゃないかな」「そうかもしれませんね」

🔗 その他Rails

つっつきボイス:「jnchitoさんがQiitaのAdvent Calendar 2021 Online Meetupと銀座Rails#41に登壇するそうです」「そうそう、例の点字メーカープログラム↓に参加した人は銀座Railsでjnchitoさんが公開レビューしてくれますよ」

JunichiIto/tenji-maker-challenge-for-ginza-rails - GitHub

参考: Qiita Advent Calendar Online Meetup – connpass
参考: 【オンライン開催】銀座Rails#41 – connpass


以下はつっつき後のツイートです。おめでとうございます🎉


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rails 7をRuby 3.1で動かす、クックパッドのRuby 3.1解説記事、Rails 6->7更新ほか(20220112)

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

The post 週刊Railsウォッチ: rails-ujs->Turboアップグレードガイド、RubyとWeb Componentsほか(2022017前編) first appeared on TechRacho.

週刊Railsウォッチ: Ruby 2.5〜3.1ベンチマーク、Opal 1.4、JRubyが20歳に、2022年のCSSほか(2022018後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 Ruby 2.5〜3.1のベンチマークを取ってみた(Ruby Weeklyより)


つっつきボイス:「HexaPDFとkramdownとgeom2dでRuby 2.5〜3.1のベンチマークを取ってみたそうです」「Ruby 2.5から3.1で機能も随分変わりましたよね」「縦のグラフは実行時間だから短いほど速いのか」「横のグラフはインストラクション/秒なので長いほど速い↓」「記事の下の方のグラフが見やすいかも」


同記事より: geom2d

「こうしてみるとYJITは速いですね」「MJITは、実行するプログラムの実行時間が長い(JITで高速化されたコードを実行する頻度が高い)、同じ処理ブロックを何度も実行するようなユースケースに合う感じですね、optcarrotのようなゲームエミュレータとか」

mame/optcarrot - GitHub

「自分は3.1のYJITを環境変数(RUBY_YJIT_ENABLE=1)で常にオンにしてます」「そうそう、環境変数でできるんですよね」「YJITはほぼノーリスクで導入できると思ってよいでしょうね: バイナリを書き換えるようなネイティブコードはめったにないでしょうし」

「YJITはまだM1チップでは動かないんでしたっけ?」「まだx86-64(amd64)のみ対応ですね」「M1だとYJITをオンにしても効かなさそう」

🔗 Opal 1.4がリリース(Ruby Weeklyより)


つっつきボイス:「Opalはずっと精力的にアップデートしていますね」「お〜すごい」「RubyからJavaScriptへのトランスパイラですね」「JSのPromiseにも対応しているとは」

「記事によると、parser↓がRuby 3.1の新機能サポートでとても役に立ったそうです」「parserはRuboCopでも使われているパーサーですね」「名前が地味でちょっと探しにくいですけどね」「RuboCopで必要なパースができるgemならたいていのことはできそう」「RuboCopはちょっと驚くような項目まで検出できるのが凄い」

whitequark/parser - GitHub

RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳)

🔗 Ruby 3.0に入ったHash#exceptRuby Weeklyより)

jemma = { name: "Jemma",
          username: "jemma",
          password: "super secure" }

jemma.except(:password)
# => { name: "Jemma", username: "jemma" }

つっつきボイス:「Ruby Weeklyの巻末でRuby 3.0に入ったHash#exceptを知りました」「そうそう、これ入っていましたね」

参考: class Hash (Ruby 3.1 リファレンスマニュアル)

「Ruby Weeklyの記事によると、RailsのActive SupportにHash#exceptが入ったのが15年も前で↓、それがついにRuby 3.0で取り入れられたそうです」「15年も!」「コミットを見るとRubyがSubversionを使ってた頃じゃないですか」「まだGitHubもなかった頃かも」「15年越しの出世メソッド」「こういう情報があると楽しい😋

参考: Added Hash#except which is the inverse of Hash#slice — return the ha… · rails/rails@cb23816
参考: Apache Subversion – Wikipedia

ちなみにRails 7のActive Supportには今もHash#exceptが入っています↓。

参考: Rails API Hash#except

🔗 standard: Rubyのスタイルガイド・linter・フォーマッタ(Ruby Weeklyより)

testdouble/standard - GitHub

参考: standardスタイルを採用しているRubyプロジェクトのリスト


つっつきボイス:「こういうコーディング標準は他にもいろいろ出ていますよね」「このgemはStandardJS↓の精神にインスパイアされて作ったものだそうです」

参考: JavaScript Standard Style
参考: Ruby Style Guide — ShopifyのRubyスタイルガイド
参考: styleguide/ruby.ja.md at master · cookpad/styleguide — CookpadのRubyスタイルガイド

「ゼロコンフィグでオートコレクトできて、RuboCopとも仲良くやれるそうです」「RuboCopならたいていのものは定義できますけどね」「導入するかどうかはプロジェクトや好みによるのかなとは思いますが、rubocop -aのようにオートコレクトできるものなら即導入できるでしょうね👍」「たしかに」

🔗 その他Ruby

つっつきボイス:「ojというJSONパーサー兼Objectマーシャリングgemを改修したそうです」「へ〜、Cのsnprintf関数にRuby版もあるのか」

ohler55/oj - GitHub

参考: snprintf | Programming Place Plus C言語編 標準ライブラリのリファレンス


「今年の10月13〜14日にヨーロッパのRubyイベントであるEurokoが開催されるそうです」「オンラインとリモート両方なのか」「他の国のイベントは感染リスクを管理することで開催するようになってきていますけど、日本はなかなかそうならないのが残念」「まだ続くのかな…」

「今回の開催地はフィンランドのヘルシンキだそうです」「行きたいけど遠い国」「修士の頃に学会で一度ヘルシンキに行ったことがありますけど、夜になっても本当に明るいんですよ」「さすが白夜の国」「明るくても20時頃になると人通りが急に減るのが面白かった」


「JRubyも20歳🎉」「Rubyの別実装であるJRubyがこれだけの期間ユーザーに使われていて、しかも継続的にメンテナンスされているのがつくづくすごいですよね」

「ところで今のJRubyってCRubyのどのバージョンに該当するんだろう?」「あ、そういえば」「公式サイトを見るとRuby 2.6.xコンパチと書かれてる↓」「2.6だと本家のサポート終了も遠くないですよね」「CRuby 2.6のサポート終了って今年の3月末じゃないですか↓」「2.7に追いつかないと」

参考: Home — JRuby.org
参考: Ruby Maintenance Branches

🔗DB

🔗 immudb: ゼロトラストのデータベース(Ruby Weeklyより)

codenotary/immudb - GitHub


つっつきボイス:「Ruby Weeklyで以下のimmdb-rubyが紹介されていて↓、上のimmudbのラッパーだそうです」「immudbのREADMEを見たところ、SQLとキーバリューストアのインターフェイスを両方持っているけど、スキーマフリーのアーキテクチャとあるのでRDBとは少し違うものかな: gRPCやACIDをサポートしているので用途に合うなら使いたい人はいそう」

ankane/immudb-ruby - GitHub

「実装でMarkle Treeというデータ構造を使っているのがポイントみたい↓」「マークルツリーって初めて聞きました」「ブロックチェーンなどで使われるハッシュツリーの一種のようなので、そういう用途に合いそう」「レコードは追加のみで削除や変更を行わないのがゼロトラストということみたい」「改ざんの検出に強そう」

参考: トランザクションデータを要約する技術「マークルツリー」

「immdb-rubyは普通にラッパーのようだけど、verified_setverified_getというのもあるのね↓」

# ankane/immudb-rubyより
immudb.verified_set("hello", "world")
immudb.verified_get("hello")

🔗JavaScript

🔗 JavaScriptの配列


つっつきボイス:「手元で動かしてみたら本当にこうなりました↓」「くっついて34ができちゃってますね」

$ node
Welcome to Node.js v16.3.0.
Type ".help" for more information.
> [1, 2, 3] + [4, 5, 6]
'1,2,34,5,6'
>

「JSの配列に+演算子がないから文字列にキャストされてから文字列として結合されたんでしょうね、以下のような感じで↓」「お〜JSだとそうなるのか」

> String([1, 2, 3])
'1,2,3'

「言われれば説明は可能だけど、Rubyに慣れているとちょっとした驚きがありますね↓」「たしかに」

$ irb
» [1, 2, 3] + [4, 5, 6]
#>[1, 2, 3, 4, 5, 6]

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 フロントエンド学習用無料リソース


つっつきボイス:「paizaさんの記事です」「冒頭にあるのは以前取り上げた学習ロードマップですね(ウォッチ20211215)」「そういえば見覚えあります」「最初にロードマップを見るのはいいと思います👍:」「地図なしで進むよりは地図がある方がいいですよね」

「こういう調べ物用の情報が紹介されているのもいいですね↓」

参考: DevDocs API Documentation

「以下はデザイン関連リソースですね↓」「デザインで悩ましいのは、英語サイトのデザインを日本語サイトで使うと見た目がダサくなりやすいこと」「それほんとに」「英語のデザイン情報の基本概念は日本語とそう変わらないけど、特にテクニック系の情報はなかなかうまくはまらない」「英語前提のサイトデザインに日本語コンテンツを当てはめると何だか残念になってしまいがちですよね」

bradtraversy/design-resources-for-developers - GitHub


「この記事で紹介されているどれでもいいんですが、初めての人はとにかく手を動かして作ってみるのが大事」「ほんとそうですよね」「チュートリアルを一度走り抜けるのが、結局いろんな意味で近道なんですよ」

「逆にコードをクリーンに書くとかプロジェクトのガイドラインとかは、もっと先に進んで中級者になってからでいいと思います: 初めての段階でコードのきれいさにこだわってしまうと、完成までたどり着けなくなりがちなので」「それほんと」「とにかく最初は完走してたくさん作ることが大事ですね」

「さっき見つけたこれも取っ掛かりによさそう↓」「ぷよぷよだ」「サイトに『気合いでソースコードを写経入力する』とさりげなく大事なことが書かれている」「結局は気合いですね」

🔗 2022年のCSS


つっつきボイス:「2022年のCSSについてこの間も似たような記事を見ましたけど、こちらはもう少し手広く扱っているようです」「そういえばコンテナクエリのあたりとかは見覚えあるな」

カラー関数

「カラー関数は初めて見た↓」「お?」「fromのオリジナル色をどのぐらい変えるかを相対指定したりできるらしい: 今までだとopacity(不透明度)でやることが多いんですけど透明化処理の負荷が大きそう」「ダークモードとかに使えそうかな」「色を論理的に指定できるような趣ですね」

// 同記事より
:root {
  --color: #ff0000;
}

.selector {
  /* change the transparency */
  color: hsl(from var(--color) h s l / .5);

  /* change the hue */
  color: hsl(from var(--color) calc(h + 180deg) s l);

  /* change the saturation */
  color: hsl(from var(--color) h calc(s + 5%) l);
}


同記事より

参考: – CSS: カスケーディングスタイルシート | MDN

オーバースクロール

「あ〜、これは操作してみるとわかると思いますけど、以下にはスクロールバーが外側と内側にそれぞれあって、従来だと内側にマウスオーバーした状態でホイールでスクロールすると下に達したときに外側のスクロールバーまで動いてしまっていたんですよ: Enable overscroll-behavior: containチェックボックスをオンにすると、内側のスクロールバーが下に達しても外側のスクロールバーが動かなくなる」

See the Pen
overscroll-behavior: contain
by Aaron Iker (@aaroniker)
on CodePen.

「どれどれ…」「あ、ほんとだ」「外側にマウスオーバーするとちゃんと外側だけスクロールするんですね」「これ以前から気持ち悪いと思ってた動作なので嬉しいです」「これがデフォルトでいいのではとすら思えますね」

アクセントカラー

「これは、OSのUIコンポーネントのアクセントカラーをCSSでオーバーライドできるんですね」「へ〜!」

See the Pen
HTML elements with accent-color
by web.dev (@web-dot-dev)
on CodePen.

メディアクエリのレンジ

「これも話題になってますね」「これは欲しかった機能」「メディアクエリを書くと合っているのかどうかわからなくて不安になりがち」「メディアクエリでサイズの種類が増えると、大きくするときはよくても小さくするときに期待通りにならないみたいなことが起きがちですよね」

/* 同記事より */
/* Old Way */
@media (max-width: 750px) {
    …
}
/* New Way */
@media (width <= 750px) {
    …
}

See the Pen
CSS Media Query Range Context Test
by Bramus (@bramus)
on CodePen.

🔗言語/ツール/OS/CPU

🔗 curlとwgetの違い(StatusCode Weeklyより)


つっつきボイス:「curlとwget対決!」「自分はwget派なんですが、環境にwgetが入っていないことが多くて仕方なくcurlを使うことがよくあります」「まったく同じです〜」

参考: cURL – Wikipedia
参考: GNU Wget – Wikipedia

「wgetがライブラリに依存しない単体バイナリというのをこれで初めて知りました」「curlは、他のライブラリを入れると一緒に入ってくることが多いですよね」「そうそう、wgetはライブラリ依存がない分、インストールしないと入っていないことが多いんですよ」「よく悲しい思いをしてます」「特にDockerで作業していると、curlはたいてい入っているけどwgetはまず入っていない」

「新たに勉強するなら、curlの方が機能が多いのでおすすめかなと思います」「そうそう」

「ただwgetはURLを渡すだけでファイルを保存してくれるけど、curlは-oを渡さないとファイルを保存しないんですよ」「そうそう、バイナリファイルが標準出力に表示されるとコンソールが壊れたりしますね」「curlの-oオプションは忘れがち」

🔗 その他ツール

つっつきボイス:「GitHubのapproveって、誰でもできたんですか?プロジェクトにコミットしていなくても?」「プロジェクトがオープンだとできてしまっていたということですね」「ここに設定がありますね↓」「approveに期限も設定できるんですって」

参考: Limiting interactions in your repository – GitHub Docs


後編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: rails-ujs->Turboアップグレードガイド、RubyとWeb Componentsほか(2022017前編)

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

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

Ruby Weekly

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ: Ruby 2.5〜3.1ベンチマーク、Opal 1.4、JRubyが20歳に、2022年のCSSほか(2022018後編) first appeared on TechRacho.

Rails: サーバーのIPアドレスを環境変数で設定するにはHOSTではなくBINDINGを使う

$
0
0

Rails 7でrails newするときにアセットをビルドするオプションを渡すと、以下が作成されます。

  • bin/dev
    • このbinstubから以下のProcfile.devをforeman経由で呼び出す
  • Procfile.dev

このProcfile.devは、rails newで指定するオプションによって変わりますが、たとえば以下のようになります。

web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch

このセッティングでbin/devを実行すると、サーバーがlistenするIPアドレスは127.0.0.1になります。

しかし、bin/devをdocker-compose内で起動する場合は、サーバーがlistenするIPアドレスを0.0.0.0にしないと手元のブラウザでアクセスできません。

$ dip dev
Creating rails701_ruby31_rails_run ... done
04:09:35 web.1  | started with pid 8
04:09:35 css.1  | started with pid 9
04:09:36 web.1  | => Booting Puma
04:09:36 web.1  | => Rails 7.0.1 application starting in development
04:09:36 web.1  | => Run `bin/rails server --help` for more startup options
04:09:37 web.1  | Puma starting in single mode...
04:09:37 web.1  | * Puma version: 5.5.2 (ruby 3.1.0-p0) ("Zawgyi")
04:09:37 web.1  | *  Min threads: 5
04:09:37 web.1  | *  Max threads: 5
04:09:37 web.1  | *  Environment: development
04:09:37 web.1  | *          PID: 8
04:09:37 web.1  | * Listening on http://127.0.0.1:3000  # これだとブラウザから見えない
04:09:37 web.1  | Use Ctrl-C to stop
04:09:38 css.1  |
04:09:38 css.1  | Rebuilding...
04:09:38 css.1  | Done in 497ms.

参考: Quickstart: Compose and Rails | Docker Documentation

普通に考えれば、Procfile.devに以下のように-b 0.0.0.0を追加することでブラウザからアクセスできるようになります(実際できました)。想像ですが、bin/devを書き換えずにカスタマイズするためにProcfile.devがあるのかもしれません。

web: bin/rails server -p 3000 -b 0.0.0.0
css: bin/rails tailwindcss:watch

これでおしまいにしてもいいのですが、自分は0.0.0.0をdocker-compose.ymlの環境変数で指定したかったので、そのあたりを調べてみました。

ホストアドレスはBINDING環境変数で設定する

調べてみると、現在のRailsでホストがlistenするIPを設定する環境変数はHOSTではありませんでした。

HOST環境変数はかつてRails 5.1で導入されていました↓。

しかしその後、HOST環境変数がSuSE Linuxでバッティングすることがわかり、HOSTを非推奨にしてBINDINGに変更することになりました。

その後、Rails 6.1でHOSTが削除されてBINDINGのみとなり、現在に至ります。

これがわかったので、docker-compose.ymlでBINDING=0.0.0.0を指定することで、Procfile.devを書き換えずにbin/devを使えるようになりました。Procfile.devを使わないpropshaftや普通のアセットパイプラインのセットアップでも同様に0.0.0.0が設定されます。

# docker-compose.yml(抜粋)
  backend: &backend
    <<: *app
    stdin_open: true
    tty: true
    volumes:
      - .:/app:cached
      - rails_cache:/app/tmp/cache
      - bundle:/bundle
      - .dockerdev/.bashrc:/root/.bashrc:ro
      - .dockerdev/.bashrc:/root/.irbrc:ro
      - node_modules:/node_modules
    environment:
      - RUBY_YJIT_ENABLE=1
      - NODE_ENV=development
      - RAILS_ENV=${RAILS_ENV:-development}
      - BINDING=0.0.0.0   # ここで設定
      - BOOTSNAP_CACHE_DIR=/bundle/bootsnap
      - WEB_CONCURRENCY=0
      - HISTFILE=/app/log/.bash_history
      - EDITOR=vi

  runner:
    <<: *backend
    command: /bin/bash
    ports:
      - "3000:3000"
      - "3002:3002"

  rails:
    <<: *backend
    command: bin/dev
    ports:
      - "3000:3000"
$ dip dev
Creating rails701_ruby31_rails_run ... done
03:44:50 web.1  | started with pid 8
03:44:50 css.1  | started with pid 9
03:44:51 web.1  | => Booting Puma
03:44:51 web.1  | => Rails 7.0.1 application starting in development
03:44:51 web.1  | => Run `bin/rails server --help` for more startup options
03:44:51 web.1  | Puma starting in single mode...
03:44:51 web.1  | * Puma version: 5.5.2 (ruby 3.1.0-p0) ("Zawgyi")
03:44:51 web.1  | *  Min threads: 5
03:44:51 web.1  | *  Max threads: 5
03:44:51 web.1  | *  Environment: development
03:44:51 web.1  | *          PID: 8
03:44:51 web.1  | * Listening on http://0.0.0.0:3000  # これでブラウザからアクセスできる
03:44:51 web.1  | Use Ctrl-C to stop
03:44:53 css.1  |
03:44:53 css.1  | Rebuilding...
03:44:53 css.1  | Done in 536ms.

Procfile.devに既に-pオプションがあるので今回は使いませんでしたが、listenするポート番号はPORT環境変数でも設定可能です。

関連記事

Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた

The post Rails: サーバーのIPアドレスを環境変数で設定するにはHOSTではなくBINDINGを使う first appeared on TechRacho.

Viewing all 1823 articles
Browse latest View live