Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

A REST API server that uses C++20 coroutines
PrevUpHomeNext

This example assumes you have gone through the setup.

/**
 * Implements a HTTP REST API using Boost.MySQL and Boost.Beast.
 * The API models a simplified order management system for an online store.
 * Using the API, users can query the store's product catalog, create and
 * edit orders, and check them out for payment.
 *
 * The API defines the following endpoints:
 *
 *    GET    /products?search={s}       Returns a list of products
 *    GET    /orders                    Returns all orders
 *    GET    /orders?id={}              Returns a single order
 *    POST   /orders                    Creates a new order
 *    POST   /orders/items              Adds a new order item to an existing order
 *    DELETE /orders/items?id={}        Deletes an order item
 *    POST   /orders/checkout?id={}     Checks out an order
 *    POST   /orders/complete?id={}     Completes an order
 *
 * Each order can have any number of order items. An order item
 * represents an individual product that has been added to an order.
 * Orders are created empty, in a 'draft' state. Items can then be
 * added and removed from the order. After adding the desired items,
 * orders can be checked out for payment. A third-party service, like Stripe,
 * would be used to collect the payment. For simplicity, we've left this part
 * out of the example. Once checked out, an order is no longer editable.
 * Finally, after successful payment, order are transitioned to the
 * 'complete' status.
 *
 * The server uses C++20 coroutines and is multi-threaded.
 * It also requires linking to Boost::json and Boost::url.
 * The database schema is defined in db_setup.sql, in the same directory as this file.
 * You need to source this file before running the example.
 */

#include <boost/mysql/any_address.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/pool_params.hpp>

#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/system/error_code.hpp>

#include <cstddef>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <string>

#include "server.hpp"

using namespace orders;
namespace mysql = boost::mysql;
namespace asio = boost::asio;

// The number of threads to use
static constexpr std::size_t num_threads = 5;

int main_impl(int argc, char* argv[])
{
    // Check command line arguments.
    if (argc != 5)
    {
        std::cerr << "Usage: " << argv[0] << " <username> <password> <mysql-hostname> <port>\n";
        return EXIT_FAILURE;
    }

    // Application config
    const char* mysql_username = argv[1];
    const char* mysql_password = argv[2];
    const char* mysql_hostname = argv[3];
    auto port = static_cast<unsigned short>(std::stoi(argv[4]));

    // An event loop, where the application will run.
    // We will use the main thread to run the pool, too, so we use
    // one thread less than configured
    asio::thread_pool th_pool(num_threads - 1);

    // Create a connection pool
    mysql::connection_pool pool(
        // Use the thread pool as execution context
        th_pool,

        // Pool configuration
        mysql::pool_params{
            // Connect using TCP, to the given hostname and using the default port
            .server_address = mysql::host_and_port{mysql_hostname},

            // Authenticate using the given username
            .username = mysql_username,

            // Password for the above username
            .password = mysql_password,

            // Database to use when connecting
            .database = "boost_mysql_orders",

            // We're using multi-queries
            .multi_queries = true,

            // Using thread_safe will make the pool thread-safe by internally
            // creating and using a strand.
            // This allows us to share the pool between sessions, which may run
            // concurrently, on different threads.
            .thread_safe = true,
        }
    );

    // Launch the MySQL pool
    pool.async_run(asio::detached);

    // A signal_set allows us to intercept SIGINT and SIGTERM and
    // exit gracefully
    asio::signal_set signals{th_pool.get_executor(), SIGINT, SIGTERM};

    // Capture SIGINT and SIGTERM to perform a clean shutdown
    signals.async_wait([&th_pool](boost::system::error_code, int) {
        // Stop the execution context. This will cause main to exit
        th_pool.stop();
    });

    // Start listening for HTTP connections. This will run until the context is stopped
    asio::co_spawn(
        // Use the thread pool to run the listener coroutine
        th_pool,

        // The coroutine to run
        [&pool, port] { return run_server(pool, port); },

        // If an exception is thrown in the listener coroutine, propagate it
        [](std::exception_ptr exc) {
            if (exc)
                std::rethrow_exception(exc);
        }
    );

    // Attach the current thread to the thread pool. This will block
    // until stop() is called
    th_pool.attach();

    // Wait until all threads have exited
    th_pool.join();

    std::cout << "Server exiting" << std::endl;

    // (If we get here, it means we got a SIGINT or SIGTERM)
    return EXIT_SUCCESS;
}

int main(int argc, char** argv)
{
    try
    {
        main_impl(argc, argv);
    }
    catch (const std::exception& err)
    {
        std::cerr << "Error: " << err.what() << std::endl;
        return 1;
    }
}
//
// File: types.hpp
//
// Contains type definitions used in the REST API and database code.
// We use Boost.Describe (BOOST_DESCRIBE_STRUCT) to add reflection
// capabilities to our types. This allows using Boost.MySQL
// static interface (i.e. static_results<T>) to parse query results,
// and Boost.JSON automatic serialization/deserialization.

#include <boost/describe/class.hpp>

#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace orders {

// A product object, as defined in the database and in the GET /products endpoint
struct product
{
    // The unique database ID of the object.
    std::int64_t id;

    // The product's display name
    std::string short_name;

    // The product's description
    std::optional<std::string> descr;

    // The product's price, in dollar cents
    std::int64_t price;
};
BOOST_DESCRIBE_STRUCT(product, (), (id, short_name, descr, price))

// An order object, as defined in the database and in some REST endpoints.
// This object does not include the items associated to the order.
struct order
{
    // The unique database ID of the object.
    std::int64_t id;

    // The order status. One of "draft", "pending_payment" or "complete".
    std::string status;
};
BOOST_DESCRIBE_STRUCT(order, (), (id, status))

// Constants for the order::status member
inline constexpr std::string_view status_draft = "draft";
inline constexpr std::string_view status_pending_payment = "pending_payment";
inline constexpr std::string_view status_complete = "complete";

// An order item object, as defined in the database and in some REST endpoints.
// Does not include the order_id database field.
struct order_item
{
    // The unique database ID of the object.
    std::int64_t id;

    // The ID of the product that this order item represents
    std::int64_t product_id;

    // The number of units of the product that this item represents.
    // For instance, if product_id=2 and quantity=3,
    // the user wants to buy 3 units of the product with ID 2.
    std::int64_t quantity;
};
BOOST_DESCRIBE_STRUCT(order_item, (), (id, product_id, quantity))

// An order object, with its associated order items.
// Used in some REST endpoints.
struct order_with_items
{
    // The unique database ID of the object.
    std::int64_t id;

    // The order status. One of "draft", "pending_payment" or "complete".
    std::string status;

    // The items associated to this order.
    std::vector<order_item> items;
};
BOOST_DESCRIBE_STRUCT(order_with_items, (), (id, status, items))

// REST request for POST /orders/items
struct add_order_item_request
{
    // Identifies the order to which the item should be added.
    std::int64_t order_id;

    // Identifies the product that should be added to the order.
    std::int64_t product_id;

    // The number of units of the above product that should be added to the order.
    std::int64_t quantity;
};
BOOST_DESCRIBE_STRUCT(add_order_item_request, (), (order_id, product_id, quantity))

}  // namespace orders
//
// File: error.hpp
//
// Contains an errc enumeration and the required pieces to
// use it with boost::system::error_code.
// We use this indirectly in the DB repository class,
// when using the error codes in boost::system::result.

#include <boost/system/error_category.hpp>

#include <mutex>
#include <string_view>
#include <type_traits>

namespace orders {

// Error code enum for errors originated within our application
enum class errc
{
    not_found,             // couldn't retrieve or modify a certain resource because it doesn't exist
    order_invalid_status,  // an operation found an order in a status != the one expected (e.g. not editable)
    product_not_found,     // a product referenced by a request doesn't exist
};

// To use errc with boost::system::error_code, we need
// to define an error category (see the cpp file).
const boost::system::error_category& get_orders_category();

// Called when constructing an error_code from an errc value.
inline boost::system::error_code make_error_code(errc v)
{
    // Roughly, an error_code is an int and a category defining what the int means.
    return boost::system::error_code(static_cast<int>(v), get_orders_category());
}

// In multi-threaded programs, using std::cerr without any locking
// can result in interleaved output.
// Locks a mutex guarding std::cerr to prevent this.
// All uses of std::cerr should respect this.
std::unique_lock<std::mutex> lock_cerr();

// A helper function for the common case where we want to log an error code
void log_error(std::string_view header, boost::system::error_code ec);

}  // namespace orders

// This specialization is required to construct error_code's from errc values
template <>
struct boost::system::is_error_code_enum<orders::errc> : std::true_type
{
};
#include <boost/system/error_category.hpp>

#include <iostream>
#include <mutex>

#include "error.hpp"

namespace {

// Converts an orders::errc to string
const char* error_to_string(orders::errc value)
{
    switch (value)
    {
    case orders::errc::not_found: return "not_found";
    case orders::errc::order_invalid_status: return "order_invalid_status";
    case orders::errc::product_not_found: return "product_not_found";
    default: return "<unknown orders::errc>";
    }
}

// The category to be returned by get_orders_category
class orders_category final : public boost::system::error_category
{
public:
    // Identifies the error category. Used when converting error_codes to string
    const char* name() const noexcept final override { return "orders"; }

    // Given a numeric error belonging to this category, convert it to a string
    std::string message(int ev) const final override
    {
        return error_to_string(static_cast<orders::errc>(ev));
    }
};

// The error category
static const orders_category g_category;

// The std::mutex that guards std::cerr
static std::mutex g_cerr_mutex;

}  // namespace

//
// External interface
//
const boost::system::error_category& orders::get_orders_category() { return g_category; }

std::unique_lock<std::mutex> orders::lock_cerr() { return std::unique_lock{g_cerr_mutex}; }

void orders::log_error(std::string_view header, boost::system::error_code ec)
{
    // Lock the mutex
    auto guard = lock_cerr();

    // Logging the error code prints the number and category. Add the message, too
    std::cerr << header << ": " << ec << " " << ec.message() << std::endl;
}
//
// File: repository.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/awaitable.hpp>
#include <boost/system/result.hpp>

#include <cstdint>
#include <string_view>
#include <vector>

#include "types.hpp"

namespace orders {

// Encapsulates database logic.
// If the database is unavailable, these functions throw.
// Additionally, functions that may fail depending on the supplied input
// return boost::system::result<T>, avoiding exceptions in common cases.
class db_repository
{
    boost::mysql::connection_pool& pool_;

public:
    // Constructor (this is a cheap-to-construct object)
    db_repository(boost::mysql::connection_pool& pool) noexcept : pool_(pool) {}

    // Retrieves products using a full-text search
    boost::asio::awaitable<std::vector<product>> get_products(std::string_view search);

    // Retrieves all the orders in the database
    boost::asio::awaitable<std::vector<order>> get_orders();

    // Retrieves an order by ID.
    // Returns an error if the ID doesn't match any order.
    boost::asio::awaitable<boost::system::result<order_with_items>> get_order_by_id(std::int64_t id);

    // Creates an empty order. Returns the created order.
    boost::asio::awaitable<order_with_items> create_order();

    // Adds an item to an order. Retrieves the updated order.
    // Returns an error if the ID doesn't match any order, the order
    // is not editable, or the product_id doesn't match any product
    boost::asio::awaitable<boost::system::result<order_with_items>> add_order_item(
        std::int64_t order_id,
        std::int64_t product_id,
        std::int64_t quantity
    );

    // Removes an item from an order. Retrieves the updated order.
    // Returns an error if the ID doesn't match any order item
    // or the order is not editable.
    boost::asio::awaitable<boost::system::result<order_with_items>> remove_order_item(std::int64_t item_id);

    // Checks an order out, transitioning it to the pending_payment status.
    // Returns an error if the ID doesn't match any order
    // or the order is not editable.
    boost::asio::awaitable<boost::system::result<order_with_items>> checkout_order(std::int64_t id);

    // Completes an order, transitioning it to the complete status.
    // Returns an error if the ID doesn't match any order
    // or the order is not checked out.
    boost::asio::awaitable<boost::system::result<order_with_items>> complete_order(std::int64_t id);
};

}  // namespace orders
//
// File: repository.cpp
//
// See the db_setup.sql file in this folder for the table definitions

#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/static_results.hpp>
#include <boost/mysql/with_params.hpp>

#include <boost/asio/awaitable.hpp>
#include <boost/system/result.hpp>

#include <string>
#include <string_view>
#include <tuple>
#include <vector>

#include "error.hpp"
#include "repository.hpp"
#include "types.hpp"

namespace mysql = boost::mysql;
namespace asio = boost::asio;
using namespace orders;

asio::awaitable<std::vector<product>> db_repository::get_products(std::string_view search)
{
    // Get a connection from the pool
    auto conn = co_await pool_.async_get_connection();

    // Get the products using the MySQL built-in full-text search feature.
    // Look for the query string in the short_name and descr fields.
    // Parse the query results into product struct instances
    mysql::static_results<product> res;
    co_await conn->async_execute(
        mysql::with_params(
            "SELECT id, short_name, descr, price FROM products "
            "WHERE MATCH(short_name, descr) AGAINST({}) "
            "LIMIT 10",
            search
        ),
        res
    );

    // By default, connections are reset after they are returned to the pool
    // (by using any_connection::async_reset_connection). This will reset any
    // session state we changed while we were using the connection
    // (e.g. it will deallocate any statements we prepared).
    // We did nothing to mutate session state, so we can tell the pool to skip
    // this step, providing a minor performance gain.
    // We use pooled_connection::return_without_reset to do this.
    // If an exception was raised, the connection would be reset, for safety.
    conn.return_without_reset();

    // Return the result
    co_return std::vector<product>{res.rows().begin(), res.rows().end()};
}

asio::awaitable<std::vector<order>> db_repository::get_orders()
{
    // Get a connection from the pool
    auto conn = co_await pool_.async_get_connection();

    // Get all the orders.
    // Parse the result into order structs.
    mysql::static_results<order> res;
    co_await conn->async_execute("SELECT id, status FROM orders", res);

    // We didn't mutate session state, so we can skip resetting the connection
    conn.return_without_reset();

    // Return the result
    co_return std::vector<order>{res.rows().begin(), res.rows().end()};
}

asio::awaitable<boost::system::result<order_with_items>> db_repository::get_order_by_id(std::int64_t id)
{
    // Get a connection from the pool
    auto conn = co_await pool_.async_get_connection();

    // Get a single order and all its associated items.
    // The transaction ensures atomicity between the two SELECTs.
    // We issued 4 queries, so we get 4 resultsets back.
    // Ignore the 1st and 4th, and parse the other two into order and order_item structs
    mysql::static_results<std::tuple<>, order, order_item, std::tuple<>> result;
    co_await conn->async_execute(
        mysql::with_params(
            "START TRANSACTION READ ONLY;"
            "SELECT id, status FROM orders WHERE id = {0};"
            "SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};"
            "COMMIT",
            id
        ),
        result
    );

    // We didn't mutate session state
    conn.return_without_reset();

    // result.rows<N> returns the rows for the N-th resultset, as a span
    auto orders = result.rows<1>();
    auto order_items = result.rows<2>();

    // Did we find the order we're looking for?
    if (orders.empty())
        co_return orders::errc::not_found;
    const order& ord = orders[0];

    // If we did, compose the result
    co_return order_with_items{
        ord.id,
        ord.status,
        {order_items.begin(), order_items.end()}
    };
}

asio::awaitable<order_with_items> db_repository::create_order()
{
    // Get a connection from the pool
    auto conn = co_await pool_.async_get_connection();

    // Create the new order.
    // Orders are created empty, with all fields defaulted.
    // MySQL does not have an INSERT ... RETURNING statement, so we use
    // a transaction with an INSERT and a SELECT to create the order
    // and retrieve it atomically.
    // This yields 4 resultsets, one per SQL statement.
    // Ignore all except the SELECT, and parse it into an order struct.
    mysql::static_results<std::tuple<>, std::tuple<>, order, std::tuple<>> result;
    co_await conn->async_execute(
        "START TRANSACTION;"
        "INSERT INTO orders () VALUES ();"
        "SELECT id, status FROM orders WHERE id = LAST_INSERT_ID();"
        "COMMIT",
        result
    );

    // We didn't mutate session state
    conn.return_without_reset();

    // This must always yield one row. Return it.
    const order& ord = result.rows<2>().front();
    co_return order_with_items{
        ord.id,
        ord.status,
        {}  // A newly created order never has items
    };
}

asio::awaitable<boost::system::result<order_with_items>> db_repository::add_order_item(
    std::int64_t order_id,
    std::int64_t product_id,
    std::int64_t quantity
)
{
    // Get a connection from the pool
    auto conn = co_await pool_.async_get_connection();

    // Retrieve the order and the product.
    // SELECT ... FOR UPDATE places a lock on the retrieved rows,
    // so they're not modified by other transactions while we use them.
    // If you're targeting MySQL 8.0+, you can also use SELECT ... FOR SHARE.
    // For the product, we only need to check that it does exist,
    // so we get its ID and parse the returned rows into a std::tuple.
    mysql::static_results<std::tuple<>, order, std::tuple<std::int64_t>> result1;
    co_await conn->async_execute(
        mysql::with_params(
            "START TRANSACTION;"
            "SELECT id, status FROM orders WHERE id = {} FOR UPDATE;"
            "SELECT id FROM products WHERE id = {} FOR UPDATE",
            order_id,
            product_id
        ),
        result1
    );

    // Check that the order exists
    if (result1.rows<1>().empty())
    {
        // Not found. We did mutate session state by opening a transaction,
        // so we can't use return_without_reset
        co_return orders::errc::not_found;
    }
    const order& ord = result1.rows<1>().front();

    // Verify that the order is editable.
    // Using SELECT ... FOR UPDATE prevents race conditions with this check.
    if (ord.status != status_draft)
    {
        co_return orders::errc::order_invalid_status;
    }

    // Check that the product exists
    if (result1.rows<2>().empty())
    {
        co_return orders::errc::product_not_found;
    }

    // Insert the new item and retrieve all the items associated to this order
    mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2;
    co_await conn->async_execute(
        mysql::with_params(
            "INSERT INTO order_items (order_id, product_id, quantity) VALUES ({0}, {1}, {2});"
            "SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};"
            "COMMIT",
            order_id,
            product_id,
            quantity
        ),
        result2
    );

    // If everything went well, we didn't mutate session state
    conn.return_without_reset();

    // Compose the return value
    co_return order_with_items{
        ord.id,
        ord.status,
        {result2.rows<1>().begin(), result2.rows<1>().end()}
    };
}

asio::awaitable<boost::system::result<order_with_items>> db_repository::remove_order_item(std::int64_t item_id
)
{
    // Get a connection from the pool
    auto conn = co_await pool_.async_get_connection();

    // Retrieve the order.
    // SELECT ... FOR UPDATE places a lock on the order and the item,
    // so they're not modified by other transactions while we use them.
    mysql::static_results<std::tuple<>, order> result1;
    co_await conn->async_execute(
        mysql::with_params(
            "START TRANSACTION;"
            "SELECT ord.id AS id, status FROM orders ord"
            "  JOIN order_items it ON (ord.id = it.order_id)"
            "  WHERE it.id = {} FOR UPDATE",
            item_id
        ),
        result1
    );

    // Check that the item exists
    if (result1.rows<1>().empty())
    {
        // Not found. We did mutate session state by opening a transaction,
        // so we can't use return_without_reset
        co_return orders::errc::not_found;
    }
    const order& ord = result1.rows<1>().front();

    // Check that the order is editable
    if (ord.status != orders::status_draft)
    {
        co_return orders::errc::order_invalid_status;
    }

    // Perform the deletion and retrieve the items
    mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2;
    co_await conn->async_execute(
        mysql::with_params(
            "DELETE FROM order_items WHERE id = {};"
            "SELECT id, product_id, quantity FROM order_items WHERE order_id = {};"
            "COMMIT",
            item_id,
            ord.id
        ),
        result2
    );

    // If everything went well, we didn't mutate session state
    conn.return_without_reset();

    // Compose the return value
    co_return order_with_items{
        ord.id,
        ord.status,
        {result2.rows<1>().begin(), result2.rows<1>().end()}
    };
}

// Helper function to implement checkout_order and complete_order
static asio::awaitable<boost::system::result<order_with_items>> change_order_status(
    mysql::connection_pool& pool,
    std::int64_t order_id,
    std::string_view original_status,  // The status that the order should have
    std::string_view target_status     // The status to transition the order to
)
{
    // Get a connection from the pool
    auto conn = co_await pool.async_get_connection();

    // Retrieve the order and lock it.
    // FOR UPDATE places an exclusive lock on the order,
    // preventing other concurrent transactions (including the ones
    // related to adding/removing items) from changing the order
    mysql::static_results<std::tuple<>, std::tuple<std::string>> result1;
    co_await conn->async_execute(
        mysql::with_params(
            "START TRANSACTION;"
            "SELECT status FROM orders WHERE id = {} FOR UPDATE;",
            order_id
        ),
        result1
    );

    // Check that the order exists
    if (result1.rows<1>().empty())
    {
        co_return orders::errc::not_found;
    }

    // Check that the order is in the expected status
    if (std::get<0>(result1.rows<1>().front()) != original_status)
    {
        co_return orders::errc::order_invalid_status;
    }

    // Update the order and retrieve the order details
    mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2;
    co_await conn->async_execute(
        mysql::with_params(
            "UPDATE orders SET status = {1} WHERE id = {0};"
            "SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};"
            "COMMIT",
            order_id,
            target_status
        ),
        result2
    );

    // If everything went well, we didn't mutate session state
    conn.return_without_reset();

    // Compose the return value
    co_return order_with_items{
        order_id,
        std::string(target_status),
        {result2.rows<1>().begin(), result2.rows<1>().end()}
    };
}

asio::awaitable<boost::system::result<order_with_items>> db_repository::checkout_order(std::int64_t id)
{
    return change_order_status(pool_, id, status_draft, status_pending_payment);
}

asio::awaitable<boost::system::result<order_with_items>> db_repository::complete_order(std::int64_t id)
{
    return change_order_status(pool_, id, status_pending_payment, status_complete);
}
//
// File: handle_request.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/awaitable.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>

namespace orders {

// Handles an individual HTTP request, producing a response.
// The caller of this function should use response::version,
// response::keep_alive and response::prepare_payload to adjust the response.
boost::asio::awaitable<boost::beast::http::response<boost::beast::http::string_body>> handle_request(
    const boost::beast::http::request<boost::beast::http::string_body>& request,
    boost::mysql::connection_pool& pool
);

}  // namespace orders
//
// File: handle_request.cpp
//
// This file contains all the boilerplate code to dispatch HTTP
// requests to API endpoints. Functions here end up calling
// db_repository fuctions.

#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_with_diagnostics.hpp>

#include <boost/asio/awaitable.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <boost/system/error_code.hpp>
#include <boost/system/result.hpp>
#include <boost/url/parse.hpp>
#include <boost/url/url_view.hpp>

#include <algorithm>
#include <charconv>
#include <cstdint>
#include <exception>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>

#include "error.hpp"
#include "handle_request.hpp"
#include "repository.hpp"
#include "types.hpp"

namespace asio = boost::asio;
namespace http = boost::beast::http;
namespace mysql = boost::mysql;
using boost::system::result;

namespace {

// Helper function that logs errors thrown by db_repository
// when an unexpected database error happens
void log_mysql_error(boost::system::error_code ec, const mysql::diagnostics& diag)
{
    // Lock std::cerr, to avoid race conditions
    auto guard = orders::lock_cerr();

    // Inserting the error code only prints the number and category. Add the message, too.
    std::cerr << "MySQL error: " << ec << " " << ec.message();

    // client_message() contains client-side generated messages that don't
    // contain user-input. This is usually embedded in exceptions.
    // When working with error codes, we need to log it explicitly
    if (!diag.client_message().empty())
    {
        std::cerr << ": " << diag.client_message();
    }

    // server_message() contains server-side messages, and thus may
    // contain user-supplied input. Printing it is safe.
    if (!diag.server_message().empty())
    {
        std::cerr << ": " << diag.server_message();
    }

    // Done
    std::cerr << std::endl;
}

// Attempts to parse a numeric ID from a string
std::optional<std::int64_t> parse_id(std::string_view from)
{
    std::int64_t id{};
    auto res = std::from_chars(from.data(), from.data() + from.size(), id);
    if (res.ec != std::errc{} || res.ptr != from.data() + from.size())
        return std::nullopt;
    return id;
}

// Helpers to create error responses with a single line of code
http::response<http::string_body> error_response(http::status code, std::string_view msg)
{
    http::response<http::string_body> res;
    res.result(code);
    res.body() = msg;
    return res;
}

// Like error_response, but always uses a 400 status code
http::response<http::string_body> bad_request(std::string_view body)
{
    return error_response(http::status::bad_request, body);
}

// Like error_response, but always uses a 500 status code and
// never provides extra information that might help potential attackers.
http::response<http::string_body> internal_server_error()
{
    return error_response(http::status::internal_server_error, "Internal server error");
}

// 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 <class T>
http::response<http::string_body> json_response(const T& body)
{
    http::response<http::string_body> res;

    // Set the content-type header
    res.set("Content-Type", "application/json");

    // 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));

    // Done
    return res;
}

// Attempts to parse a string 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 <class T>
result<T> parse_json(std::string_view json_string)
{
    // Attempt to parse the request into a json::value.
    // This will fail if the provided body isn't valid JSON.
    boost::system::error_code ec;
    auto val = boost::json::parse(json_string, 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<T>(val);
}

// Generates an HTTP error response based on an error code
// returned by db_repository.
http::response<http::string_body> response_from_db_error(boost::system::error_code ec)
{
    if (ec.category() == orders::get_orders_category())
    {
        switch (static_cast<orders::errc>(ec.value()))
        {
        case orders::errc::not_found:
            return error_response(http::status::not_found, "The referenced entity does not exist");
        case orders::errc::product_not_found:
            return error_response(
                http::status::unprocessable_entity,
                "The referenced product does not exist"
            );
        case orders::errc::order_invalid_status:
            return error_response(
                http::status::unprocessable_entity,
                "The referenced order doesn't have the status required by the operation"
            );
        default: return internal_server_error();
        }
    }
    else
    {
        return internal_server_error();
    }
}

// Contains data associated to an HTTP request.
// To be passed to individual handler functions
struct request_data
{
    // The incoming request
    const http::request<http::string_body>& request;

    // The URL the request is targeting
    boost::urls::url_view target;

    // Connection pool
    mysql::connection_pool& pool;

    orders::db_repository repo() const { return orders::db_repository(pool); }
};

//
// Endpoint handlers. They should be functions with signature
// asio::awaitable<http::response<http::string_body>>(const request_data&).
// Handlers are associated to a single URL path and HTTP method
//

// GET /products?search={s}: returns a list of products.
// The 'search' parameter is mandatory.
asio::awaitable<http::response<http::string_body>> handle_get_products(const request_data& input)
{
    // Parse the query parameter
    auto params_it = input.target.params().find("search");
    if (params_it == input.target.params().end())
        co_return bad_request("Missing mandatory query parameter: 'search'");
    auto search = (*params_it).value;

    // Invoke the database logic
    std::vector<orders::product> products = co_await input.repo().get_products(search);

    // Return the response
    co_return json_response(products);
}

// GET /orders: returns all orders
// GET /orders?id={}: returns a single order
// Both endpoints share handler because they share path and method
asio::awaitable<http::response<http::string_body>> handle_get_orders(const request_data& input)
{
    // Parse the query parameter
    auto params_it = input.target.params().find("id");

    // Which of the two endpoints are we serving?
    if (params_it == input.target.params().end())
    {
        // GET /orders
        // Invoke the database logic
        std::vector<orders::order> orders = co_await input.repo().get_orders();

        // Return the response
        co_return json_response(orders);
    }
    else
    {
        // GET /orders?id={}
        // Parse the query parameter
        auto order_id = parse_id((*params_it).value);
        if (!order_id.has_value())
            co_return bad_request("URL parameter 'id' should be a valid integer");

        // Invoke the database logic
        result<orders::order_with_items> order = co_await input.repo().get_order_by_id(*order_id);
        if (order.has_error())
            co_return response_from_db_error(order.error());

        // Return the response
        co_return json_response(*order);
    }
}

// POST /orders: creates a new order.
// Orders are created empty, so this request has no body.
asio::awaitable<http::response<http::string_body>> handle_create_order(const request_data& input)
{
    // Invoke the database logic
    orders::order_with_items order = co_await input.repo().create_order();

    // Return the response
    co_return json_response(order);
}

// POST /orders/items: adds a new order item to an existing order.
// The request has a JSON body, described by the add_order_item_request struct.
asio::awaitable<http::response<http::string_body>> handle_add_order_item(const request_data& input)
{
    // Check that the request has the appropriate content type
    auto it = input.request.find("Content-Type");
    if (it == input.request.end() || it->value() != "application/json")
        co_return bad_request("Invalid Content-Type: expected 'application/json'");

    // Parse the request body
    auto req = parse_json<orders::add_order_item_request>(input.request.body());
    if (req.has_error())
        co_return bad_request("Invalid JSON body");

    // Invoke the database logic
    result<orders::order_with_items> res = co_await input.repo()
                                               .add_order_item(req->order_id, req->product_id, req->quantity);
    if (res.has_error())
        co_return response_from_db_error(res.error());

    // Return the response
    co_return json_response(*res);
}

// DELETE /orders/items?id={}: deletes an order item.
// The request has no body.
asio::awaitable<http::response<http::string_body>> handle_remove_order_item(const request_data& input)
{
    // Parse the query parameter
    auto params_it = input.target.params().find("id");
    if (params_it == input.target.params().end())
        co_return bad_request("Mandatory URL parameter 'id' not found");
    auto id = parse_id((*params_it).value);
    if (!id.has_value())
        co_return bad_request("URL parameter 'id' should be a valid integer");

    // Invoke the database logic
    result<orders::order_with_items> res = co_await input.repo().remove_order_item(*id);
    if (res.has_error())
        co_return response_from_db_error(res.error());

    // Return the response
    co_return json_response(*res);
}

// POST /orders/checkout?id={}: checks out an order.
// The request has no body.
asio::awaitable<http::response<http::string_body>> handle_checkout_order(const request_data& input)
{
    // Parse the query parameter
    auto params_it = input.target.params().find("id");
    if (params_it == input.target.params().end())
        co_return bad_request("Mandatory URL parameter 'id' not found");
    auto id = parse_id((*params_it).value);
    if (!id.has_value())
        co_return bad_request("URL parameter 'id' should be a valid integer");

    // Invoke the database logic
    result<orders::order_with_items> res = co_await input.repo().checkout_order(*id);
    if (res.has_error())
        co_return response_from_db_error(res.error());

    // Return the response
    co_return json_response(*res);
}

// POST /orders/complete?id={}: marks an order as completed.
// The request has no body.
asio::awaitable<http::response<http::string_body>> handle_complete_order(const request_data& input)
{
    // Parse the query parameter
    auto params_it = input.target.params().find("id");
    if (params_it == input.target.params().end())
        co_return bad_request("Mandatory URL parameter 'id' not found");
    auto id = parse_id((*params_it).value);
    if (!id.has_value())
        co_return bad_request("URL parameter 'id' should be a valid integer");

    // Invoke the database logic
    result<orders::order_with_items> res = co_await input.repo().complete_order(*id);
    if (res.has_error())
        co_return response_from_db_error(res.error());

    // Return the response
    co_return json_response(*res);
}

// handle_request uses a table to dispatch to each endpoint.
// This is the table's element type.
struct http_endpoint
{
    // The HTTP method associated to this endpoint.
    http::verb method;

    // The endpoint handler.
    asio::awaitable<http::response<http::string_body>> (*handler)(const request_data&);
};

// Maps from a URL path to an endpoint handler.
// A URL path might be present more than once, for different methods.
const std::unordered_multimap<std::string_view, http_endpoint> endpoint_table{
    {"/products",        {http::verb::get, &handle_get_products}         },
    {"/orders",          {http::verb::get, &handle_get_orders}           },
    {"/orders",          {http::verb::post, &handle_create_order}        },
    {"/orders/items",    {http::verb::post, &handle_add_order_item}      },
    {"/orders/items",    {http::verb::delete_, &handle_remove_order_item}},
    {"/orders/checkout", {http::verb::post, &handle_checkout_order}      },
    {"/orders/complete", {http::verb::post, &handle_complete_order}      },
};

}  // namespace

// External interface
asio::awaitable<http::response<http::string_body>> orders::handle_request(
    const http::request<http::string_body>& request,
    mysql::connection_pool& pool
)
{
    // Parse the request target
    auto target = boost::urls::parse_origin_form(request.target());
    if (!target.has_value())
        co_return bad_request("Invalid request target");

    // Try to find an endpoint
    auto [it1, it2] = endpoint_table.equal_range(target->path());
    if (it1 == endpoint_table.end())
        co_return error_response(http::status::not_found, "The requested endpoint does not exist");

    // Match the verb. The table structure that we created
    // allows us to distinguish between an "endpoint does not exist" error
    // and an "unsupported method" error.
    auto it3 = std::find_if(it1, it2, [&request](const std::pair<std::string_view, http_endpoint>& ep) {
        return ep.second.method == request.method();
    });
    if (it3 == it2)
        co_return error_response(http::status::method_not_allowed, "Unsupported HTTP method");

    // Invoke the handler
    try
    {
        // Attempt to handle the request
        co_return co_await it3->second.handler(request_data{request, *target, pool});
    }
    catch (const 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
        log_mysql_error(err.code(), err.get_diagnostics());

        // Never disclose error info to a potential attacker
        co_return internal_server_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.
        {
            auto guard = orders::lock_cerr();
            std::cerr << "Uncaught exception: " << err.what() << std::endl;
        }
        co_return internal_server_error();
    }
}
//
// File: server.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/awaitable.hpp>

namespace orders {

// Launches a HTTP server that will listen on 0.0.0.0:port.
// If the server fails to launch (e.g. because the port is already in use),
// throws an exception. The server runs until the underlying execution
// context is stopped.
boost::asio::awaitable<void> run_server(boost::mysql::connection_pool& pool, unsigned short port);

}  // namespace orders
//
// File: server.cpp
//
// This file contains all the boilerplate code to implement a HTTP
// server. Functions here end up invoking handle_request.

#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/error_code.hpp>

#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/parser.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp>

#include <cstdlib>
#include <exception>
#include <iostream>
#include <string_view>

#include "error.hpp"
#include "handle_request.hpp"
#include "server.hpp"

namespace asio = boost::asio;
namespace http = boost::beast::http;
namespace mysql = boost::mysql;

namespace {

// Runs a single HTTP session until the client closes the connection.
// This coroutine will be spawned on a strand, to prevent data races.
asio::awaitable<void> run_http_session(asio::ip::tcp::socket sock, mysql::connection_pool& pool)
{
    using namespace std::chrono_literals;

    boost::system::error_code ec;

    // A buffer to read incoming client requests
    boost::beast::flat_buffer buff;

    // A timer, to use with asio::cancel_after to implement timeouts.
    // Re-using the same timer multiple times with cancel_after
    // is more efficient than using raw cancel_after,
    // since the timer doesn't need to be re-created for every operation.
    asio::steady_timer timer(co_await asio::this_coro::executor);

    // A HTTP session might involve more than one message if
    // keep-alive semantics are used. Loop until the connection closes.
    while (true)
    {
        // Construct a new parser for each message
        http::request_parser<http::string_body> parser;

        // Apply a reasonable limit to the allowed size
        // of the body in bytes to prevent abuse.
        parser.body_limit(10000);

        // Read a request. redirect_error prevents exceptions from being thrown
        // on error. We use cancel_after to set a timeout for the overall read operation.
        co_await http::async_read(
            sock,
            buff,
            parser.get(),
            asio::cancel_after(timer, 60s, asio::redirect_error(ec))
        );

        if (ec)
        {
            if (ec == http::error::end_of_stream)
            {
                // This means they closed the connection
                sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
            }
            else
            {
                // An unknown error happened
                orders::log_error("Error reading HTTP request: ", ec);
            }
            co_return;
        }

        const auto& request = parser.get();

        // Process the request to generate a response.
        // This invokes the business logic, which will need to access MySQL data.
        // Apply a timeout to the overall request handling process.
        auto response = co_await asio::co_spawn(
            // Use the same executor as this coroutine (it will be a strand)
            co_await asio::this_coro::executor,

            // The logic to invoke
            [&] { return orders::handle_request(request, pool); },

            // Completion token. Returns an object that can be co_await'ed
            asio::cancel_after(timer, 30s)
        );

        // Adjust the response, setting fields common to all responses
        bool keep_alive = response.keep_alive();
        response.version(request.version());
        response.keep_alive(keep_alive);
        response.prepare_payload();

        // Send the response
        co_await http::async_write(sock, response, asio::cancel_after(timer, 60s, asio::redirect_error(ec)));
        if (ec)
        {
            orders::log_error("Error writing HTTP response: ", ec);
            co_return;
        }

        // This means we should close the connection, usually because
        // the response indicated the "Connection: close" semantic.
        if (!keep_alive)
        {
            sock.shutdown(asio::ip::tcp::socket::shutdown_send, ec);
            co_return;
        }
    }
}

}  // namespace

asio::awaitable<void> orders::run_server(mysql::connection_pool& pool, unsigned short port)
{
    // An object that allows us to accept incoming TCP connections
    asio::ip::tcp::acceptor acc(co_await asio::this_coro::executor);

    // The endpoint where the server will listen. Edit this if you want to
    // change the address or port we bind to.
    asio::ip::tcp::endpoint listening_endpoint(asio::ip::make_address("0.0.0.0"), port);

    // Open the acceptor
    acc.open(listening_endpoint.protocol());

    // Allow address reuse
    acc.set_option(asio::socket_base::reuse_address(true));

    // Bind to the server address
    acc.bind(listening_endpoint);

    // Start listening for connections
    acc.listen(asio::socket_base::max_listen_connections);

    std::cout << "Server listening at " << acc.local_endpoint() << std::endl;

    // Start the acceptor loop
    while (true)
    {
        // Accept a new connection
        asio::ip::tcp::socket sock = co_await acc.async_accept();

        // Function implementing our session logic.
        // Takes ownership of the socket.
        // Having this as a named variable workarounds a gcc bug
        // (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107288)
        auto session_logic = [&pool, socket = std::move(sock)]() mutable {
            return run_http_session(std::move(socket), pool);
        };

        // Launch a new session for this connection. Each session gets its
        // own coroutine, so we can get back to listening for new connections.
        asio::co_spawn(
            // Every session gets its own strand. This prevents data races.
            asio::make_strand(co_await asio::this_coro::executor),

            // The actual coroutine
            std::move(session_logic),

            // Callback to run when the coroutine finishes
            [](std::exception_ptr ptr) {
                if (ptr)
                {
                    // For extra safety, log the exception but don't propagate it.
                    // If we failed to anticipate an error condition that ends up raising an exception,
                    // terminate only the affected session, instead of crashing the server.
                    try
                    {
                        std::rethrow_exception(ptr);
                    }
                    catch (const std::exception& exc)
                    {
                        auto guard = lock_cerr();
                        std::cerr << "Uncaught error in a session: " << exc.what() << std::endl;
                    }
                }
            }
        );
    }
}

PrevUpHomeNext