DEV Community

Sergei Malykh
Sergei Malykh

Posted on

2 1

Валидация даты в 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, не удалось с наскока решить эту проблему. Возможно у кого-то есть еще идеи?

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay