diff --git a/doc/qbk/22_examples.qbk b/doc/qbk/22_examples.qbk index 63ab0734c..7820daec6 100644 --- a/doc/qbk/22_examples.qbk +++ b/doc/qbk/22_examples.qbk @@ -353,6 +353,12 @@ File `repository.hpp`: File `repository.cpp`: [example_connection_pool_repository_cpp] +File `handle_request.hpp`: +[example_connection_pool_handle_request_hpp] + +File `handle_request.cpp`: +[example_connection_pool_handle_request_cpp] + File `server.hpp`: [example_connection_pool_server_hpp] diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 28e28d60d..3ff6526dd 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -130,6 +130,7 @@ add_example( add_executable( boost_mysql_example_connection_pool connection_pool/repository.cpp + connection_pool/handle_request.cpp connection_pool/server.cpp connection_pool/main.cpp ) diff --git a/example/Jamfile b/example/Jamfile index 1ab8c6a92..af16bd779 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -97,6 +97,7 @@ run run connection_pool/main.cpp connection_pool/repository.cpp + connection_pool/handle_request.cpp connection_pool/server.cpp /boost/mysql/test//boost_mysql_compiled /boost/mysql/test//boost_context_lib diff --git a/example/connection_pool/handle_request.cpp b/example/connection_pool/handle_request.cpp new file mode 100644 index 000000000..31590d1ea --- /dev/null +++ b/example/connection_pool/handle_request.cpp @@ -0,0 +1,357 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 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) +// + +//[example_connection_pool_handle_request_cpp + +#include + +#ifdef BOOST_MYSQL_CXX14 + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "handle_request.hpp" +#include "repository.hpp" +#include "types.hpp" + +// This file contains all the boilerplate code to dispatch HTTP +// requests to API endpoints. Functions here end up calling +// note_repository fuctions. + +namespace asio = boost::asio; +namespace http = boost::beast::http; +using boost::mysql::error_code; +using boost::mysql::string_view; +using namespace notes; + +namespace { + +// Attempts to parse a numeric ID from a string. +// If you're using C++17, you can use std::from_chars, instead +static boost::optional parse_id(const std::string& from) +{ + try + { + std::size_t consumed = 0; + int res = std::stoi(from, &consumed); + if (consumed != from.size()) + return {}; + else if (res < 0) + return {}; + return res; + } + catch (const std::exception&) + { + return {}; + } +} + +// Encapsulates the logic required to match a HTTP request +// to an API endpoint, call the relevant note_repository function, +// and return an HTTP response. +class request_handler +{ + // The HTTP request we're handling. Requests are small in size, + // so we use http::request + const http::request& request_; + + // The repository to access MySQL + note_repository repo_; + + // Creates an error response + http::response error_response(http::status code, string_view msg) const + { + http::response res; + + // Set the status code + res.result(code); + + // Set the keep alive option + res.keep_alive(request_.keep_alive()); + + // Set the body + res.body() = msg; + + // Adjust the content-length field + res.prepare_payload(); + + // Done + return res; + } + + // Used when the request's Content-Type header doesn't match what we expect + http::response invalid_content_type() const + { + return error_response(http::status::bad_request, "Invalid content-type"); + } + + // Used when the request body didn't match the format we expect + http::response invalid_body() const + { + return error_response(http::status::bad_request, "Invalid body"); + } + + // Used when the request's method didn't match the ones allowed by the endpoint + http::response method_not_allowed() const + { + return error_response(http::status::method_not_allowed, "Method not allowed"); + } + + // Used when the request target couldn't be matched to any API endpoint + http::response endpoint_not_found() const + { + return error_response(http::status::not_found, "The requested resource was not found"); + } + + // Used when the user requested a note (e.g. using GET /note/ or PUT /note/) + // but the note doesn't exist + http::response note_not_found() const + { + return error_response(http::status::not_found, "The requested note was not found"); + } + + // Creates a response with a serialized JSON body. + // T should be a type with Boost.Describe metadata containing the + // body data to be serialized + template + http::response json_response(const T& body) const + { + http::response res; + + // A JSON response is always a 200 + res.result(http::status::ok); + + // Set the content-type header + res.set("Content-Type", "application/json"); + + // Set the keep-alive option + res.keep_alive(request_.keep_alive()); + + // Serialize the body data into a string and use it as the response body. + // We use Boost.JSON's automatic serialization feature, which uses Boost.Describe + // reflection data to generate a serialization function for us. + res.body() = boost::json::serialize(boost::json::value_from(body)); + + // Adjust the content-length header + res.prepare_payload(); + + // Done + return res; + } + + // Returns true if the request's Content-Type is set to JSON + bool has_json_content_type() const + { + auto it = request_.find("Content-Type"); + return it != request_.end() && it->value() == "application/json"; + } + + // Attempts to parse the request body as a JSON into an object of type T. + // T should be a type with Boost.Describe metadata. + // We use boost::system::result, which may contain a result or an error. + template + boost::system::result parse_json_request() const + { + error_code ec; + + // Attempt to parse the request into a json::value. + // This will fail if the provided body isn't valid JSON. + auto val = boost::json::parse(request_.body(), ec); + if (ec) + return ec; + + // Attempt to parse the json::value into a T. This will + // fail if the provided JSON doesn't match T's shape. + return boost::json::try_value_to(val); + } + + http::response handle_request_impl(boost::asio::yield_context yield) + { + // Parse the request target. We use Boost.Url to do this. + auto url = boost::urls::parse_origin_form(request_.target()); + if (url.has_error()) + return error_response(http::status::bad_request, "Invalid request target"); + + // We will be iterating over the target's segments to determine + // which endpoint we are being requested + auto segs = url->segments(); + auto segit = segs.begin(); + auto seg = *segit++; + + // All endpoints start with /notes + if (seg != "notes") + return endpoint_not_found(); + + if (segit == segs.end()) + { + if (request_.method() == http::verb::get) + { + // GET /notes: retrieves all the notes. + // The request doesn't have a body. + // The response has a JSON body with multi_notes_response format + auto res = repo_.get_notes(yield); + return json_response(multi_notes_response{std::move(res)}); + } + else if (request_.method() == http::verb::post) + { + // POST /notes: creates a note. + // The request has a JSON body with note_request_body format. + // The response has a JSON body with single_note_response format. + + // Parse the request body + if (!has_json_content_type()) + return invalid_content_type(); + auto args = parse_json_request(); + if (args.has_error()) + return invalid_body(); + + // Actually create the note + auto res = repo_.create_note(args->title, args->content, yield); + + // Return the newly crated note as response + return json_response(single_note_response{std::move(res)}); + } + else + { + return method_not_allowed(); + } + } + else + { + // The URL has the form /notes/. Parse the note ID. + auto note_id = parse_id(*segit++); + if (!note_id.has_value()) + { + return error_response( + http::status::bad_request, + "Invalid note_id specified in request target" + ); + } + + // /notes// is not a valid endpoint + if (segit != segs.end()) + return endpoint_not_found(); + + if (request_.method() == http::verb::get) + { + // GET /notes/: retrieves a single note. + // The request doesn't have a body. + // The response has a JSON body with single_note_response format + + // Get the note + auto res = repo_.get_note(*note_id, yield); + + // If we didn't find it, return a 404 error + if (!res.has_value()) + return note_not_found(); + + // Return it as response + return json_response(single_note_response{std::move(*res)}); + } + else if (request_.method() == http::verb::put) + { + // PUT /notes/: replaces a note. + // The request has a JSON body with note_request_body format. + // The response has a JSON body with single_note_response format. + + // Parse the JSON body + if (!has_json_content_type()) + return invalid_content_type(); + auto args = parse_json_request(); + if (args.has_error()) + return invalid_body(); + + // Perform the update + auto res = repo_.replace_note(*note_id, args->title, args->content, yield); + + // Check that it took effect. Otherwise, it's because the note wasn't there + if (!res.has_value()) + return note_not_found(); + + // Return the updated note as response + return json_response(single_note_response{std::move(*res)}); + } + else if (request_.method() == http::verb::delete_) + { + // DELETE /notes/: deletes a note. + // The request doesn't have a body. + // The response has a JSON body with delete_note_response format. + + // Attempt to delete the note + bool deleted = repo_.delete_note(*note_id, yield); + + // Return whether the delete was successful in the response. + // We don't fail DELETEs for notes that don't exist. + return json_response(delete_note_response{deleted}); + } + else + { + return method_not_allowed(); + } + } + } + +public: + // Constructor + request_handler(const http::request& req, note_repository repo) + : request_(req), repo_(repo) + { + } + + // Generates a response for the request passed to the constructor + http::response handle_request(boost::asio::yield_context yield) + { + try + { + // Attempt to handle the request + return handle_request_impl(yield); + } + catch (const boost::mysql::error_with_diagnostics& err) + { + // A Boost.MySQL error. This will happen if you don't have connectivity + // to your database, your schema is incorrect or your credentials are invalid. + // Log the error, including diagnostics, and return a generic 500 + std::cerr << "Uncaught exception: " << err.what() + << "\nServer diagnostics: " << err.get_diagnostics().server_message() << '\n'; + return error_response(http::status::internal_server_error, "Internal error"); + } + catch (const std::exception& err) + { + // Another kind of error. This indicates a programming error or a severe + // server condition (e.g. out of memory). Same procedure as above. + std::cerr << "Uncaught exception: " << err.what() << '\n'; + return error_response(http::status::internal_server_error, "Internal error"); + } + } +}; + +} // namespace + +// External interface +boost::beast::http::response notes::handle_request( + const boost::beast::http::request& request, + note_repository repo, + boost::asio::yield_context yield +) +{ + return request_handler(request, repo).handle_request(yield); +} + +#endif diff --git a/example/connection_pool/handle_request.hpp b/example/connection_pool/handle_request.hpp new file mode 100644 index 000000000..c396d3516 --- /dev/null +++ b/example/connection_pool/handle_request.hpp @@ -0,0 +1,33 @@ +// +// Copyright (c) 2019-2023 Ruben Perez Hidalgo (rubenperez038 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) +// + +#ifndef BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_HANDLE_REQUEST_HPP +#define BOOST_MYSQL_EXAMPLE_CONNECTION_POOL_HANDLE_REQUEST_HPP + +//[example_connection_pool_handle_request_hpp + +#include +#include +#include +#include + +#include "repository.hpp" + +namespace notes { + +// Handles an individual HTTP request, producing a response. +boost::beast::http::response handle_request( + const boost::beast::http::request& request, + note_repository repo, + boost::asio::yield_context yield +); + +} // namespace notes + +//] + +#endif diff --git a/example/connection_pool/server.cpp b/example/connection_pool/server.cpp index 04b0d1ac9..31c40a7b0 100644 --- a/example/connection_pool/server.cpp +++ b/example/connection_pool/server.cpp @@ -13,11 +13,8 @@ #include #include -#include -#include #include -#include #include #include #include @@ -27,38 +24,27 @@ #include #include #include -#include #include -#include #include -#include -#include -#include -#include -#include -#include -#include #include -#include #include #include #include #include #include +#include "handle_request.hpp" #include "repository.hpp" #include "server.hpp" #include "types.hpp" // This file contains all the boilerplate code to implement a HTTP -// server that provides the REST API described in main.cpp. -// Functions here end up invoking note_repository member functions. +// server. Functions here end up invoking handle_request. namespace asio = boost::asio; namespace http = boost::beast::http; using boost::mysql::error_code; -using boost::mysql::string_view; using namespace notes; namespace { @@ -69,305 +55,6 @@ static void log_error(error_code ec, const char* msg) std::cerr << "Error in " << msg << ": " << ec << std::endl; } -// Attempts to parse a numeric ID from a string. -// If you're using C++17, you can use std::from_chars, instead -static boost::optional parse_id(const std::string& from) -{ - try - { - std::size_t consumed = 0; - int res = std::stoi(from, &consumed); - if (consumed != from.size()) - return {}; - else if (res < 0) - return {}; - return res; - } - catch (const std::exception&) - { - return {}; - } -} - -// Encapsulates the logic required to match a HTTP request -// to an API endpoint, call the relevant note_repository function, -// and return an HTTP response. -class request_handler -{ - // The HTTP request we're handling. Requests are small in size, - // so we use http::request - const http::request& request_; - - // The repository to access MySQL - note_repository repo_; - - // Creates an error response - http::response error_response(http::status code, string_view msg) const - { - http::response res; - - // Set the status code - res.result(code); - - // Set the keep alive option - res.keep_alive(request_.keep_alive()); - - // Set the body - res.body() = msg; - - // Adjust the content-length field - res.prepare_payload(); - - // Done - return res; - } - - // Used when the request's Content-Type header doesn't match what we expect - http::response invalid_content_type() const - { - return error_response(http::status::bad_request, "Invalid content-type"); - } - - // Used when the request body didn't match the format we expect - http::response invalid_body() const - { - return error_response(http::status::bad_request, "Invalid body"); - } - - // Used when the request's method didn't match the ones allowed by the endpoint - http::response method_not_allowed() const - { - return error_response(http::status::method_not_allowed, "Method not allowed"); - } - - // Used when the request target couldn't be matched to any API endpoint - http::response endpoint_not_found() const - { - return error_response(http::status::not_found, "The requested resource was not found"); - } - - // Used when the user requested a note (e.g. using GET /note/ or PUT /note/) - // but the note doesn't exist - http::response note_not_found() const - { - return error_response(http::status::not_found, "The requested note was not found"); - } - - // Creates a response with a serialized JSON body. - // T should be a type with Boost.Describe metadata containing the - // body data to be serialized - template - http::response json_response(const T& body) const - { - http::response res; - - // A JSON response is always a 200 - res.result(http::status::ok); - - // Set the content-type header - res.set("Content-Type", "application/json"); - - // Set the keep-alive option - res.keep_alive(request_.keep_alive()); - - // Serialize the body data into a string and use it as the response body. - // We use Boost.JSON's automatic serialization feature, which uses Boost.Describe - // reflection data to generate a serialization function for us. - res.body() = boost::json::serialize(boost::json::value_from(body)); - - // Adjust the content-length header - res.prepare_payload(); - - // Done - return res; - } - - // Returns true if the request's Content-Type is set to JSON - bool has_json_content_type() const - { - auto it = request_.find("Content-Type"); - return it != request_.end() && it->value() == "application/json"; - } - - // Attempts to parse the request body as a JSON into an object of type T. - // T should be a type with Boost.Describe metadata. - // We use boost::system::result, which may contain a result or an error. - template - boost::system::result parse_json_request() const - { - error_code ec; - - // Attempt to parse the request into a json::value. - // This will fail if the provided body isn't valid JSON. - auto val = boost::json::parse(request_.body(), ec); - if (ec) - return ec; - - // Attempt to parse the json::value into a T. This will - // fail if the provided JSON doesn't match T's shape. - return boost::json::try_value_to(val); - } - - http::response handle_request_impl(boost::asio::yield_context yield) - { - // Parse the request target. We use Boost.Url to do this. - auto url = boost::urls::parse_origin_form(request_.target()); - if (url.has_error()) - return error_response(http::status::bad_request, "Invalid request target"); - - // We will be iterating over the target's segments to determine - // which endpoint we are being requested - auto segs = url->segments(); - auto segit = segs.begin(); - auto seg = *segit++; - - // All endpoints start with /notes - if (seg != "notes") - return endpoint_not_found(); - - if (segit == segs.end()) - { - if (request_.method() == http::verb::get) - { - // GET /notes: retrieves all the notes. - // The request doesn't have a body. - // The response has a JSON body with multi_notes_response format - auto res = repo_.get_notes(yield); - return json_response(multi_notes_response{std::move(res)}); - } - else if (request_.method() == http::verb::post) - { - // POST /notes: creates a note. - // The request has a JSON body with note_request_body format. - // The response has a JSON body with single_note_response format. - - // Parse the request body - if (!has_json_content_type()) - return invalid_content_type(); - auto args = parse_json_request(); - if (args.has_error()) - return invalid_body(); - - // Actually create the note - auto res = repo_.create_note(args->title, args->content, yield); - - // Return the newly crated note as response - return json_response(single_note_response{std::move(res)}); - } - else - { - return method_not_allowed(); - } - } - else - { - // The URL has the form /notes/. Parse the note ID. - auto note_id = parse_id(*segit++); - if (!note_id.has_value()) - { - return error_response( - http::status::bad_request, - "Invalid note_id specified in request target" - ); - } - - // /notes// is not a valid endpoint - if (segit != segs.end()) - return endpoint_not_found(); - - if (request_.method() == http::verb::get) - { - // GET /notes/: retrieves a single note. - // The request doesn't have a body. - // The response has a JSON body with single_note_response format - - // Get the note - auto res = repo_.get_note(*note_id, yield); - - // If we didn't find it, return a 404 error - if (!res.has_value()) - return note_not_found(); - - // Return it as response - return json_response(single_note_response{std::move(*res)}); - } - else if (request_.method() == http::verb::put) - { - // PUT /notes/: replaces a note. - // The request has a JSON body with note_request_body format. - // The response has a JSON body with single_note_response format. - - // Parse the JSON body - if (!has_json_content_type()) - return invalid_content_type(); - auto args = parse_json_request(); - if (args.has_error()) - return invalid_body(); - - // Perform the update - auto res = repo_.replace_note(*note_id, args->title, args->content, yield); - - // Check that it took effect. Otherwise, it's because the note wasn't there - if (!res.has_value()) - return note_not_found(); - - // Return the updated note as response - return json_response(single_note_response{std::move(*res)}); - } - else if (request_.method() == http::verb::delete_) - { - // DELETE /notes/: deletes a note. - // The request doesn't have a body. - // The response has a JSON body with delete_note_response format. - - // Attempt to delete the note - bool deleted = repo_.delete_note(*note_id, yield); - - // Return whether the delete was successful in the response. - // We don't fail DELETEs for notes that don't exist. - return json_response(delete_note_response{deleted}); - } - else - { - return method_not_allowed(); - } - } - } - -public: - // Constructor - request_handler(const http::request& req, note_repository repo) - : request_(req), repo_(repo) - { - } - - // Generates a response for the request passed to the constructor - http::response handle_request(boost::asio::yield_context yield) - { - try - { - // Attempt to handle the request - return handle_request_impl(yield); - } - catch (const boost::mysql::error_with_diagnostics& err) - { - // A Boost.MySQL error. This will happen if you don't have connectivity - // to your database, your schema is incorrect or your credentials are invalid. - // Log the error, including diagnostics, and return a generic 500 - std::cerr << "Uncaught exception: " << err.what() - << "\nServer diagnostics: " << err.get_diagnostics().server_message() << '\n'; - return error_response(http::status::internal_server_error, "Internal error"); - } - catch (const std::exception& err) - { - // Another kind of error. This indicates a programming error or a severe - // server condition (e.g. out of memory). Same procedure as above. - std::cerr << "Uncaught exception: " << err.what() << '\n'; - return error_response(http::status::internal_server_error, "Internal error"); - } - } -}; - static void run_http_session( boost::asio::ip::tcp::socket sock, std::shared_ptr st, @@ -408,7 +95,7 @@ static void run_http_session( // Process the request to generate a response. // This invokes the business logic, which will need to access MySQL data - auto response = request_handler(parser.get(), note_repository(st->pool)).handle_request(yield); + auto response = handle_request(parser.get(), note_repository(st->pool), yield); // Determine if we should close the connection bool keep_alive = response.keep_alive();