:inherit
, что заставляет программу наследовать выходные данные своего родительского модуля, которым является наш терминал.
Выполнение этого файла через crystal src/transform.cr
приводит к тому же результату, что и в предыдущем примере jq, который удовлетворяет второму требованию нашего CLI. Однако нам все еще нужно выполнить требования 1 и 3. Давайте начнем с этого.
Преобразование данных
Следуя предыдущей рекомендации, я собираюсь создать новый файл, который будет содержать логику преобразования. Для начала создайте файл src/yaml.cr со следующим содержимым:
require "yaml"
require "json"
module Transform::YAML
def self.deserialize(input : String) : String
::YAML.parse(input).to_json
end
def self.serialize(input : String) : String
JSON.parse(input).to_yaml
end
end
Кроме того, не забудьте запросить этот файл в src/transform.cr, добавив require "./ yaml"
в начало файла.
Crystal поставляется с довольно надежной стандартной библиотекой общих / полезных функций. Хорошим примером этого являются модули https://crystal-lang.org/api/YAML.html и https://crystal-lang.org/api/JSON.html, которые упрощают написание логики преобразования. Я определил два метода: один для обработки YAML => JSON, а другой для обработки JSON => YAML. Обратите внимание, что я использую ::YAML
для ссылки на модуль стандартной библиотеки. Это связано с тем, что метод уже определен в пространстве имен YAML. Без ::
Crystal будет искать метод .parse
в своем текущем пространстве имен вместо того, чтобы обращаться к стандартной библиотеке. Этот синтаксис также работает с методами, что может пригодиться, если вы случайно определите свой собственный метод #raise
, а затем захотите, например, также вызвать реализацию стандартной библиотеки.
Затем я обновил файл src/transform.cr, чтобы он выглядел следующим образом:
require "./yaml"
module Transform
VERSION = "0.1.0"
INPUT_DATA = <←YAML
---
- id: 1
author:
name: Jim
- id: 2
author:
name: Bob
YAML
output_data = String.build do |str|
Process.run(
"jq",
[%([.[] | {"id": (.id + 1), "name": .author.name}])],
input: IO::Memory.new(
Transform::YAML.deserialize(INPUT_DATA)
),
output: str
)
end
puts Transform::YAML.serialize(output_data)
end
Код в основном тот же, но теперь он предоставляет входные данные на языке YAML и включает нашу логику преобразования. Стоит отметить, что теперь мы используем String.build
для создания строки в коде, как вы могли видеть на своем терминале ранее. Основная причина этого заключается в том, что строка нужна нам для того, чтобы преобразовать ее обратно в YAML перед выводом на экран нашего терминала.
На данный момент у нас есть рабочая базовая реализация, которая соответствует нашим целям, но код на самом деле не пригоден для повторного использования, поскольку все это определено на верхнем уровне нашего пространства имен transform
. Нам следует исправить это, прежде чем мы сможем назвать это завершенным.
Улучшение возможности повторного использования
С этого момента мы начнем использовать файл src/transform_cli.cr. Чтобы решить эту проблему повторного использования, мы планируем определить тип процессора, который будет содержать логику, связанную с вызовом jq и преобразованием данных.
Давайте начнем с создания файла src/processor.cr, обязательно указав его в src/transform.cr, со следующим содержимым:
class Transform::Processor
def process(input : String) : String
output_data = String.build do |str|
Process.run(
"jq",
[%([.[] | {"id": (.id + 1), "name": .author.name}])],
input: IO::Memory.new(
Transform::YAML.deserialize input
),
output: str
)
end
Transform::YAML.serialize output_data
end
end
Наличие этого класса делает наш код намного более гибким и пригодным для повторного использования. Мы можем создать объект Transform::Processor
и вызывать его метод #process
несколько раз с различными входными строками. Далее, давайте используем этот новый тип в src/transform_cli.cr:
require "./transform"
INPUT_DATA = <←YAML
---
- id: 1
author:
name: Jim
- id: 2
author:
name: Bob
YAML
puts Transform::Processor.new.process INPUT_DATA
Наконец, src/transform.cr теперь должен выглядеть следующим образом:
require "./processor"
require "./yaml"
module Transform
VERSION = "0.1.0"
end
Запуск src/transform_cli.cr по-прежнему приводит к тому же результату, что и раньше, но теперь можно повторно использовать нашу логику преобразования для разных входных данных. Однако цель CLI – разрешить использование аргументов из терминала и использовать значения внутри CLI. Учитывая, что в настоящее время входной фильтр жестко привязан к типу процессора, я думаю, что это то, к чему нам следует обратиться, прежде чем завершать начальную реализацию.
Аргументы, передаваемые программе CLI, отображаются через константу ARGV в виде Array(String)
. Сам код, позволяющий использовать это, довольно прост, учитывая, что аргументы jq уже принимают массив строк, который у нас на данный момент жестко запрограммирован. Мы можем просто заменить этот массив константой ARGV, и все будет в порядке. src/processor.cr теперь выглядит следующим образом:
class Transform::Processor
def process(input : String) : String
output_data = String.build do |str|
Process.run("jq",
ARGV,
input: IO::Memory.new(Transform::YAML.deserialize
input
),
output: str
)
end
Transform::YAML.serialize output_data
end
end
Кроме того, поскольку фильтр больше не является жестко запрограммированным, нам нужно будет ввести его вручную. Запуск crystal src/transform_cli.cr '[.[] | {"id": (.id + 1), "name": .author.name}]'
снова выдает тот же результат, но гораздо более гибким способом.
Если вы предпочитаете использовать crystal run, команду нужно будет немного изменить, чтобы учесть различную семантику каждого варианта. В этом случае команда была бы crystal run src/transform_cli.cr -- '[.[] | {"id": (.id + 1), "name": .author.name }]'
, где параметр --
сообщает команде запуска, что должны быть переданы будущие аргументы к исполняемому файлу, а не в качестве аргументов для самой команды запуска.
Стандартная библиотека Crystal также включает тип OptionParser
, который предоставляет DSL, позволяющий описывать аргументы, которые принимает CLI, обрабатывать их синтаксический анализ из ARGV и генерировать справочную информацию на основе этих параметров. Мы будем использовать этот тип в одной из следующих глав, так что следите за обновлениями!
Резюме