はじめに
Bonjour! 近ごろ Duolingo のフランス語コースにハマっている @ima1zumi です。
さて、最近リクエストパラメータにバリデーションをかけたいことがありました。そのために、ユーザの入力を検証する専用のクラスを作ると便利だったので紹介します。
動作確認環境
- Ruby 3.0.1
- Ruby on Rails 6.1.4
実現したいこと
ここでは例として User
の一覧を表示する UsersController#index
に、表示順を制御する order_key
と order_direction
というリクエストパラメータを追加します。
order_key
は User
モデルのカラム名を、 order_direction
は asc
か desc
のみ受けつけます。これらのパラメータにバリデーションをかけて、規定の値のみ受け付けるようにします。
class UserController < ApplicationController def index # order に渡すクエリパラメータのバリデーションを行いたい @users = User.all.order(user_params[:order_key] => user_params[:order_direction]) end private def user_params params.permit(:order_key, :order_direction) end end
Input object の責務
ここではバリデーションをかけるクラスを Input object と呼ぶことにします。
これは Shopify の Upgrow (現在は非公開) の Input object の考え方を参考にしています。
このオブジェクトの責務は ユーザーの入力を検証しオブジェクト化する ことです。
参考:Upgrow: Railsアプリの保守性を高めるためのShopifyのアプローチ / Upgrow - Speaker Deck
Input object の実装
まず、 ActiveModel::Model
と ActiveModel::Attributes
を include
したクラスを作成します。
app/inputs/user_order_input.rb
class UserOrderInput include ActiveModel::Model include ActiveModel::Attributes end
ActiveModel::Attributes
ActiveModel::Attributes
を include
すると attribute
クラスメソッドを使えるようになります。 attribute
で属性を宣言するとアクセサが定義されたり、 initialize
でその値を渡すことができるようになります。attribute
に属性名と型を渡すと型をキャストします。
class UserOrderInput include ActiveModel::Model include ActiveModel::Attributes # 属性の設定を宣言的に記述する attribute :order_key, :string attribute :order_direction, :string, default: 'desc' end
ActiveModel::Attributes
のドキュメントはありませんので rails のコードを直接ご覧ください。
キャストできる型はこちらに書いてあります。
サンプルコード
attribute
で宣言した属性にアクセスすることができます。
user_order_input = UserOrderInput.new(order_key: 'name', order_direction: 'asc') p user_order_input.order_key # => name
いろいろな型にキャストしてみました。Boolean などキャストできて便利です。
class SampleInput include ActiveModel::Model include ActiveModel::Attributes attribute :number, :integer attribute :flag, :boolean attribute :birthday, :date end sample = SampleInput.new(number: '1', flag: 'true', birthday: '1993-02-24') p sample.number # => 1 p sample.number.class # => Integer p sample.flag # => true p sample.flag.class # => TrueClass p sample.birthday # => Wed, 24 Feb 1993 p sample.birthday.class # => Date
ちなみに、ActiveModel::Attributes
-> ActiveModel::Model
の順番で include
すると initialize
メソッドの評価順がおかしくなり動かなくなるので注意してください。
class SampleInput include ActiveModel::Attributes include ActiveModel::Model attribute :name, :string end # => #<Concurrent::Map:0x00007f8ab03225e8 entries=0 default_proc=nil> SampleInput.new(name: 'hoge') # => /Users/mi/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/activemodel-6.1.4/lib/active_model/attributes.rb:124:in `_write_attribute': undefined method `write_from_user' for nil:NilClass (NoMethodError)
ActiveModel::Model
ActiveModel::Model
を include
すると validates
クラスメソッドなどのバリデーションメソッドを使えるようになります。
class UserOrderInput include ActiveModel::Model include ActiveModel::Attributes attribute :order_key, :string attribute :order_direction, :string, default: 'desc' # 各属性のバリデーションを記述する validates :order_key, inclusion: { in: User.column_names }, presence: true validates :order_direction, inclusion: { in: %w(asc desc) } end
サンプルコード
インスタンスに対して valid?
/ invalid?
でバリデーション結果を確認することが出来ます。
user_order_input = UserOrderInput.new(order_direction: 'asc') p user_order_input.valid? # => true
user_order_input = UserOrderInput.new(order_direction: 'hoge') p user_order_input.valid? # => false
コントローラから呼び出す
ここでは UsersController
の index
アクションでバリデーションをかけます。
class UserController < ApplicationController before_action :valid_params, only: [:index] def index @users = User.all.order(user_order_input.order_key => user_order_input.order_direction) end private def valid_params head :bad_request if user_order_input.invalid? end def user_order_input @user_order_input ||= UserOrderInput.new(user_params) end def user_params params.permit(:order_key, :order_direction) end end
このようにコントローラとバリデーションの責務を分けることができました。
Input object と Form object の違い
Form object はいろいろな定義がありますが、ここでは willnet さんの定義を引用します。
form_withのmodelオプション*1にActive Record以外のオブジェクトを渡すデザインパターンです。form_withのmodelオプションに渡すオブジェクト自体もform objectと呼びます。
(参考:form objectを使ってみよう - メドピア開発者ブログ)
Form object と Input object の一番の違いは、 Input object は入力値の検証とインスタンスの作成のみ行うということです。
Form object は名前や性質がフォームと強く結びついています。また Form object を使って save
することをスコープに入れる場合があります。
今回紹介した方法では、フォームでなくてもよいです。また、 save
は行いません。
ここでは、リクエストパラメータのバリデーションに使える Input object の実装例を紹介しました。