Ensuring that only one of n keys is present and valid

Today I had a go at writing a schema to model the validation that Pin Payments’ /charges API endpoint would need to offer.

One of the more interesting rules it has is that only one piece of card-related info is provided: the post data should have only one of the card, card_token or customer_token keys.

Each one of those keys would have it’s own related validation rules, of course. For example:

Dry::Validation.Schema do
  optional(:card).maybe do
    schema do
      required(:number).filled(size?: 16)
    end
  end
  optional(:card_token).maybe(format?: /^card_/)
  optional(:customer_token).maybe(format?: /^cus_/)
end

What I’m not sure about is how to write a rule that requires only one of these keys to be actually present. Would any of you know how to do this (and whether it’s currently possible with dry-validation)?

I initially went to create a high-level rule that would operation on the values of card, card_token and customer_token, and then I wanted to somehow put those values into an array, strip away the empty ones, and then check that the array’s length is only one, but I wasn’t sure how I could bundle up all three of those values and pass them to a single predicate.

Thanks for any help! :pray:

Hey Tim,

I guess the new input macro could be used in this case, so ie:

Dry::Validation.Schema do
  configure do
    def valid_keys?(input)
      input.keys.size == 1
    end
  end

  input :hash?, :valid_keys?

  optional(:card).maybe do
    schema do
      required(:number).filled(size?: 16)
    end
  end

  optional(:card_token).maybe(format?: /^card_/)
  optional(:customer_token).maybe(format?: /^cus_/)
end

Lemme know if that works for you.

1 Like

Thanks for the pointer to the input macro here, @solnic, it makes perfect sense!

However, the example code you’ve posted is crashing:

require "dry-validation"

schema = Dry::Validation.Schema do
  configure do
    def valid_keys?(input)
      input.keys.size == 1
    end
  end

  input :hash?, :valid_keys?

  optional(:card).maybe do
    schema do
      required(:number).filled(size?: 16)
    end
  end

  optional(:card_token).maybe(format?: /^card_/)
  optional(:customer_token).maybe(format?: /^cus_/)
end

input = {
  card: {number: "4200000000000000"},
  customer_token: "cus_1234",
}

result = schema.(input)

p result.messages

Running this gives the following error:

/Users/tim/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/dry-validation-3d090eeafac9/lib/dry/validation/schema/rule.rb:141:in `and': undefined method `to_ast' for #<UnboundMethod: #<Class:0x007ff15c11e7e8>#valid_keys?> (NoMethodError)
Did you mean?  to_s
	from /Users/tim/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/dry-validation-3d090eeafac9/lib/dry/validation/schema/value.rb:185:in `each'
	from /Users/tim/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/dry-validation-3d090eeafac9/lib/dry/validation/schema/value.rb:185:in `reduce'
	from /Users/tim/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/dry-validation-3d090eeafac9/lib/dry/validation/schema/value.rb:185:in `infer_predicates'
	from /Users/tim/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/dry-validation-3d090eeafac9/lib/dry/validation/schema/class_interface.rb:51:in `define'
	from /Users/tim/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/bundler/gems/dry-validation-3d090eeafac9/lib/dry/validation.rb:39:in `Schema'
	from test2.rb:3:in `<main>'

I’ll file a bug in dry-v for this.