Rails Tasks の CoC と DRY

[Ruby on Rails](https://rubyonrails.org/) の [Command Line](https://guides.rubyonrails.org/v8.0/command_line.html) は豊富で、開発や運用に必要なタスクが揃っています。
[カスタム Rake タスク](https://guides.rubyonrails.org/v8.0/command_line.html#custom-rake-tasks)も便利で、開発や運用に必要なタスクを追加することができます。
カスタム Rake タスクの数が増えると継続的な開発が困難になるので、規約を検討しました。

## Rails Tasks CoC

主に検討した[規約(CoC)](https://ja.wikipedia.org/wiki/%E8%A8%AD%E5%AE%9A%E3%82%88%E3%82%8A%E8%A6%8F%E7%B4%84)は３つ

- Rake タスクの _ネームスペース_ で、単一機能ごとに `.rake` ファイルを分離する。
- Rake タスクの `.rake` ファイルには、単一機能ごとのクラスの `.call` を記述する。
- Rake タスクの DB の操作は _dry run_ で試行ができ、 _trail_ で結果を確認できる。

## Hands-on

- Rails 8 で追加され[認証ジェネレータ](https://techracho.bpsinc.jp/hachi8833/2024_10_21/145343)の Hands-on で Rails Tasks の CoC を紹介します。

### Rails new

- `rails new` コマンドで Rails に必要なファイルが作成されます。
- Rails Tasks はデフォルトでも 60 件以上 あります。
  - 昔のRailsは `rake --tasks` コマンドでしたが、今は`rails --tasks` も使えます。

```sh
$ rails new myapp && cd myapp
$ rails --tasks | wc -l # 61
```

### Rails Generate Authentication

- `rails generate authentication` コマンドで `current`, `user`, `session` のモデルやコントローラーなど必要なファイルが作成されます。
- `rails db:migrate` コマンドでデータベースに `users`, `sessions` のテーブルが作成されます。

```sh
$ rails generate authentication
$ rails db:migrate
```

### Custom Rails Tasks

- データベースの users のテーブルのレコードの作成に Custom Rake(Rails) Tasks を利用します。
- `rails generate task users create` コマンドで `lib/tasks/users.rake` ファイルが作成されます。
  - `rails generate task` コマンドの第一引数は `namespace` で、 第二引数は `task` です。

```sh
$ rails generate task users create
```

- `rails --tasks` コマンドを実行すると、`bin/rails users:create` が追加されています。

```ruby:lib/tasks/users.rake
namespace :users do
  desc "TODO"
  task create: :environment do
  end
end
```

#### Namespace

- Rake タスクを単一機能ごとに分離するために `namespace` を利用します。
- `mkdir` コマンド と `mv` コマンドで単一機能の `.rake` ファイルを配置します。

```sh
mkdir lib/tasks/users
mv lib/tasks/users.rake lib/tasks/users/create.rake
```

- `tasks/users/create` の順番になるように `tasks` の `namespace` 上位階層に追加します。
- `rails --tasks` コマンドを実行すると、`bin/rails tasks:users:create` が追加されています。

```ruby:lib/tasks/users/create.rake
namespace :tasks do
  namespace :users do
    desc "create users"
    task create: :environment do
    end
  end
end
```

#### Call

- ユーザーを作成する `Users::CreateTask` クラスの `.call` を記述します。
- ログの出力は `Rails.logger` を利用します。
- `rails --tasks` コマンドを実行すると、`bin/rails tasks:users:create[password]` が追加されています。

```ruby:lib/tasks/users/create.rake
# bin/rails tasks:users:create[123]
namespace :tasks do
  namespace :users do
    desc "create users"
    task :create, %i[password] => :environment do |_task, args|
      Rails.logger = Logger.new($stdout)
      Rails.logger.info Users::CreateTask.call(password: args.password)
    end
  end
end
```

### Application Tasks

- 単一機能ごとのクラスは `app/tasks` にファイルを配置します。
- `ApplicationTask` クラスで `call` メソッド のみを利用するように `private_class_method` で制御します。

```sh
$ mkdir app/tasks
$ touch app/tasks/application_task.rb
```

- attr_accessor で `dry_run`, `trail` をインスタンス変数で読み書きできるようにします。

```ruby:app/tasks/application_task.rb
class ApplicationTask
  private_class_method :new

  def self.call(*params)
    new(*params).send(:call)
  end

  attr_accessor :dry_run, :trail

  def initialize(params)
    @dry_run = ENV['DRY_RUN'].present?
    @trail = { dry_run:, params:, message: {} }
  end
end
```

#### Users Create Task

- ユーザーを作成する `Users::CreateTask` クラスは `app/tasks/users` にファイルを配置します。

```sh
mkdir app/tasks/users
touch app/tasks/users/create_task.rb
```

```ruby:app/tasks/users/create_task.rb
class Users::CreateTask < ApplicationTask
  def initialize(params)
    @password = params[:password]
    super
  end

  def call
    ActiveRecord::Base.transaction do
      user = User.create_with(password: @password).find_or_create_by!(email_address: "dev+#{User.count + 1}@example.com")
      trail[:message][:user] = { email_address: user.email_address }
      raise ActiveRecord::Rollback if dry_run
    end
    trail
  end
end
```

#### Users Create Tests

- テストは `test` や `spec` にファイルを配置します。
  - `test/tasks/users/create_task_test.rb`
  - `spec/tasks/users/create_task_spec.rb`

(今回は割愛します。)

### Rails Tasks Run

- `bin/rails tasks:users:create[123]` コマンドで、ユーザーが作成されます。

```sh
$ bin/rails tasks:users:create[123]
```

#### dry run

- `dry run` は引数やネームスペースで対応を検討しましたが、環境変数の利用が[DRY](https://ja.wikipedia.org/wiki/Don%27t_repeat_yourself)に記述できるので採用しました。

```sh
$ DRY_RUN=true bin/rails tasks:users:create[123]
```

### Rails Authentication

- `rails generate controller Home index` コマンドで Signed を確認する `Welcome` ページに必要なファイルが作成されます。

```sh
$ rails generate controller Home index
```

#### Views

- [Turbo](https://railsguides.jp/v8.0/working_with_javascript_in_rails.html#turbo) を利用しているので `turbo_method: :delete` を設定します。

```html:app/views/home/index.html.erb
<h1>Welcome</h1>

<p>Signed in as: <%= Current.user.email_address %></p>
<%= link_to 'Sign out', session_path, data: { turbo_method: :delete } %>
```

#### Routes

- `root` に `home#index` を設定します。

```ruby:config/routes.rb
Rails.application.routes.draw do
  resource :session
  resources :passwords, param: :token
  root "home#index"
end
```

### Rails Server Run

- `rails server` コマンドで起動して http://localhost:3000/ にアクセスします。
- UnSigned の場合は http://localhost:3000/session/new にリダイレクトされます。

```sh
$ rails server
```

#### Sign in

- Rails Tasks で作成した メールアドレス と パスワード を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8369/76f2ccdd-8a69-78f8-5107-64c19aa4be8f.png)


#### Signed in

- メールアドレス と パスワード が一致すると Welcome ページが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8369/64ddcf50-9a90-149f-945a-1a6aafd06661.png)


## まとめ

カスタム Rake タスク も CoC や DRY で 継続的な開発が保てるようにしましょう。
認証ジェネレータは Hands-on をするときに、必要なファイルが作成されるので便利です。

[ポート株式会社 サービス開発部 Advent Calendar 2024](https://adventar.org/calendars/10620) の10日目と並走しています。