Следуя нашей идее разрешить использование нашего приложения в чужом проекте, нам нужно улучшить еще одну вещь. В настоящее время мы выходим из процесса, если вызов jq завершается неудачей. Было бы нехорошо, если бы кто-то использовал это, например, в веб-фреймворке, а мы случайно отключили его сервер! К счастью, исправить это просто. Вместо вызова exit 1 нам следует просто вызвать исключение, которое мы можем проверить в точке входа, специфичной для CLI. Или, другими словами, замените эту строку на raise RuntimeError.new, если только run.success?. Затем обновите src/transform_cli.cr следующим образом:
require "./transform"
begin
Transform::Processor.new.process ARGV, STDIN, STDOUT, STDERR rescue ex : RuntimeError
exit 1
end
Сделав это таким образом, мы по-прежнему будем иметь правильный код завершения при использовании в качестве CLI, но также сможем лучше использовать наше приложение в контексте библиотеки, поскольку исключение можно будет спасти и корректно обработать. Но подождите — мы много говорили об использовании нашего приложения в качестве библиотеки в другом проекте, но как это выглядит?
Во-первых, пользователям нашей библиотеки необходимо будет установить наш проект как сегмент — подробнее об этом в Главе 8 «Использование внешних библиотек». Тогда они могли бы потребовать, чтобы наш src/transform.cr имел доступ к нашему процессору и логике преобразования. Это было бы намного сложнее, если бы мы не использовали отдельную точку входа для контекста CLI. Отсюда они могли создать тип Processor и использовать его в соответствии со своими потребностями. Например, предположим, что они хотят обработать тело ответа HTTP-запроса, выведя преобразованные данные в файл. Это будет выглядеть примерно так:
require "http/client"
require "transform"
private FILTER = %({"name": .info.title, "swagger_version": .swagger, "endpoints": .paths | keys})
HTTP::Client.get "https://petstore.swagger.io/v2/swagger.yaml" do
|response|
File.open("./out.yml", "wb") do |file|
Transform::Processor.new.process [FILTER], response.body_io, file
end
end
В результате файл будет следующим:
---
name: Swagger Petstore swagger_version: "2.0" endpoints:
- /pet
- /pet/findByStatus
- /pet/findByTags
- /pet/{petId}
- /pet/{petId}/uploadImage
- /store/inventory
- /store/order
- /store/order/{orderId}
- /user
- /user/createWithArray
- /user/createWithList
- /user/login
- /user/logout
- /user/{username}
Эта способность может быть очень ценной для кого-то другого, поскольку может означать, что им не придется реализовывать эту логику самостоятельно.
Теперь, когда и наш процессор, и типы преобразования используют IO, мы можем сделать еще одну оптимизацию. Текущая логика преобразования использует метод класса .parse в соответствующем модуле формата. Этот метод очень удобен, но имеет один главный недостаток: он загружает все входные данные в память. Возможно, это не проблема для небольших тестов, которые мы проводили, но представьте себе, что вы пытаетесь преобразовать гораздо более крупные файлы/входные данные? Вполне вероятно, что это приведет к тому, что наше приложение будет использовать много (и, возможно, исчерпать) памяти.
К счастью для нас, JSON и, как следствие, YAML являются форматами потоковой сериализации. Другими словами, вы можете переводить один формат в другой по одному символу за раз, не загружая все данные заранее. Как упоминалось ранее, это одно из основных преимуществ создания нашего приложения на основе IO. Мы можем использовать это, обновив нашу логику преобразования для вывода преобразованных выходных данных, одновременно анализируя входные данные. Начнем с метода .deserialize в src/yaml.cr. Код этого метода довольно длинный, его можно найти на Github по адресу https://github.com/PacktPublishing/Crystal-Programming/blob/main/Chapter05/yaml_v2.cr.
Здесь много всего происходит, поэтому давайте немного разберем алгоритм:
1. Мы начинаем использовать некоторые новые типы в модуле каждого формата вместо того, чтобы оба они полагались на метод .parse: