Продолжение истории, начатой некоторое время назад. Первая попытка сделать некий удобный в использовании набор инструментов для парсинга 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++ была решена неоднократно. Но у предыдущих решений был один фатальный недостаток... ;)
Похоже на закат солнца вручную.
ОтветитьУдалить@Grigory Demchenko
ОтветитьУдалитьСобственно, это он и есть.
А как закатывать не вручную?
В общем случае никак. Я бы начал с простых случаев, которые встречаются в 99% и сделал бы для них отдельные функции.
ОтветитьУдалитьВнутри, кстати, можно использовать такую портянку, если очень хочется.
ОтветитьУдалить@Grigory Demchenko
ОтветитьУдалитьТак идея именно в том, чтобы сделать базовый механизм парсинга HTTP-полей. Затем на его основе сделать готовые высокоуровневые инструменты разбора определенных в RFC HTTP-полей (типа Accept и Content-Type). А так же эти механизмы могут использоваться пользователем для разбора его собственных, нестандартных HTTP-полей.