Rackミドルウェアのコードを読んでみた
この記事は「フィヨルドブートキャンプ Advent Calendar 2025」の9日目の記事です。 https://fjord-calendar.jp/calendars/2025
昨日の8日目の記事は、machidaさんの Kaigi on Rails 2025 のデザインについて Vol.1 メインビジュアルの3つの案 | FJORD BOOT CAMP(フィヨルドブートキャンプ) です。
私は、今年の2月にフィヨルドブートキャンプで開催された「Rackを理解するための技術ハンズオンワークショップ」に参加し、さらにRackを深掘りしたいと思ったので、今回はRackミドルウェアのコードを読んでみました。
ワークショップに参加した時の記事はこちら。
疑問
Rackミドルウェアの挙動を確認するために、次のコードを動かしてみました。
class SampleApp def call(env) p '----- SampleApp#call is called -----' [200, { 'content-type' => 'text/html' }, ['Hello World!']] end end class MiddlewareA def initialize(app) @app = app end def call(env) p '----- Middleware A receive request -----' status, headers, body = @app.call(env) p '----- Middleware A return response -----' [status, headers, body] end end class MiddlewareB def initialize(app) @app = app end def call(env) p '----- Middleware B receive request -----' status, headers, body = @app.call(env) p '----- Middleware B return response -----' [status, headers, body] end end use MiddlewareA use MiddlewareB run SampleApp.new
http://localhost:9292にアクセスした時のログ。
% rackup sample_app.ru Puma starting in single mode... ... Use Ctrl-C to stop "----- Middleware A receive request -----" "----- Middleware B receive request -----" "----- SampleApp#call is called -----" "----- Middleware B return response -----" "----- Middleware A return response -----" ::1 - - [02/Dec/2025:16:36:41 +0900] "GET / HTTP/1.1" 200 - 0.0005
ミドルウェアのcallメソッド内で@app.callが呼ばれる前に書かれた処理はミドルウェアの登録順に実行され、@app.callが呼ばれた後に書かれた処理は登録順の逆に実行されています。
つまり、@app.callが呼ばれる前に書かれた処理は、RackサーバーからRackアプリケーションにリクエストが渡される際に実行されます。一方、@app.callが呼ばれた後に書かれた処理は、RackアプリケーションからRackサーバーへレスポンスを返す際に実行されることになります。
@app.callの前後で処理される順番が変わるのか気になったため、Rackのコードを見てみました。
Rack::Builder#use
まず、ミドルウェアを登録するuseメソッドを見てみます。
https://github.com/rack/rack/blob/37ec310b5631abb62d119cf72136bec4deb160eb/lib/rack/builder.rb#L159
def use(middleware, *args, &block) if @map mapping, @map = @map, nil @use << proc { |app| generate_map(app, mapping) } end @use << proc { |app| middleware.new(app, *args, &block) } nil end
@use << proc { |app| middleware.new(app, *args, &block) }に注目すると、@useにProcを追加していることが分かります。
Proc内では、useメソッドに引数で渡されたミドルウェアをインスタンス化していますね。
Rack::Builder#run
次に、アプリケーションを起動するrunメソッドを見てみます。
https://github.com/rack/rack/blob/37ec310b5631abb62d119cf72136bec4deb160eb/lib/rack/builder.rb#L195
def run(app = nil, &block) raise ArgumentError, "Both app and block given!" if app && block_given? @run = app || block nil end
@runに引数で渡されたアプリケーションまたはブロックを代入しています。
私のサンプルコードでは、runメソッドにSampleAppのインスタンスを渡しています。
以下のようにブロックを渡しても良いようです。
App = lambda do |env| [200, { "content-type" => "text/html"}, ["Hello world!"]] end run App
Rack::Builder#to_app
Rack::Builder#to_appメソッドで、@useと@runをもとにRackアプリケーションを作成します。
https://github.com/rack/rack/blob/37ec310b5631abb62d119cf72136bec4deb160eb/lib/rack/builder.rb#L270
def to_app app = @map ? generate_map(@run, @map) : @run fail "missing run or map statement" unless app app.freeze if @freeze_app app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } } @warmup.call(app) if @warmup app end
分かりやすくするため、@mapと@freeze_appを除いた以下のコードで考えます。
app = @run app = @use.reverse.inject(app) { |a, e| e[a] }
appに@runを代入し、@useを逆順に並べてinjectを実行しています。
injectのブロックではProc#[]が呼ばれ、@useに格納されているブロックが実行されて、その返り値が次回のループに渡されます。
Proc#=== (Ruby 3.4 リファレンスマニュアル)
具体的に何が行われているか、私のサンプルコードで確認してみましょう。
appにはRackアプリケーションのインスタンスが格納されています。
app = SampleApp.new
@useには、Procオブジェクトが格納されています。
[proc { |app| MiddlewareA.new(app, *args, &block) }, proc { |app| MiddlewareB.new(app, *args, &block) }]
@use.reverseで逆順に並べられて、inject(app) { |a, e| e[a] }を実行します。
繰り返し1回目では次のようになります。
proc { |app| MiddlewareB.new(app, *args, &block) }[app]
Proc#[]を実行するとMiddlewareB.new(app)となり、アプリケーションを引数にしてミドルウェアをインスタンス化しています。
繰り返し2回目では、次のようになります。
proc { |app| MiddlewareA.new(app, *args, &block) }[middleware_b_instance]
MiddlewareA.new(middleware_b_instance) となり、ミドルウェアを引数にしてインスタンス化しています。
つまり、@use.reverse.inject(app) { |a, e| e[a] } はMiddlewareA.new(MiddlewareB.new(SampleApp.new)) となっており、アプリケーションをミドルウェアでラップしていました。
では、to_app.call(env)を実行するとどうなるでしょうか?私のサンプルコードの場合、次のようになります。
MiddleWareA#callが呼ばれるp '----- Middleware A receive request -----'@app.call(MiddleWareB#call) が呼ばれるp '----- Middleware B receive request -----'@app.call(SampleApp#call) が呼ばれるp '----- SampleApp#call is called -----'[200, { 'content-type' => 'text/html' }, ['Hello World!']]が返るSampleApp#callの返り値を変数に代入p '----- Middleware B return response -----'[200, { 'content-type' => 'text/html' }, ['Hello World!']]が返るMiddlewareB#callの返り値を変数に代入p '----- Middleware A return response -----'[200, { 'content-type' => 'text/html' }, ['Hello World!']]が返る
@app.callの前後で処理される順番が変わる謎が解けました!
いろいろなRackミドルウェア
リクエストに変更を加えるミドルウェアと、レスポンスに変更を加えるミドルウェアを見てみましょう。
Rack::MethodOverrideでは、env[REQUEST_METHOD]の値を書き換えています。
Railsのフォームと関連がありそうです。
def call(env) if allowed_methods.include?(env[REQUEST_METHOD]) method = method_override(env) if HTTP_METHODS.include?(method) env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD] env[REQUEST_METHOD] = method end end @app.call(env) end
Rack::ContentLengthは、受け取ったレスポンスボディの文字列のバイト長を取得して、headers[CONTENT_LENGTH]に格納しています。
def call(env) status, headers, body = response = @app.call(env) if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && !headers[CONTENT_LENGTH] && !headers[TRANSFER_ENCODING] && body.respond_to?(:to_ary) response[2] = body = body.to_ary headers[CONTENT_LENGTH] = body.sum(&:bytesize).to_s end response end
他にもたくさんのミドルウェアが存在します。 Railsで利用されているミドルウェアを調べるのも面白そうです。
おわりに
Rackミドルウェアの挙動が気になってコードを読んでみたのですが、とても面白かったです。
コードを動かし内部のコードを読んでみて、Rackミドルウェアについて理解を深めることができました。
Rackアプリ、Rackミドルウェアについて調べたので、次はRackサーバーについて調べようと思います。まだまだ興味は尽きません🔥
参考文献
FBCのチーム開発プラクティスの議事録作成と出席管理を行うアプリ「Fjord Minutes」をリリースしました!

はじめに
カッシー(@cassy0220)と申します。2022年からプログラミングスクール「フィヨルドブートキャンプ」(以下FBC)に参加し、Webエンジニアとして就職すること目指して学習中です。
今回、FBCの最終課題として開発した「Fjord Minutes」をリリースしました。このサービスは、FBCのチーム開発プラクティスの議事録作成と出席管理を効率化するサービスです。
この記事は、Fjord Minutesの概要や使い方をご紹介するとともに、開発における学びや苦労した点を振り返るものとなります。
サービス概要
チーム開発プラクティスについて
FBCでは、実践的なスクラム開発を学ぶためのチーム開発プラクティスがあります。
チーム開発プラクティスでは、FBCが運営するサービスのリポジトリのIssueが割り振られ機能の開発や修正などを行ったり、他のチームメンバーが作成した`Pull Requestのレビューを行ったりします。
また、チームメンバーは月2回開催されるふりかえり・計画ミーティングに参加し、進捗報告や意見交換を行います。
直面していた課題
しかし、このミーティング運営には以下のような課題がありました。
- 議事録作成に手間がかかる
- 出席管理の煩雑さ
- 昼の部・夜の部での出席者の管理が煩雑で、議事録に出席状況を正しく記録できていなかった
Fjord Minutesの主な機能
これらの課題を解決するため、以下の機能を実装しました。
- 議事録管理機能
- 出席管理
- チームメンバーがミーティングの出席を登録できる
- 出席履歴を一覧で表示
サービスの使い方
ミーティングに出席を登録する
毎週水曜日のミーティング終了後、翌木曜日の00:00に次回ミーティングの議事録が自動で作成されます。 作成完了後、FBCのDiscordサーバーのチーム開発チャンネルに以下のような通知が届きます。
通知内のURLから出席登録ページにアクセスし、以下のいずれかを選択します。
- 昼の部に出席
- 夜の部に出席
- 欠席
- 欠席の場合は、欠席理由と進捗報告の入力が必要です
出席を登録ボタンを押すと出席が登録され、議事録編集ページに遷移します。
先ほど選択した出席情報を確認することができます。
もし出席を変更したい場合は、出席を編集ボタンから出席を変更することが可能です。
議事録の内容を編集する
議事録編集ページで議事録の内容を編集することができます。

議事録の編集できる内容は管理者とチームメンバーで異なります。
- 管理者が編集できる内容
- リリースブランチ / リリースノート
- 話題にしたいこと・心配事
- その他
- 次回のミーティング開催日
- チームメンバーが編集できる内容
- 話題にしたいこと・心配事
議事録の確認
議事録詳細ページでは、Markdown形式とプレビュー形で議事録の内容を確認することができます。

議事録をGitHub Wikiに反映
管理者は、議事録詳細ページから議事録をGitHub Wikiに反映することができます。

議事録をGitHub Wikiに反映させると、GitHub Wikiのページのリンクが表示されます。

チームメンバーの出席を確認する
メンバー一覧ページでは、各チームメンバーの直近6ヶ月の出席が表示されます。
チーム全体のミーティング参加傾向を把握することができます。
個人の全ての出席はメンバー詳細ページで確認することが可能です。
各コースの議事録を確認する
各コースの議事録は年度ごとに表示され、個別に閲覧することが可能です。

技術スタック
- Ruby 3.4.2
- Ruby on Rails 7.2.2.1
- redis
- PostgreSQL
フロントエンド
認証
Linter/Formatter
- rubocop
- erb_lint
- ESLint
- Prettier
テスト
- RSpec
- FactoryBot
CI/CD
- GitHub Actions
インフラ
- Heroku
定期処理
工夫したところ
サービスの機能
議事録のリアルタイム更新機能
ActionCableとReactを使って、議事録の内容を更新すると他のユーザーの画面でもリアルタイムに反映されるようになっています。

開発当初はesaのように一つのテキストエディタを複数人で同時に編集する機能の実装を目指していました。
これを実現するために、Yjsやyrb-actioncableというライブラリの調査を行っていました。
しかし、議事録の雛形をテキストエディタに反映させる際に問題が発生し、サービスで採用することを見送ることとなりました。
共同編集は無理でも、個別に編集した議事録の内容をリアルタイムで反映させることはできないかと模索した中で、Action CableとReactの組み合わせに辿り着きました。
Action Cableを学習する際はAction CableとWebSocketに関する用語に苦戦しましたが、実際にコードを書いて動かし理解を深めていくことで、リアルタイム反映機能を実装することができました。
実装がうまくいった時、とても嬉しかったです。
開発の進め方
コミュニケーションを密に取る
開発を進める中で最も重視したのは、サービス利用者と細かくコミュニケーションを行うことです。
仕様に関する疑問が生じた際は、まず質問内容を整理し、FBCの質問・雑談タイム1を活用して、実際のサービス利用者である駒形さんや町田さんと直接相談を行いました。
話し合いで決まった仕様を実装したら、次はアプリを動かしながらデモを行ってフィードバックをいただきました。
自分が考えていた仕様と駒形さん町田さんが想定していた仕様が食い違っていることが時々あり、この「質問→相談→実装→フィードバック」というサイクルを通じて、開発側と利用者側の認識の齟齬を最小限に抑えることができたと考えています。
また、このサイクルを通じてデモの重要性を学びました。「この箇所はおかしいからこうしてほしい」という具体的なフィードバックは実際に動くアプリを見ることでしか得られないものであり、とても貴重なものです。
「包括的なドキュメントよりも、動くソフトウェアを」というアジャイルの考えはその通りだと実感しました。
苦労したこと
ユーザーの状態管理をどう扱うか
開発の中で最も苦労したのは、チームメンバーがチーム開発から離脱する際の仕様の設計でした。
チームメンバーには「既定ポイント分Issueを完了してチーム開発を修了する場合」と「スクールを休会してチーム開発をお休みする場合」という2つの離脱パターンがあります。
当初は両者を「休会」という一つの状態で管理していましたが、これではチーム開発を修了した方に対して適切な表現とは言えないという指摘を受けました。
そこで「修了」という新しい状態を追加することを検討したのですが、この案では離脱時にユーザーに「休会」か「修了」のどちらかを選択させる必要が生じ、UXの観点から改善の余地がありました。
試行錯誤の末、「チーム開発からの離脱」という統一的な扱いにまとめることで、この問題を解決することができました。
この仕様変更に伴って、次の機能の仕様も変わっていきました。
- 離脱期間の出席表示
- チームメンバー一覧での離脱メンバーの表示
最終的な仕様に落ち着くまで時間がかかり、とても苦労したことを覚えています。
この過程で、駒形さんと町田さんから「どうしたらユーザーのメリットになるかを考えて仕様を考える必要がある」という貴重なアドバイスをいただきました。この言葉は、技術的な実装の前に、まずユーザーにとっての価値を考えることの重要性を気づかせてくれました。
この経験から、優れたサービスを作るためには、技術力だけでなく、ユーザーのニーズを適切に理解し、それを仕様として落とし込む力が必要だと実感しました。今後はより一層ユーザー視点での設計力を磨いていきたいと考えています。
新しい技術のキャッチアップ
本サービスの開発の中で、Heroku、TailwindCSS、RSpec、GitHub Actions、omniauth、Action Cableなど、多くの新しい技術を学ぶこととなりました。
学習にあたっては、公式ドキュメントを基礎として、『Everyday Rails』などの書籍を参考にしながら、実際にコードを書いて動かすことを重視しました。
新しい技術を学ぶ中で特に印象深かったのは、RSpecでテストを書けるようになったことです。
アプリのコードレビューをしていただいた際、クラス設計に問題があることが判明し、クラスの追加や削除を行うこととなりました。
修正に取り掛かる前はシステム全体にどれくらい影響があるか分からなかったのですが、テストを実行することで影響範囲が一目で分かり、必要な修正を効率的に進めることができました。
テストはアプリの安全網として機能してくれることを実感し、テストの重要性を実感する貴重な機会となりました。
新しい技術の習得には時間と努力を要しますが、一つ一つの技術と真摯に向き合い、実践的な理解を深めていくことの大切さを学びました。今回の経験を踏まえ、これからも着実に技術力を磨いていきたいと考えています。
身についたこと
自作サービス開発を通して、基本的なスキルを成長させることができました。
一つ目として、コミュニケーションスキルです。以前は相談や報告の際に話が冗長になりがちでしたが、要点を整理して簡潔に伝えることを意識的に練習しました。相談や報告の際は事前に準備して分かりやすく伝える習慣を身につけられたことは、一つの財産となっています。
二つ目として、深く調査するスキルです。分からないことがあれば、ドキュメントを読むだけでなく、ライブラリの内部実装を確認するようになり、コードリーディング能力も少しずつ向上していると実感しています。
おわりに
FBCに参加して約3年、紆余曲折ありましたがここまで学習を続けられたのは、駒形さん町田さんをはじめとするメンターの皆様、受講生・卒業生の皆様のおかげです。本当にありがとうございました!
特に自作サービス開発においては、仕様の決定から実装のアドバイスまで駒形さん町田さんに多大なサポートをいただきました。改めて感謝申し上げます。
このサービスが、チーム開発ミーティングの効率化に少しでも貢献できれば幸いです。今後も改善を重ねていきたいと考えていますので、お気づきの点がございましたら、ぜひフィードバックをいただけると嬉しく思います。
初めてプログラミングのワークショップに参加したお話
2024年2月1日にフィヨルドブートキャンプで開催された、「Rackを理解するための技術ハンズオンワークショップ」に参加してきました。
一昨年のアドベントカレンダーでRackについて調べたことがあったため、今回の内容に興味を惹かれ参加したのですが、とても勉強になりました。
ふりかえりを兼ねて、ワークショップについて書きたいと思います。
ワークショップの流れ
ワークショップは以下のような流れで行われました。
- 開会挨拶とイベントについての説明
- スポンサートーク
- コーチの方の自己紹介
- ワークショップ
- hogelogさんへの質問
- 集合写真撮影
- クロージング
最初の20分ほどでイベントの説明やhogelog さんによるSTORESさんの会社説明などがざっと行われ、Rackについての説明があってからワークショップが始まりました。
Rackについての説明資料はこちら。
6人ほどのグループに分かれてhogelogさんが用意された教材を読みながら各自コードを書いて理解し、質問があれば各自サポーターの方に質問を行うという形で進んでいきます。
教材はこちら
用意された教材は以下のような3章構成となっています。
- Rackアプリケーションについて
- Rackミドルウェアについて
- Rackサーバーについて
各章で学んだことをまとめていきます。
Rackアプリケーション
- Rackアプリは
callメソッドを呼び出せるcallメソッドは引数envを受け取り、ステータスコード、ヘッダー、ボディを格納した配列([status, headers, body])を返す
- .ru という拡張子はrackup configファイルの拡張子を表し、
runなどのRack独自のメソッドを利用できる
Railsアプリ内にもconfig.ruというファイルが存在しています。
私が自作サービスプラクティスで開発中のRailsアプリ(7.2.1)のconfig.ruファイルは次のようになっています。
# This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server
callに渡されるenv引数は、リクエストに関する情報を格納したオブジェクト- リクエストメソッド、パス、ヘッダー、クエリパラメーターなどの情報が格納されている
Rackにはリクエストやレスポンスを扱いやすくしてくれるライブラリが用意してある
ドキュメントを見てみると、実にたくさんのメソッドを利用できることがわかります。
Class: Rack::Request — Documentation for rack (3.1.9)
Rackミドルウェア
- Rackミドルウェアとは、リクエストを受け取る・返す層とRackアプリケーションの間に存在するコンポーネント
- Rackに渡されるリクエストを加工したり、Rackから返されるレスポンスに処理を加えたりすることが仕事
以下は教材内で使用したRackミドルウェアのサンプルコードです。
class App def call(env) [200, {}, ["hello"]] end end class Middleware def initialize(app, name) @app = app @name = name end def call(env) status, headers, body = @app.call(env) headers["hello"] = @name [status, headers, body] end end use Middleware, "rails" run App.new
useキーワードを使ってMiddlewareミドルウェアを登録しています。
このRackアプリケーションにリクエストが送られると、次のような流れで処理が実行されます。
Middleware#callが呼ばれ、リクエストの処理が開始される@app.call(env)が実行され、内部のRackアプリケーションでリクエストが処理される- 返却された情報に対し、レスポンスヘッダーに
"hello": "rails"という値を追加する - ステータスコード、レスポンスヘッダー、レスポンスボディを配列に詰め直し、レスポンスとして返す
この場合はレスポンスヘッダーに対して処理を行っていますね。
- Rackの標準ミドルウェアが存在し、便利な機能を提供してくれる
教材内ではリクエストの処理時間を測定できるRack::Runtimeと、Basic認証を行うRack::Auth::Basicを利用しています。
ミドルウェアを追加してブラウザからアクセスすると、次のような入力欄が表示されました。

入力欄の表示にJavaScriptが関係しているのではないかと考えていましたが、質問したところこのBasic認証はブラウザに搭載されている標準的な認証の仕組みであることがわかりました。
今ではあまり使われることのない認証とのことだったのですが、こちらも勉強になりました。
Rackサーバー
- Rackサーバーとしては、Puma、Pitchfor、Unicorn、WEBRickなどが代表的
- Rackサーバーで受け取ったリクエストをもとに
envオブジェクトを作成し、app.call(env)でRackアプリケーションに渡している - Rackアプリケーションから受け取ったレスポンス情報を元にHTTPレスポンスを作成し、クライアントに返している
教材のコードの詳細な理解には至りませんでしたが、Rackサーバーの基本的な処理の流れを把握することができました。
hogelogさんいわく、RackアプリやRackミドルウェアは比較的自由に記述できる一方、Rackサーバーの処理はHTTP通信の複雑な処理を担うため、とても大変な部分なのだそうです。
大変な処理をになってくれる縁の下の力持ちのRackサーバーに感謝していきたいです。
おわりに
3時間のワークショップでしたが、実際にコードを動かし質問をする中であっという間に過ぎていきました。
このワークショップを通じて、Railsアプリケーションにおけるリクエスト処理の全体像、特にRailsの外側に存在するRackミドルウェアやRackサーバーの役割について理解することができました。
また、TCPSocketクラスなど、新たに気になることも見つかったので、これらについても今後学習していきたいと思います。
最後に、このような学びの機会を提供してくださった以下の皆様に感謝申し上げます。ありがとうございました。
- 開催 : FjordBootCamp様
- スポンサー : STORES様
- 講師 : hogelog様
- サポート : Shia様、komagata様、jnchito様
ActionCableのコネクションのIDについて
この記事は、「フィヨルドブートキャンプ Part 2 Advent Calendar 2024」12日目の記事です。
フィヨルドブートキャンプ Part 2 Advent Calendar 2024 - Adventar
Advent Calender Part1はこちらになります。
フィヨルドブートキャンプ Part 1 Advent Calendar 2024 - Adventar
前回は、jyoumaさんのFBCと歩む私の小さな一歩:感謝と思い切り挑戦する日々でした。
子育てしながら学習をされているjyoumaさん凄いです!
この記事では、Railsの機能であるAction CableのコネクションのIDについて調べたことを書こうと思います。
RailsガイドではコネクションIDの設定方法について記述がありますが、内部でどのようにコネクションIDが決定されるのか気になったため、コードを読んでみました。
コネクションについて
Action Cableのコネクションとは、Action CableサーバーがWebSocket接続を受け取ると作成されるもので、1つのWebSocket接続に対して1つ作成されます。
詳しくはRailsガイドのAction Cableのページを参照してください。
コネクションのIDの設定
Railsガイドには、コネクションの設定の例として以下のようなコードが記載されています。
# app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection end end end end
上記のコードでWebSocket接続が行われた際、以下のような流れでコネクションにIDが設定されます。
identified_by :current_userで、コネクションのIDがcurrent_user属性の値で決定されることを宣言する。- WebSocket接続が確立した際、
connectメソッドが呼ばれる。 ApplicationCable::Connectionインスタンスのcurrent_user属性に、cookieの値から取得したユーザーオブジェクトを代入する。
つまり、ActionCableのコネクションのIDはコネクションインスタンスのcurrent_user属性に保存されているユーザーオブジェクトによって決定される、ということになります。
では、保存された値を元にどのようにしてIDが決定されるのかを追っていきます。
identified_by
はじめにidentified_byメソッドを見ていきます。
このメソッドはActionCable::Connection::Identificationに定義されています。
module ActionCable module Connection module Identification extend ActiveSupport::Concern included do class_attribute :identifiers, default: Set.new end module ClassMethods # Mark a key as being a connection identifier index that can then be used to # find the specific connection again later. Common identifiers are current_user # and current_account, but could be anything, really. # # Note that anything marked as an identifier will automatically create a # delegate by the same name on any channel instances created off the connection. def identified_by(*identifiers) Array(identifiers).each { |identifier| attr_accessor identifier } self.identifiers += identifiers end end
identified_byは以下のことを行なっています。
attr_accessorを呼び出して、受け取った引数のアクセサメソッドを追加する。identifiers属性に引数を追加する。
WebSocket接続時に行なっていた代入(self.current_user = find_verified_user)は、identified_byによってcurrent_userというアクセサメソッドが追加されているため可能となっていました。
identifiersはデフォルトで空の集合となっており、identified_byの引数をその集合に追加しています。
connection_identifier
コネクションのIDはconnection_identifierによって返されます。
このメソッドはidentified_byと同じActionCable::Connection::Identificationに定義されています。
# Return a single connection identifier that combines the value of all the # registered identifiers into a single gid. def connection_identifier unless defined? @connection_identifier @connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") } end @connection_identifier end private def connection_gid(ids) ids.map do |o| if o.respond_to? :to_gid_param o.to_gid_param else o.to_s end end.sort.join(":") end
connection_identifierは以下のことを行なっています。
@connection_identifierが定義されていなければ、@connection_identifierに値を保存する@connection_identifierを返す
@connection_identifierに保存される値を詳しく見てみましょう。
connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") }
identifiers.filter_map { |id| instance_variable_get("@#{id}") }は、identifiersを対応する値に変換しています。
Railsガイドのコードを元に流れを追ってみます。
identifiersはSet[:current_user]Set[:current_user]にfilter_mapが呼ばれる- ブロック内で
instance_variable_get("@current_user")が呼ばれる current_user属性にはself.current_user = find_verified_userで保存したユーザーオブジェクトが格納されているため、そのオブジェクトが返る[user]が返る
そして、取得した配列をconnection_gidに渡しています。
def connection_gid(ids) ids.map do |o| if o.respond_to? :to_gid_param o.to_gid_param else o.to_s end end.sort.join(":") end
引数で受け取った配列の中身を、オブジェクトがto_gid_paramを呼び出せるならばto_gid_paramを呼び出し、呼び出せないならばto_sを呼び出して変換し、変換した値ををソートして連結しています。
to_gid_paramとはなんぞや?と思い調べてみると、次のような実装例がありました。
def to_gid_param to_global_id.to_param end
https://github.com/rails/rails/commit/952887a8bd1fc1e197a482b01c6b1b1ccbcdba2d
モデルのインスタンスに対しto_global_idというメソッドを呼び出しています。
Railsの依存関係の中にglobalidというgemが含まれており、モデルのインスタンスに対してこのメソッドが呼び出せるようになっています。
to_paramメソッドもこのgemで定義されていました。
https://github.com/rails/globalid/blob/e9548a3531a89abfaf036f88d4dac732355a5b80/lib/global_id/global_id.rb#L72
試しに開発中のアプリでメソッドを呼び出してみると、次のような値が返ってきました。
fjord-minutes(dev)> Member.first.to_global_id Member Load (1.0ms) SELECT "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT $1 [["LIMIT", 1]] => #<GlobalID:0x0000000122f8c8e8 @uri=#<URI::GID gid://fjord-minutes/Member/1>> fjord-minutes(dev)> Member.first.to_global_id.to_param Member Load (1.2ms) SELECT "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT $1 [["LIMIT", 1]] => "Z2lkOi8vZmpvcmQtbWludXRlcy9NZW1iZXIvMQ"
ActionCable::Connection::Identificationのコメントにある通り、コネクションのIDの元となる値はユーザーオブジェクト以外でも大丈夫とされています。
以下のコードでは文字列を元にコネクションのIDが決まっています。
まとめ
Action CableコネクションのIDはidentified_byで指定した属性の値を元に決定される。(複数指定も可能)
WebSocket接続時に、identified_byで指定した属性に値を保存しておく必要がある。(保存する値は通常ユーザーオブジェクト)
保存された値はto_gid_paramもしくはto_sで変換され、ソートして:で連結されたものがコネクションのIDとなる。
おわりに
なぜAction Cableのコネクションについて調べたのかというと、開発中の自作サービスでAction Cableを使っているのですが、最初はコネクションIDの設定を追加しておらず、テストを書こうとした際に行き詰まってしまったからです。
Railsのコードを読むことで、Action Cableについての理解を多少なりとも深めることができたと思います。
サーバーのコードもいくつか見てみましたが、その動きを完全に理解するには至りませんでした。これは今後の学習課題としたいと思います。
参考
Rails と Rack について
この記事は、「フィヨルドブートキャンプ Part 2 Advent Calendar 2023」15日目の記事です。
フィヨルドブートキャンプ Part 2 Advent Calendar 2023 - Adventar
Part 1はこちらになります。
フィヨルドブートキャンプ Part 1 Advent Calendar 2023 - Adventar
前回は、dawaさんの「テストとは。最初のイシューでエラーに遭遇した話。」でした。
この記事は、Rails.applicationを調べた過程で学んだことをまとめたものになります。
この記事で伝えたいことは、次の2つです。
では、私がRails.applicationの存在を知ったきっかけから始めましょう。
Rails.applicationとの出会い
以前Railsコンソール上でpolymorphic_urlの挙動を調べようとしたことがあり、その時にappメソッドの存在を知りました。
このメソッドを利用すると、次の様に名前付きルーティングヘルパーを呼び出したり、リクエストを投げることが可能です。
irb(main):001> app.books_path => "/books" irb(main):002> app.polymorphic_url(Book.new) => "http://www.example.com/books" irb(main):003> app.get "http://localhost:3000/books" Started GET "/books" for 127.0.0.1 at 2023-12-10 10:53:13 +0900 ActiveRecord::SchemaMigration Load (0.5ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC Processing by BooksController#index as HTML Rendering layout layouts/application.html.erb Rendering books/index.html.erb within layouts/application Book Load (0.6ms) SELECT "books".* FROM "books" Rendered books/_book.html.erb (Duration: 0.3ms | Allocations: 275) Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 54) Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 53) Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 53) Rendered books/index.html.erb within layouts/application (Duration: 2.7ms | Allocations: 2243) Everything's up-to-date. Nothing to do Rendered layout layouts/application.html.erb (Duration: 15.9ms | Allocations: 15124) Completed 200 OK in 30ms (Views: 17.9ms | ActiveRecord: 0.2ms | Allocations: 19944) => 200
RailsAPIにはappメソッドの説明もありました。
reference the global “app” instance, created on demand. To recreate the instance, pass a non-false value as the parameter.
「グローバルな"app"インスタンスを参照する」とあります。 グローバルな"app"インスタンスってなんだ?と気になり、コードを見てみることに。
module Rails module ConsoleMethods # reference the global "app" instance, created on demand. To recreate the # instance, pass a non-false value as the parameter. def app(create = false) @app_integration_instance = nil if create @app_integration_instance ||= new_session end # create a new session. If a block is given, the new session will be yielded # to the block before being returned. def new_session app = Rails.application session = ActionDispatch::Integration::Session.new(app) # This makes app.url_for and app.foo_path available in the console session.extend(app.routes.url_helpers) session.extend(app.routes.mounted_helpers) session end
rails/railties/lib/rails/console/app.rb at 6b93fff8af32ef5e91f4ec3cfffb081d0553faf0 · rails/rails
appメソッド内で呼ばれているnew_sessionメソッドは、Rails.applicationを引数にしてActionDispatch::Integration::Sessionをインスタンス化したものを返しています。
ActionDispatch::Integration::Sessionのインスタンスは、テストプロセスによって順次実行されるリクエストとレスポンスのセットを表す、とのこと。
Rails.applicationに関しては、Railsガイドに説明がありました。
Rails.applicationは、Railsアプリケーションにおける主要なRackアプリケーションです。Rackに準拠したWebサーバーで、Railsアプリケーションを提供するには、Rails.applicationオブジェクトを使う必要があります。
Rack って聞いたことはあるけど何なんだろう?と気になったので、Rack について調べてみることにしました。
Rackとは?
Rackについて簡単にまとめると、次の様になります。
「Rackとは、WebサーバーとRuby製Webアプリケーションがやりとりを行うための規約」のこと。
1PumaやUnicornといったWebサーバーとRailsアプリケーションがやりとりを行えるのは、RailsがRackの規約に沿って実装されているからなのです。
WebサーバーとRailsアプリケーションのやり取りの中身は、Rackアプリケーションの仕様から知ることができます。
Rackアプリケーションとは、以下の条件を満たすRubyオブジェクトのことを言います。
environmentという引数一つを受け取る、callメソッドを呼び出すことができる。statusheadersbodyという三つの値を含む、凍結されていない配列を返す
rack/SPEC.rdoc at main · rack/rack
Webサーバーは「envハッシュ」と呼ばれるHTTPリクエストの情報が格納されたハッシュを用意し、それを引数としてcallメソッドを呼び出します。
Railsアプリケーションはcallメソッドの返り値として、以下の三つの値を格納した配列をWebサーバーに返します。
status: HTTPステータスコードを表す数値headers: レスポンスヘッダーを含むハッシュbody: レスポンスのボディとなるもので、通常は文字列の配列など
では、実際にどのような値が返ってくるのか確認してみましょう!
コンソール上で実験
Railsコンソール上で、Rails.applicationに対しcall`メソッドを呼び出してみます。
下準備として、rails new helloでアプリを新規作成し、/helloにアクセスするとHello, World!と表示される様にします。

callメソッドに渡す「envハッシュは」、rack gemのRack::MockRequest.env_forを利用して用意します。
irb(main):001> env = Rack::MockRequest.env_for('http://localhost:3000/hello')
=>
{"rack.input"=>#<StringIO:0x00000001077b1760>,
...
そのままcallメソッドに渡すとBlocked hostと言われてしまうため、「envハッシュ」にHTTP_HOSTヘッダーを追加します。
irb(main):002> env['HTTP_HOST'] = 'localhost' => "localhost"
では、「envハッシュ」を引数にRails.applicationに対してcallメソッドを呼び出します。
irb(main):003> status, headers, body = Rails.application.call(env) Started GET "/hello" for at 2023-12-10 15:31:40 +0900 Cannot render console from ! Allowed networks: 127.0.0.0/127.255.255.255, ::1 ActiveRecord::SchemaMigration Load (0.3ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC Processing by HellosController#index as HTML Rendering layout layouts/application.html.erb Rendering hellos/index.html.erb within layouts/application Rendered hellos/index.html.erb within layouts/application (Duration: 0.2ms | Allocations: 209) Everything's up-to-date. Nothing to do Rendered layout layouts/application.html.erb (Duration: 12.8ms | Allocations: 13220) Completed 200 OK in 20ms (Views: 14.4ms | ActiveRecord: 0.0ms | Allocations: 17125) MiniProfiler storage failure: no implicit conversion of nil into String => [200,
Completed 200 OKとあるので、このリクエストは成功した様ですね。
返り値を確認してみると、HTTPステータス、レスポンスヘッダー、レスポンスボディに対応した値が格納されていることがわかります。
irb(main):004> status
=> 200
irb(main):005> headers
=>
{"x-frame-options"=>"SAMEORIGIN",
"x-xss-protection"=>"1; mode=block",
"x-content-type-options"=>"nosniff",
"x-download-options"=>"noopen",
"x-permitted-cross-domain-policies"=>"none",
"referrer-policy"=>"strict-origin-when-cross-origin",
"link"=>
"</assets/application.debug-ead91a25923de99455378da7f1f1bb5a6839a249af27af911ec2b81709b046b7.css>; rel=preload; as=style; nopush,</packs/js/application-d4389a205e3a305a430b.js>; rel=preload; as=script; nopush",
"content-type"=>"text/html; charset=utf-8",
"etag"=>"W/\"cd556a718561e5e4f0c288ed64067335\"",
"cache-control"=>"max-age=0, private, must-revalidate",
"set-cookie"=>
["_hello_session=oNirYlMTT8MTrQFwUPjyqDIKnSZbe1SNGXcQMfesZ5FqJkYFwgpcfBeeGJ7sinDzFkrpGj40DUfjR9G8fOGHTys7Rx3fxmEa06ErCuNjge%2Bu1hWdjLpryWkBIElXgQF9dPBKvRJ2J2m50%2Bvhm5PRHfyy9Dp%2BN6fFvRxnuNtz2igBGQbqdVt9iNnW5Fl4zNlOM4516%2FQxiSQvtdAUhjGMRIQwwU3dHkjGMiYP0jZCPZCKq%2BoqlNDGke19aXZpiu5lealK4wA%2Fu3KIGv1KoEmuXla2tQcPLw%3D%3D--%2BaxhXWvOw%2FzjtOZL--bigBnV371hxh0QYi0EMAUw%3D%3D; path=/; httponly; SameSite=Lax",
"__profilin=p%3Dt; path=/; httponly; SameSite=Lax"],
"x-request-id"=>"f404e8ff-5504-4808-8e44-a87547285fab",
"x-runtime"=>"0.048442"}
irb(main):006> body
=>
#<Rack::BodyProxy:0x0000000107adc390
@block=#<Proc:0x0000000107adc368 /Users/kashiyamashintarou/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.2/lib/action_dispatch/middleware/executor.rb:15>,
途中省略...
@response=
#<ActionDispatch::Response:0x0000000107d32630
途中省略...
@stream=
#<ActionDispatch::Response::Buffer:0x0000000106dc0ff8
@buf=
["<!DOCTYPE html>\n<html>\n <head>\n <title>Hello</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"csrf-param\" content=\"authenticity_token\" />\n<meta name=\"csrf-token\" content=\"TVqQW_tF8fRvPSePyWVN10xc4vV8Fc-nwR8ffja8FhyYM_x59CsJ8KRxawd2VPBiL30Tf0xhAzxQarpFGZONjQ\" />\n \n
\n <link rel=\"stylesheet\" href=\"/assets/application.debug-ead91a25923de99455378da7f1f1bb5a6839a249af27af911ec2b81709b046b7.css\" media=\"all\" data-turbolinks-track=\"reloa
d\" />\n <script src=\"/packs/js/application-d4389a205e3a305a430b.js\" data-turbolinks-track=\"reload\"></script>\n </head>\n\n <body>\n <p>Hello World!</p>\n\n </body>\
n</html>\n"],
Rackアプリケーションの仕様に基づいて、Rails.applicationがstatus・headers・bodyを返すことが分かりました👍
Rails.applicationのクラス
Rails.applicationはRackアプリケーションであることが分かりましたが、それはどの様なオブジェクトなのでしょうか?
クラスを確認してみます。
irb(main):001> Rails.application.class => Hello::Application
Hello::Applicationと表示されましたね。
Railsの起動プロセスについて調べてみると、次の様な記述がありました。
The application is also responsible for setting up and executing the booting process. From the moment you require config/application.rb in your app, the booting process goes like this:
アプリケーション内でconfig/application.rbをrequireした瞬間から起動プロセスが実行され、3番目に「Rails.applicationをMyApp::Application < Rails::Applicationとして定義する」とあります。
起動プロセスを通じて、Rails.applicationはアプリケーション名::Applicationとなることが分かりました。
config/application.rbを確認してみると、アプリケーション名::Applicationとなっていますね。
require_relative "boot" require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Hello class Application < Rails::Application #...
試しに名前空間を変更してみると、Rails.applicationのクラス名も変更されることが確認できます。
module GoodBye # HelloからGoodByeに変更 class Application < Rails::Application
irb(main):001> Rails.application.class => GoodBye::Application
最後に、Rails.applicationの継承関係を見てみましょう。
祖先にはRails::EngineやRails::Railtieというクラスが存在していることが分かりました。
irb(main):001> Rails.application.class => Hello::Application irb(main):002> Rails.application.class.superclass => Rails::Application irb(main):003> Rails.application.class.superclass.superclass => Rails::Engine irb(main):004> Rails.application.class.superclass.superclass.superclass => Rails::Railtie irb(main):005> Rails.application.class.superclass.superclass.superclass.superclass => Object

終わりに
色々と調べる中で、Railsアプリケーションに対する解像度が少し上がった気がします。
Railsの中身を調べることは楽しいですね(コードを読んだり調べたりするのは大変ですが😇)。
今回の調査で十分理解できなかった箇所(Railsの起動プロセスの詳細など)は、レベルアップしてから再び調べることにします。
これからも自分の好奇心を大切に、興味を持ったことをどんどん深掘りしていきたいと思います!
参考文献
小ネタ : 開発環境でRailsアプリの日付を変更する
はじめに
開発環境で特定の日付に移動したい時ってありませんか?
私は チーム開発1のプラクティスを行っていて、「祝日の場合ダッシュボードの表示を変更したい」というIssueに取り組みました。
実装を行って祝日の表示確認を行う際、アプリ内の日付を祝日に変更する必要に迫られたのです。
以下の方法で開発環境の日付を一時的に変更することが可能ですので、今回ご紹介します。
変更方法
config/environments/development.rbに以下を追加して、アプリを再起動する。
require "active_support/core_ext/integer/time" # 追加 require "active_support/testing/time_helpers" # 追加 include ActiveSupport::Testing::TimeHelpers # 追加 Rails.application.configure do config.after_initialize do travel_to Time.zone.parse('移動したい日付') # 追加 end end
試しに今日の日付を表示する簡単なアプリを作成して、アプリ内の日付が変更されているか確認します。
祝日であるかどうかの確認のために、holiday-jpというGemを利用しています。
(app/views/home/index.slim)
h2 今日の日付
p
| 今日の日付は"#{l(Date.current, format: :long)}"です。
p
| #{HolidayJp.holiday?(Date.current) ? '今日は祝日です!🥳' : '今日は祝日ではありません🫠'}
h2 現在時刻
p
| 現在時刻は"#{l(Time.current)}"です。
普通にアプリを起動した場合(日付は9/18日)。

アプリ内の日付を変更した場合(日付を9/25に変更)。

日付が変わっていることが分かります!
何を行っているの?
日付の変更を行うには、travel_toメソッドを利用します。
通常travel_toはテスト内でしか使用することができないため、travel_toを使う下準備として次のことを行います。
travel_toが定義してあるファイル(active_support/testing/time_helpers)をrequireする- モジュール(ActiveSupport::Testing::TimeHelpers)をincludeする
そして、config.after_initializeブロック内でtravel_toを実行し、変更したい日付を渡します。
config.after_initializeブロック内に記述されたコードは、Railsによってアプリケーションの初期化が完了した後に実行されます。
Rails アプリケーションを設定する - Railsガイド
travel_toは、Time.now Date.today DateTime.nowを引数で与えられた時間または日付を返すようにスタブ化することで、現在の時刻を特定の時刻に変更することができます。
スタブはテスト終了時に削除されます。
ActiveSupport::Testing::TimeHelpers
Time.currentとDate.currentについて
RailsAPIのtravel_toの頁に、次のような記述がありました。
Dates are taken as their timestamp at the beginning of the day in the application time zone. Time.current returns said timestamp, and Time.now its equivalent in the system time zone. Similarly, Date.current returns a date equal to the argument, and Date.today the date according to Time.now, which may be different. (Note that you rarely want to deal with Time.now, or Date.today, in order to honor the application time zone please always use Time.current and Date.current.)
簡単にまとめると、「Time.currentとDate.currentはアプリケーションのタイムゾーンに応じた時刻と日付を返し、Time.nowとDate,todayはシステムのタイムゾーンに応じた時刻と日付を返すため、アプリ内のタイムゾーンを尊重したい場合はTime.currentとDate.currentを使いましょう」ということが書かれています。
では、このTime.currentとDate.currentという2つのメソッドはどのように現在時刻や日付を返しているのでしょうか?
実装を見てみようと思います。
Time.current
Time.currentは次のようになっています。
def current ::Time.zone ? ::Time.zone.now : ::Time.now end
Time.zoneにはタイムゾーン情報が格納されており、これが存在する場合はTime.zone.nowを、存在しない場合はTime.nowを返しています。
config.time_zoneにタイムゾーンを設定すると、Time.zoneは次のようになります。
irb(main):001:0> Time.zone => #<ActiveSupport::TimeZone:0x000000010eab9618 @name="Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>
では、Time.zone.nowを確認しましょう。
def now time_now.utc.in_time_zone(self) end
最初にtime_nowメソッドが呼ばれています。
このメソッドはただTime.nowを読んでいるだけです。
def time_now Time.now end
次のutcメソッドですが、おそらくRubyの組み込みライブラリであるTimeクラスのメソッドが呼ばれているのではないかと思います。
https://docs.ruby-lang.org/ja/latest/method/Time/i/gmtime.html
最後にin_time_zoneメソッドが呼ばれています。
タイムゾーンが存在する場合はActiveSupport::TimeWithZoneのインスタンスを作成していることが分かります。
def in_time_zone(zone = ::Time.zone) time_zone = ::Time.find_zone! zone time = acts_like?(:time) ? self : nil if time_zone time_with_zone(time, time_zone) else time || to_time end end private def time_with_zone(time, zone) if time ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone) else ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc)) end end
コンソール上で確認すると、確かにActiveSupport::TimeWithZoneのインスタンスが返っていますね。
time_zone属性にタイムゾーンの情報も格納されていることもわかります。
irb(main):004:0> Time.current => Mon, 18 Sep 2023 14:41:00.422901000 JST +09:00 irb(main):005:0> Time.current.class => ActiveSupport::TimeWithZone irb(main):006:0> Time.current.time_zone => #<ActiveSupport::TimeZone:0x000000010e90e908 @name="Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>
Time.currentの実装を簡単ですが追ってみました。
途中でTime.nowメソッドを読んでおり、travel_toはTime.nowをスタブ化するのでTime.currentの時間も変わるということが分かりましたね。
Date.current
ではDate.currentの方も見てみましょう。
Date.currentはTime.currentと同じく、タイムゾーンが設定されていればTime.zone.todayを、そうでなければDate.todayを返します。
def current ::Time.zone ? ::Time.zone.today : ::Date.today end
では、Time.zone.todayはどうなっているでしょうか。
def today tzinfo.now.to_date end
tzinfoが呼ばれていますね。
ActiveSupport::TimeZoneが初期化される時に@tzinfoに値が格納されます。
attr_reader :name attr_reader :tzinfo def initialize(name, utc_offset = nil, tzinfo = nil) @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) end
TimeZone.find_tzinfo内ではTZInfo::Timezone.getが呼ばれています。
def find_tzinfo(name) TZInfo::Timezone.get(MAPPING[name] || name) end
TZInfo::Timezone.getはtzinfoというGemにより提供されるもので、個別のタイムゾーンを表すTZInfo::Timezoneのインスタンスを得ることができます。
A TZInfo::Timezone instance representing an individual time zone can be obtained with TZInfo::Timezone.get
https://github.com/tzinfo/tzinfo#example-usage
Time.zone.today内で呼ばれるtzinfoは、ActiveSupport::TimeZone初期化時に設定されたTZInfo::Timezoneのインスタンスであることが分かりました。
tzinfo.nowは次のようになっています。
Time.nowを渡してto_localメソッドを呼び出していますね。
def now to_local(Time.now) end def to_local(time) raise ArgumentError, 'time must be specified' unless time Timestamp.for(time) do |ts| TimestampWithOffset.set_timezone_offset(ts, period_for(ts).offset) end end
最後にto_dateメソッドを呼び出し、日付の文字列が返ってくるようになっています。
次のサンプルコードを実行してわかる通り、タイムゾーンに合わせた現在日時を取得していることが分かりますね。
irb(main):001:0> tz = TZInfo::Timezone.get('America/New_York')
=> #<TZInfo::DataTimezone: America/New_York>
irb(main):002:0> tz.now
=> 2023-09-18 02:25:04.614519 -0400
irb(main):003:0> tz.now.to_date
=> Mon, 18 Sep 2023
irb(main):004:0> tz_japan = TZInfo::Timezone.get('Asia/Tokyo')
=> #<TZInfo::DataTimezone: Asia/Tokyo>
irb(main):005:0> tz_japan.now
=> 2023-09-18 15:24:17.173699 +0900
irb(main):006:0> tz_japan.now.to_date
=> Mon, 18 Sep 2023
Date.currentの実装も簡単ですが追ってみました。
Date.currentも途中でTime.nowを読んでおり、travel_toの影響を受けることが分かりました。
終わりに
Railsアプリケーションの開発環境で日付を変更する方法と、Time.currentとDate.currentについて書きました。
Rails内のコードを読むのは難しく完璧に理解はできませんでしたが、いい勉強になりました!
また気になるコードがあれば、コードリーディングに挑戦してみたいと思います💪
参考文献
RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita
- フィヨルドブートキャンプの終盤にあるプラクティスで、フィヨルドブートキャンプアプリ内で振られたIssueの開発を行う、ということを行います。↩
コミット修正時に使う操作まとめ
かなり久しぶりの投稿となりました、cassy0220です。
前回の投稿から今までの間に、FBCで様々なプラクティスに取り組み、課題を提出してきました。
課題を提出する際、コミットを整理してから提出するようにしているのですが、コミットの修正を行う時に「この操作はこれで大丈夫かな?」と何度も調べてしまいます。
そこで、備忘録も兼ねて私がコミットを修正する際によく使う操作をまとめることにしました。
確認する操作
ファイルの変更差分を確認する
% git diff
コミットする前に変更差分を確認して、セルフレビューする癖をつけましょう(戒め)。
もし、ステージングに上げた変更差分を確認したい場合は、git diff --cachedで確認することが出来ます。
コミット履歴を確認する
% git log --oneline
git log --oneline で、コミット履歴を分かりやすく表示することが出来ます。
コミットのハッシュ値を確認したい時や、rebase 中にコミット履歴を確認したい時などによく使います。
特定のコミットの内容を確認する
% git show commit_hash
最後にファイル名を指定すると、指定したコミット内で指定したファイルのコミット内容が表示されます。
% git show commit_hash filename
コミットの指定はHEADで行うことも可能です。
% git show HEAD^
コミットを修正する操作
コミットを取り消す
% git reset
git resetコマンドを使うと、現在のHEADを指定された状態にリセットします。
※ HEADとは、一般的にそのブランチで最後のコミットを指します。
git resetコマンドには次のオプションがあり、巻き戻す内容が異なります。
次のファイルを例に、それぞれのオプションの実行結果をみていきます。
--soft
HEADだけを動かし、コミットだけが消えます。ファイルはステージ上に残ります。
「git reset --soft HEAD^ でコミット取り消し → ファイルを修正 → コミットする」という流れをよくやります。
--mixed
HEADとステージを巻き戻します。ファイルの変更差分はそのまま残ります。
--hard
HEAD、ステージ、ファイルの変更差分全てを元に戻します。
コミットの順番を変更する
% git rebase -i commit_hash
次のコミット履歴を例に、順番を並び替えてみたいと思います。
「add name」と「add age」のコミットを入れ替えてみましょう。
この場合、git rebase -iで指定するコミットハッシュは、「add name」のひとつ前のコミットを指定することに注意して下さい。
% git rebase -i 3fd37fd
すると、次のようなコミット編集画面が表示されます。
この画面で、「add name」と「add age」のコミットの順番を入れ替えて、保存します。
もしコミットを入れ替えてコンフリクトが発生した場合は、次のことを実行します。
- コンフリクトが発生した箇所を修正する
git addgit commitでコミットするgit rebase --continueでリベースを続ける
Successfully rebased and updated refs/heads/main.と表示されれば完了です。
コミットの順番を確認してみると、入れ替わっていることが分かります。
コミットをまとめる
% git rebase -i commit_hash
コミットをまとめるには、コミット入れ替えと同じくgit rebase -iを使います。
先ほどの例で、今度は「add age」と「add name」のコミットを1つにまとめてみましょう。
次のコマンドを実行して、コミット編集画面を開きます。
% git rebase -i 3fd37fd
コミット編集画面で、「add name」のコミットのpickをsに変更します。
s(quash)を指定すると、指定したコミットとそのひとつ前のコミットを結合することができます。
編集画面を保存すると、結合したコミットのコミットメッセージを編集する画面が開きます。
適当なコミットメッセージを作成して保存すれば、コミットの結合は完了です。
ログと変更差分を確認してみると、うまくいっていることが分かります。
コミットの内容を分割する
% git rebase -i commit_hash
% git add -p
コミットの内容を分割する場合も、おなじみのgit rebase -iを利用します。
また、git add -pを使うと、変更差分の一部を指定してステージングすることが出来ます。
「コミットをまとめる」で「add age」と「add name」を1つのコミットにまとめましたが、今回はまとめたコミットをもとの別々のコミットに戻してみましょう。
次のコマンドを実行して、コミット編集画面を開きます。
% g rebase -i 3fd37fd
コミット編集画面で、「add age and name」コミットのpickをeに変更します。
e(dit)を指定すると、指定したコミットで rebase が一時停止します。
「add age and name」コミットを修正したいので、まずコミットを巻き戻し、ステージング前の状態に戻します。
% git reset --mixed HEAD^
次に、変更差分を指定してステージングを行いたいので、git add -pを実行します。
% git add -p sample.rb
すると、Stage this hunk [y,n,q,a,d,e,?]?(hunk(変更差分の塊のこと)をステージングする?)と聞かれるので、eを選択します。
どの変更差分をステージングするか、編集画面が起動します。
編集画面では、次のことを行います。
- 追加差分をステージングしたくない場合は、行を削除する
- 削除差分をステージングしたくない場合は、
-を削除する
名前の追加だけをステージングしたいので、次のように編集して保存します。
これで、指定した箇所だけをステージングすることが出来ました。
git diff --cachedで確認すると、先ほど指定した箇所がステージングされていることが分かります。
この内容で、git commitを行います。
残りの変更差分もコミットすれば、コミットの分割は完了です。
最後に、まだ rebase の途中なのでgit rebase --continueして rebase を完了させます。
コミットログと変更差分を確認すると、うまくいっていることが分かります。
おわりに
FBCで勉強を始めた頃は git に対する苦手意識があったのですが、課題の提出時に何度もコミットの修正を行うことで、コミットに対する操作に慣れることができました。
これからの目標として、普段からきれいなコミットを積み上げることを大切にしつつ、問題が起きた時はすぐ修正できるようになっていきたいです。
参考
第6話 git reset 3種類をどこよりもわかりやすい図解で解説!【連載】マンガでわかるGit ~コマンド編~ - itstaffing エンジニアスタイル
[レポート] 『きれいなcommit, pull requestを知りたい/作りたい方のためのgit勉強会』に参加してきました | DevelopersIO

















