How to re-use validations in other validations?

We have a few transactions that all take a “message” as input, along with a few other arguments that differ. In dry-validations < 1.0, we did something like this:

MessageSchema = Dry::Validation.Params do
  optional(:body).maybe(:str?)
  optional(:attachments).each do
    schema do
      required(:key).filled(:str?)
      required(:size).filled(:int?)
      required(:content_type).filled(:str?)
      optional(:width).maybe(:int?)
      optional(:height).maybe(:int?)
    end
  end
  optional(:saved_reply).maybe
  optional(:flags).maybe(:array?)
  optional(:scheduled_at).maybe(:time?, :in_the_future?)

  validate(has_content: [:body, :attachments]) do |body, attachments|
    body.present? || attachments.present?
  end
end

# Then in each transaction, we have a validation like this, each with different
`required` params:
Dry::Validation.Schema do
  required(:conversation).filled
  required(:user).filled
  required(:message).schema(MessageSchema)
end

I’m trying to upgrade to dry-schema and dry-validations, and I’m having trouble accomplishing this. I can change it to required(:message).hash(MessageSchema), but I have to change MessageSchema to a dry-schema, and there’s no way to accomplish the :in_the_future? and body.present? || attachments.present? checks. If I convert MessageSchema to a Contract, I can’t pass it to required(:message).hash(MessageContract), it fails with undefined methodto_ast’ for #MessageContract:0x000055d600d0fc38`.

Is it possible to compose validations like this? I could write a :valid_message? macro that does MessageContract.new.call(value), but that would lose the validation error messages on message, wouldn’t it?

Hey Paul!

You can accomplish this with a macro:

require 'dry/validation'

class AppContract < Dry::Validation::Contract
  register_macro(:has_content?) do |macro:|
    if values[:body].nil? && values[:attachments].nil?
      key.failure("must have either body or attachments")
    end
  end
end

MessageSchema = Dry::Schema.Params do
  optional(:body).maybe(:string)
  optional(:attachments).array(:hash) do
    required(:key).filled(:string)
  end
end

class OtherContract < AppContract
  params do
    required(:message).schema(MessageSchema)
  end

  rule(:message).validate(:has_content?)
end

contract = OtherContract.new

puts contract.(message: { body: '', attachments: [] }).inspect
#<Dry::Validation::Result{:message=>{:body=>nil, :attachments=>[]}} errors={:message=>["must have either body or attachments"]}>

Let me know if this works for you.

Ok, that’s what I was suspecting.

On our case, we have several contracts that have different sets of args, but all have the same Message. So we might have

ContractA
  required(:x).filled
  required(:y).filled
  required(:message).schema(MessageSchema)

  rule(:message).validate(:has_content?)
end

ContractB
  required(:x).filled
  required(:z).filled
  required(:message).schema(MessageSchema)

  rule(:message).validate(:has_content?)
end

Given that we re-use that same “message validation” as a portion of a handful of other contracts, we’d have to be sure to specify both the schema and rule in every case?

Is there a feature on the roadmap to make them composable in a single shared validation/schema?

1 Like

Yes. I’ll be adding full support for contract inheritance (currently only rules are inherited).