litbaza книги онлайнРазная литератураCrystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 24 25 26 27 28 29 30 31 32 ... 75
Перейти на страницу:
связанные константы, добавив в них новые переменные-аргументы. Как упоминалось ранее, вывод этого метода непосредственно в STDOUT делал его не таким гибким, как тогда, когда он просто возвращал окончательные преобразованные данные. Однако теперь, когда он поддерживает любой тип IO в качестве вывода, кто-то может легко использовать String.build для получения строки преобразованных данных. Далее нам нужно будет обновить нашу логику преобразования, чтобы она также основывалась на IO.

Откройте src/yaml.cr и обновите первый аргумент, чтобы он принимал IO, а также добавьте еще один аргумент IO, который будет представлять выходные данные. Оба метода .parse поддерживают String | IO входы, поэтому нам там ничего особенного делать не нужно. Методы #to_* также имеют перегрузку на основе IO, которой мы передадим новый выходной аргумент. Наконец, поскольку этот метод больше не будет возвращать преобразованные данные в виде строки, мы можем обновить тип возвращаемого значения на Nil. В конечном итоге это должно выглядеть следующим образом:

require "yaml"

require "json"

module Transform::YAML

  def self.deserialize(input : IO, output : IO) : Nil

    ::YAML.parse(input).to_json output

  end

  def self.serialize(input : IO, output : IO) : Nil

    JSON.parse(input).to_yaml output

  end

end

Поскольку мы добавили второй аргумент, нам, конечно, также потребуется обновить процессор для передачи второго аргумента. Аналогичным образом, поскольку сейчас мы работаем исключительно с операциями IO, нам нужно будет реализовать новый способ хранения/перемещения данных. Мы можем решить обе эти задачи, используя объекты IO::Memory для хранения преобразованных данных. Кроме того, поскольку они сами относятся к типу IO, мы можем передавать их непосредственно в качестве входных данных в jq.

Конечный результат этого рефакторинга следующий:

class Transform::Processor

  def process(input_args : Array(String), input : IO,

    output : IO, error : IO) : Nil

    filter = input_args.shift

    input_buffer = IO::Memory.new

    output_buffer = IO::Memory.new

    Transform::YAML.deserialize input, input_buffer

    input_buffer.rewind

    run = Process.run(

      "jq",

      [filter],

      input: input_buffer,

      output: output_buffer,

      error: error

    )

    exit 1 unless run.success?

    output_buffer.rewind

    Transform::YAML.serialize output_buffer, output

  end

end

Мы все еще смещаем фильтр с входных аргументов. Однако вместо использования #gets_to_end для получения всех данных из IO мы теперь создаем два экземпляра IO::Memory — первый для хранения данных JSON из преобразования десериализации, а второй для хранения выходных данных JSON через jq.

По сути, это работает так: процесс десериализации будет использовать все данные входного типа IO, выводя преобразованные данные в первый IO::Memory. Затем мы передаем его в качестве входных данных в jq, который записывает обработанные данные во второй IO::Memory. Затем второй экземпляр передается в качестве входного типа IO в метод serialize, который выводит данные непосредственно в выходной тип IO.

Еще один ключевой момент, на который стоит обратить внимание, — это то, как нам нужно вызывать .rewind для буферов до/после запуска логики преобразования. Причина этого связана с тем, как работает IO::Memory. По мере записи в него данных он продолжает добавлять данные в конец.

Другой способ подумать об этом — представить, что вы пишете эссе. Чем длиннее и длиннее эссе, тем дальше и дальше вы отходите от начала. Вызов .rewind имеет тот же эффект, как если бы вы переместили курсор обратно в начало эссе. Или, в случае с нашим буфером, он сбрасывает буфер, чтобы будущие чтения начинались с самого начала. Если бы мы этого не сделали, jq — и наша логика преобразования — начали бы читать с конца буфера, что привело бы к некорректному выводу, поскольку он по существу пуст.

Следуя нашей идее разрешить использование нашего приложения в чужом проекте, нам нужно улучшить еще одну вещь. В настоящее время мы выходим из процесса, если вызов 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 в соответствующем модуле формата. Этот метод очень удобен, но имеет один главный недостаток: он загружает все входные данные

1 ... 24 25 26 27 28 29 30 31 32 ... 75
Перейти на страницу:

Комментарии
Минимальная длина комментария - 20 знаков. Уважайте себя и других!
Комментариев еще нет. Хотите быть первым?