Ruby
hanami

HanamiのInteractorでValidationを使いやすくする

More than 1 year has passed since last update.


経緯

HanamiのControllerから責務を分けるために、ビジネスロジックをInteractorに移していきたい。

同時にパラメータ検証(バリデーション)もInteractor側でやりたいが、Controllerはパラメータ管理が拡張されていて定義書くだけでできるのに対して、Interactorは普通にやるとやぼったい処理を書くことになる。

Hanami::Actionを読み込む方法案も考えたが、exposeなどで挙動の違いがあるほか、Interactorでやりたいことに閉じた拡張がしづらそうなので、ひとまず現状やりたいパラメータ検証に特化した汎用化を探ってみる。


InteractorでとりあえずValidationする場合

InteractorとValidationを組み合わせた手法をとると、例えば以下のようなController、Interactorが書ける。

参考: HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか


apps/web/controllers/items/create.rb

module Web::Controllers::Items

class Create
include Web::Action

expose :error
expose :item

def call(params)
interactor = ItemInteractor::Create.new(params).call

if interactor.successful?
redirect_to routes.item_path(interactor.item.id)
else
self.status = 422
@error = interactor.error
@item = interactor.item
end
end
end
end



lib/interactors/items/create.rb

require 'hanami/interactor'

require 'hanami/validations'

module ItemInteractor
class Create
include Hanami::Interactor

class Validation
include Hanami::Validations

validations do
required(:name).filled(:str?, size?: 1..200)
optional(:description) { str? & max_size?(1000) }
end
end

expose :item

def initialize(params)
@params = params
end

def call
@item = ItemRepository.new.create(
name: @params[:name],
description: @params[:description]
)
end

private

def valid?
validation = Validation.new(@params).validate
error(validation.messages) if validation.failure?

validation.success?
end
end
end


これをベースに拡張していけば、Entityごとのロジックは書いていけそうですが、#call以外はDRYじゃなくなりそうなのが気になる。


Interactorの汎用部分をmoduleに切り出してみる

Interactorでバリデーションを使う場合に共通化できそうな部分を切り出すと以下のような感じ。

だいぶすっきり。

Validation.class_eval のところはもっとスマートなやり方あれば知りたいところ。


lib/interactors/items/create.rb

require '../../modules/sample/interactor'

module ItemInteractor
class Create
include Sample::Interactor

Validation.class_eval do
validations do
required(:name).filled(:str?, size?: 1..200)
optional(:description) { str? & max_size?(1000) }
end
end

expose :item

def call
@item = ItemRepository.new.create(
name: @params[:name],
description: @params[:description]
)
end
end
end



lib/sample/modules/interactor.rb

require 'hanami/interactor'

require 'hanami/validations'

module Sample::Interactor
class Validation
include Hanami::Validations
end

def self.included(klass)
klass.class_eval do
include Hanami::Interactor

def initialize(params)
@params = params
end
end
end

private

def valid?
validation = Validation.new(@params).validate
error(validation.messages) if validation.failure?

validation.success?
end
end



さらにValidationまわりを切り出してみる

Interactorで毎回Validationするとも限らないので、Validationまわりを切り出して着脱可能にしておく。


lib/interactors/items/create.rb

require '../../modules/sample/interactor'

require '../../modules/sample/action/validatable'

module ItemInteractor
class Create
include Sample::Interactor
include Sample::Action::Validatable

Validation.class_eval do
validations do
required(:name).filled(:str?, size?: 1..200)
optional(:description) { str? & max_size?(1000) }
end
end

expose :item

def call
@item = ItemRepository.new.create(
name: @params[:name],
description: @params[:description]
)
end
end
end



lib/sample/modules/interactor.rb

require 'hanami/interactor'

module Sample::Interactor
def self.included(klass)
klass.class_eval do
include Hanami::Interactor

def initialize(params)
@params = params
end
end
end
end



lib/sample/modules/action/validatable.rb

require 'hanami/validations'

module Sample
module Action
module Validatable
class Validation
include Hanami::Validations
end

private

def valid?
validation = Validation.new(@params).validate
error(validation.messages) if validation.failure?

validation.success?
end
end
end
end


I18n対応なども組み込みやすくなりそう。