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
メソッドを呼び出すことができる。status
headers
body
という三つの値を含む、凍結されていない配列を返す
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> headers => #<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の起動プロセスの詳細など)は、レベルアップしてから再び調べることにします。
これからも自分の好奇心を大切に、興味を持ったことをどんどん深掘りしていきたいと思います!