DEV Community

Sergei Malykh
Sergei Malykh

Posted on

Валидация даты в dry_schema

В своих проектах мы активно используем форм-объекты из библиотеки active_dry_form, которая основана на dry_schema. Предположим, что у нас есть форма с выбором даты:

class ArticleForm < ApplicationForm
  fields do
    params do
      required(:published_on).filled(:date)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

В форме указывается дата публикации. Примем, что это должна быть дата в будущем. Первая мысль - использовать для валидации синтаксис предикатов:

required(:published_on).filled(:date, gt?: Time.zone.today)
Enter fullscreen mode Exit fullscreen mode

Тут мы можем попасть на частую ошибку неопытных разработчиков. Указанная таким образом дата закешируется при старте приложения. Немного покопавшись в документации, можно прийти к такому варианту:

required(:published_on).filled(:date) { gt?(Time.zone.today) }
Enter fullscreen mode Exit fullscreen mode

На первый взгляд, это именно то, что надо. Вызов даты использован внутри блока, при каждом выполнении предиката должно подставляться актуальное значение. К сожалению, это не так. Предикат кешируется вместе с переданным значением.

Последний вариант выглядит наглядно, попробуем реализовать нечто подобное, расширив функционал 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(&macro.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
Enter fullscreen mode Exit fullscreen mode

Созданный макрос используется так:

class ArticleForm < ApplicationForm
  fields do
    params do
      required(:published_on).filled(:date)
    end

    rule(:published_on).validate(predicate?: -> { gt?(Time.zone.today) })
  end
end
Enter fullscreen mode Exit fullscreen mode

В составе dry_validation есть расширение predicates_as_macros. Само по себе оно нам не интересно, но в нем задан класс PredicateRegistry, который можно использовать для выполнения предиката и генерации сообщений об ошибках. Переданный блок с валидацией даты можно найти в аргументах макроса macro.args[0]. Код в блоке подразумевает использование нужного контекста для предиката, создадим его Dry::Schema::Macros::Value.new и скомпилируем предикат instance_exec(&macro.args[0]). Получив предикат, выполняем его PREDICATES.call(capture.name, ...) и возвращаем ошибку, если неудачно.

PS:
У предложенного метода есть небольшой недостаток. Таким образом не получится использовать алгебру предикатов, указав, например, одновременно верхнюю и нижнуюю границу дат:

rule(:published_on).validate(predicate?: -> { gt?(Time.zone.today) & lt?(Time.zone.today + 1.month) })
Enter fullscreen mode Exit fullscreen mode

Комбинированное условие имеет класс Dry::Logic::Operations::And, не удалось с наскока решить эту проблему. Возможно у кого-то есть еще идеи?

Top comments (0)