RailsのController-Viewのエラー表現は返り値を意識しないと色々困るのでメモした

## Rubyの返り値って
rubyは最後に評価された値を返り値にしていますが、静的型付け言語ではほとんどの場合返り値を指定してメソッドを作ります。

```java
public int sum(int a, int b){ return a + b }
```
```kotlin
public fun sum(a: Int, b: Int): Int = a + b
```

rubyの場合わざわざ返り値の型を宣言しなくていいので、そこを意識するとRailsでいうところの
ControllerからViewまでのエラー表現がシンプルになりそうということのメモです.

```ruby
def sum(a, b)
  ...
end
```

## RailsにおけるControllerの処理系のパターン
更新系の処理に絞っています。
また、`@model`が保存できるかという条件分岐を分けるのが`save`というメソッドが `true or false` になるという以下の処理系がScaffoldでも採用されています。

- 綺麗に済む一般的なcrudのパターン

```ruby
def create
  @model = Model.new(create_params)
  if @model.save
    redirect_to ...........
  else
    render :new
  end
end

### View
  - @model.errors.full_messages.each do |msg|
    %p.error= msg
```

なんらか複雑な、トランザクションを含んだ処理型では例外を発生させて、発生しうる例外を
捕捉するようにしています。

- 例外を発生させて捕捉するパターン

```ruby
  def create
    @model = Model.new(create_params)
    Model.transaction do
      @model.save!
      @model.store_image!
      redirect_to ...........
    end
  rescue 例外A、B、C、D
    render :new
  end

### View
  - @model.errors.full_messages.each do |msg|
    %p= msg
```

エラーメッセージなのですが、問題があった操作に対しては`@model.errors`に含まれるエラーメッセージを表示するというのが一般的でしょう。

ただ、捕捉すべき例外は削除されたり、増えたりする可能性があります。
最近ではこのあたりです 。https://qiita.com/ryohashimoto/items/a8ba7bb8d6f340894188#activerecordnotnullviolation

必要が無いなら分岐を作る場合はなるべく**true or false**で分岐を作ってあげるのが良さそうです。

Viewには@modelのerrorsを渡せば正規化されたエラーの原因が
分かるのですが、`store_image!`で発生し得る例外を幾つか考えてみなければいけません。
なので、@modelのerrorsに加えて、想定し得ない例外をユーザーに提示する場合このようになります。

```ruby
  def create
    @model = Model.new(create_params)
    Model.transaction do
      @model.save!
      @model.store_image!
      redirect_to ...........
    end
  rescue 例外A、B、C、D => e
    flash[:alart].now = e.message
    render :new
  end
### View
  - @model.errors.full_messages.each do |msg|
    %p= msg
  = flash[:alert]
 ```
途端に怪しくなりました。

flashと@modelのエラーのそれぞれをviewでユーザーに見せるような形になっています。
加えて、素の例外のメッセージは大方英語で、ユーザーが理解できないような形になります。(例えなので普通そんなメッセージはユーザーに見せませんが、、)

単純に全てをrescueするのは把握できない理由も含めて失敗したと捉えるもので、
例外の原因を探るために一手間入れないと行けなくなります。

例えばユーザー向けのメッセージは単一にして、開発者には例外通知のメールを送信するなどです。

```ruby
  rescue => e
    ExceptionNotifier.notify_exception(e)
    flash[:alart].now = "何らかのエラーが発生しました"
    render :new
```
ただ、こういったことを続けていても、場当たり的にこのような処理を色々な箇所に追加することになってしまうので
基本的には捕捉できる例外は捕捉して、捕捉し得ない例外にだけ自動で例外通知を送るように
するのが自明だと考えています。

つまり**捕捉できる例外**を開発者が調べて都度追加する必要があるのではないかということを考えると、
なるべくControllerの分岐処理は`true or false`で判断できる形がいいということでした。

## それでも例外処理が必要になる場合
ひとつのアクションで複数の事をしようとした場合に例外が発生しうるケースはほとんどのシステムで
そのようなケースはあります。
例外のメッセージが@modelのエラーに含まれる形になっていないため、
Controllerから具体的な処理を別のクラスにしてエラーをまとめたいと思いました。

Controllerに@errorsをupdaterの返り値で渡して true or falseの形にまとめるようにしました。

作成したPOROからは、どのようなケースでも最後に@errorsを返すようにしています。
エラーがなければ、Viewに表示されるエラーメッセージは無く、
逆は先述の`@modelのerrorsに加えて、想定し得ない例外をユーザーに提示する`形です。

結果、Controllerの@errorがupdaterに収められているかで条件分岐をif式で行う事ができ、
Viewに表示するエラーも@errorsにまとめることができました。

```ruby
# Controller
  def create
    @model = Model.new(create_params)
    updater = ModelUpdater.new(@model)
    
    if (@errors = updater.run).present?
      redirect_to ...........
    else
      render :new
    end
  end

# PORO
class ModelUpdater
  def initialize(model)
    @model = model
    @errors = []
  end

  def run
    Teacher.transaction do
      @model.save!
      @model.store_image!
    end
    @errors
  rescue 例外A、B、C、D => e
    @errors.push e.message
    @model.errors.full_messages | @errors # @errorsの配列を返す
  end
end

# Viewのエラー表示
- @errors.each do |err|
  %p err
```

ただ、このケースだと@errorsなるインスタンス変数を余計にControllerに持たせることになるため
テストを少し多めに書かなければなりそうです。

シンプルに以下のような形でエラーを表出させることができないかを考えて見たのですが、

```ruby
  - @model.errors.full_messages.each do |msg|
    %p= msg
```
例外のメッセージ含めそのままerrors.add(:base, :some_error)等としてしまいActiveModel|Recordのエラーとして
しまうのが個人的には良さそうな方法でした。

```ruby
# PORO
class ModelUpdater

  def initialize(model)
    @model = model
  end

  def run
    Teacher.transaction do
      @model.save!
      @model.store_image!
    end
    true #処理を駆け抜けたらtrueを返す
  rescue 例外A、B、C、D => e
    @model.errors.add(:base, e.class.to_s.underscore)
    false # 失敗したらfalseを返す
  end
end

# Locale
ja:
  activerecord:
    errors:
      models:
        model:
          attributes:
            base:
              some_error_a: "some_errorA occurred"
              some_error_b: "some_errorB occurred"
              some_error_c: "some_errorC occurred"
              some_error_d: "some_errorD occurred"
# View
    - @model.errors.full_messages.each do |msg|
    %p= msg
```
これによりControllerは以下のようにtrue or falseによる分岐処理に専念させることができ、
Viewにおけるエラー表示も正規化することができました。

```ruby
# Controller
  def create
    @model = Model.new(create_params)
    updater = ModelUpdater.new(@model)
    
    if updater.run
      redirect_to ...........
    else
      render :new
    end
  end
  ```

## まとめ
使う側、使われる側における条件分岐を`true or false`に仕向けること、
使う側が気にするべき状態を使われる側から減らしてやることで処理をシンプルにできました。

言いたいこと自体は、使われる側が使う側を楽にハンドリングさせるために返り値を正規化してあげれば楽になれる箇所があるということだったのですが、エラーメッセージのハンドリングに終始してしまいました。。

スコープが多岐に渡る変数が氾濫していると、どこで何のために使われるものなのかが
見づらくなりますし、他のアクションではまた別の実装方法で`@errors`をもたせたりなど、
基本的な事なのですが関連するプロジェクトで見受けられたのでまとめました。

最終的な例では主体となるActiveRecord|ActiveModelインスタンス(`@model`)を継承したクラスインスタンスを想定して`errors.add`としていますが、
これがない場合以下のようなカスタムエラーオブジェクトを返却してもいいかもしれません。

エラーメッセージの表現に関してActiveRecord|ActiveModelの恩恵に授かるのが今のところ楽な感触でした。

```ruby
class SomeErrorModel
  include ActiveModel::Model
  delegate :full_messages, to: :errors
  ERRORS = [:some_error_a, :some_error_b, :some_error_c, :some_error_d].freeze

  def add(code)
    raise ArgumentError unless code.in?(ERRORS)
    errors.add(:base, code)
  end
end
```

ピュアなRubyなら以下のような形で、色々な箇所から共用できるようなエラーを表現できるインスタンスをクラス化しておけば汎用的に使えそうです(apiとかバッチ処理とか)

```ruby
class SomeErrorModel
  attr_reader :errors
  ERRORS = {
    ok:      {code: :E_A, msg: "error a occured"},
    invalid: {code: :E_B, msg: "error b occured"},
    taken:   {code: :E_C, msg: "error c occured"}
  }.freeze
  
  def add(code)
    raise ArgumentError unless code.in?(ERRORS.keys)
    erros << ERRORS[code]
  end
  
  def full_messages
    erros.map{|h| h[:msg]}.uniq
  end
end
```