Exclusive disjunction on schema fields

Hi all!

I’ve a schema that allows certain requests to send an object’s uid or a set of data for a new object. So you can send either:

{
  customer: {
    id: 2
  }
}

xor:

{
  customer: {
    first_name: 'John',
    last_name: 'Doe'
  }
}

However it needs to be and exclusive disjunction. You are not allowed to send both id and data. So my first implementation looked like this:

required(:customer).schema do
  optional(:id).filled(:int?)

  optional(:first_name).filled(:str?)
  optional(:last_name).filled(:str?)
end

# Example is simplified, but in real life customer is nested deeper.
# Complicated ruler names need to exist for proper error.yml extraction

# Force customer attributes to be filled if no id given
%i[first_name last_name].each do |key|
  rule(
    "customer__#{key}".to_sym => [
      %i[customer id],
      %i[customer].push(key)
    ]
  ) do |id, param|
    id.none?.then(param.filled?)
  end
end

# Force id to be empty if other credit card attributes are filled
rule(
  customer__id: [
    %i[customer first_name],
    %i[customer last_name]
  ]
) do |id, first_name, last_name|
  (first_name.filled? | last_name.filled?).then(uid.none?)
end

but it’s ugly AF. Do you have any other ideas how to implement it better?
It’d be perfect if it looked like this:

required(:customer).xor(
  schema { required(:id).filled(:int?) },
  schema do
    required(:first_name).filled(:str?)
    required(:last_name).filled(:str?)
  end
end

It’d love more versatile solution, as I’ll have more cases like this in my code customer_data/customer_id, credit_card_data/credit_card_id etc.

EDIT 1: It can be either 0.13 or 1.0, but I prefer later, as I’ll be upgrading the gem soon.

I recommend upgrading to dry-validation 1.0.0 as the first step. You can easily define rules for this kind of cases. Keep the schema simple and just define the expected input structure and types and then have rules to handle all the complex validation cases.