Прикольный все-таки PEG-парсер получился в рамках развития RESTinio. Ниже показан код, который нужен для разбора вот такого выражения:
language-range = (1*8ALPHA *('-' 1*8alphanum)) / '*'
Т.е. language-range может быть представлен либо просто звездочкой либо последовательностью фрагментов в которой первый фрагмент может состоять только из букв (в количестве от одной до восьми штук), а последующие фрагменты начинаются с дефиса и могут содержать от одной до восьми букв или цифр.
Так вот при работе над очередным релизом RESTinio для разбора такого выражения был написан следующий код:
RESTINIO_NODISCARD inline auto make_language_tag_producer() { return produce<std::string>( repeat(1u, 8u, alpha_symbol_producer() >> to_container()), repeat(0u, N, symbol_producer('-') >> to_container(), repeat(1u, 8u, alphanum_symbol_producer() >> to_container()) ) ); } RESTINIO_NODISCARD inline auto make_language_range_producer() { return produce<std::string>( alternatives( symbol_producer('*') >> to_container(), make_language_tag_producer() >> as_result() ) ); } |
Функция make_language_tag_producer() формирует объект, который будет отвечать за разбор первой альтернативы для language-range, а именно:
language-range = 1*8ALPHA *('-' 1*8alphanum)
Поэтому в make_language_tag_producer() практически так и говорится:
- результатом разбора будет объект std::string. Этот объект будет служить результирующим контейнером, в который будут складываться найденные символы;
- ожидается последовательность от одной до восьми букв. Каждая найденная буква добавляется в результирующий контейнер;
- затем ожидается ноль или неограниченное количество повторений дефиса за которым следует последовательность из одной до восьми букв или цифр. Все найденные символы (дефис, буквы, цифры) добавляются в результирующий контейнер.
Функция make_language_range_producer() формирует объект, который будет выбирать одну из двух альтернатив. Что, опять же, практически прямолинейно и записано в коде:
- результатом разбора будет std::string, который и будет результирующим контейнером для разобранных значений;
- если во входном потоке встретится звездочка, то она должна быть сохранена в результирующем контейнере;
- если же во входном потоке встретится вторая часть правила (парсер для второй части создает make_language_tag_producer), то этот результат разбора этой части должен стать и результатом всего правила.
В принципе, можно было бы записать и вот так:
inline auto make_language_range_producer() { return produce<std::string>( alternatives( symbol_producer('*') >> to_container(), produce<std::string>( repeat(1u, 8u, alpha_symbol_producer() >> to_container()), repeat(0u, N, symbol_producer('-') >> to_container(), repeat(1u, 8u, alphanum_symbol_producer() >> to_container()) ) ) >> as_result() ) ); } |
Но мне проще было написать две маленьких функции с простыми правилами, чем одну с многословным правилом внутри.
Показанное выше правило для language-range является частью другого правила:
Accept-Language = 1#( language-range [ weight ] )
language-range = <language-range, see [RFC4647], Section 2.1>
И парсер для Accept-Language записывается в коде вот так:
static auto make_parser() { using namespace accept_language_details; return produce< accept_language_value_t >( non_empty_comma_separated_list_producer< item_container_t >( produce< item_t >( make_language_range_producer() >> &item_t::language_range, maybe( weight_producer() >> &item_t::weight ) ) ) >> &accept_language_value_t::languages ); } |
В общем, получилось прикольно. Пока более-менее нравится. Посмотрим как пойдет дальше работа над реализацией средств для разбора HTTP-полей. Если все будет хорошо и дальше, то может быть появится неплохая тема для рассказа на C++Russia в следующем году ;)
PS. Кому интересна ретроспектива эпопеи вокруг инструментов для парсинга, то можно начать отсюда и далее в историю по ссылкам на предыдущие блог-посты.