Здесь мы определяем изменяемую константу, которая будет содержать зарегистрированные типы, сами типы и макрос, который будет их регистрировать. Мы также вызываем #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
содержит их. Наконец, это еще раз показывает, что его можно было разрешить при вызове до или после регистрации связанного типа. Как упоминалось ранее в этой главе, макросы не имеют такой же типизации, как обычный код Crystal. Из-за этого к макросам невозможно добавлять ограничения типов. Это означает, что пользователь может передать в макрос .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'.