пятница, 6 декабря 2019 г.

[prog.c++] Пользуюсь плодами упарывания шаблонами (PEG парсер в RESTinio)

Прикольный все-таки PEG-парсер получился в рамках развития RESTinio. Ниже показан код, который нужен для разбора вот такого выражения:

language-range = (1*8ALPHA *('-' 1*8alphanum)) / '*'

Т.е. language-range может быть представлен либо просто звездочкой либо последовательностью фрагментов в которой первый фрагмент может состоять только из букв (в количестве от одной до восьми штук), а последующие фрагменты начинаются с дефиса и могут содержать от одной до восьми букв или цифр.

Так вот при работе над очередным релизом RESTinio для разбора такого выражения был написан следующий код:

RESTINIO_NODISCARD
inline auto
make_language_tag_producer()
{
   return produce<std::string>(
         repeat(1u8u, alpha_symbol_producer() >> to_container()),
         repeat(0u, N,
               symbol_producer('-') >> to_container(),
               repeat(1u8u, 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(1u8u, alpha_symbol_producer() >> to_container()),
                     repeat(0u, N,
                           symbol_producer('-') >> to_container(),
                           repeat(1u8u, 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. Кому интересна ретроспектива эпопеи вокруг инструментов для парсинга, то можно начать отсюда и далее в историю по ссылкам на предыдущие блог-посты.

Комментариев нет: