こんにちは。最近筋トレにはまっている wat-aro です。
blog.agile.esm.co.jp で Input object が紹介されていますが、実際に使っていくとネストしたパラメータの扱いに困ったためその解決方法を紹介します。
この記事のコードはすべて https://github.com/wat-aro/input_object にあります。
ここでは題材として GitHub REST API の
POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews
を扱います。
API の定義は以下です。
https://docs.github.com/ja/rest/reference/pulls#create-a-review-for-a-pull-request
type RequestBody = { commit_id?: string; body?: string; event?: string; comments?: Array<{ path: string; position?: number; body: string; line?: number; side?: string; start_line?: number; start_side?: string }> }
ネストしたパラメータ
題材にしたAPI定義の comments
のようにリクエストパラメータがネストしている場合がよくあります。
Input object で API のリクエストパラメータを受け取るようにしていると、このネストしたパラメータの扱いに困ります。
先の記事に紹介されたように、 AtiveModel::Attributes
API を使うと ActiveRecord
と同じようにキャストした値が扱えるのですが、 ActiveModel::Type
にはネストしたパラメータを扱えるようなものがありません。
class ReviewInput include ActiveModel::Model include ActiveModel::Attributes attribute :commit_id, :string attribute :body, :string attribute :event, :string, default: 'COMMENT' # attribute :comments, ここでネストしたパラメータを扱いたい end
これを解決するためにハッシュや配列から別オブジェクトにキャストできるクラスを作成します。
まずは comment 用の Input object を用意します。
class Review::CommentInput include ActiveModel::Model include ActiveModel::Attributes attribute :path, :string attribute :position, :integer attribute :body, :string attribute :line, :integer attribute :side, :string attribute :start_line, :integer attribute :start_side, :string end
この Review::CommentInput
を ReviewInput
の attribute
に指定できるようにします。
Input クラスを引数にとって initialize できる ActiveModel::Type
を作成します。
class InputType < ActiveModel::Type::Value def initialize(input_class, array: false) @input_class = input_class @array = array end def type :"#{@input_class.to_s.tableize.singularize}" end private def cast_value(value) if array? if value.is_a?(Array) value.map {|v| to_input(v) } else [] end else to_input(value) end end def array? @array end def to_input(value) @input_class.new(value) rescue ArgumentError nil end end
InputType
を使うと Review::CommentInput
を attribute
で指定できるようになります。
attribute :comments, InputType.new(Review::CommentInput, array: true), default: []
これでネストしたパラメータや配列になっているパラメータを Input object として扱えるようになりました。
ネストしたパラメータのバリデーション
ネストしたパラメータを扱えるようになりましたが、今のままだとバリデーションが少し不便です。
ReviewInput#valid?
を呼び出してもネストした comments
のバリデーションが実行されません。
バリデーションメソッドを使って comments
のバリデーションを実行したとしても、そのままでは ReviewInput#errors
に追加されませんし、どのフィールドでエラーがでているかを翻訳できる形で表示するには一工夫必要です。
ここではカスタムバリデータを作成してこの問題を解決します。
このカスタムバリデータは以下のように書いて呼び出します。
validates :comments, input: true
これは、comments
に対して valid?
を呼び出し、errors
に翻訳できる形でエラーを追加します。
class InputValidator < ActiveModel::EachValidator def validate_each(record, attribute, input) if input.respond_to?(:each) input.each do |i| validate_input(record, attribute, i) end else validate_input(record, attribute, input) end end private def validate_input(record, attribute, input) if input.respond_to?(:valid?) return if input.valid? input.errors.messages.each do |key, values| values.each do |value| record.errors.add("#{record.class.to_s.tableize.singularize}/#{attribute}.#{key}", value) end end else record.errors.add(attribute, :invalid) end end end
翻訳ファイルについては
--- ja: activemodel: attributes: 'review_input/comments': body: コメントの本文 path: コメントのパス
のようにしておくとエラーがあった場合に
irb(#<Repositories::PullRequests::ReviewsController:0x00005634fe0e8a38>):003:0> e.record.errors.messages => {:"review_input/comments.body"=>["を入力してください"]} irb(#<Repositories::PullRequests::ReviewsController:0x00005634fe0e8a38>):004:0> e.record.errors.full_messages => ["コメントの本文を入力してください"]
のようにエラーメッセージを出せるようになります。
さいごに
Input object を実際に扱う上で困るネストしたパラメータに対応する方法を紹介しました。
この記事がいいと思った方はチャンネル登録とグッドボタンをよろしくおねがいします。