Reusable Validators/Contracts

Is it possible to reuse a validator which includes it’s schema and rules? Take the following, for example:

#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
  gem "dry-validation"
end

class Inner < Dry::Validation::Contract
  json do
    required(:label).filled(:string)
  end

  rule :label do
    key.failure("value can only be: Prometheus") unless value == "Prometheus"
  end
end

class Outer < Dry::Validation::Contract
  json do
    required(:context).schema(Inner.schema)
  end
end

result = Outer.new.call context: {label: "Hades"}
ap result.errors.to_h  # {}

Notice that I include Inner as a required schema within Outer but I don’t see an error for :label since only the schema is include but not the rules. Is there a way to fold in both the schema and the rules from another validator?

I was searching around and I see a lot of schema inclusion discussion but nothing with rules. I can’t see any support for this in the source code but wanted to ask anyway.

@i2w was doing some work on contract composition, but I don’t believe that has been completed.

This is still scheduled as a new feature but it’s hard to tell when somebody will have time to actually implement it.

Adam: Thanks. Actually, I was spending time reading that thread yesterday but noticed the discussion was six years ago. :sweat_smile: I’m going to take a deeper look at that discussion today.

Peter: Hmm, yeah, maybe I can figure out some way to contribute back myself. Maybe. :crossed_fingers:

I’m using this workaround and it works great for me. Composable contracts · Issue #593 · dry-rb/dry-validation · GitHub

1 Like

Brandon: Thanks that was a good tip and callout. I ended up modifying the solution a bit further to great effect. I need to spend more time within this space but the following is working as a temporary solution:

Dry::Validation.register_macro :contract do |macro:|
  result = macro.args.first.new.call value

  next if result.success?

  result.errors.each { |error| key(key.path.to_a + error.path).failure error.text }
end

Example:

class Outer < Dry::Validation::Contract
  json { required(:inner).schema Inner.schema }

  rule(:inner).validate(contract: Inner)
end

result = Outer.new.call context: {label: "Hades"}
ap result.errors.to_h   # {context: {label: ["value can only be: Prometheus"]}}
1 Like