Здесь мы определяем изменяемую константу, которая будет содержать зарегистрированные типы, сами типы и макрос, который будет их регистрировать. Мы также вызываем #resolve
Path
. Метод #resolve
преобразует путь в TypeNode
, который представляет собой типы переменных экземпляра. Метод #resolve
необходимо использовать только в том случае, если тип передается по имени, например, в качестве аргумента макроса, тогда как макропеременная @type
всегда будет TypeNode
.Теперь, когда у нас определена сторона регистрации, мы можем перейти к стороне времени выполнения. Эта часть представляет собой просто метод, который генерирует оператор case
MODELS
, например:def model_by_name(name)
{% begin %}
case name
{% for model in MODELS %}
when {{model.name.stringify}} then {{model}}
{% end %}
else
raise "model unknown"
end
{% end %}
end
Отсюда мы можем пойти дальше и добавить следующий код:
pp {{ MODELS }}
pp model_by_name "Cat"
register_model Cat
register_model Dog
pp {{ MODELS }}
pp model_by_name "Cat"
После его запуска вы увидите следующее, напечатанное на вашем терминале:
[]
Cat
[Cat, Dog]
Cat
Мы видим, что первый массив пуст, поскольку ни один тип не был зарегистрирован, хотя строка “Cat"
После регистрации двух типов мы видим, что массив MODELS
.register_model
все, что пожелает, что может привести к не столь очевидным ошибкам. Например, если они случайно передали "Time"
вместо Time
, это приведет к следующей ошибке: неопределенный метод макроса 'StringLiteral#resolve'
. В следующем разделе мы собираемся изучить способ сделать источник ошибки более очевидным.Создание пользовательских ошибок времени компиляции
Ошибки времени компиляции — одно из преимуществ компилируемого языка. Вы сразу же узнаете о проблемах, вместо того, чтобы ждать, пока этот код будет выполнен, чтобы обнаружить ошибку. Однако, поскольку Crystal не знает контекста конкретной ошибки, он всегда будет выводить одно и то же сообщение об ошибке одного и того же типа. Последняя функция, которую мы собираемся обсудить в этой главе, связана с выдачей ваших собственных ошибок во время компиляции.
Пользовательские ошибки времени компиляции могут быть отличным способом добавить дополнительную информацию к сообщению об ошибке, что значительно облегчает жизнь конечному пользователю, поскольку ему становится понятнее, что необходимо сделать для устранения проблемы. Возвращаясь к примеру в конце последнего раздела, давайте обновим наш макрос .exclude_type
В последних нескольких главах мы использовали различные макрометоды верхнего уровня, такие как #env
#flag
и #debug
. Другой метод верхнего уровня — #raise
, который вызывает ошибку во время компиляции и позволяет предоставить собственное сообщение. Мы можем использовать это с некоторой условной логикой, чтобы определить, не является ли значение, переданное нашему макросу, Path
. Наш обновленный макрос будет выглядеть так:macro exclude_type(type)
{% raise %(Expected argument to 'exclude_type' to be
'Path', got '#{type.class_name.id}'.) unless type.is_a?
Path %}
{% EXCLUDED_TYPES << type.resolve %}
end
Теперь, если бы мы вызвали макрос с "Time"
In mutable_constants.cr:43:1
43 | exclude_type "Time"
^-----------
Error: Expected argument to 'exclude_type' to be 'Path', got 'StringLiteral'.