diff --git a/doc/qbk/07_concepts/BuffersGenerator.qbk b/doc/qbk/07_concepts/BuffersGenerator.qbk index 9b442896..1f2dcd23 100644 --- a/doc/qbk/07_concepts/BuffersGenerator.qbk +++ b/doc/qbk/07_concepts/BuffersGenerator.qbk @@ -16,6 +16,11 @@ The generator will be asked to produce buffers. The consuming code will signal how much of the data has been consumed, and repeatedly query for buffers until no more data is available, or the generator indicates an error condition. +In this way, serializers can be adapted as [*BuffersGenerator], for example +[link beast.ref.boost__beast__http__message_generator +`http::message_generator`] which provides a type-erased interface for a variety +of concrete http message types. + Overloads of [link beast.ref.boost__beast__write `write`] and [link beast.ref.boost__beast__async_write `async_write`] operations are provided as free functions. These operations will consume the output of a @@ -33,6 +38,7 @@ In this table: * `G` denotes a type meeting the requirements of [*BuffersGenerator]. * `g` denotes a value of type `G`. +* `c` denotes a possibly-const value of type `G`. * `n` is a value of type `std::size_t`. * `ec` is a value of type [link beast.ref.boost__beast__error_code `error_code&`]. @@ -46,7 +52,7 @@ In this table: This is the type of buffer returned by `g.prepare(ec)`. ] ][ - [`g.is_done()`] + [`c.is_done()`] [`bool`] [ Called to ask the generator for its completion status. @@ -56,8 +62,6 @@ In this table: [*Note:] The result of invoking `prepare` on `g` once it has completed is unspecified. - - This member function does not alter the state of the generator. ] ][ [`g.prepare(ec)`] @@ -106,7 +110,7 @@ In this table: ] ][ [`is_buffers_generator`] - [`std::true_type`] + [`std::bool_constant`] [ An alias for `std::true_type` for `G`, otherwise an alias for `std::false_type`. @@ -121,7 +125,7 @@ In this table: { using const_buffers_type = net::const_buffer; - bool is_done(); + bool is_done() const; const_buffers_type prepare( error_code& ec ); void consume( std::size_t n ); }; @@ -132,6 +136,8 @@ In this table: [heading Models] +* [link beast.ref.boost__beast__http__message_generator `http::message_generator`] + [heading Algorithms] * [link beast.ref.boost__beast__async_write `async_write`] diff --git a/doc/qbk/quickref.xml b/doc/qbk/quickref.xml index 3367c8ef..cc4b3fd2 100644 --- a/doc/qbk/quickref.xml +++ b/doc/qbk/quickref.xml @@ -205,6 +205,7 @@ file_body header message + message_generator parser request request_header diff --git a/include/boost/beast/core/buffers_generator.hpp b/include/boost/beast/core/buffers_generator.hpp index b57b603f..1398903c 100644 --- a/include/boost/beast/core/buffers_generator.hpp +++ b/include/boost/beast/core/buffers_generator.hpp @@ -11,11 +11,9 @@ #define BOOST_BEAST_CORE_BUFFERS_GENERATOR_HPP #include - #include #include #include - #include namespace boost { @@ -63,7 +61,7 @@ struct is_buffers_generator< by a caller-provided BuffersGenerator to a stream. The call will block until one of the following conditions is true: - @li The generator returns an empty buffers sequence. + @li A call to the generator's `is_done` returns `false`. @li An error occurs. @@ -85,8 +83,7 @@ template< class SyncWriteStream, class BuffersGenerator #if ! BOOST_BEAST_DOXYGEN - , - typename std::enable_if:: type>::value>::type* = nullptr #endif @@ -103,7 +100,7 @@ write( by a caller-provided BuffersGenerator to a stream. The call will block until one of the following conditions is true: - @li The generator returns an empty buffers sequence. + @li A call to the generator's `is_done` returns `false`. @li An error occurs. @@ -125,8 +122,7 @@ template< class SyncWriteStream, class BuffersGenerator #if ! BOOST_BEAST_DOXYGEN - , - typename std::enable_if:: type>::value>::type* = nullptr #endif @@ -145,7 +141,7 @@ write( operation will continue until one of the following conditions is true: - @li The generator returns an empty buffers sequence. + @li A call to the generator's `is_done` returns `false`. @li An error occurs. @@ -182,8 +178,7 @@ template< class BuffersGenerator, class CompletionToken #if !BOOST_BEAST_DOXYGEN - , - typename std::enable_if::value>::type* = nullptr #endif > @@ -191,16 +186,15 @@ auto async_write( AsyncWriteStream& stream, BuffersGenerator generator, - CompletionToken&& token) // - -> typename net::async_result< - typename std::decay< - CompletionToken>::type, - void(error_code, std::size_t)>:: - return_type; + CompletionToken&& token) -> + typename net::async_result< + typename std::decay< + CompletionToken>::type, + void(error_code, std::size_t)>::return_type; -} // namespace beast -} // namespace boost +} // beast +} // boost -#include +#include #endif diff --git a/include/boost/beast/http.hpp b/include/boost/beast/http.hpp index e2172c20..a11ed56b 100644 --- a/include/boost/beast/http.hpp +++ b/include/boost/beast/http.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/beast/http/impl/message_generator.hpp b/include/boost/beast/http/impl/message_generator.hpp new file mode 100644 index 00000000..7b1030f9 --- /dev/null +++ b/include/boost/beast/http/impl/message_generator.hpp @@ -0,0 +1,104 @@ +// +// Copyright (c) 2022 Seth Heeren (sgheeren at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +#ifndef BOOST_BEAST_HTTP_IMPL_MESSAGE_GENERATOR_HPP +#define BOOST_BEAST_HTTP_IMPL_MESSAGE_GENERATOR_HPP + +#include +#include +#include + +namespace boost { +namespace beast { +namespace http { + +template +message_generator::message_generator( + http::message&& m) + : impl_(boost::make_unique< + generator_impl>( + std::move(m))) +{ +} + +template +struct message_generator::generator_impl + : message_generator::impl_base +{ + explicit generator_impl( + http::message&& m) + : m_(std::move(m)) + , sr_(m_) + { + } + + bool + is_done() override + { + return sr_.is_done(); + } + + const_buffers_type + prepare(error_code& ec) override + { + sr_.next(ec, visit{*this}); + return current_; + } + + void + consume(std::size_t n) override + { + sr_.consume((std::min)(n, beast::buffer_bytes(current_))); + } + + bool + keep_alive() const noexcept override + { + return m_.keep_alive(); + } + +private: + static constexpr unsigned max_fixed_bufs = 12; + + http::message m_; + http::serializer sr_; + + std::array bs_; + const_buffers_type current_ = bs_; // subspan + + struct visit + { + generator_impl& self_; + + template + void + operator()(error_code&, ConstBufferSequence const& buffers) + { + auto& s = self_.bs_; + auto& cur = self_.current_; + + auto it = net::buffer_sequence_begin(buffers); + + std::size_t n = + std::distance(it, net::buffer_sequence_end(buffers)); + + n = (std::min)(s.size(), n); + + cur = { s.data(), n }; + std::copy_n(it, n, cur.begin()); + } + }; + +}; + +} // namespace http +} // namespace beast +} // namespace boost + +#endif diff --git a/include/boost/beast/http/message_generator.hpp b/include/boost/beast/http/message_generator.hpp new file mode 100644 index 00000000..f2c2d549 --- /dev/null +++ b/include/boost/beast/http/message_generator.hpp @@ -0,0 +1,99 @@ +// +// Copyright (c) 2022 Seth Heeren (sgheeren at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// +#ifndef BOOST_BEAST_HTTP_MESSAGE_GENERATOR_HPP +#define BOOST_BEAST_HTTP_MESSAGE_GENERATOR_HPP + +#include +#include +#include +#include + +namespace boost { +namespace beast { +namespace http { + +/** Type-erased buffers generator for @ref http::message + + Implements the BuffersGenerator concept for any concrete instance of the + @ref http::message template. + + @ref http::message_generator takes ownership of a message on construction, + erasing the concrete type from the interface. + + This makes it practical for use in server applications to implement request + handling: + + @code + template + http::message_generator handle_request( + string_view doc_root, + http::request&& request); + @endcode + + The @ref beast::write and @ref beast::async_write operations are provided + for BuffersGenerator. The @ref http::message::keep_alive property is made + available for use after writing the message. +*/ +class message_generator +{ +public: + using const_buffers_type = span; + + template + message_generator(http::message&&); + + /// @ref BuffersGenerator + bool is_done() const { + return impl_->is_done(); + } + + /// @ref BuffersGenerator + const_buffers_type + prepare(error_code& ec) + { + return impl_->prepare(ec); + } + + /// @ref BuffersGenerator + void + consume(std::size_t n) + { + impl_->consume(n); + } + + /// Returns the result of `m.keep_alive()` on the underlying message + bool + keep_alive() const noexcept + { + return impl_->keep_alive(); + } + +private: + struct impl_base + { + virtual ~impl_base() = default; + virtual bool is_done() = 0; + virtual const_buffers_type prepare(error_code& ec) = 0; + virtual void consume(std::size_t n) = 0; + virtual bool keep_alive() const noexcept = 0; + }; + + std::unique_ptr impl_; + + template + struct generator_impl; +}; + +} // namespace http +} // namespace beast +} // namespace boost + +#include + +#endif diff --git a/test/beast/core/buffers_generator.cpp b/test/beast/core/buffers_generator.cpp index 15169329..1a7397d8 100644 --- a/test/beast/core/buffers_generator.cpp +++ b/test/beast/core/buffers_generator.cpp @@ -27,7 +27,8 @@ namespace beast { namespace detail { -struct test_buffers_generator { +struct test_buffers_generator +{ using underlying_buffer_sequence = std::array; using const_buffers_type = buffers_suffix; std::size_t iterations_ = 5; @@ -212,7 +213,8 @@ public: BEAST_EXPECT(30 == in.nwrite_bytes()); BEAST_EXPECT( "abcde12345abcd1234abc123ab12a1" == in.str()); - } else + } + else { BEAST_EXPECT(1 == out.nwrite()); BEAST_EXPECT(10 == in.nwrite_bytes()); @@ -254,7 +256,8 @@ public: BEAST_EXPECT(30 == in.nwrite_bytes()); BEAST_EXPECT( "abcde12345abcd1234abc123ab12a1" == in.str()); - } else + } + else { BEAST_EXPECT(1 == out.nwrite()); BEAST_EXPECT(10 == in.nwrite_bytes()); diff --git a/test/beast/http/CMakeLists.txt b/test/beast/http/CMakeLists.txt index cfbbb422..cf19b84d 100644 --- a/test/beast/http/CMakeLists.txt +++ b/test/beast/http/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable (tests-beast-http field_compiles.cpp fields.cpp file_body.cpp + message_generator.cpp message.cpp parser.cpp read.cpp diff --git a/test/beast/http/Jamfile b/test/beast/http/Jamfile index 4bde0939..19d24493 100644 --- a/test/beast/http/Jamfile +++ b/test/beast/http/Jamfile @@ -19,6 +19,7 @@ local SOURCES = field_compiles.cpp fields.cpp file_body.cpp + message_generator.cpp message.cpp parser.cpp read.cpp diff --git a/test/beast/http/message_generator.cpp b/test/beast/http/message_generator.cpp new file mode 100644 index 00000000..4905ed98 --- /dev/null +++ b/test/beast/http/message_generator.cpp @@ -0,0 +1,315 @@ +// +// Copyright (c) 2022 Seth Heeren (sgheeren at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +// Test that header file is self-contained. +#include + +#include +#include +#include //for operator<< + +#include +#include +#include // for is_buffers_generator and [async_]write overloads +#include + +#include + +#include +#include + +namespace boost { +namespace beast { +namespace http { + +namespace detail { + +// Degenerate body generator to trigger dynamic generator buffer +// allocation +// +// Warning: magic numbers ahead +// +// Arbitrarily decided on 65 buffers of which two are "some" and "body", the +// other bufs are chosen by the "seed fragment" +struct fragmented_test_body +{ + using value_type = net::const_buffer; + + static std::uint64_t + size(net::const_buffer fragment) + { + return 8 + 63 * fragment.size(); + } + + struct writer + { + using const_buffers_type = std::array; + + bool done_ = false; + net::const_buffer seed_fragment_; + + template + writer(Fields const&, net::const_buffer fragment) + : seed_fragment_(fragment) + { + } + + void + init(beast::error_code&) const + { + } + + boost::optional> + get(beast::error_code&) + { + const_buffers_type cb; + std::fill(begin(cb), end(cb), seed_fragment_); + cb.at(7) = { "some", 4 }; + cb.at(27) = { "body", 4 }; + return std::make_pair(cb, exchange(done_, true)); + } + }; +}; +} + +namespace detail { +template<> +struct is_body_sized + : std::true_type +{ +}; +} // namespace detail + +class message_generator_test : public beast::unit_test::suite +{ + static_assert( + is_buffers_generator::value, + "buffers_generator not modeled"); + BOOST_STATIC_ASSERT( + std::is_constructible< + message_generator, + message&&>::value); + BOOST_STATIC_ASSERT( + std::is_constructible< + message_generator, + message&&>::value); + + // only rvalue refs + BOOST_STATIC_ASSERT( + not std::is_constructible< + message_generator, + message&>::value); + BOOST_STATIC_ASSERT( + not std::is_constructible< + message_generator, + message const&>::value); + + static request make_get() { + return request{ + verb::get, "/path/query?1", 11, + "Serializable but ignored on GET" + }; + } + + static http::response + make_fragmented_body_response(net::const_buffer seed_fragment) + { + http::response msg( + http::status::ok, 11, seed_fragment); + msg.prepare_payload(); + + BEAST_EXPECT(msg.has_content_length()); + BEAST_EXPECT( + msg.at(http::field::content_length) == + std::to_string(8 + 63 * seed_fragment.size())); + return msg; + } + +public: + void + testGenerate() + { + message_generator gen(make_get()); + error_code ec; + + std::string received; + + while(! gen.is_done()) + { + message_generator::const_buffers_type b = gen.prepare(ec); + BEAST_EXPECT(! ec); + received += buffers_to_string(b); + + gen.consume(buffer_bytes(b)); + } + + BEAST_EXPECT(received == + "GET /path/query?1 HTTP/1.1\r\n\r\n" + "Serializable but ignored on GET"); + } + + void + testGenerateSlowConsumer() + { + message_generator gen(make_get()); + error_code ec; + + std::vector received; + + while(! gen.is_done()) + { + message_generator::const_buffers_type b = gen.prepare(ec); + BEAST_EXPECT(! ec); + received.push_back(buffers_to_string(b).substr(0, 3)); + + gen.consume(3); // allowed > buffer_bytes(b) + } + + BEAST_EXPECT( + (received == + std::vector{ + "GET", " /p", "ath", "/qu", "ery", + "?1 ", "HTT", "P/1", ".1\r", "\n\r\n", + "Ser", "ial", "iza", "ble", " bu", + "t i", "gno", "red", " on", " GE", + "T", + })); + } + + void + testAsyncWrite() + { + net::io_context ioc; + test::stream out(ioc), in(ioc); + { + test::connect(out, in); + + message_generator gen(make_get()); + + beast::async_write(out, std::move(gen), + [&](error_code ec, size_t total) { + BEAST_EXPECT(total == 61); + BEAST_EXPECT(!ec.failed()); + }); + + ioc.run(); + } + + BEAST_EXPECT(61 == in.nwrite_bytes()); + BEAST_EXPECT(in.str() == + "GET /path/query?1 HTTP/1.1\r\n\r\n" + "Serializable but ignored on GET"); + } + + void + testWrite() + { + net::io_context ioc; + test::stream out(ioc), in(ioc); + test::connect(out, in); + + { + message_generator gen(make_get()); + + error_code ec; + std::size_t total = beast::write(out, gen, ec); + + BEAST_EXPECT(total == 61); + BEAST_EXPECT(!ec.failed()); + + BEAST_EXPECT(61 == in.nwrite_bytes()); + BEAST_EXPECT(in.str() == + "GET /path/query?1 HTTP/1.1\r\n\r\n" + "Serializable but ignored on GET"); + } + + in.clear(); + + { + // rvalue accepted + std::size_t total = + beast::write(out, message_generator{ make_get() }); + BEAST_EXPECT(total == 61); + BEAST_EXPECT(in.str() == + "GET /path/query?1 HTTP/1.1\r\n\r\n" + "Serializable but ignored on GET"); + } + } + + void + testFragmentedBody() + { + net::io_context ioc; + test::stream out(ioc), in(ioc); + test::connect(out, in); + + { + message_generator gen(make_fragmented_body_response(net::buffer("", 0))); + + error_code ec; + std::size_t total = beast::write(out, gen, ec); + + BEAST_EXPECT(total == 46); + BEAST_EXPECT(! ec.failed()); + + BEAST_EXPECT( + in.str() == + "HTTP/1.1 200 OK\r\n" + "Content-Length: 8\r\n\r\nsomebody"); + } + + in.clear(); + + { + message_generator gen(make_fragmented_body_response(net::buffer("x", 1))); + + error_code ec; + std::size_t total = beast::write(out, gen, ec); + + BEAST_EXPECT(total == 47 + 63); + BEAST_EXPECT(! ec.failed()); + + BEAST_EXPECT( + in.str() == + "HTTP/1.1 200 OK\r\n" + "Content-Length: 71\r\n\r\nxxxxxxxsomexxxxxxxxxxxxxxxxxxxbodyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + } + } + + void + testKeepAlive() + { + auto request = [](int version) + { + return http::request( + http::verb::post, "/", version); + }; + BEAST_EXPECT( + ! http::message_generator(request(10)).keep_alive()); + BEAST_EXPECT( + http::message_generator(request(11)).keep_alive()); + } + + void + run() override + { + testGenerate(); + testGenerateSlowConsumer(); + testAsyncWrite(); + testWrite(); + testFragmentedBody(); + testKeepAlive(); + } +}; + +BEAST_DEFINE_TESTSUITE(beast,http,message_generator); + +} // http +} // beast +} // boost