ActionDispatch等をスタンドアローンで動かしてミニマムなRailsのような何かをDIYしたらRailsともっと仲良くなれた話

# 前置き
Webアプリケーションエンジニアとして働き始めて4年といくらか、そこそこの浮気期間は挟みつつも長らく付き合い続けているRuby on Railsだが、たまに必要に迫られてコードを見るくらいでその中身に関しては正直まるで知らない。

Railsのことをもっと知りたい。いったいどうすれば良いだろう...

![o0300022814160302731.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/c4e26d6a-6a27-207b-1687-7679b2c6767e.jpeg)

![スクリーンショット 2023-12-17 11.51.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/7e4c2f25-909d-5447-8de2-1b79dcf7a457.png)

もちろんRailsになっちまうのはDHHでもなければ無理なので、Railsのコンポーネント、今回の場合は特にActionPack(ActionDispatch, ActionController辺り)を使って、自作の超ミニマムなRailsっぽい何かを作りました。イメージとしては自作PCです。

# 予防線
- 「超ミニマムなRailsっぽい何か」ですが、これはフレームワークですらありません。rails newの結果できる類の「Railsアプリケーションの土台」をミニマムな形で作った感じです。おそらくはrailtiesの一部に該当するような話なのかなと思ってます(調べ切れてませんが)
- Railsのコードは多少は見ていますがその構造まで模倣はしていません。実際のRailsの構造とは全く違うものになっています。
- あくまで学習目的で当然ながら使えるものではないですので、諸々の粗も含めてその辺りはご承知おきください。
- 解釈とかに誤りがあったらぜひ(優しく)教えてください。

# 記事の流れ
以下の流れで「超ミニマムなRailsっぽい何か」 を作成します。(今後 **MiniRailsDiy** と呼称)

1. Pumaで動く最低限のRackアプリケーションを作る
1. ActionDispatch, ActionController等をスタンドアローンで動かす
1. 「**MiniRailsDiy**」のアプリケーションにする

各章では作業や検討の流れを口語体で冗長に書いています。内容だけ知りたい方は各章の **「作ったコード」** の項目だけみていただければ良いと思います。

結論だけ知りたいよという方は、[こちら](https://github.com/takahiromasui001/mini_rails_diy)から最終的なコードをご参照ください。

# MiniRailsDiyの作成
## 1. Pumaで動く最低限のRackアプリケーションを作る
何はともかく土台を作ろう。
Railsアプリケーションの運用において、よく用いられるアプリケーションサーバーといえばPumaだろう。PumaはRackベースのアプリケーションサーバーで、Railsに限らずRackアプリケーションであれば何でも動かすことができる。
Puma上で動く最低限のRackアプリケーションを作れば、これが **MiniRailsDiy** を作る第一歩になる...気がする。

### 作ったコード
ということで簡単に作ってみた。

```ruby:Gemfile
gem 'puma'
gem 'rack'
```

```ruby:config.ru
require 'rack'

run lambda { |_env|
  [200, { 'Content-Type' => 'text/html' }, ['Hello world!']]
}
```

ソースコード全体は[こちら](https://github.com/takahiromasui001/mini_rails_diy/tree/1_prepare_puma_environment)。
これはもうほぼPumaのサンプルコードだな。

### 実行結果
実行すると以下のようにHello world!が返ってくる。

![スクリーンショット 2023-12-16 19.32.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/3d87245b-5724-9b65-6953-54b053253630.png)

![スクリーンショット 2023-12-16 19.34.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/763a2472-f220-8f3c-2444-53a839778e26.png)

まぁとりあえず...最低限これで良いか。

### コードの説明
config.ruで最低限のRackアプリケーションを定義して、Pumaを動作させただけ。Pumaは[デフォルトでconfig.ruを探しにいく](https://github.com/puma/puma?tab=readme-ov-file#quick-start)ので、特に追加の設定も必要ない。

ただconfig.rubにおける以下のコードは、今後も何度か出てくる概念が含まれているので少し説明する。

```ruby
run lambda { |_env|
  [200, { 'Content-Type' => 'text/html' }, ['Hello world!']]
}
```
#### 1. runメソッド
このメソッドはRackアプリケーションのエントリポイントとして機能し、サーバーから受け取ったリクエストを最終的に処理するアプリケーションの定義に使われる。つまり、run メソッドは **リクエストの処理を担当するアプリケーション部分を指定する** ために使用されている。

参考: https://github.com/rack/rack/blob/64ad26e3381da2ce1853638a2c4ea241c2ad3729/lib/rack/builder.rb#L34C5-L34C76

#### 2. lamda式の中身とRackのレスポンス
lamda式は1つの配列(`[200, { 'Content-Type' => 'text/html' }, ['Hello world!']]`)を返している。この配列の要素はそれぞれ、ステータスコード、レスポンスヘッダ、レスポンスボディを表しており、以下の[Rack Specification](https://github.com/rack/rack/blob/main/SPEC.rdoc)に則ったものになっている。

> A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns a non-frozen Array of exactly three values: The status, the headers, and the body.
(https://github.com/rack/rack/blob/main/SPEC.rdoc)

# 2. ActionDispatch, ActionController等をスタンドアローンで動かす
とりあえず土台はできたので拡張していくことにする。何かしらRailsのコンポーネントを組み入れて、**MiniRailsDiy**をRailsっぽい感じに近づけていきたい。では最初に何を入れるのが良いだろうか。

やはりRailsといえば`ActiveRecord`だろうか？ ただActiveRecordのインスタンスがポツンあっても別にRailsっぽい感じにはならない気がするな。

そうなると次の候補としてはルーティングとかcontrollerかな？ route.rbのようにルーティングを定義できて、いつもの書き方でcontrollerを定義できて、何かしらレスポンスを返せるなら、けっこうRailsっぽくなる気がする。

それをやるなら...そう、`ActionPack`だね。

> Action Pack is a framework for handling and responding to web requests. It provides mechanisms for routing (mapping request URLs to actions), defining controllers that implement actions, and generating responses. In short, Action Pack provides the controller layer in the MVC paradigm.
(https://github.com/rails/rails/tree/main/actionpack)

上記はActionPackの説明文だが、まさに求めている機能だ。これを組み込めれば結構いい感じになるんじゃないか？

ただちょっと不安なんだけど、これってスタンドアローンで動かすことができるものなのだろうか？

> However, these modules are designed to function on their own and can be used outside of Rails.

まぁ...一応こう書いてはあるな。信じてやってみるか...。

### 作ったコード

できた。こんな感じ。

```ruby:config.ru
require 'rack'
require 'action_dispatch'
require 'active_support/all'
require 'abstract_controller'
require 'action_controller'
Dir.glob('./app/controllers/**/*.rb').sort.each { |file| require file }

app = ActionDispatch::Routing::RouteSet.new
app.draw do
  get '/home', to: 'home#index'
end

run app
```

```ruby:app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    render plain: 'test index'
  end
end
```

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base; end
```

ソースコード全体は[こちら](https://github.com/takahiromasui001/mini_rails_diy/tree/1_prepare_puma_environment)。
(少し差分はありますがスルーしてください)

...Railsっぽくなってきたような？

config.ru上ではあるものの、routes.rbに似た形でルーティングを設定されているし、app/controllers以下はほぼRailsのControllerそのままだ。


### 実行結果
実行すると以下のように`test index`が返ってくる。

![スクリーンショット 2023-12-16 22.30.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/95bf1aff-827f-01ee-a016-fb0d201ecbdf.png)

### コードの説明
#### ActionDispatch::Routing::RouteSet.new について
おそらくコードを読んで一番疑問に思うのは、config.ruの`ActionDispatch::Routing::RouteSet.new`だと思う。急に出てきたこいつは一体何者で、何故ここでrunしているのか？以下でその理由をできる範囲で説明する。
(正直理解し切れていない面はある。誤ってたら申し訳ない。コメントください。)

Railsアプリケーションのroutes.rbは通常以下のようになっている。

```ruby:(よくあるRailsアプリの)routes.rb
Rails.application.routes.draw do
  # get 'posts/index'
end
```

Railsと同じルーティングを実現するためには、これと同じことができればよさそうだ。では`Rails.application.routes`とは一体なんなのか？というと...

```bash
> Rails.application.routes
=> #<ActionDispatch::Routing::RouteSet:0x000000010c405828>
```

どうやらActionDispatch関連のもので、ActionDispatch::Routing::RouteSetのインスタンスが入っているらしい。

ActionDispatch::Routingに関する説明を読んでみると、この辺りでroutes.rbに書くようなDSLが実現されていることも間違いなさそうだ。

> The routing module provides URL rewriting in native Ruby. It's a way to redirect incoming requests to controllers and actions. This replaces mod_rewrite rules. Best of all, Rails' \Routing works with any web server.
Routes are defined in <tt>config/routes.rb</tt>.
(https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/routing.rb#L4)


へーそうなんだ。ってことは　ActionDispatch::Routing::RouteSet を自分で作って、drawメソッドを実行すればルーティングは設定できてしまうのかな？

```ruby:ActionDispatch::Routing::RouteSet を自分で作る
app = ActionDispatch::Routing::RouteSet.new
app.draw do
  get '/home', to: 'home#index'
end
```

なんとなく方向性は見えてきた気がする。
ただ仮にそこまでは良いとしても、これをどうやってRackアプリケーションとして仕上げれば良いだろうか？ 

ここで急な話題展開になるが[Railsガイドの「RailsとRack」](https://railsguides.jp/rails_on_rack.html#%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B)を参考にして、`bin/rails middleware`を実施してみると、RackアプリケーションとしてのRailsがやっていることが見えてくる。

```
use ActionDispatch::HostAuthorization
use Rack::Sendfile
...
use Rack::TempfileReaper
run MyApp::Application.routes
```

色々あるが、最終的には`run MyApp::Application.routes`をやっているようだ。なんか見たことある形だが...route.rbの`Rails.application.routes(.draw)`にちょっと似ているな。ただまたも突然出てきた`MyApp::Application`とはなんだ？

調べてみるとどうやらこれは`Rails.application`の中身らしい。
(HelloRailsは私の手元のRailsアプリの名称)
![スクリーンショット 2023-12-17 18.17.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/201c90a1-d68b-3d66-f8e0-42ea82571cbe.png)

つまり`MyApp::Application.routes`の中には、`Rails.application.routes`と同様に`ActionDispatch::Routing::RouteSet`のインスタンスが入っているということになる。

であるなら`ActionDispatch::Routing::RouteSet`のインスタンスは既にRackアプリケーションの仕様を満たしていることになるのか。

そうなら話は早い。MiniRailsDiyでも同じようにしてみよう。

```ruby: config.rb
app = ActionDispatch::Routing::RouteSet.new
...
run app
```

この考えはおおよそあっていたようで、前述の通り期待通りの実行結果となった。

RailsをRackアプリケーションという観点から見ると、その核となるのはActionDispatch(::Routing::RouteSetのインスタンス)という捉え方が、もしかしたらできるのかもしれない。

(本当はRailsのコードも見てもっと裏とりをすべき箇所なのだけれども今回は時間の関係でそこまでできなかった。この先はキミの目で確かめてくれ！ )

## Controllerに関して
ActionController::Base を利用するために以下のrequireが必要になる。

```
require 'abstract_controller'
require 'action_controller'
```

これは本当にただrequireしただけ。app/controllersというディレクトリ構造も自分で作ったが特にroutes.rbとの繋ぎこみ設定等も不要だった。おそらくActionDispatchなるActionControllerなりがデフォルトで設定を持つなり名前空間を参照しているからrequireで事足りるなりでうまいことやってるのだろう。

# 「MiniRailsDiy」のアプリケーションにする
ここまででMiniRailsDiyはかなり「ミニマムなRailsっぽい何か」に近づいてきた。ただまだ現状3つほど気になることがある。

1つ目はconfig.ruに直接ルーティングが書かれていること。ここはroutes.rbと同じように別ファイルに切り出したい。

2つ目は、今のconfig.ruでActionDispatch由来のインスタンスを直接runしていること。[Railsアプリのconfig.ru](https://github.com/rails/rails/blob/32afa64e17837ebdba30d9c3bca26bc02fc44c3c/railties/lib/rails/generators/rails/app/templates/config.ru.tt#L4)では、`run Rails.application`というように見た目上Rails由来っぽいものをrunしているのでこれに合わせたい。

3つ目はソースコード上にいまだに**MiniRailsDiy**が登場していないことだ。せっかく呼称を作ったのだから使ってあげたい。

さてどうしようか...
少なくとも、`Rails.application`にあたるものを作成する必要はあるかな。ひとまずこれを`MiniRailsDiy.application`とでもしておこう。

その上で`MiniRailsDiy.application`は以下を満たす必要がありそうだ。

1. `MiniRailsDiy.application` はRackアプリを返す必要がある。
1. 1.のRackアプリは、実際の動作としては`ActionDispatch::Routing::RouteSet`のインスタンスをrunするものである必要がある
1. `MiniRailsDiy.application.routes`は`ActionDispatch::Routing::RouteSet`のインスタンスを返す必要がある 

なんか結構面倒そうだな...初期化処理を用意して、`MiniRailsDiy.application`のインスタンスを作成して云々やれば良いのかな...?

### 作ったコード
やってみたら普通にできた。
主要なコードは以下の通り。

```ruby:config.ru
require './config/initialize'

run MiniRailsDiy.application
```

```ruby:routes.rb
MiniRailsDiy.application.routes.draw do
  get '/home', to: 'home#index'
end
```

```ruby:initialize.rb
require 'rack'
require 'action_dispatch'
require 'active_support/all'
require 'abstract_controller'
require 'action_controller'
require './mini_rails_diy'
require './config/routes'
Dir.glob('./app/controllers/**/*.rb').sort.each { |file| require file }
```

```ruby:mini_rails_diy.rb
class MiniRailsDiy
  attr_reader :routes, :application

  def initialize
    @routes = ActionDispatch::Routing::RouteSet.new
    routes = @routes
    @application = Rack::Builder.new do
      run routes
    end
  end

  def call(env)
    @application.call(env)
  end

  class << self
    attr_accessor :application
  end
end

MiniRailsDiy.application = MiniRailsDiy.new
```
最終的なコードは[こちら](https://github.com/takahiromasui001/mini_rails_diy/tree/4_rails_like_config_ru)

### 実行結果
![スクリーンショット 2023-12-16 22.30.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283780/9838de29-74c3-206b-dad3-098361a463e4.png)

以前と同様に、`test index`が表示される。

だいぶスッキリしてきた気がする。

### コードの説明
ここでは`MiniRailsDiy(mini_rails_diy.rb)`について説明する。
このクラスは前述の3つの要件を以下の形で満たしている。

#### `MiniRailsDiy.application` はRackアプリである&`ActionDispatch::Routing::RouteSet`のインスタンスをrunする

Rackアプリの最低限の要件は、[前述の通り](https://qiita.com/drafts/6659e5847ab462cfe550/edit#2-lamda%E5%BC%8F%E3%81%AE%E4%B8%AD%E8%BA%AB%E3%81%A8rack%E3%81%AE%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9)以下が挙げられる
- callメソッドを持つこと
- 要素が3つの配列(ステータスコード、リクエストヘッダ、リクエストボディ)を返すこと

MiniRailsDiyでは、シングルトンパターン的な実装で、MiniRailsDiy.applicationが常に1つのMiniRailsDiyのインスタンスを返すようにしている。

```ruby
class MiniRailsDiy
  ...
  class << self
    attr_accessor :application
  end
end

MiniRailsDiy.application = MiniRailsDiy.new
```

またcallメソッドを持つためRackアプリケーションとしてPumaからのリクエストを受け取ることができる。
```ruby
class MiniRailsDiy
  attr_reader :routes, :application

  def initialize
    ...
    @application = ...
  end

  def call(env)
    @application.call(env)
  end
  ...
end
...
```

加えてcallメソッドは、内部でActionDispatch::Routing::RouteSet由来のRackアプリケーションをcallしているので、実際の動作はActionDispatch::Routing::RouteSetをrunしているのと同等になる。

```ruby
class MiniRailsDiy
  attr_reader :routes, :application

  def initialize
    @routes = ActionDispatch::Routing::RouteSet.new
    routes = @routes

    @application = Rack::Builder.new do
      run routes
    end
  end

  def call(env)
    # 内部的にはActionDispatch::Routing::RouteSetのRackアプリケーションを保持し、それをcallしている
    @application.call(env)
  end
end
...
```

#### `MiniRailsDiy.application.routes`は`ActionDispatch::Routing::RouteSet`のインスタンスを返す

ここは既に前項の話で実現できている。
初期化時に作成した ActionDispatch::Routing::RouteSet.new を routesメソッドからアクセスできるようにすれば良い

```ruby
class MiniRailsDiy
  # 初期化時にroutesに ActionDispatch::Routing::RouteSet.new のインスタンスを入れている
  attr_reader :routes, ...

  def initialize
    @routes = ActionDispatch::Routing::RouteSet.new
    ...
  end
end
```

# 作ったものを振り返ってみる
さて最後に作ったものを振り返ってみる。
そこそこにRailsっぽくなったのではないだろうか。

```ruby:Gemfile
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'actionpack', "~> 7.1.2"
gem 'activesupport'
gem 'puma'
gem 'rack'
```

```ruby:config.ru
require './config/initialize'

run MiniRailsDiy.application
```

```ruby:initialize.rb
require 'rack'
require 'action_dispatch'
require 'active_support/all'
require 'abstract_controller'
require 'action_controller'

require './mini_rails_diy'
require './config/routes'
Dir.glob('./app/controllers/**/*.rb').sort.each { |file| require file }
```
```ruby:routes.rb
MiniRailsDiy.application.routes.draw do
  get '/home', to: 'home#index'
end
```
```ruby:application_controller.rb
class ApplicationController < ActionController::Base; end
```
```ruby:home_controller.rb
class HomeController < ApplicationController
  def index
    render plain: 'test index'
  end
end
```
```ruby:mini_rails_diy.rb
class MiniRailsDiy
  attr_reader :routes, :application

  def initialize
    @routes = ActionDispatch::Routing::RouteSet.new
    routes = @routes
    @application = Rack::Builder.new do
      run routes
    end
  end

  def call(env)
    @application.call(env)
  end

  class << self
    attr_accessor :application
  end
end

MiniRailsDiy.application = MiniRailsDiy.new
```

でもmini_rails_diy.rbはちょっと変なことしているような気がしなくもないな。良い書き方があったらコメントください。

あとはこの先やるとすればひとまずは以下だろうか。実現すればさらにRailsっぽくなりそうだ。
- initialize.rbやmini_rails_diy.rbのGem化
- ActiveRecordやActionViewの組み込み

でももう疲れたので今回はやらない...

# 所感
とても長い記事になってしまった。いつも同じことを言っている気がする。なぜ私が記事を書くとこんなに長くなってしまうのだろうか。

ただその分、Ruby on Railsがどういうものか、これまでとは違う側面から理解が進んだように思う。以下2つに分けて学んだことを記載する。

## Railsアプリケーションとその背後に存在するRackについて
Railsアプリケーションが根底でRackアプリケーションであることは、普段あまり意識も注目もしていなかったが、ミニマムなRailsを構築する過程でこの点を強く実感することができた。この取り組みを通じて、Rackベースのアプリケーションの具体的な構造についてもより深く学ぶ機会になった。

特に、アプリケーションサーバーからのリクエストをcallメソッドで受け取り、それを処理して配列形式のレスポンス（ステータスコード、レスポンスヘッダ、レスポンスボディ）を返すというRackの仕様に則った基本的な仕組みを理解することができた。この仕組みを適切に実装することで、RackベースのWebサーバーやアプリケーションサーバーとスムーズに連携することができる。その点にRackの価値があるのだろう。

このプロセスを経て、Railsアプリケーションの背後にあるRackの役割とその意義について、自分なりの理解を深めることができたと感じている。

## RackアプリケーションとActionPackの役割
RailsアプリケーションをRackアプリケーションとして捉えるとき、その核となる部分はActionPack（特にActionDispatchとActionController）によって構成されている、そう表現してもそこまで間違っていないのではないかと感じた。
自分が作成したMiniRailsDiyでは、ActionDispatchとActionControllerの二つのコンポーネントの、さらにその一部の機能だけを利用している。にもかかわらず、Railsのような見た目のWebアプリケーションを実現することができた。

ここに他のコンポーネント、例えばActiveRecordやActionViewなどを組み入れたなら、Railsの基本的な外観と機能を模倣するには十分であるように感じる。もちろん、Railsがその価値を最大限に発揮するのは、これらのコンポーネントだけでなく、多くの他の機能との連携を通じてであることも理解している。ただこの取り組みを通じて、Railsの基本的な構造とその機能の仕組みについて、より深い理解を得ることができた。

まぁぶっちゃけすごくしょぼいrailtiesを作っただけな気はしている。それが合っているのかいないのか、その辺もおいおい調べていこうと思う。

# 参考サイト
https://github.com/rack/rack
https://github.com/puma/puma
https://github.com/rails/rails/tree/main/actionpack
https://github.com/rails/rails/tree/main/railties
https://qiita.com/ogawatti/items/8f58aaee506c4646a094