Setting values in dry-validation rules

TL;DR

In a dry-validation rule, is values[key_name] = "new value" considered public API?

Long version

Lets assume I have a contract where the fallback value of a key depends on another value and only applies under certain conditions.
Both, the user supplied and autogenerated values must pass another rule.

This simplified code example works just as expected, but:

  • Is this a supported use-case?
  • If it is, could there be a value=(v) method on the Evaluator that would make this more convenient?
class UpdatePostContract < Dry::Validation::Contract
  option :existing_slugs
  option :slug_builder

  params do
    required(:title).filled(:string)
    optional(:slug).value(:string)
  end

  rule(:slug) do
    if key? && !schema_error?(:title)
      # Only autogenerate slug when empty string is given
      if value.empty?
        values[key_name] = slug_builder.call(values[:title])
      end

      if existing_slugs.include?(value)
        key.failure("slug is already taken")
      end
    end
  end
end

contract = UpdatePostContract.new(existing_slugs: ["foo"], slug_builder: ->(s) { s.strip.downcase.downcase.gsub(/\s+/, "-") })

r1 = contract.call(title: "Foo Bar")
pp r1.to_h          #=> {:title=>"Foo Bar"}
pp r1.errors.to_h   #=> {}

r2 = contract.call(title: "Foo Bar", slug: "")
pp r2.to_h         #=> {:title=>"Foo Bar", :slug=>"foo-bar"}
pp r2.errors.to_h  #=> {}

r3 = contract.call(title: "Foo", slug: "foo")
pp r3.to_h         #=> {:title=>"Foo", :slug=>"foo"}
pp r3.errors.to_h  #=> {:slug=>["slug is already taken"]}

r4 = contract.call(title: "Foo", slug: "")
pp r4.to_h         #=> {:title=>"Foo", :slug=>"foo"}
pp r4.errors.to_h  #=> {:slug=>["slug is already taken"]}
1 Like

There’s context for this

  rule(:slug) do |context:|
    if key? && !schema_error?(:title)
      # Only autogenerate slug when empty string is given
      if value.empty?
        context[key_name] = slug_builder.call(values[:title])
      else
         context[key_name] = value
      end

      if existing_slugs.include?(value)
        key.failure("slug is already taken")
      end
    end

contract = UpdatePostContract.new(...)

r = contract.call(...)
r.context[:slug] # => ...

One can have an API wrapper that merges values and context but it’s not OOTB

Thanks!

I’m already using contexts for exporting objects loaded (from db) for other keys. So basically, rules should not interfere with values, like types could/would.

Now, and how could it be any different, things are a bit more complicated. There are many contracts using this, and other rules on the same key via macros. It would be a bit tedious to have all places using the slug param somehow check in two different places.

I guess that’s where an “API wrapper” comes into place.

… and of course, later rules need to be aware of that and look for the real value in either value or context. (I guess, that was the reason for “just” changing the value in my original attempt)

Dry-rb strongly emphasizes data immutability. dry-validation is intended to validate extant data, not mutate it. Mutation is a different concern that should happen elsewhere.

Instead of treating the contract as The Thing that does all of this, I think you will have more success treating them as steps along the path of this process that are composed into something else.

2 Likes

That makes total sense.
But in the end you want to have a reusable Thing that all that. Basic validation of input data, generate default/fallback values, re-validate those fallbacks.
From a user perspective that’s a “Contract”, but not a necessarily dry-validation contract.
I guess, I’m trying to figure out how such a Thing could be build in a way that behaves like a normal dry-v contract, but does more without bending the internal of dry-v.

For now, I have a simple class that wraps the execution of a dry-v contract instance, wrapping its Result which merges specifically marked context keys into its own values without modifying the contract data at all. For now, that works but it feels a bit too generic and might lead to contract rules getting out of hand.