Rails7 で TODOアプリを作ろう ⑨ (Enumや日時のバリデーション)

## はじめに

### 前回は

#### バリデーションメッセージのテンプレート化 として

https://qiita.com/tomoaki-kimura/items/57eb705ceeb228161fe6

- コンソール
- バリデーション
- errors
- バリデーションメッセージ

について学びました。

### 今回は

#### Enumや日時のバリデーション として

- Enumのバリデーション
- カスタムバリデーション
- 日時のバリデーション
- タイムゾーン

について学びます。

では、はじめていきましょう。

## １. バリデーションヘルパー

### ヘルパー一覧

バリデーションに関しては、かなりの項目数になるので、一度Railsガイドに目を通してみると良いでしょう。

https://railsguides.jp/active_record_validations.html

今回は、Rails７以降で追加された便利なバリデーションを含めてやっていきましょう。

## ２. Enumのバリデーション

### validate: true

Enumの `validate` オプションはRails7.1から導入されたようです。

Enumは決められた値以外のものを割り当てると、例外エラーとなってしまいますので、これを防ぐ役割があります。

では、コンソールで検証してみましょう。

```zsh
 todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001> 
```

でコンソールを起動します。

```zsh
irb(main):001> Task.new
=> 
#<Task:0x00000001100d9380
 id: nil,
 title: nil,
 description: nil,
 start_time: nil,
 end_time: nil,
 status: "not_started",
 created_at: nil,
 updated_at: nil>
```

空でインスタンスを作ると、 `status` にデフォルト値が入った状態で作成されました。

次にenumが想定していない `not_started` `in_progress` `closed` 以外の値を入れてみます。

```zsh
irb(main):002> Task.new(status: :hello)
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/enum.rb:215:in `assert_valid_value': 'hello' is not a valid status (ArgumentError)

          raise ArgumentError, "'#{value}' is not a valid #{name}"
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

例外エラーとなりました。 `enum` のHashが参照出来ないようです。

もしも何かのミスで想定しない値が入った場合に例外ではなくバリデーションで拾いたいので、 `enum` の記述に少し追記します。

Taskモデルを見ましょう。

app/models/task.rb【確認】
```ruby
class Task < ApplicationRecord
  enum status: { not_started: 0, in_progress: 1, closed: 2 }

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true
end
```

Enumの場合、 `validates` メソッドを使わずに `enum` を使いますので、

```ruby
enum status: { not_started: 0, in_progress: 1, closed: 2 }
```

この行を、

```ruby
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true
```

と書き換えます。 `:status` の部分の書き方も変わっているので注意して下さい。

app/models/task.rb【編集】
```ruby
class Task < ApplicationRecord
  enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true #←ここ

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true
end
```

書き足したら、再読み込みを行います。

```zsh
irb(main):003> reload!
Reloading...
=> true
```

次に、もう一度先程のコードを実行します。

```zsh
irb(main):004> Task.new(status: :hello)
=> 
#<Task:0x000000010565f8c8
 id: nil,
 title: nil,
 description: nil,
 start_time: nil,
 end_time: nil,
 status: :hello,
 created_at: nil,
 updated_at: nil>
```

今度はエラーになりませんでした。(＊再び例外エラーになった場合は一旦コンソールを抜けて再起動してみて下さい。)

バリデーションをチェックしましょう。

```zsh
irb(main):005> task = Task.new(title: "test", start_time: Time.current, status: :hello)
=>
#<Task:0x0000000106751690
...
```

これで `starus` だけが不正な値のはずです。

```zsh
irb(main):006> task.valid?
=> false
irb(main):007> task.errors.full_messages
=> ["進捗は一覧にありません"]
```

想定通りとなりました。

## ３. カスタムバリデーション

### validate

Rails７.1 になって益々便利になりましたが、それでもバリデーションを自分で書きたい場合等もあります。

そういった場合は、 `validate` を使います。

書き方は、

```
validate :対象カラム名, :自作のメソッド名
```

となります。

例えば `start_time` は必須入力にはなっていますが、日付そのものには制約がありません。

app/models/task.rb【確認】
```ruby
class Task < ApplicationRecord
  enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true
end
```

これに `現在時刻の日のスタート時、0:00からしか入力出来ない` バリデーションを付けたいとなった場合、

```ruby
def only_after_today
  today = Time.current.beginning_of_day
  if today > start_time
    errors.add(:start_time, "は#{today}より大きい値にしてください")
  end
end
```

このようなメソッドを作って、条件にあてはまる場合のみ `errors` にメッセージを追加します。

```ruby
  validate :start_time, :only_after_today
```

`validates` で呼び出します。

編集後は、以下のようになります。

app/models/task.rb【編集】
```ruby
class Task < ApplicationRecord
  enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true
  #ここから追加
  validate :start_time, :only_after_today
  
  private
  
  def only_after_today
    today = Time.current.beginning_of_day
    if today > start_time
      errors.add(:start_time, "は#{today}より大きい値にしてください")
    end
  end
  #ここまで追加
end
```

また、 I18n も使えますので、

`"は#{today}より大きい値にしてください"`

の部分は、

`"は#{I18n.l(today, format: :long)}より大きい値にしてください"`

などのようにすることも可能です。

では検証します。

```zsh
todo_app % rails c
```

でコンソールを起動します。

`title` だけに値を入れておきましょう。

```zsh
irb(main):008> task = Task.new(title: "test")
=>
#<Task:0x0000000108c74580
...
```
`
次に、 `start_time` に現在時刻から遡った前日の値を入れます。

```zsh
irb(main):009> task.start_time = Time.current.yesterday
=> Mon, 20 Nov 2023 05:17:45.074460000 UTC +00:00
```

バリデーションを試します。

```zsh
irb(main):010> task.valid?
=> false
irb(main):011> task.errors.full_messages
=> ["開始日は2023-11-21 05:15:46 UTCより大きい値にしてください"]
```

通りませんでしたが、時間を入れ直すと通ります。

```zsh
irb(main):012> task.start_time = Time.current
=> Tue, 21 Nov 2023 05:24:27.811528000 UTC +00:00
irb(main):013> task.valid?
=> true
```

これで、`start_time` は現在時刻よりも前に登録できなくなりました。

## ４. 日付のバリデーション

### comparison

せっかくカスタムバリデーションを書いたのですが、日付においてはもう少し便利になったようです。

これもRails７から導入されたオプションとなりますが、イメージ的には `:numericality` に近い使い方で記述する事ができます。

`comparison` を使って、同じ事を実現してみましょう。

Taskモデルは、先程編集したので以下の通りです。

app/models/task.rb【確認】
```ruby
class Task < ApplicationRecord
  enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true
  validate :start_time, :only_after_today

  private
  
  def only_after_today
    today = Time.current.beginning_of_day
    if today > start_time
      errors.add(:start_time, "は#{today}より大きい値にしてください")
    end
  end
end
```

これに、 `comparison` を使うと以下のようにすっきりします。

app/models/task.rb【編集】
```ruby
class Task < ApplicationRecord
  enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true

  validates :title, presence: true, length: { minimum: 3, maximum: 20 }
  validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
  validates :start_time, presence: true, comparison: { greater_than_or_equal_to: Time.current.beginning_of_day } #←ここ
end
```

モデルは開発が進むと肥大化しますので、可読性を失わない程度にコンパクトになると良いですね。

検証すると

```zsh
 todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001> task = Task.new(title: "test", start_time: Time.current.yesterday)
=> 
#<Task:0x0000000105df0268
...
irb(main):002> task.valid?
=> false
irb(main):003> task.errors.full_messages
=> ["開始日は2023-11-21 00:00:00 +0900以上の値にしてください"]
```

同じ結果となりました。

## 5. タイムゾーン

また、お気付きの方もいらっしゃると思いますが、現状明らかに時刻が合っていません。

```zsh
irb(main):012> task.start_time = Time.current
=> Tue, 21 Nov 2023 05:24:27.811528000 UTC +00:00
```

これは、Railsの `タイムゾーン` の設定がデフォルトになっている為です。

`UTC +00:00` が `JST +09:00` となるように設定を行います。

### application.rb

では、application.rb で現状の設定を確認しましょう。

config/application.rb【確認】
```ruby
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 TodoApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w(assets tasks))

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
    config.i18n.default_locale = :ja
  end
end
```

24行目辺りに、コメントアウトされた設定があります。

``` ruby
# config.time_zone = "Central Time (US & Canada)"
```

これを日本時間に設定してしまいましょう。

```ruby
    config.time_zone='Tokyo'
    config.active_record.default_timezone= :local
```

このように置き換えて設定しておきます。

config/application.rb【編集】
```ruby
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 TodoApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w(assets tasks))

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    config.time_zone='Tokyo'
    config.active_record.default_timezone= :local
    # config.eager_load_paths << Rails.root.join("extras")
    config.i18n.default_locale = :ja
  end
end
```

では、改めてコンソールで確認しましょう。

`config` を書き換えていますので、一旦入り直します。

```zsh
 todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001> 
```

`start_time` に値が入ったインスタンスを作ります。

```zsh
irb(main):001> task = Task.new(title: "test", start_time: Time.current)
=> 
#<Task:0x0000000106429b98
...
irb(main):002> task.start_time
=> Tue, 21 Nov 2023 17:44:25.004300000 JST +09:00
```

`JST +09:00` になりました。これでOKです。

## 6. 【課題】バリデーションについての復習

課題です。ここは復習として自分で調べて解決してみましょう。

### 【課題1】　`end_time` にバリデーションを設定しましょう。

現在 `Task` モデルの `end_time` にはバリデーションが設定されていません。

これにバリデーションを設定してみましょう。仕様は以下の通りとします。

- 入力は必須ではない
- start_timeの時間よりも後の時間でなければならない。（同じ時間は含まない）

この条件を満たすバリデーションを書いてみましょう。

#### 課題は以上です。

## 7. git

### git

ここまでを保存しておきましょう。

```zsh
todo_app % git add .
todo_app % git commit -m "Enumや日時のバリデーション"
```

### GitHub

Githubの設定ができているのであれば、同名のリポジトリを作成し、pushしておきましょう。

## おわりに

日付関連ですごく参考になる記事を紹介させて下さい。

https://qiita.com/jnchito/items/cae89ee43c30f5d6fa2c

https://zenn.dev/ryouzi/articles/dda18594f2dbd3



本チャプターはここまでとなります。

### 次回は

- カラムの追加
- アソシエーション
- Viewヘルパー

を学びます。

ここまでお疲れ様でした。