Apple недавно анонсировала новую библиотеку ArgumentParser, которая использует оболочки свойств и является отличным примером хорошо написанного декларативного API. Но декларативный характер этого имеет некоторые недостатки — в основном, если вам нужно сделать что-то нестандартное, для чего библиотека не предназначена, вам нужно проявить творческий подход.
Введение
Недавно я закончил миграцию с парсера, который используется в библиотеке TSCUtility (в настоящее время все еще используется SPM), на новый для tuist, который помогает в обслуживании и работе с проектами Xcode. Большая часть миграции прошла гладко 🙇♂️, за исключением одной команды. Эта команда называется scaffold
, и позвольте мне кратко представить ее вам. Когда вы запускаете tuist scaffold framework --name FrameworkName
, он создает новый компонент в вашем проекте из шаблона с именем framework
. У каждого шаблона есть собственный манифест с именем Template.swift
и собственный набор аргументов. Что мы хотим сделать, так это проанализировать tuist scaffold framework
, процесс, в котором пользователь создает шаблон с именем framework
, затем проанализировать манифест в каталоге framework
, добавить его аргументы и позволить ArgumentParser сделать остальное за нас. Уф, это много, мы рассмотрим это шаг за шагом, так что не волнуйтесь.
Начинающийся
Давайте запачкаем руки кодом. 😉 Если вы хотите продолжить, вы можете скачать стартовый проект здесь. Если нет, тоже хорошо. 👍
Вот как изначально выглядит наша реализация команды:
Мы используем пользовательскую функцию preprocess
, поскольку мы хотим добавить пользовательские аргументы до начала процесса синтаксического анализа. Это выполняется перед Scaffold.main()
с try? Scaffold.preprocess(CommandLine.arguments)
в main.swift
. Чтобы правильно обрабатывать ошибки, вам нужно будет определить пользовательскую функцию main
для ScaffoldCommand
, но это выходит за рамки данного руководства. Функция Preprocess
теперь находит manifest.json
с каталогом из пользовательского ввода и анализирует определенные там атрибуты. В примере проекта у нас есть массив ["name"]
.
Теперь попробуем запустить scaffold framework --name FrameworkName
. Но если вы это сделаете, вы получите следующую ошибку: Error: Unexpected argument 'FrameworkName'
. Если подумать, в этом есть смысл — мы не определяем параметр --name
, поэтому у ArgumentParser
нет шансов успешно проанализировать ввод.
Примечание. О том, как работает ArgumentParser
Здесь нам нужно сделать небольшое отступление, чтобы понять, как ArgumentParser работает под капотом, чтобы иметь возможность внедрять наши динамические аргументы. Мы могли бы свести нашу команду примерно к следующему:
Как вы можете видеть, ArgumentParser каким-то волшебным образом способен распознавать аргументы, которые вы хотите проанализировать, просто из того факта, что вы объявляете их с помощью соответствующей оболочки свойства (в нашем случае @Argument
). Если вам нужно полное объяснение всего, что происходит в процессе парсинга, я бы порекомендовал этот замечательный пост. Но нам достаточно знать, что для каждой команды ArgumentParser инициализирует ArgumentSet
, что и используется при последующем анализе. Если вы заглянете в исходный код, то вот как выглядит инициализация:
Вот строка, которая нас интересует больше всего: let a: [ArgumentSet] = Mirror(reflecting: type.init())
. Другими словами, ArgumentParser перебирает дочерние элементы зеркала команды, и именно так он может волшебным образом распознавать аргументы только из их объявления. 🤯
Использование наших новых знаний
С нашими новыми открытиями мы должны быть в состоянии внедрить наши динамические аргументы. Для этого мы можем использовать CustomReflectable
, куда мы передадим наш пользовательский массив дочерних элементов. Но перед этим нам нужно сохранить атрибуты из manifest.json
, впрочем, это довольно просто:
Мы просто сохраняем attributes
в его аналог static
. Мы хотим, чтобы это было static
, потому что у нас нет экземпляра команды Scaffold
, которую мы могли бы использовать. Увы, теперь мы можем добавить их в реализацию протокола CustomReflectable
:
Итак, это немного менее простой код, но давайте рассмотрим его:
В #1 мы просто перебираем объявленный нами массив attributes
. В #2 мы инициализируем кортеж (name: String, option: Option<String>)
. name
— это имя атрибута, а что такое Option<String>
? Ну, это оболочка свойства, которую вы обычно объявляете следующим образом:
Но мы, очевидно, не можем этого сделать, поэтому нам нужно инициализировать его напрямую. В #3 мы затем создаем Mirror.Child
, который мы возвращаем в конце. В #4 мы добавляем наш @Argument template
. Для значения дочернего элемента нам нужно передать _template
, так мы получаем само свойство, а не его обернутое значение (также известное как Argument<String>
). И, наконец, в #5 мы просто возвращаем сам Mirror
с нашей инициализированной командой и дочерними элементами attributes
и template
.
Будем надеяться, что теперь мы сможем просто запустить scaffold framework --name FrameworkName
и все пойдет хорошо. Но у меня плохие новости - этого не будет. 😞 Если вы это сделаете, вы получите следующую ошибку:
«Аргумент name
определен без соответствующего CodingKey
».
CodingKey
, ну, похоже, что где-то сбоит декодирование. 🤔
Расшифровка
Если вы посмотрите на определение протокола ParsableCommand
, вы увидите, что оно также соответствует ParsableArgument
, который затем соответствует Decodable
. Но что именно не так? Ну, в нашем пользовательском зеркале мы сказали ArgumentParser
, какие аргументы он должен *ожидать*, но он не анализирует сами значения. Если мы посмотрим на наш пример, мы успешно проанализируем наш @Argument template
, но не сможем декодировать динамическую опцию --name
. Это связано с тем, что ArgumentParser
ожидает, что параметр --name
будет там, но когда он доберется до его декодирования, он не знает, как это сделать, поскольку он не определен в реализации Decodable
, сгенерированной компилятором. Но мы можем это исправить. 💪 Давайте просто определим наш собственный метод init(from decoder: Decoder) throws
- во-первых, без наших динамических аргументов:
Большая часть кода может быть вам знакома — мы определяем пользовательский CodingKeys
enum
, который содержит наш аргумент template
, а затем декодируем его как String
. Простой! Но мы все еще не обрабатываем наши пользовательские аргументы. ☝️
Мы добавим к нашему CodingKeys
enum
еще один случай под названием dynamic(String)
. Это будет инкапсулировать все наши динамические аргументы. К сожалению, Свифт теперь не умеет автоматически конвертировать отдельные случаи в String
. Мы протянем ему руку помощи:
Нам нужен только вычисленный stringValue
и пользовательский инициализатор из stringValue
.
А теперь последняя часть головоломки:
В № 1 мы определяем новое свойство attributes
, которое представляет собой словарь [String: String]
, где ключом будет имя атрибута, а значением будет… ну, значение из пользовательского ввода. В № 2 мы перебираем Scaffold.attributes
(не путать с нашим новым свойством) и декодируем их так же, как мы это делали с template
— теперь мы просто сохраняем наш результат в наш новый словарь.
Финишная черта
Да, мы приближаемся к финишу. 🏁 Давайте сделаем последнее дополнение к нашему коду и добавим следующее в функцию run
:
Теперь, когда вы запустите scaffold framework --name MyFramework
, вы увидите:
["имя": "имя_фреймворка"]
И это именно то, что мы хотим, наконец. 🥳 Теперь мы можем даже добавить новый manifest.json
с ["platform"]
в каталог app
. Когда мы вызываем scaffold app --platform iOS
, все еще работает!
Резюме того, чего мы только что достигли:
- Предварительно обработайте наш ввод и распознайте имя шаблона
- Загрузите файл
.json
в каталог предварительно обработанного шаблона. - Разобрать
.json
и динамически добавить его атрибуты вParsableCommand
- Создан Swift CLI с ArgumentParser, который теперь может работать с динамическими атрибутами ✅
Я чувствую, что это много. 😉 Вы можете продолжить и поиграть с окончательным проектом, который вы можете найти здесь.
Первоначально опубликовано на https://www.ackee.cz 5 августа 2020 г.