Rails: dry-rbでForm Objectを作る(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Rails Form Objects With dry-rb 原文公開日: 2016/09/06 著者: Michał Gutowski サイト: http://cucumbersome.net/ Rails: dry-rbでForm Objectを作る(翻訳) 現代のRailsでは、Form Objectを作るのは珍しくありません。多くのRuby開発者はVirtusやActiveModel::ValidationsをincludeしてForm Objectを作成することに慣れています。本記事では、dry-typeとdry-validationを使ってForm Objectを作成する方法をご紹介したいと思います。 絵ハガキ(postcard)を作成する簡単なForm Objectを作ってみましょう。このアプリには次の3つのモデルがあります。 Country: フィールドはnameとis_state_required。2つ目のフィールドは正しいアドレスの作成に使われ、米国などのユーザーは州名の入力が必要です。 Country::State: フィールドはnameとcountry_id。 Postcard: フィールドはstate_id、country_id、content、address。 完了までの作業を定義する フォームで新しい絵ハガキを作成する(だいたいおわかりですね) 住所、市町村、郵便番号、コンテンツ、国のバリデーションを行う 郵便番号フォーマットのバリデーションを行う コンテンツの長さのバリデーションを行う(ツィートやテキストメッセージ並に短くしたい場合) 選択した国で州名が必要な場合、州名の存在のバリデーションも必要 属性と型 まずは属性の定義から行います。Form ObjectはDry::Types::Structから継承する必要があります。必要なゲッターやコンストラクタはDryで定義されます。 class Postcard module Types include Dry::Types.module end class CreateForm < Dry::Types::Struct attribute :address, Types::Coercible::String attribute :city, Types::Coercible::String attribute :zip_code, Types::Coercible::String attribute :content, Types::Coercible::String end end Dry::Types.moduleをincludeするだけでDry-typesの型を使えるようになります。Dry-typesでは変更に応じた多くのプリミティブ型を選択できます。 Railsモデルを使う場合はもう少し複雑です。これらの型で属性を作成するには、型を登録する必要があり、TypeName = Dry::Types::Definition.new(::MyRubyClass)のように行います。.constructorをブロック付きで呼び出すと、dry-typesで構成される型を指定できます。 定義は以下のような感じになります。 module Types include Dry::Types.module Country = Dry::Types::Definition.new(::Country) CountryState = Dry::Types::Definition.new(::Country::State) end これで、CountryとCountryStateを型として使えるようになりました。最終的なフォームの定義は次のようになります。 class CreateForm < Dry::Types::Struct attribute :address, Types::Coercible::String attribute :city, Types::Coercible::String attribute :zip_code, Types::Coercible::String attribute :content, Types::Coercible::String attribute :country, Types::Country attribute :state, Types::CountryState end これでやっと、シンプルなstructを作成できました。 メモ: dry-typesのstructコンストラクタについて コンストラクタの種類を指定しないと、strictコンストラクタが生成されます。この場合、属性が見つからないとArgumentErrorをスローします。存在のバリデーションはdry-validationで行うので、より多くの情報を含むコンストラクタであるschemaコンストラクタやsymbolizedコンストラクタを使うことになります。schemaコンストラクタを使うには、クラス本体の中でconstructor_type(:schema)を呼ぶ必要があります。 バリデーション Form Object内部でバリデーションを実行するには、dry-validation gemを使います。これにはさまざまな述語(メソッド)が含まれており、使い方も簡単です。まずは存在のバリデーションを行ってみましょう。 PostcardSchema = Dry::Validation.Schema do required(:address).filled required(:city).filled required(:zip_code).filled required(:content).filled required(:country).filled end 先ほど定義したモデルの属性を渡すスキーマを次のように定義します。 errors = PostcardSchema.call(to_hash).messages(full: true) それではこの動作を見てみましょう。 to_hash(またはto_h): 属性をハッシュベースで生成する .messages(full: true): 完全なエラーメッセージを返す。 フォーマットや長さなど、渡すバリデーションの要件を増やすには、単に.filledメソッドにパラメータを渡します。contentを例に取ると、存在バリデーションの他に、20文字より長いこともバリデーションされます。 required(:content).filled(min_size?: 20) 利用できる述語の全リストはこちらをご覧ください。 バリデーションロジックがさらに複雑な場合 存在や長さのバリデーション機能はdry-validationによって提供されます。残念なことに(?)、実際に動くアプリではこれだけでは不十分です。そのため、dry-validationで独自の述語を書けるようになっています。 まずは簡単なものから。バリデーションに渡されたcountryにstateが必要な場合は以下のように書きます。 PostcardSchema = Dry::Validation.Schema do configure do config.messages_file = Rails.root.join(‘config/locales/errors.yml’) def state_required?(country) country.is_state_required end end # (…) end このとおり簡単です。errors.ymlに正しいエラーメッセージを書いておくのをお忘れなく。エラーファイルについて詳しくはこちらをどうぞ。 次はいよいよ、countryで必要になった場合にのみstateの存在をチェックしましょう。stateが存在するかどうかをバリデーションに伝える必要があります。これは、スキーマに以下の行を書くだけでできます。 required(:state).maybe ルール自体を定義する ルール自体は次のように定義します。 rule(country_requires_state: [:country, :state]) do |country, state| country.state_required? > state.filled? end これも見てのとおり簡単です。 ルール内で必要となるフィールドに沿ったルール名を渡します。ここではcountryとstateを使います。 これらの変数はブロックにyieldされます。 「stateが必要な場合は、stateの存在をチェックする」というようにルールが変換されます。 より高度なルールについて詳しくはこちらをどうぞ。 完成したForm Object class … Continue reading Rails: dry-rbでForm Objectを作る(翻訳)