`rails new` せずに小さなRailsアプリを作成

Railsアプリを作る際は最初に `rails new` を実行する。すると大量のファイルが作られ、その中には様々な設定が最初から書かれている。はっきり言ってどれが何をしているのかよく分からない。

そこで `rails new` せずに、**サーバー実行時のエラーを見ながら少しずつファイルや設定を加えてみた**。 "Hello, world!\n" の14バイトのテキストを返すだけのアプリができるまでをまとめる。

* Gitリポジトリ
	* https://github.com/hmmnrst/rails_mini_app
		* 本記事の内容はmasterブランチにある
		* 派生ブランチを各種実験に使っている
* 参考資料
	* [Rails の初期化プロセス - Rails ガイド](https://railsguides.jp/initialization.html)
	* [rails/railties at v6.0.0 · rails/rails](https://github.com/rails/rails/tree/v6.0.0/railties)

## TL;DR
必要なファイルまとめ

| 一般的なパス | パスの定義元 | 書くべき処理 |
|:--|:--|:--|
| `bin/rails` | `railties/lib/rails/app_loader.rb` | 定数 `APP_PATH` を定義した後、 `rails/commands` を読み込む |
| `config/application.rb` | ユーザー定義定数 `APP_PATH` | `Rails::Application` を継承したクラスを定義する |
| `config.ru` | `rails server --config=XXX` | Railsアプリを初期化、実行する<br>（今回はルーティングも書いた） |
| `app/controllers/xxx_controller.rb` | ルーティング・自動読み込み | リクエストに応じて処理する |

今回のアプリでは、上記ファイルは合計で20行しかない。

```shell-session
$ git ls-files | xargs wc --lines
   5 .gitignore
   7 Gemfile
 135 Gemfile.lock
   5 app/controllers/mini_controller.rb
   3 bin/rails
   8 config.ru
   4 config/application.rb
 167 total
```

## 前準備
Windows Subsystem for Linux上のUbuntu 16.04で試している。

### Rubyのバージョン選択
既にrbenvが入っているとする。新しくRuby 2.6.4が出ていたのでインストールする。あまり他に影響を与えたくないので、バージョン指定はこのシェルでのみ有効なようにする。

```bash
(cd $(rbenv root)/plugins/ruby-build/ && git pull)
rbenv install --skip-existing 2.6.4
rbenv shell 2.6.4
```

### Gitリポジトリの作成
適宜保存するために作成しておく。

```bash
mkdir --parents ~/projects/rails_mini_app
cd ~/projects/rails_mini_app

git init
git commit --allow-empty # 一応、空っぽの時点で保存しておく
```

### Railsのインストール

```bash
bundle init
sed --in-place --expression='s/^# *gem/gem/' Gemfile
```

これで、gem `rails` をインストールするGemfileが完成する。（2019/09/14時点の最新版は6.0.0）

```ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

gem "rails"
```

不要になったら跡形もなく消せるよう、gemは作業ディレクトリ内にインストールする。git管理は不要なので `.gitignore` で除外しておく。

```bash
bundle install --path=vendor/bundle

cat << EOF >> .gitignore
/.bundle
/vendor
EOF
```

## Railsアプリ作成
### 初期状態で確認
早速railsサーバーを起動してみる。

```bash
bundle exec rails server
```

```
Usage:
  rails new APP_PATH [options]

Options:
  ...

Example:
    rails new ~/Code/Ruby/weblog

    This generates a skeletal Rails installation in ~/Code/Ruby/weblog.
```

`rails new` のヘルプが表示された。暗にこれを実行しろと言われている。

### `bin/rails` の作成と、定数 `APP_PATH` の定義
ヘルプからでは必要なファイルがわからないので、「[Railsの初期化プロセス](https://railsguides.jp/initialization.html)」を読む。gem以外で最初のファイルは **`bin/rails`** であるらしい。

> **1.2 `railties/lib/rails/app_loader.rb`**
>
> `exec_app` の主な目的は、Railsアプリケーションにある `bin/rails` を実行することです。カレントディレクトリに `bin/rails` がない場合、 `bin/rails` が見つかるまでディレクトリを上に向って探索します。これにより、Railsアプリケーション内のどのディレクトリからでも `rails` コマンドを実行できるようになります。

ただし試したところ、適当なコードだと `rails new` のヘルプが表示されるままだった。ヘルプを回避するには `APP_PATH` という定数を定義する必要があった。…と思ったら、文字列として含んでいれば十分だった。

```ruby:bin/rails
#!/usr/bin/env ruby
# APP_PATH
```

この状態でサーバーを起動すると、ヘルプは表示されなくなるが他のことも起きない。

### `rails/commands` の読み込みと、 `APP_PATH` の指すファイル準備

サーバー起動処理（railsコマンドの実行）へ進むには、 `rails/commands` を読み込ませる必要がある。また、その中で定数 `APP_PATH` のファイルも読み込まれるので、一般的なパスである `config/application.rb` を*とりあえず空ファイルで*作っておく。

```ruby:bin/rails
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require 'rails/commands'
```

これでサーバーを起動するとNoMethodErrorが発生する。

```
/home/user/projects/rails_mini_app/vendor/bundle/ruby/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:142:in `block in perform': undefined method `root' for nil:NilClass (NoMethodError)
```

```ruby:railties-6.0.0/lib/rails/commands/server/server_command.rb#L133-152
      def perform
        extract_environment_option_from_argument
        set_application_directory!
        prepare_restart

        Rails::Server.new(server_options).tap do |server|
          # Require application after server sets environment to propagate
          # the --environment option.
          require APP_PATH
          Dir.chdir(Rails.application.root) # ここでエラー

          if server.serveable?
            print_boot_information(server.server, server.served_url)
            after_stop_callback = -> { say "Exiting" unless options[:daemon] }
            server.start(after_stop_callback)
          else
            say rack_server_suggestion(using)
          end
        end
      end
```

### Railsアプリの用意
`Rails.application` が無いのが問題らしい。 `config/application.rb` の中にRailsアプリを書く。

```ruby:config/application.rb
class MiniApp < Rails::Application
end
```

これでサーバーを起動すると、親切に次の作業のヒントを出してくれる。

```
=> Booting WEBrick
=> Rails 6.0.0 application starting in development http://localhost:3000
=> Run `rails server --help` for more startup options
configuration config.ru not found
Exiting
```

なお、設定ファイルは `--config` オプションで指定できるので、 `config.ru` でなくてもいい。

### `config.ru` の用意
空ファイルを用意するだけだと、サーバー起動時にエラーになる。

```
=> Booting WEBrick
=> Rails 6.0.0 application starting in development http://localhost:3000
=> Run `rails server --help` for more startup options
Exiting
Traceback (most recent call last):
	...
	1: from config.ru:2:in `<main>'
/home/user/projects/rails_mini_app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:146:in `to_app': missing run or map statement (RuntimeError)
```

`run` が必要らしいので、実際のアプリに存在する1行を加える。

```ruby:config.ru
run Rails.application
```

これでサーバーを起動すると、またNoMethodErrorが発生する。

```
=> Booting WEBrick
=> Rails 6.0.0 application starting in development http://localhost:3000
=> Run `rails server --help` for more startup options
Exiting
Traceback (most recent call last):
	...
/home/user/projects/rails_mini_app/vendor/bundle/ruby/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:80:in `log_to_stdout': undefined method `formatter' for nil:NilClass (NoMethodError)
```

```ruby:railties-6.0.0/lib/rails/commands/server/server_command.rb#L76-86
      def log_to_stdout
        wrapped_app # touch the app so the logger is set up

        console = ActiveSupport::Logger.new(STDOUT)
        console.formatter = Rails.logger.formatter # ここでエラー
        console.level = Rails.logger.level

        unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
          Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
        end
      end
```

### Railsアプリの初期化
loggerといえばいつも `config/environments/<env>.rb` で何か設定している覚えがある。今回は `config.ru` で `require_relative 'config/environment'` をしていないため、抜けている処理がそこにあると考えられる。探したら怪しい1行があったので、直接 `config.ru` に加える。

```ruby:config.ru
Rails.application.initialize!

run Rails.application
```

これでついにサーバーが起動する。 `log/` と `tmp/` が作られるので、 `.gitignore` で除外しておくといい。

```
=> Booting WEBrick
=> Rails 6.0.0 application starting in development http://localhost:3000
=> Run `rails server --help` for more startup options
config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly:

  * development - set it to false
  * test - set it to false (unless you use a tool that preloads your test environment)
  * production - set it to true

[%Y-%m-%d %H:%M:%S] INFO  WEBrick 1.4.2
[%Y-%m-%d %H:%M:%S] INFO  ruby 2.6.4 (2019-08-28) [x86_64-linux]
[%Y-%m-%d %H:%M:%S] INFO  WEBrick::HTTPServer#start: pid=XXXXX port=3000
```

しかしクライアントからアクセスすると500エラーになる。

```
[%Y-%m-%d %H:%M:%S] ERROR NameError: uninitialized constant ActionController::Base
	/home/user/projects/rails_mini_app/vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/middleware/static.rb:78:in `ext'
	...
127.0.0.1 - - [%d/%b/%Y:%H:%M:%S %Z] "GET / HTTP/1.1" 500 325
- -> /
```

### Rails関連のgemの読み込み
普通のアプリと比較して抜けている処理のうち、 `ActionController` の読み込みに関わっていそうなものは、railsのgemの読み込みと考えられる。今回は機能が非常に少ないので `rails/all` ではなく `action_controller/railtie` のみを読み込む。

```ruby:config/application.rb
require 'action_controller/railtie'

class MiniApp < Rails::Application
end
```

これでサーバーを起動してクライアントからリクエストすると、RoutingErrorになる。あと一歩のところまで来た。

```
Started GET "/" for 127.0.0.1 at %Y-%m-%d %H:%M:%S %z

ActionController::RoutingError (No route matches [GET] "/"):

...
127.0.0.1 - - [%d/%b/%Y:%H:%M:%S %Z] "GET / HTTP/1.1" 404 0
- -> /
```

### ルーティングとcontrollerを定義
普通は `config/routes.rb` に書く（初期化時の読み込み対象）が、ファイルを作らず他の場所に書けないか試した。その結果、Railsアプリ初期化後なら設定が反映された。

```ruby:config.ru
Rails.application.initialize!

Rails.application.routes.draw do
  root          to: 'mini#index', via: :all
  match '*any', to: 'mini#index', via: :all
end

run Rails.application
```

もちろんcontrollerが無いとエラーになるので、ルーティングに対応するものを作成しておく。 `app/` 直下は自動読み込みの探索対象なので、ディレクトリ名は `controllers` でなくても構わない。

```ruby:app/controllers/mini_controller.rb
class MiniController < ActionController::Base
  def index
    render plain: "Hello, world!\n"
  end
end
```

これでついにサーバーが **200 OK** を返す。

```
Started GET "/" for 127.0.0.1 at %Y-%m-%d %H:%M:%S %z
Processing by MiniController#index as */*
  Rendering text template
  Rendered text template (Duration: 0.1ms | Allocations: 3)
Completed 200 OK in 17ms (Views: 16.7ms | Allocations: 1269)


127.0.0.1 - - [%d/%b/%Y:%H:%M:%S %Z] "GET / HTTP/1.1" 200 14
- -> /
```

```shell-session:クライアント側
$ wget --quiet --save-headers --output-document=/dev/stdout http://localhost:3000
HTTP/1.1 200 OK
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
Content-Type: text/plain; charset=utf-8
Etag: W/"d9014c4624844aa5bac314773d6b689a"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 211fdde9-cb2b-4d7f-83de-7f7b6bc6619c
X-Runtime: 0.040717
Server: WEBrick/1.4.2 (Ruby/2.6.4/2019-08-28)
Date: %a, %d %b %Y %H:%M:%S %Z
Content-Length: 14
Connection: Keep-Alive

Hello, world!
```

---

ルーティングをRails初期化前に書くと無効なようで、development環境ではRailsのwelcome画面が表示された。

```
Started GET "/" for 127.0.0.1 at %Y-%m-%d %H:%M:%S %z
Processing by Rails::WelcomeController#index as */*
  Rendering vendor/bundle/ruby/2.6.0/gems/railties-6.0.0/lib/rails/templates/rails/welcome/index.html.erb
  Rendered vendor/bundle/ruby/2.6.0/gems/railties-6.0.0/lib/rails/templates/rails/welcome/index.html.erb (Duration: 129.8ms | Allocations: 415)
Completed 200 OK in 183ms (Views: 154.5ms | Allocations: 2171)


127.0.0.1 - - [%d/%b/%Y:%H:%M:%S %Z] "GET / HTTP/1.1" 200 399720
- -> /
```

これらのルーティングはこっそり加えられている。

```ruby:railties-6.0.0/lib/rails/application/finisher.rb#L74-86
      initializer :add_builtin_route do |app|
        if Rails.env.development?
          app.routes.prepend do
            get "/rails/info/properties" => "rails/info#properties", internal: true
            get "/rails/info/routes"     => "rails/info#routes", internal: true
            get "/rails/info"            => "rails/info#index", internal: true
          end

          app.routes.append do
            get "/"                      => "rails/welcome#index", internal: true
          end
        end
      end
```

## その他
### 動作しないこと

* Rakeタスク全般
	* No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)
	* メッセージにある通り、 `Rakefile` を用意すればいい<br>(例) https://github.com/hmmnrst/rails_mini_app/commit/fc9617c
* `bin/rails` による実行
	* cannot load such file -- rails/commands (LoadError)
	* `bundle exec` を省くための設定が必要
		* 普通は `config/boot.rb` に記載して読み込み
* `bin/rails` 以外のbinスクリプト実行
	* `require 'rails/commands'` が無い場合、railsのモジュールなどが用意されない
	* どこかで `config/application.rb` を明示的に読み込むのが楽
* ルーティング確認（`bundle exec rails routes`）
	* 次節を参照

### `rails routes` を有効化
このコマンドはサーバーを起動せず、**設定の読み込みのみを実施する**。そのため `config.ru` に全て書く方法ではうまくいかない。

「設定の読み込み」は、具体的には `config/environment.rb` を読み込むことで行う。そのため、最低でも以下のようにファイル分割する必要がある。

```ruby:config.ru
require_relative 'config/environment'
run Rails.application
```

```ruby:config/environment.rb
Rails.application.initialize!

Rails.application.routes.draw do
  root          to: 'mini#index', via: :all
  match '*any', to: 'mini#index', via: :all
end
```

これで動作する。

```shell-session:ルーティング確認
$ bundle exec rails routes 2>/dev/null
Prefix Verb URI Pattern     Controller#Action
  root      /               mini#index
            /*any(.:format) mini#index
```

### Gemの削減
gem `rails` は `action*` や `active*` などに依存しているが、そのほとんどは今回使用していない。エラーに出てきたのは `railties` なので、それに差し替える。

```ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

gem "railties"
```

これでgemが43個から24個に減った。

---

ところで、railsの中心的なgemとしてよく **"railties"** という名前が出てきたが、この語句はどういう意味なのか気になった。 `action_controller/railtie` があったので、単数形は "railtie" と思われる。

辞書などを調べてみたところ、どうやら**「[枕木](https://ja.wikipedia.org/wiki/枕木)」（[railroad tie](https://en.wikipedia.org/wiki/Railroad_tie)）**らしい。 "[tie](https://ejje.weblio.jp/content/tie)" だけでも枕木の意味を持つ。レールを支えるもの、なるほど。

### Herokuへのデプロイ
[普通の手順](https://devcenter.heroku.com/articles/getting-started-with-ruby#deploy-the-app)でデプロイして問題なく動く。必要な環境変数 `RAILS_ENV` と `SECRET_KEY_BASE` はHerokuで勝手に設定してくれる。

```bash
heroku create
git push heroku master
```