При задании схемы есть возможность указать кастомный тип данных на основе конструктора одного из базовых типов. Для примера возьмем конвертер римских чисел в Integer. При вводе невалидной комбинации должна возращаться человеко-читаемая ошибка.
RomanNumber =
Dry.Types::Integer.constructor do |input|
roman_to_int(input)
end
schema =
Dry::Schema.Params do
optional(:int).maybe(:integer)
optional(:roman).maybe(RomanNumber)
end
Реализация функции roman_to_int
не важна. Возьмем, например, такую:
H = {"VI"=>4, "XI"=>9, "LX"=>40, "CX"=>90, "DC"=>400, "MC"=>900, "I"=>1, "V"=>5, "X"=>10, "L"=>50, "C"=>100, "D"=>500, "M"=>1000}.transform_values { " #{_1}" }
def roman_to_int(roman_string)
roman_string.reverse.gsub(Regexp.union(H.keys), H).split.sum(&:to_i)
end
Проверим, что будет получаться при валидации схемы:
schema.call(int: 'invalid')
# => #<Dry::Schema::Result{:int=>"invalid"} errors={:int=>["must be an integer"]} path=[]>
schema.call(roman: 'invalid')
# => #<Dry::Schema::Result{:roman=>0} errors={} path=[]>
Первый результат ожидаем и нагляден, а вот второй - просто не корректен. Вместо ошибки о невалидности мы получили 0. Нужно добавить проверку римской нотации. Например, такую:
RomanNumber =
Dry.Types::Integer.constructor do |input|
if roman_valid?(input)
roman_to_int(input)
end
end
INVALID_ROMAN = /[MDCLXVI]/i
def roman_valid?(s)
!s.match?(INVALID_ROMAN)
end
Возникает вопрос, а что делать, если нотация не валидна? Никаких подсказок в документации нет, попробуем вызвать exception:
RomanNumber =
Dry.Types::Integer.constructor do |input|
if roman_valid?(input)
roman_to_int(input)
else
raise 'invalid roman number'
end
end
Метод тыка не сработал, в результате вызова получаем RuntimeError, а не красивую ошибку:
schema.call(roman: 'invalid')
# RuntimeError: invalid roman number
Придется покопаться в исходниках... Библиотека dry-types
специальным образом обрабатывает 3 класса ошибок: NoMethodError
, TypeError
, ArgumentError
. Поэтому, чтобы увидеть желаемую ошибку валидации, нужно использовать один из перечисленных:
RomanNumber =
Dry.Types::Integer.constructor do |input|
if roman_valid?(input)
roman_to_int(input)
else
raise TypeError, 'invalid roman number'
end
end
schema.call(roman: 'invalid')
# => #<Dry::Schema::Result{:roman=>"invalid"} errors={:roman=>["must be an integer"]} path=[]>
Вот теперь результат нам подходит.
Стоит отметить, что решение с перехватом ошибок конструктора не однозначно. NoMethodError
довольно частая ошибка в коде. Все наверняка встречали ситуацию, когда вместо ожидаемого значения, в переменной оказывается nil
. В этом случае вызов большинства методов вызовет ту самую NoMethodError
:
roman_to_int(nil)
# => NoMethodError: undefined method `reverse' for nil:NilClass
Ошибка может возникнуть как вследствие невалидных данных, так и вследствие ошибок в самом коде. Следует об этом помнить, когда пишете кастомный конструктор.
Итоговый код:
H = {"VI"=>4, "XI"=>9, "LX"=>40, "CX"=>90, "DC"=>400, "MC"=>900, "I"=>1, "V"=>5, "X"=>10, "L"=>50, "C"=>100, "D"=>500, "M"=>1000}.transform_values { " #{_1}" }
def roman_to_int(roman_string)
roman_string.reverse.gsub(Regexp.union(H.keys), H).split.sum(&:to_i)
end
INVALID_ROMAN = /[MDCLXVI]/i
def roman_valid?(s)
!s.match?(INVALID_ROMAN)
end
RomanNumber =
Dry.Types::Integer.constructor do |input|
if roman_valid?(input)
roman_to_int(input)
else
raise TypeError, 'invalid roman number'
end
end
schema =
Dry::Schema.Params do
optional(:int).maybe(:integer)
optional(:roman).maybe(RomanNumber)
end
schema.call(roman: 'invalid')
Top comments (0)