Шрифт:
Интервал:
Закладка:
Поскольку сами привязки не зависят от нашего приложения CLI, мы можем протестировать их независимо. Мы можем сделать это, добавив следующие строки в наш файл привязки, запускающий его, и обязательно удалив этот тестовый код после его запуска:
LibNotify.notify_init "Transform"
notification = LibNotify.notify_notification_new "Hello",
"From Crystal!", nil
LibNotify.notify_notification_show notification, nil LibNotify.notify_uninit
Если все работает правильно, вы должны увидеть уведомление на рабочем столе с заголовком “Привет” и текстом “От Crystal!”. Мы передаем nil аргументам, для которых не имеем значения. Это работает нормально, поскольку эти аргументы являются необязательными, и Crystal автоматически преобразует их в нулевой указатель. Однако это не сработало бы, если бы переменная представляла собой объединение Pointer и Nil. Работа с необработанными привязками функциональна, но не удобна для пользователя. Обычной практикой является определение стандартных типов Crystal, которые обертывают типы привязки C. Это позволяет скрыть внутренние компоненты библиотеки C за API, который более удобен для пользователя и его легче документировать. Давайте начнем с этого сейчас.
Абстрагирование привязок
Основываясь на логике C, которую мы использовали ранее, нам нужны следующие две основные абстракции:
• Лучший способ отправить уведомление, чтобы избежать необходимости вызывать методы init и uninit.
• Улучшен способ создания/редактирования уведомления, ожидающего отправки.
Чтобы обработать первую абстракцию, давайте создадим новый файл src/lib_notify/notify.cr со следующим кодом:
require "./lib_notify"
class Transform::Notification
@notification : LibNotify::NotifyNotification*
getter summary : String
getter body : String
getter icon : String
def initialize(@summary : String, @body : String, @icon : String = "")
@notification = LibNotify.notify_notification_new @summary, @body, @icon
end
def summary=(@summary : String) : Nil
self.update
end
def body=(@body : String) : Nil
self.update
end
def icon=(@icon : String?) : Nil
self.update
end
def to_unsafe : LibNotify::NotifyNotification* @notification
end
private def update : Nil
LibNotify.notify_notification_update @notification, @summary, @body, @icon
end
end
По сути, этот класс представляет собой просто обертку вокруг указателя уведомления C. Мы определяем метод #to_unsafe, который возвращает завернутый указатель, чтобы позволить предоставить экземпляр этого класса функциям C. В этом типе мы также будем использовать notify_notification_update. Этот тип реализует установщики для каждого свойства уведомления, которые обновляют значение внутри типа-оболочки, а также обновляют значения структур C.
libnotify также имеет различные дополнительные функции, с которыми мы могли бы поиграть, такие как приоритет уведомления или установка задержки перед отображением уведомления. На самом деле нам не нужны эти функции для нашего CLI, но вы можете свободно исследовать libnotify и настраивать все по своему усмотрению! Далее давайте создадим тип, который поможет отправлять эти экземпляры уведомлений.
Создайте новый файл src/lib_notify/notification_emitter.cr со следующим кодом:
require "./lib_notify"
require "./notification"
class Transform: :NotificationEmitter
@@initialized : Bool = false
at_exit { LibNotify.notify_uninit if @@initialized }
def emit(summary : String, body : String) : Nil
self.emit Transform::Notification.new summary, body
end
def emit(notification : Transform::Notification) : Nil
self.init
LibNotify.notify_notification_show notification, nil
end
private def init : Nil
return if @@initialized
LibNotify.notify_init "Transform"
@@initialized = true
end
end
Основным методом этого типа является #emit, который отображает предоставленное уведомление, гарантируя предварительную инициализацию libnotify. Первая перегрузка принимает сводку и тело, создает уведомление, а затем передает его второй перегрузке. Мы сохраняем статус инициализации libnotify как переменную класса, поскольку он не привязан к конкретному экземпляру NotificationEmitter. Мы также зарегистрировали обработчик at_exit, который деинициализирует libnotify перед завершением работы программы, если она была инициализирована ранее.
Также стоит отметить, что обработка инициализации libnotify в многопоточном приложении будет немного более затруднительной, поскольку libnotify необходимо инициализировать только один раз, а не для каждого потока или волокна. Однако, поскольку поддержка многопоточности в Crystal все еще считается экспериментальной, и эта тема немного выходит за рамки рассмотрения, мы просто пропустим этот сценарий. На данный момент мы будем использовать наше приложение. Это не будет проблемой.
Теперь, когда у нас есть абстракции, мы можем перейти к их реализации в нашем CLI.
Интеграция привязок
Учитывая то, что мы сделали в последнем разделе, это будет самая простая часть главы, и останется только один вопрос: какое уведомление мы хотим отправить? Хорошим вариантом использования было бы выдавать его при возникновении ошибки в процессе преобразования. Уведомление привлечет внимание пользователя к тому, что ему необходимо принять меры по поводу чего-то, что в противном случае могло бы остаться незамеченным, если бы ожидалось, что это займет некоторое время.
Теперь вы, возможно, думаете, что мы просто создаем новые экземпляры NotificationEmitter по мере необходимости и используем их для каждого контекста. Однако мы собираемся применить несколько иной подход. План состоит в том, чтобы добавить инициализатор к нашему типу процессора, который будет хранить ссылку на эмиттер в качестве переменной экземпляра. Это будет выглядеть так: def initialize(@emitter : Transform::NotificationEmitter = Transform::NotificationEmitter.new); end. Я не буду объяснять причину этого, поскольку она будет рассмотрена в Главе 14 «Тестирование».
Давайте сначала сосредоточимся на обработке контекста ошибки. К сожалению, поскольку jq будет выводить сообщения об ошибках непосредственно на IO, ошибок, мы не сможем их обработать. Однако мы можем обрабатывать реальные исключения из нашего кода Crystal. Поскольку мы хотим обрабатывать любые исключения, возникающие в нашем методе #process, мы можем использовать короткую форму для определения блока rescue:
rescue ex : Exception
if message = ex.message
@emitter.emit "Oh no!", message
end
raise ex
Этот код должен располагаться непосредственно под последней строкой каждого метода, но перед закрывающим тегом метода. Этот блок спасет любое исключение, возникшее в методе. Затем он отправит уведомление с сообщением об исключении в качестве тела уведомления. Не все исключения имеют сообщение, поэтому мы обрабатываем этот случай, проверяя его перед