大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【自戒】継承関係を把握しようという話。

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

2月の毎日投稿達成まで本記事を合わせて残すところ2つとなりました。

正直に言うと最近はPF課題で詰まってましたので、新しい知識が学べない状況でして、そろそろネタ切れ感がうっすらと伝わってないか心配ですw

今回の記事は「継承関係はしっかり意識しよう」です。これは自戒の意味を込めて記事にしようと思い至りました。継承関係なんて超初学者向けと思われますが、当然自分も継承関係自体は理解していました。しかし、継承関係は視野が狭いととても見えづらいものになります。

この記事を書こうと思った発端はまさにこの継承関係を把握してなかったことから起こりました。継承関係を把握しておらず簡単な実装ができない理由を探すのに1日以上詰まってしまったお話を自戒の念を込めてさせていただきます!

またAPIについてよく理解していない方はまずはAPIについて勉強してください!API知らないと理解できないので。。。

どのような実装をしたいのか

本記事ではRUNTEQの課題のコードを使用しますので、コード自体はあまり見せられませんがご容赦ください。テストする内容はユーザーの新規登録機能のAPIです。登録されたユーザーの情報をAPIとして取得します。

{
    "data": {
        "id": "1",
        "type": "user",
        "attributes": {
            "name": "sample_name",
            "email": "sample@example.com"
        }
    }
}

上記のJSONを取得するためにPostmanで送るパラメータは以下の画像です。

https://i.gyazo.com/5a55c22be135f92f9bd04b16d31f3fd5.png

しかし、nameのパラメータを消して、emailを同じままで送ると下記のようなエラーメッセージのJSONが返ってきます。

{
    "message": "Bad Request",
    "errors": [
        "Name can't be blank",
        "Email has already been taken"
    ]
}

これはUserモデルにてバリデーションが設定されているからです。

# app/model/user.rb
class User < ApplicationRecord
・
  validates :name, presence: true
  validates :email, presence: true, uniqueness: trueend

今回実装方法に苦闘したのはまさにこの部分です。自分が実装したかった内容は、「JSONのerrorsにバリデーションエラーを表示させる」ことです。

現状のエラーハンドリング

実際のところ、パラメータが誤っている時下記JSONを出力するところまでは実装できました。

{
    "message": "Bad Request",
    "errors": [
        "ActionController::BadRequest"
    ]
}

このエラーはどのように表示しているのかというと、まず間違ったパラメータが渡った時にraiseメソッドで例外を明示的に起こすようにしました。@user.savefalseであれば例外が発生します。

# app/controllers/api/v1/registrations_controller.rb
raise ActionController::BadRequest unless @user.save

ActionController::BadRequestは別ファイルでrescue_fromでエラーハンドリングをしています。

これにより、ActionController::BadRequestのエラーが発生した時に行う処理をメソッドで設定することができます。今回のファイルでいえば、rescue400ActionController::BadRequest発生時に行う処理です。

module ApiErrorHandle
  extend ActiveSupport::Concern
  included do
    rescue_from ActionController::BadRequest, with: :rescue400
  end
・
・
・
    def rescue400(exception = nil, messages = nil)
    render_error(400, 'Bad Request', exception&.message, *messages)
  end

  private

  def render_error(code, message, *error_messages)
    response = {
      message: message,
      errors: error_messages.compact
    }

    render json: response, status: code
  end

rescue400のメソッドは基本的にprivateメソッドであるrender_errorとほぼ同じです。しかし、ActionController::BadRequestが発生しているので、引数にHTTPステータスコードである400と、エラーメッセージ'Bad Request'をそれぞれ指定しています。

第三引数について、exceptionの中身は#<ActionController::BadRequest: ActionController::BadRequest>となっておりnilではないのでexception&.messageでエラーは起きず"ActionController::BadRequest"を返しています。第四引数のmessagesrescue400メソッドの第二引数デフォルト値nilを返しています。

[1] pry(#<Api::V1::RegistrationController>)> exception
=> #<ActionController::BadRequest: ActionController::BadRequest>
[2] pry(#<Api::V1::RegistrationController>)> exception&.message
=> "ActionController::BadRequest"
[3] pry(#<Api::V1::RegistrationController>)> messages
=> nil

そして、render400メソッドの中で実行されるrender_errorメソッドですが、上述した通り、第一引数は400で第二引数は'Bad Request'です。第三引数である*error_messagesexception&.messageで出力された"ActionController::BadRequest"のみですので、要素数が一つの配列で["ActionController::BadRequest"]と出力されます。

[1] pry(#<Api::V1::RegistrationController>)> code
=> 400
[2] pry(#<Api::V1::RegistrationController>)> message
=> "Bad Request"
[3] pry(#<Api::V1::RegistrationController>)> error_messages
=> ["ActionController::BadRequest"]

よって変数responseの中身は以下のようになります。

[1] pry(#<Api::V1::RegistrationController>)> response
=> {:message=>"Bad Request", :errors=>["ActionController::BadRequest"]}

render json: response, status: coderesponseがレンダーされるわけですので、現状のJSONファイルがレンダリングされるわけです。

[再掲]

{
    "message": "Bad Request",
    "errors": [
        "ActionController::BadRequest"
    ]
}

この時の自分は完全にこのエラーハンドリングのロジックを理解していました。そこまで難しいわけでもなかったですし、このエラーハンドリングの実装自体は1つ、2つ前の課題でも実装済みです。

実際、何が問題かは把握していました。現在取得しているエラーメッセージは引数で指定したものですが、これをモデルのバリデーションエラーのエラーメッセージで指定できれば、バリデーションエラーをJSON形式で表示できるはずです。

{
    "message": "Bad Request",
    "errors": [
        "Name can't be blank",
        "Email has already been taken"
    ]
}

しかしながら、それの実装方法がわからなかったのです。バリデーションエラーをどう取得していいのかがわからなかったのです。

エラーハンドリングしているモジュールはユーザー登録用のコントローラーと違うファイルにあるので、モジュール内で@user.errors.full_messagesは取得できません。ですので、コントローラー内でエラーハンドリングをする必要があるのですが、エラーハンドリングようのモジュールを設けているのでコントローラーにも別でエラーハンドリングのロジックを記述するのは汚いコードを書くことになるのでできれば避けるべきです。

この部分で完全に自分は詰まってしまいました。

継承関係を理解する

仕方なく質問したところ講師から以下の返答をもらいました。

rescue400のメソッドはこのエラーハンドラーのモジュール内だけでしか利用しない想定ですかね?直接このメソッドを利用したりできそうな気がしませんか?

ぴかーーーーーーーーーーーーーーーん!!!!!

脳に電光石火が走るぐらいピカんとしました笑

rescue400メソッドはモジュール内でしか利用できないと思い込んでいました。しかし、APIのエラーハンドリングのモジュールはBaseControllerでincluedeしてました。

module Api
  module V1
    class BaseController < ApplicationController
      include ApiErrorHandle

そして、ユーザー登録用のコントローラーはBaseControllerを継承していることがわかりました。

class RegistrationsController < BaseController

つまり、ユーザー登録用のRegistrationsControllerでもAPIのエラーハンドリングのモジュールで定義されているメソッドを使用可能ということです!!!!これがわかった時本当に気持ちよかったです!

下記のように定義していたcreateアクションを変更しました。

def create
  @user = User.new(user_params)
  raise ActionController::BadRequest unless @user.save
  json_string = UserSerializer.new(@user).serialized_json
  render json: json_string
end

↓                

def create
  @user = User.new(user_params)
  if @user.save
    json_string = UserSerializer.new(@user).serialized_json
    render json: json_string
  else
    rescue400(nil, @user.errors.full_messages)
  end
end

これで再度誤ったパラメータ(nameのパラメータなし、emailを登録されたものにする)を送信すると、期待した通りのJSONが返却されました!!

{
    "message": "Bad Request",
    "errors": [
        "Name can't be blank",
        "Email can't be blank"
    ]
}

なんで解決できないかわからなかったのですが、本当に簡単に解決できましたね。

めちゃくちゃスッキリしました笑

終わりに

今回なぜこのように詰まったのかというと、親クラスがmoduleを継承していたことを完全に失念していたことにあります。本当に深く反省しています。そのためこの記事を書きましたし。

自戒の念もしっかり込めたのでこれぐらいで勘弁していただければと思いますww

さてさて、明日はいよいよ28日目の投稿!長かった毎日投稿も明日で最後です!

明日の記事ですが、技術ブログではなくブログの続け方について書いていこうと思います!

以上、大ちゃんの駆け出し技術ブログでした!