суббота, 19 октября 2019 г.

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

Продолжение темы, начатой еще в сентябре. Предыдущие части саги: один, два, три.

В результате переосмысления того, что получилось на прошлой неделе, некоторого упрощения использованных концепций, набитых шишек и внезапно обнаруженных подводных камней (один из которых чуть было не пустил еще недостроенное суденышко на дно), получилось переписать пример, показанный в третьей части саги, вот таким образом:

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<classclass 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<classclass 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...

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