Выбрать главу

Первым шагом в этом является повторное введение аргументов в Processor#process: один для входных аргументов, входной IO, выходной IO и IO ошибок. В конечном итоге это будет выглядеть так:

class Transform::Processor

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

    output : IO, error : IO) : Nil

    filter = input_args.shift

    input = input.gets_to_end

    output_data = String.build do |str|

      run = Process.run(

        "jq",

        [filter],

        input: IO::Memory.new(

          Transform::YAML.deserialize input

        ),

        output: str,

        error: error

      )

      exit 1 unless run.success?

    End

    output.puts Transform::YAML.serialize output_data

  end

end

Затем мы, конечно, должны обновить связанные константы, добавив в них новые переменные-аргументы. Как упоминалось ранее, вывод этого метода непосредственно в 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 — и наша логика преобразования — начали бы читать с конца буфера, что привело бы к некорректному выводу, поскольку он по существу пуст.