Top level logic validation

Hey there!

Is there any way I can validate, that my whole schema is one of existing?
For example I have 2 schemas:

UserSchema = Dry::Schema.Params do
  required(:name).filled(:string)
end

AdminSchema = Dry::Schema.Params do
  required(:email).filled(:string)
end

can I make some kind of contract, that would be valid if one of schemas is valid? like

my_contract = Dry::Validation::Contract.build do
  params(AdminSchema | UserSchema) do
  end
end
end

And also are you planning to add negation operation to schema predicate?
Thanks for the help! :3

Hey Stas. :wave: This question is tangentially similar to what was asked earlier here but I think your question can probably be solved through pattern matching in this situation. Here’s a little script you can tinker with that should demonstrate a potential solution for you:

#! /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-schema"
end

module Schemas
  User = Dry::Schema.Params { required(:name).filled :string }
  Admin = Dry::Schema.Params { required(:email).filled :string }
  Unknown = proc { "Unknown parameters." }
end

class Validator
  SCHEMAS = {user: Schemas::User, admin: Schemas::Admin, unknown: Schemas::Unknown}.freeze

  def initialize schemas: SCHEMAS
    @schemas = schemas
  end

  def call(parameters) = find_schema(parameters).call parameters

  private

  attr_reader :schemas

  def find_schema parameters
    case parameters
      in name:, **remainder unless remainder.key? :email then schemas.fetch :user
      in email:, **remainder unless remainder.key? :name then schemas.fetch :admin
      else schemas.fetch :unknown
    end
  end
end

validator = Validator.new

ap validator.call({name: "Jane Doe"})
ap validator.call({email: "Jill Smith"})
ap validator.call({name: "Invalid", email: "admin@example.com"})

When running the above you’ll see the following output:

#<Dry::Schema::Result{:name=>"Jane Doe"} errors={} path=[]>
#<Dry::Schema::Result{:email=>"Jill Smith"} errors={} path=[]>
"Unknown parameters."

You’ll notice that the validator handles a user, admin, and unknown data (simple use case but you can make it as sophisticated as needed). Basically, I’m using pattern matching to delegate to the appropriate schema to determine which schema to validate against. This assumes you have distinguishing keys withing your parameters to route to the appropriate schema, though.

Anyway, might be of interest as one solution.

Hey, thanks! But it’s not what I wanna do, in my case I have input like

{
  name: 'John',
  email: 'test@test.ru',
  phone: '898989898',
....
}

and I wanna it would be correct and invalid only when name and email blank.
Honestly, its for dynamic schema build, but I try to avoid monkey patching DSL or making something like Schema.from_ast 'cause it’s looks like it would take too much time :sweat_smile:

Yes, you can compose a schema that has multiple parents. Like so:

my_contract = Dry::Validation::Contract.build do
  params Dry::Schema.Params(parent: [UserSchema, AdminSchema])
end

In this case I have both validation from UserSchema and AdminSchema, and it’s only valid when both of validation passed, but I need, that result contract be valid even if one of schemas passed (like UserSchema || AdminSchema)

Apologies, I misunderstood your question. What you’re trying to achieve was intended to be done with Contract objects, not schemas.

In this case, you cannot reuse UserSchema and AdminSchema to do this because you need to change the semantics of the schema to express this rule.

At the schema level, both keys must be optional because one or the other can be missing. The rule would be:

my_contract = Dry::Validation::Contract.build do
  params do
    optional(:name).filled(:string)
    optional(:email).filled(:string)
  end

  rule :name, :email do
    unless values[:name] || values[:email]
      base.failure("must have one of: name, email")
    end
  end
end

Stas: Oh, OK, I see what you are saying. I thought you needed to toggle between different schema versions in a sense but I see what you are saying now.

Adam: Thanks. I wasn’t aware you could have parents so that was fun to learn.