Продолжение темы, начатой еще в сентябре. Предыдущие части саги: один, два, три.
В результате переосмысления того, что получилось на прошлой неделе, некоторого упрощения использованных концепций, набитых шишек и внезапно обнаруженных подводных камней (один из которых чуть было не пустил еще недостроенное суденышко на дно), получилось переписать пример, показанный в третьей части саги, вот таким образом:
struct cache_control_value_t { using directive_t = std::pair< std::string, restinio::optional_t<std::string> >; using directive_container_t = std::map< std::string, restinio::optional_t<std::string> >; directive_container_t m_directives; static auto make_parser() { using namespace restinio::http_field_parser; using namespace restinio::http_field_parser::rfc; return produce< cache_control_value_t >( one_or_more_of_producer< directive_container_t >( produce< directive_t >( token_producer() >> to_lower() >> &directive_t::first, maybe( symbol('='), alternatives( token_producer() >> &directive_t::second, quoted_string_producer() >> &directive_t::second ) ) ) ) >> &cache_control_value_t::m_directives ); } static std::pair< bool, cache_control_value_t > try_parse( string_view_t what ) { using namespace restinio::http_field_parser; return try_parse_field_value( what, make_parser() ); } }; |
Напомню, что этот код решает проблему парсинга и получения значения из HTTP-поля Cache-Control, которое в RFC специфицируется следующим образом:
Cache-Control = 1#cache-directive
cache-directive = token [ "=" ( token / quoted-string ) ]
где запись 1#element означает вот такое:
1#element => *( "," OWS ) element *( OWS "," [ OWS element ] )
Собственно, в методе make_parser идет декларативное описание и того, что мы должны получить в результате парсинга, и самой структуры парсера:
// В результате всего разбора нам нужен единственный объект типа // cache_control_value_t. Именно его и создает вызов produce<T>. produce< cache_control_value_t >( // Внутри cache_control_value_t нам нужно иметь контейнер // элементов, которые имеют вид `name[=value]` или `name[="string"]`. // Вызов one_or_more_of_producer<C> как раз создает и наполняет // контейнер заданного типа. // // При этом в контейнере должен быть, как минимум, один элемент. one_or_more_of_producer< directive_container_t >( // Это обращение к produce<T> говорит, что на каждой итерации // цикла наполнения контейнера нам нужно получать объекты // типа directive_t. И эти объекты будут автоматически // сохраняться в directive_container_t. produce< directive_t >( // У каждого экземпляра directive_t есть обязательное имя, // которое должно быть преобразовано к нижнему регистру и // сохранено в поле first. token_producer() >> to_lower() >> &directive_t::first, // Так же может быть, а может и не быть значение. maybe( // Если значение есть, то ему предшествует `=`. symbol('='), // А само значение может быть токеном или строкой в кавычках. // Как бы оно не было задано, значение должно быть сохранено // в поле second объекта directive_t. alternatives( token_producer() >> &directive_t::second, quoted_string_producer() >> &directive_t::second ) ) ) ) >> &cache_control_value_t::m_directives // Контейнер, который был // сформирован вызовом one_or_more_of_producer, // должен быть сохранен в поле m_directives. ); |
Кстати говоря, при создании объекта-парсера в методе make_parser ничего не выделяется в динамической памяти и занимает результирующий объект всего 88 байт (на x64). Изрядную долю из этих 88 байт, как я подозреваю, занимают указатели на поля объектов.
Ключевой момент в показанном фрагменте -- это использование one_or_more_of_producer, который реализуется следующим образом:
template< typename Container, template<class> class Container_Adaptor = default_container_adaptor, typename Element_Producer > RESTINIO_NODISCARD auto one_or_more_of_producer( Element_Producer element ) { static_assert( impl::is_producer_v<Element_Producer>, "Element_Producer should be a value producer type" ); return impl::rfc::one_or_more_of_producer_t< Container, Container_Adaptor, Element_Producer >{ std::move(element) }; } |
Где тип one_or_more_producer_t определяется так:
template< typename Container, template<class> class Container_Adaptor, typename Element_Producer > class one_or_more_of_producer_t : public producer_tag< Container > { static_assert( impl::is_producer_v<Element_Producer>, "Element_Producer should be a value producer type" ); Element_Producer m_element; public : one_or_more_of_producer_t( Element_Producer && element ) : m_element{ std::move(element) } {} RESTINIO_NODISCARD std::pair< bool, Container > try_parse( source_t & from ) { std::pair< bool, Container > result; result.first = false; const auto appender = to_container<Container_Adaptor>(); using restinio::http_field_parser::rfc::ows; result.first = sequence( repeat( 0, N, symbol(','), ows() ), m_element >> appender, repeat( 0, N, ows(), symbol(','), maybe( ows(), m_element >> appender ) ) ).try_process( from, result.second ); return result; } }; |
Можно обратить внимание, как в C++ коде записывается вот это правило из RFC:
1#element => *( "," OWS ) element *( OWS "," [ OWS element ] )
выглядит это правило на C++ном DSL так:
sequence( repeat( 0, N, symbol(','), ows() ), m_element >> appender, repeat( 0, N, ows(), symbol(','), maybe( ows(), m_element >> appender ) ) ) |
И это кардинально отличается от реализации one_or_more_of на предыдущей итерации.
В качестве промежуточного резюме: на данный момент получается что-то близкое к тому, что мне бы хотелось иметь. Остается попробовать реализовать образом, подобным показанному выше Cache-Control, другие HTTP-поля. В особенности Accept, у которого, наверное, одно из самых навороченных описаний. Но это уже на следующей неделе. Так что, если кому-то интересно, то stay tunned...
Комментариев нет:
Отправить комментарий