Шрифт:
Интервал:
Закладка:
Остальная логика внутри нашего типа процессора остается прежней, включая String.build, чтобы вернуть вывод jq в виде строки, чтобы мы могли преобразовать его обратно в YAML перед выводом на терминал. Однако в следующем разделе будут представлены некоторые рефакторинги, которые сделают это ненужным.
Мы можем убедиться, что наше изменение работает, запустив echo $'---n- id: 1n author:n name: Jimn- id: 2n author:n name: Bobn' | crystal src/transform_cli.cr '[.[] | {"id": (.id + 1), "name": .author.name}]', который должен выводиться так же, как и раньше:
---
- id: 2
name: Jim
- id: 3
name: Bob
Хотя сейчас мы читаем входные данные из STDIN, было бы также хорошим улучшением, если бы мы разрешили передачу входного файла для чтения входных данных. Crystal определяет константу ARGF, которая позволяет считывать данные из файла и возвращаться к STDIN, если файлы не предоставлены. ARGF также является вводом-выводом, поэтому мы можем просто заменить STDIN на ARGF в src/transform_cli.cr. Мы можем проверить это изменение, записав выходные данные последнего вызова в файл, скажем, input.yaml. Затем запустите приложение, передав файл в качестве второго аргумента после фильтра. Полная команда будет выглядеть так: crystal src/transform_cli.cr. input.yaml. Однако при запуске вы заметите ошибки: Необработанное исключение: Ошибка чтения файла: Является каталогом (IO::Error). Вы можете задаться вопросом, почему это так, но ответ заключается в том, как работает ARGF.
ARGF сначала проверит, пуст ли ARGV. Если да, то будет выполнено чтение из STDIN. Если ARGV не пуст, предполагается, что каждое значение в ARGV представляет файл для чтения. В нашем случае ARGV не пуст, поскольку содержит [.", "input.yaml"], поэтому он пытается прочитать первый файл, который в данном случае представляет собой точку, обозначающую текущую папку. Поскольку папку нельзя прочитать как файл, возникает исключение, которое мы видели. Чтобы обойти эту проблему, нам нужно убедиться, что ARGV содержит только тот файл, который мы хотим прочитать, прежде чем вызывать ARGF#gets_to_end. Самый простой способ справиться с этой проблемой — вызвать метод #shift для ARGV, который работает, поскольку это массив. Этот метод удаляет первый элемент массива и возвращает его, в результате чего в ARGV остается только файл.
Однако есть еще одна проблема, которую нам также необходимо решить. Поскольку мы используем ARGV напрямую для предоставления входных аргументов jq, нам нужно будет провести некоторый рефакторинг, чтобы иметь возможность получить доступ к фильтру перед вызовом #gets_to_end. Мы можем добиться этого, переместив часть логики из src/transform_cli.cr в src/processor.cr! Обновите src/processor.cr, чтобы он выглядел так:
class Transform::Processor
def process : Nil
filter = ARGV.shift
input = ARGF.gets_to_end
output_data = String.build do |str|
Process.run(
"jq",
[filter],
input: IO::Memory.new(
Transform::YAML.deserialize input
),
output: str
)
end
STDOUT.puts Transform::YAML.serialize output_data
end
end
Ключевым дополнением здесь является введение filter = ARGV.shift, который гарантирует, что остальная часть ARGV будет содержать только тот файл, который мы хотим использовать в качестве входных данных. Затем мы используем нашу переменную как единственный элемент в массиве, представляющий аргументы, которые мы передаем в jq, заменяя жестко закодированную ссылку ARGV.
Также обратите внимание, что мы удалили входной аргумент из метода #process. Причина этого в том, что все входные данные теперь получаются изнутри самого метода, и поэтому нет смысла принимать внешние входные данные. Еще одним примечательным изменением было изменение типа возвращаемого значения метода на Nil, поскольку мы выводим его непосредственно в STDOUT. Это немного снижает гибкость метода, но об этом также будет сказано в следующем разделе.
Есть еще одна вещь, которую нам нужно обработать, прежде чем мы сможем объявить рефакторинг завершенным: что произойдет, если в jq будет передан недопустимый фильтр (или данные)? В настоящее время это вызовет не очень дружелюбное исключение. Что нам действительно нужно сделать, так это проверить, успешно ли выполнен jq, и если нет, записать сообщение об ошибке в STDERR и выйти из приложения, внеся следующие изменения в src/processor.cr:
class Transform::Processor
def process : Nil
filter = ARGV.shift
input = ARGF.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: STDERR
)
exit 1 unless run.success?
end
STDOUT.puts Transform::YAML.serialize output_data
end
end
Два основных улучшения заключаются в том, что любой вывод ошибок, возникающий во время работы jq, должен выводиться в STDERR и что программа должна завершиться раньше, если jq не выполнился успешно.
Эти два улучшения позволяют пользователю понять, что пошло не так, и предотвращают дальнейшее выполнение приложения, которое в противном случае привело бы к попытке преобразовать сообщение об ошибке в YAML.
Поддержка других IO
В последнем разделе мы уже внесли немало улучшений: нам больше не нужно жестко кодировать входные данные, и мы лучше справляемся с ошибками, исходящими от jq. Но помните, как мы также хотели поддержать использование нашего приложения в контексте библиотеки? Как кто-то будет обрабатывать тело ответа HTTP и выводить его в файл, если наш процессор тесно связан с концепцией терминала?
В этом разделе мы собираемся устранить этот недостаток, снова проведя рефакторинг, чтобы разрешить любой тип IO, а не только типы IO на основе терминала.
Первым шагом в этом является повторное введение аргументов в 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
Затем мы, конечно, должны обновить