Custom predicates again question

Hello, I need to filter field data before coercion, but for this logic, I need a custom predicate.

class User::Create < OperationBase
  option :params, DryTypes::Strict::Hash

  class Contract < ContractBase
    option :record

    params do
      optional(:phone).filter(:phone_plausible?).filled(DryTypes::NormalizedPhone)
    end
    rule(:phone).validate(:unique)
  end

  def call
    user = User.new
    result = yield(validate(record: user))
    user.assign_attributes(result.to_h)
    user.save!
    Success(user)
  end
end

but I cant archive this

module DryCustomPredicates
  include Dry::Logic::Predicates

  predicate(:phone_plausible?) do |value|
    Phony.plausible?(value)
  end
end

class ContractBase < Dry::Validation::Contract
  # config.predicates = Dry::Schema::PredicateRegistry.new(DryCustomPredicates)
  config.messages.backend = :i18n
  # config.messages.load_paths << 'config/locales/validation_errors.yml'
  config.messages.default_locale = :ru

  register_macro(:unique) do
    key.failure(:non_unique_value) unless record.class
      .where.not(id: record.id)
      .where(keys.first => value).empty?
  end

  register_macro(:email_format) do
    unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
      key.failure(:invalid_url_format)
    end
  end
end

suggest, please any true way for archive this logic, without monkey patching.
Currently I use non-strict coercion

  NormalizedPhone = DryTypes::Strict::String.constructor do |phone|
    Phone::Normalize.call(phone)
  end


class Phone::Normalize
  include Service

  param :phone_string, DryTypes::Strict::String

  def call
    return @phone_string unless can_normalize?
    Phony.format(
      normalized_phone,
      format: '+%<cc>s%<ndc>s%<local>s',
      spaces: '',
      local_spaces: ''
    )
  end

  private

  def can_normalize?
    !@phone_string.blank? && Phony.plausible?(phone_string_without_trash)
  end

  def normalized_phone
    Phony.normalize(phone_string_without_trash)
  end

  def phone_string_without_trash
    @phone_string.gsub(/-/, '').gsub(/^8/, '7')
  end
end

and then use rule
rule(:phone) do
key.failure(:invalid_phone_format) if key? && !Phony.plausible?(value)
end

but this method seems not beautiful

I believe you can achieve what you want in a simpler way:

require "dry/validation"
require "phony"

module Types
  include Dry.Types()

  NormalizedPhone = String.constructor { |value| Phony.normalize(value) if Phony.plausible?(value) }
end

class Contract < Dry::Validation::Contract
  params do
    optional(:phone).filter(:str?, :filled?).value(Types::NormalizedPhone)
  end
end

contract = Contract.new

puts contract.(phone: "12501602703").inspect
# #<Dry::Validation::Result{:phone=>"12501602703"} errors={}>

puts contract.(phone: "12345678").errors.to_h.inspect
# {:phone=>["must be a string"]}

puts contract.(phone: []).errors.to_h.inspect
# {:phone=>["must be a string"]}

puts contract.(phone: nil).errors.to_h.inspect
# {:phone=>["must be a string"]}

puts contract.(phone: "").errors.to_h.inspect
# {:phone=>["must be filled"]}

puts contract.(phone: "123-oops-45678").errors.to_h.inspect
# {:phone=>["must be a string"]}

This doesn’t require a custom predicate and keeps usage of Phony in a single place.

1 Like

Thanks! Now, dry-rb fully meets my requirements, I’m happy :slight_smile:

2 Likes