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