Showing details of external validator's failures

Hi there! I have a question about external dependencies section,

On the page linked above, you can see AddressValidator being used, to validatate the address. However, at the end, we loose all the information related to the original address validation failure details.

Is there a way, to use nested/dynami contracts, but pass the error information to the parent’s failure?

TIP: In the 0,13 you could do sth like this:

# frozen_string_literal: true

module Schemas
  module Reports
    Address1Schema = Dry::Schema.define do
      required(:city).filled(:str?)
    end

    Address2Schema = Dry::Schema.define do
      optional(:city).filled(:str?)
    end

    UserSchema = Dry::Schema.define do
      required(:address_type).filled(:str?)
      required(:address).filled?(:hash?)
      
      rule(address: [:address, :address_type]) do |address, address_type|
        # s3 -> S3Source
        address_type.eql?('1').then(
          data_source_params.schema(Address1Schema)
        ) &
          addresss_type.eql?('2').then(
            data_source_params.schema(Address2Schema)
          )
      end
    end
  end
end

But with “new” syntax, where schemas were extracted, now the key.failure needs to be invoked. This accepts string as a message, and I can’t find a way to pass the details of the Address1Schema validation.

PS: I know it’s not the best example, I just need to call dynamic validator and pass the error details.

Thank you for help! cc: @solnic @flash-gordon

This is perhaps a problem with the documentation, calling the dependency address_validator may give the impression that this is intended to allow composing contracts together.

I don’t believe this is the case at all. It’s essentially just dry-initializer exposed as public API so that you can inject dependencies. I think the use-case for this is for injecting data that your rules consume for defining contract validations.

Here’s an (abridged) example of a contract I wrote that validates currency codes

module Billing
  module Contracts
    class Product < Contract
      option :currency_codes, default: -> { Billing.config.currency.codes }

      schema do
        optional(:line_items).array do
          schema do
            required(:key).value(Types::LineItem::Key)

            required(:price).schema do
              required(:unit).value(Types::Denomination)
            end

            optional(:code).value(:string)
            optional(:quantity).value(Types::Positive)
          end
        end
      end

      rule(:line_items).each do
        next unless (denomination = value[:price][:unit]).is_a?(String)

        unless currency_codes.include?(denomination)
          key.failure("unknown currency: #{denomination}")
        end
      end
    end
  end
end
1 Like

At the moment I weite random examples, to not bothering with NDA, but the use case is that we have a settings loader, which loads YAML defined step ctures, that may be nested to several levels and tricky sometimes.

For example, you can configure file storage - and this should use different structure validator depending on the chosen type.

This is why I need this and in the older version it was supported, so I wondered how to keep the functionality untouched after upgrading to new dry-validation.

I solved it for now but in a very dirty way, mapping nested contract errors and setting proper key failure for each.

I understand, you want to dynamically apply a schema to a subkey at runtime.

I don’t believe that is supported officially but it’s not hard to make that happen:

#!/usr/bin/env ruby

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'dry-validation'
end

require 'dry/validation'

Types = Dry::Types(default: :strict)

module Types
  Filled = String.constrained(filled: true)
end

module AddressSchema
  V1 = Dry::Schema.define do
    required(:city).value(Types::Filled)
  end

  V2 = Dry::Schema.define do
    optional(:city).value(Types::Filled)
  end
end

class UserSchema < Dry::Validation::Contract
  AddressType = Types::String.enum('V1', 'V2')

  schema do
    required(:address_type).value(AddressType)
    required(:address).hash
  end

  rule :address_type, :address do
    schema = AddressSchema.const_get(values[:address_type])

    schema.(values[:address]).errors.each do |msg|
      key(:address).opts << {message: msg.text, path: [:address, *msg.path], tokens: {}}
    end
  end
end

puts UserSchema.new.({ address_type: 'V1', address: { street_address: '123 main st' }}).errors.to_h

Thank you! This is exactly how I solved this in my project, however it looks ugly, and I thought there may be a more official way that I can’t figure out - especially because it was supported out of the box in the past versions.

Thanks for the confirmation, I’ll stick with it for now.

Perhaps you could make a macro that hides this, if not I would write a refinement/extension to the Key object.

something like

key(:address).schema(AddressSchema::V1)

should be easy enough to implement

1 Like