В своих проектах мы активно используем форм-объекты из библиотеки active_dry_form, которая основана на dry_schema. Предположим, что у нас есть форма с выбором даты:
class ArticleForm < ApplicationForm
fields do
params do
required(:published_on).filled(:date)
end
end
end
В форме указывается дата публикации. Примем, что это должна быть дата в будущем. Первая мысль - использовать для валидации синтаксис предикатов:
required(:published_on).filled(:date, gt?: Time.zone.today)
Тут мы можем попасть на частую ошибку неопытных разработчиков. Указанная таким образом дата закешируется при старте приложения. Немного покопавшись в документации, можно прийти к такому варианту:
required(:published_on).filled(:date) { gt?(Time.zone.today) }
На первый взгляд, это именно то, что надо. Вызов даты использован внутри блока, при каждом выполнении предиката должно подставляться актуальное значение. К сожалению, это не так. Предикат кешируется вместе с переданным значением.
Последний вариант выглядит наглядно, попробуем реализовать нечто подобное, расширив функционал dry. Формы в active_dry_form наследуют контракты общего ApplicationContract
:
require 'dry/validation/extensions/predicates_as_macros'
class ApplicationContract < ActiveDryForm::BaseContract
PREDICATES = Dry::Validation::PredicateRegistry.new
register_macro(:predicate?) do |macro:|
next unless value # optional
capture = Dry::Schema::Macros::Value.new.instance_exec(¯o.args[0])
predicate_args = [*capture.args, value]
message_opts = PREDICATES.message_opts(capture.name, predicate_args)
key.failure(capture.name, message_opts) unless PREDICATES.call(capture.name, predicate_args)
end
Созданный макрос используется так:
class ArticleForm < ApplicationForm
fields do
params do
required(:published_on).filled(:date)
end
rule(:published_on).validate(predicate?: -> { gt?(Time.zone.today) })
end
end
В составе dry_validation есть расширение predicates_as_macros. Само по себе оно нам не интересно, но в нем задан класс PredicateRegistry
, который можно использовать для выполнения предиката и генерации сообщений об ошибках. Переданный блок с валидацией даты можно найти в аргументах макроса macro.args[0]
. Код в блоке подразумевает использование нужного контекста для предиката, создадим его Dry::Schema::Macros::Value.new
и скомпилируем предикат instance_exec(¯o.args[0])
. Получив предикат, выполняем его PREDICATES.call(capture.name, ...)
и возвращаем ошибку, если неудачно.
PS:
У предложенного метода есть небольшой недостаток. Таким образом не получится использовать алгебру предикатов, указав, например, одновременно верхнюю и нижнуюю границу дат:
rule(:published_on).validate(predicate?: -> { gt?(Time.zone.today) & lt?(Time.zone.today + 1.month) })
Комбинированное условие имеет класс Dry::Logic::Operations::And
, не удалось с наскока решить эту проблему. Возможно у кого-то есть еще идеи?
Top comments (0)