среда, 9 октября 2019 г.

[prog.c++] Первые результаты второго подхода к удобному парсеру HTTP-полей в RESTinio

Продолжение истории, начатой некоторое время назад. Первая попытка сделать некий удобный в использовании набор инструментов для парсинга HTTP-полей в RESTinio завершилась результатом, который вызывал противоречивые ощущения и меня лично не удовлетворил. В том числе и потому, что сложные HTTP-поля, вроде поля Accept, парсить было бы совсем не просто.

Поэтому когда представилось время вернуться к доработке RESTinio, то была предпринята следующая попытка. И вот сегодня были получены первые результаты. Кому интересно милости прошу под кат:

На данный момент пример использования нового парсера HTTP-полей выглядит так:

TEST_CASE( "Simple content_type value (two-params)""[simple][content-type][two-params]" )
{
   using namespace restinio::http_field_parser;

   const char src[] = R"(multipart/form-data; boundary=---12345; another=value)";

   const auto result = try_parse_field_value< content_type_parsed_value_t >(
         src,
         rfc::token( content_type_parsed_value_t::type ),
         rfc::delimiter( '/' ),
         rfc::token( content_type_parsed_value_t::subtype ),
         any_occurences_of< std::vector< parameter_t > >(
               content_type_parsed_value_t::parameters,
               rfc::semicolon(),
               rfc::ows(),
               rfc::token( parameter_t::name ),
               rfc::delimiter( '=' ),
               rfc::token( parameter_t::value ) )
         );

   REQUIRE( result.first );
   REQUIRE( "multipart" == result.second.m_media_type.m_type );
   REQUIRE( "form-data" == result.second.m_media_type.m_subtype );

   REQUIRE( 2u == result.second.m_parameters.size() );
   REQUIRE( "boundary" == result.second.m_parameters[0].m_name );
   REQUIRE( "---12345" == result.second.m_parameters[0].m_value );
   REQUIRE( "another" == result.second.m_parameters[1].m_name );
   REQUIRE( "value" == result.second.m_parameters[1].m_value );
}

Происходит тут следующее:

  • вызывается функция try_parse_field_value при обращении к которой я указываю, что в результате успешного парсинга у меня должен появиться объект типа content_type_parsed_value_t. Внутри этого объекта будут находиться все интересующие меня значения из HTTP-поля:

    struct content_type_parsed_value_t
    {
       media_type_t m_media_type;
       std::vector< parameter_t > m_parameters;

       static void
       type( content_type_parsed_value_t & to, std::string && what )
       {
          media_type_t::type( to.m_media_type, std::move(what) );
       }

       static void
       subtype( content_type_parsed_value_t & to, std::string && what )
       {
          media_type_t::subtype( to.m_media_type, std::move(what) );
       }

       static void
       parameters( 
          content_type_parsed_value_t & to,
          std::vector< parameter_t > && params )
       {
          to.m_parameters = std::move(params);
       }
    };
  • парсить try_parse_field_value будет содержимое строки src;
  • внутри поля ожидается некий токен в том виде, в котором понятие token определено в RFC. Само значение токена должно быть сохранено в результирующем объекте content_type_parsed_value_t посредством статического метода-сеттера content_type_parsed_value_t::type;
  • затем ожидается разделитель в виде прямой косой черты;
  • затем ожидается еще один token, который сохраняется внутри результирующего объекта посредством метода-сеттера content_type_parsed_value_t::subtype. Таким образом происходит разбор пары "type/subtype", которая задает MIME-type в HTTP-поле Content-Type;
  • далее ожидается ноль или более значений вида "name=value", которые отделяются от MIME-type и друг друга точкой с запятой. Все эти значения я хочу получить в виде std::vector<parameter_t>. Т.е. каждая пара "name=value" будет представлена отдельным экземпляром типа parameter_t. Как раз вызов any_occurences_of и задает правило разбора повторяющейся последовательности, в результате которого будет сформирован нужный мне контейнер. Значение этого контейнера будет сохранено в результирующем объекте типа content_type_parsed_value_t посредством метода-сеттера content_type_parsed_value_t::parameters;
  • сама же последовательность элементов "name=value" определяется простыми правилами: сперва должна быть точка с запятой, затем могут быть необязательные пробелы (OWS из соответствующего RFC), затем токен соответствующий "name", знак равенства и токен, соответствующий "value".

Функция try_parse_field_value возвращает std::pair<bool, T>, где T -- это результирующий тип, который я сам заказал. В примере выше будет возвращено std::pair<bool, content_type_parsed_value_t>. Которое легко проверять как в C++14, так и в C++17 с его structured binding.

Пока еще все это находится на начальной стадии и еще нет таких важных вещей, как optional-правила и alternatives. По идее, даже показанный выше пример должен был бы быть записан как:

const auto result = try_parse_field_value< content_type_parsed_value_t >(
      src,
      alternatives(
            rfc::token( content_type_parsed_value_t::type ),
            expect( "*", content_type_parsed_value_t::type ) ),
      rfc::delimiter( '/' ),
      alternatives(
            rfc::token( content_type_parsed_value_t::subtype ),
            expect( "*", content_type_parsed_value_t::subtype ) ),
      any_occurences_of< std::vector< parameter_t > >(
            content_type_parsed_value_t::parameters,
            rfc::semicolon(),
            rfc::ows(),
            rfc::token( parameter_t::name ),
            rfc::delimiter( '=' ),
            alternatives(
                  rfc::token( parameter_t::value ),
                  rfc::qdtext( parameter_t::value ) )
      )
);

Но до этих вещей пока еще просто не дошли руки.

Пока что получается многословно. Но есть ощущение, что посредством такого подхода можно будет в более-менее читабельном виде описывать парсеры HTTP-полей. В том числе и таких непростых, как Accept. В том числе этот подход можно будет использовать и для парсинга кастомных HTTP-полей. Плюс мне нравится то, что возвращается не некий абстрактный hash-map, а уже вполне себе пригодный к непосредственной работе прикладной объект.

Конечно, на данный момент это выглядит корявенько. Посмотрим, куда это все заведет.

PS. Использовать что-то вроде Boost.Spirit-а не хотелось, дабы не привязывать RESTinio к еще одной "тяжелой" зависимости. Но, в принципе, есть ощущение, что дабы не плодить зависимостей, приходится строить собственный лисапед :(

С другой стороны, RESTinio ведь ни что иное, как еще один лисапед для задачи, которая в C++ была решена неоднократно. Но у предыдущих решений был один фатальный недостаток... ;)

5 комментариев:

Grigory Demchenko комментирует...

Похоже на закат солнца вручную.

eao197 комментирует...

@Grigory Demchenko

Собственно, это он и есть.

А как закатывать не вручную?

Grigory Demchenko комментирует...

В общем случае никак. Я бы начал с простых случаев, которые встречаются в 99% и сделал бы для них отдельные функции.

Grigory Demchenko комментирует...

Внутри, кстати, можно использовать такую портянку, если очень хочется.

eao197 комментирует...

@Grigory Demchenko

Так идея именно в том, чтобы сделать базовый механизм парсинга HTTP-полей. Затем на его основе сделать готовые высокоуровневые инструменты разбора определенных в RFC HTTP-полей (типа Accept и Content-Type). А так же эти механизмы могут использоваться пользователем для разбора его собственных, нестандартных HTTP-полей.