こんにちは。最近筋トレにはまっている 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'
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 を実際に扱う上で困るネストしたパラメータに対応する方法を紹介しました。
この記事がいいと思った方はチャンネル登録とグッドボタンをよろしくおねがいします。
agile.esm.co.jp