[Dry-Schema] Adding rules to Dry::Schema

Hi, I’m currently migrating from dry-validation-0.13 to dry-schema-1.6.1 and couldn’t find a way to add validation rules.
Here’s an example I had when using Dry::Validation

Dry::Validation.Params do
  ...
  rule(role_for_manage_kind: %i[filter_by_kind filter_by_role]) do |filter_by_kind, filter_by_role|
    filter_by_kind.eql?('manage').then(filter_by_role.filled?)
  end
end

This is basically a conditional validation, if the value of filter_by_kind is equal ‘manage’, the field filter_by_role should be required
Any idea of how I go about this but using “Dry::Schema” instead?

Thanks in advance

@solnic Would you know if this is possible?

Why are you migrating to dry-schema and not dry-validation 1.x? Rules are supported by dry-validation, there’s no such concept in dry-schema.

This is somewhat related.

In a pre-1.0 dry-validation version, I had a Dry::Validation.Schema setup which allowed the use of custom methods to validate some fields. I think this can be replicated with rules in Dry::Validation::Contract, but previously we could also reuse/embed the schema around.

Somehow, taking from email examples, I’d like to have something like this:

class ExampleAddress < Dry::Validation::Contract do
  schema do
    required(:name).filled(:string)
    required(:email).filled(:string)
  end

  rule(:email) do 
    # ... custom validation
  end
end

class ExampleForm < Dry::Validation::Contract do
  schema do
    required(:message)
    required(:to).hash(ExampleAddress)
    required(:from).hash(ExampleAddress)
  end
end

Of course, that does not work. I’ve navigated the docs of dry-validation and dry-schema but I’ve been unable to find an answer. :sweat_smile:

Can this be done at all with the 1.6 version, or did the feature disappear?

Currently, Contracts don’t support composition. There is an open issue with an unfinished implementation of this feature, but this is still a work in progress.

Schemas may be composed, though, so I would recommend splitting this up along those lines. You may share rules as macros in 1.6, although this is considered an experimental feature so maybe you don’t want to rely on it.

Dry::Validation.register_macro(:email_format) do
  unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
    key.failure('not a valid email format')
  end
end

EmailAddress = Dry::Schema.Params do
  required(:name).filled
  required(:email).filled
end

class ExampleForm < Dry::Validation::Contract
  schema do
    required(:message)
    required(:to).schema(EmailAddress)
    required(:from).schema(EmailAddress)
  end

  rule(to: :email).validate(:email_format)
  rule(from: :email).validate(:email_format)
end
1 Like

This could work, thanks!

BTW the website’s docs have a different syntax for this, which fails to compile (validate isn’t defined for the class). I know how docs VS code can be, just thought I’d mention it. :slight_smile:

The macro-based approach looks messy to be honest. After playing with it, I know that it won’t work in my particular case because my Form class validates an array of messages, so the macro block gets a nil value in that case. I’m not sure if that’s a bug of just that I’m abusing a feature in a new way. :slight_smile:

class MoreForm < Dry::Validation::Contract
  register_macro(:email_macro) do
    # value => nil
  end

  schema do
    required(:data).array(:hash) do
      required(:to).schema(AddressSchema)
      required(:from).schema(AddressSchema)
      # ...
    end
  end

  rule(to: :email).validate(:email_macro)
end

Reading the docs, I get the sense that you’re encouraged to build a hierarchy of contracts classes, where macros and configuration are inherited. Wouldn’t that be simpler to have plain old ruby methods instead of passing around code blocks in that case?

I would recommend having at least an ApplicationContract with your default configuration and macros, yes