Валидация даты в dry_schema
Tags: ruby
Published: 2023-05-18 08:19:22

---
title: Валидация даты в dry_schema
published: true
description: Кастомный макрос для валидации даты
tags: ruby
# published_at: 2023-05-17 11:05 +0000
---

В своих проектах мы активно используем форм-объекты из библиотеки [active_dry_form](https://github.com/corp-gp/active_dry_form/), которая основана на [dry_schema](https://github.com/dry-rb/dry-schema). Предположим, что у нас есть форма с выбором даты:

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

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

```ruby
required(:published_on).filled(:date, gt?: Time.zone.today)
```

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

```ruby
required(:published_on).filled(:date) { gt?(Time.zone.today) }
```

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

Последний вариант выглядит наглядно, попробуем реализовать нечто подобное, расширив функционал dry. Формы в active_dry_form наследуют контракты общего `ApplicationContract`:

```ruby
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
```

Созданный макрос используется так:
```ruby
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](https://dry-rb.org/gems/dry-validation/main/extensions/#predicates-as-macros). Само по себе оно нам не интересно, но в нем задан класс `PredicateRegistry`, который можно использовать для выполнения предиката и генерации сообщений об ошибках. Переданный блок с валидацией даты можно найти в аргументах макроса `macro.args[0]`. Код в блоке подразумевает использование нужного контекста для предиката, создадим его `Dry::Schema::Macros::Value.new` и скомпилируем предикат `instance_exec(&macro.args[0])`. Получив предикат, выполняем его `PREDICATES.call(capture.name, ...)` и возвращаем ошибку, если неудачно.

PS:
У предложенного метода есть небольшой недостаток. Таким образом не получится использовать [алгебру предикатов](https://dry-rb.org/gems/dry-schema/1.13/advanced/predicate-logic/), указав, например, одновременно верхнюю и нижнуюю границу дат:

```ruby
rule(:published_on).validate(predicate?: -> { gt?(Time.zone.today) & lt?(Time.zone.today + 1.month) })
```

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