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

PrevUpHomeNext

A REST API server that uses connection pooling

This example assumes you have gone through the setup.

/**
 * This example demonstrates how to use a connection_pool.
 * It implements a minimal REST API to manage notes.
 * A note is a simple object containing a user-defined title and content.
 * The REST API offers CRUD operations on such objects:
 *    POST   /notes        Creates a new note.
 *    GET    /notes        Retrieves all notes.
 *    GET    /notes/<id>   Retrieves a single note.
 *    PUT    /notes/<id>   Replaces a note, changing its title and content.
 *    DELETE /notes/<id>   Deletes a note.
 *
 * Notes are stored in MySQL. The note_repository class encapsulates
 *   access to MySQL, offering friendly functions to manipulate notes.
 * server.cpp encapsulates all the boilerplate to launch an HTTP server,
 *   match URLs to API endpoints, and invoke the relevant note_repository functions.
 * All communication happens asynchronously. We use stackful coroutines to simplify
 *   development, using boost::asio::spawn and boost::asio::yield_context.
 * This example requires linking to Boost::context, Boost::json and Boost::url.
 */

#include <boost/mysql/any_address.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/pool_params.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 <iostream>
#include <memory>
#include <string>

#include "server.hpp"

using namespace notes;

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

int main(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
    boost::asio::thread_pool th_pool(num_threads - 1);

    // Configuration for the connection pool
    boost::mysql::pool_params pool_prms{
        // Connect using TCP, to the given hostname and using the default port
        boost::mysql::host_and_port{mysql_hostname},

        // Authenticate using the given username
        mysql_username,

        // Password for the above username
        mysql_password,

        // Database to use when connecting
        "boost_mysql_examples",
    };

    // 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.
    pool_prms.thread_safe = true;

    // Create the connection pool
    auto shared_st = std::make_shared<shared_state>(
        boost::mysql::connection_pool(th_pool, std::move(pool_prms))
    );

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

    // Launch the MySQL pool
    shared_st->pool.async_run(boost::asio::detached);

    // Start listening for HTTP connections. This will run until the context is stopped
    auto ec = launch_server(th_pool.get_executor(), shared_st, port);
    if (ec)
    {
        log_error("Error launching server: ", ec);
        exit(EXIT_FAILURE);
    }

    // Capture SIGINT and SIGTERM to perform a clean shutdown
    signals.async_wait([shared_st, &th_pool](boost::system::error_code, int) {
        // Cancel the pool. This will cause async_run to complete.
        shared_st->pool.cancel();

        // Stop the execution context. This will cause main to exit
        th_pool.stop();
    });

    // 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;
}
//
// File: types.hpp
//

#include <boost/core/span.hpp>
#include <boost/describe/class.hpp>
#include <boost/optional/optional.hpp>

#include <string>
#include <vector>

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

namespace notes {

struct note_t
{
    // The unique database ID of the object.
    std::int64_t id;

    // The note's title.
    std::string title;

    // The note's actual content.
    std::string content;
};
BOOST_DESCRIBE_STRUCT(note_t, (), (id, title, content))

//
// REST API requests.
//

// Used for creating and replacing notes
struct note_request_body
{
    // The title that the new note should have.
    std::string title;

    // The content that the new note should have.
    std::string content;
};
BOOST_DESCRIBE_STRUCT(note_request_body, (), (title, content))

//
// REST API responses.
//

// Used by endpoints returning several notes (like GET /notes).
struct multi_notes_response
{
    // The retrieved notes.
    std::vector<note_t> notes;
};
BOOST_DESCRIBE_STRUCT(multi_notes_response, (), (notes))

// Used by endpoints returning a single note (like GET /notes/<id>)
struct single_note_response
{
    // The retrieved note.
    note_t note;
};
BOOST_DESCRIBE_STRUCT(single_note_response, (), (note))

// Used by DELETE /notes/<id>
struct delete_note_response
{
    // true if the note was found and deleted, false if the note didn't exist.
    bool deleted;
};
BOOST_DESCRIBE_STRUCT(delete_note_response, (), (deleted))

}  // namespace notes
//
// File: repository.hpp
//

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

#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/optional/optional.hpp>

#include <cstdint>

#include "types.hpp"

namespace notes {

using boost::optional;
using boost::mysql::string_view;

// A lightweight wrapper around a connection_pool that allows
// creating, updating, retrieving and deleting notes in MySQL.
// This class encapsulates the database logic.
// All operations are async, and use stackful coroutines (boost::asio::yield_context).
// If the database can't be contacted, or unexpected database errors are found,
// an exception of type boost::mysql::error_with_diagnostics is thrown.
class note_repository
{
    boost::mysql::connection_pool& pool_;

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

    // Retrieves all notes present in the database
    std::vector<note_t> get_notes(boost::asio::yield_context yield);

    // Retrieves a single note by ID. Returns an empty optional
    // if no note with the given ID is present in the database.
    optional<note_t> get_note(std::int64_t note_id, boost::asio::yield_context yield);

    // Creates a new note in the database with the given components.
    // Returns the newly created note, including the newly allocated ID.
    note_t create_note(string_view title, string_view content, boost::asio::yield_context yield);

    // Replaces the note identified by note_id, setting its components to the
    // ones passed. Returns the updated note. If no note with ID matching
    // note_id can be found, an empty optional is returned.
    optional<note_t> replace_note(
        std::int64_t note_id,
        string_view title,
        string_view content,
        boost::asio::yield_context yield
    );

    // Deletes the note identified by note_id. Returns true if
    // a matching note was deleted, false otherwise.
    bool delete_note(std::int64_t note_id, boost::asio::yield_context yield);
};

}  // namespace notes
//
// File: repository.cpp
//

#include <boost/mysql/statement.hpp>
#include <boost/mysql/static_results.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_diagnostics.hpp>
#include <boost/mysql/with_params.hpp>

#include <iterator>
#include <tuple>
#include <utility>

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

using namespace notes;
namespace mysql = boost::mysql;
using mysql::with_diagnostics;

// SQL code to create the notes table is located under $REPO_ROOT/example/db_setup.sql
// The table looks like this:
//
// CREATE TABLE notes(
//     id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
//     title TEXT NOT NULL,
//     content TEXT NOT NULL
// );

std::vector<note_t> note_repository::get_notes(boost::asio::yield_context yield)
{
    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    // with_diagnostics ensures that thrown exceptions include diagnostic information
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // Execute the query to retrieve all notes. We use the static interface to
    // parse results directly into static_results.
    mysql::static_results<note_t> result;
    conn->async_execute("SELECT id, title, content FROM notes", result, with_diagnostics(yield));

    // 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.
    conn.return_without_reset();

    // Move note_t objects into the result vector to save allocations
    return std::vector<note_t>(
        std::make_move_iterator(result.rows().begin()),
        std::make_move_iterator(result.rows().end())
    );

    // If an exception is thrown, pooled_connection's destructor will
    // return the connection automatically to the pool.
}

optional<note_t> note_repository::get_note(std::int64_t note_id, boost::asio::yield_context yield)
{
    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // When executed, with_params expands a query client-side before sending it to the server.
    // Placeholders are marked with {}
    mysql::static_results<note_t> result;
    conn->async_execute(
        mysql::with_params("SELECT id, title, content FROM notes WHERE id = {}", note_id),
        result,
        with_diagnostics(yield)
    );

    // We did nothing to mutate session state, so we can skip reset
    conn.return_without_reset();

    // An empty results object indicates that no note was found
    if (result.rows().empty())
        return {};
    else
        return std::move(result.rows()[0]);
}

note_t note_repository::create_note(string_view title, string_view content, boost::asio::yield_context yield)
{
    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // We will use statements in this function for the sake of example.
    // We don't need to deallocate the statement explicitly,
    // since the pool takes care of it after the connection is returned.
    // You can also use with_params instead of statements.
    mysql::statement stmt = conn->async_prepare_statement(
        "INSERT INTO notes (title, content) VALUES (?, ?)",
        with_diagnostics(yield)
    );

    // Execute the statement. The statement won't produce any rows,
    // so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> result;
    conn->async_execute(stmt.bind(title, content), result, with_diagnostics(yield));

    // MySQL reports last_insert_id as a uint64_t regardless of the actual ID type.
    // Given our table definition, this cast is safe
    auto new_id = static_cast<std::int64_t>(result.last_insert_id());

    return note_t{new_id, title, content};

    // There's no need to return the connection explicitly to the pool,
    // pooled_connection's destructor takes care of it.
}

optional<note_t> note_repository::replace_note(
    std::int64_t note_id,
    string_view title,
    string_view content,
    boost::asio::yield_context yield
)
{
    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // Expand and execute the query.
    // It won't produce any rows, so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> empty_result;
    conn->async_execute(
        mysql::with_params(
            "UPDATE notes SET title = {}, content = {} WHERE id = {}",
            title,
            content,
            note_id
        ),
        empty_result,
        with_diagnostics(yield)
    );

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

    // No affected rows means that the note doesn't exist
    if (empty_result.affected_rows() == 0u)
        return {};

    return note_t{note_id, title, content};
}

bool note_repository::delete_note(std::int64_t note_id, boost::asio::yield_context yield)
{
    // Get a fresh connection from the pool. This returns a pooled_connection object,
    // which is a proxy to an any_connection object. Connections are returned to the
    // pool when the proxy object is destroyed.
    mysql::pooled_connection conn = pool_.async_get_connection(with_diagnostics(yield));

    // Expand and execute the query.
    // It won't produce any rows, so we can use static_results<std::tuple<>>
    mysql::static_results<std::tuple<>> empty_result;
    conn->async_execute(
        mysql::with_params("DELETE FROM notes WHERE id = {}", note_id),
        empty_result,
        with_diagnostics(yield)
    );

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

    // No affected rows means that the note didn't exist
    return empty_result.affected_rows() != 0u;
}
//
// File: handle_request.hpp
//

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

#include "repository.hpp"

namespace notes {

// Handles an individual HTTP request, producing a response.
boost::beast::http::response<boost::beast::http::string_body> handle_request(
    const boost::beast::http::request<boost::beast::http::string_body>& request,
    note_repository repo,
    boost::asio::yield_context yield
);

}  // namespace notes
//
// File: handle_request.cpp
//

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

#include <boost/asio/cancel_after.hpp>
#include <boost/asio/spawn.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/optional/optional.hpp>
#include <boost/url/parse.hpp>
#include <boost/variant2/variant.hpp>

#include <chrono>
#include <cstdint>
#include <exception>
#include <string>

#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<std::int64_t> 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<http::string_body>
    const http::request<http::string_body>& request_;

    // The repository to access MySQL
    note_repository repo_;

    // Creates an error response
    http::response<http::string_body> error_response(http::status code, string_view msg) const
    {
        http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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<http::string_body> 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/<id> or PUT /note/<id>)
    // but the note doesn't exist
    http::response<http::string_body> 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 <class T>
    http::response<http::string_body> json_response(const T& body) const
    {
        http::response<http::string_body> 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 <class T>
    boost::system::result<T> 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<T>(val);
    }

    http::response<http::string_body> 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<note_request_body>();
                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/<note-id>. 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/<note-id>/<something-else> is not a valid endpoint
            if (segit != segs.end())
                return endpoint_not_found();

            if (request_.method() == http::verb::get)
            {
                // GET /notes/<note-id>: 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/<note-id>: 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<note_request_body>();
                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/<note-id>: 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<http::string_body>& req, note_repository repo)
        : request_(req), repo_(repo)
    {
    }

    // Generates a response for the request passed to the constructor
    http::response<http::string_body> handle_request(boost::asio::yield_context yield)
    {
        try
        {
            // Attempt to handle the request. We use cancel_after to set
            // a timeout to the overall operation
            return asio::spawn(
                yield.get_executor(),
                [this](asio::yield_context yield2) { return handle_request_impl(yield2); },
                asio::cancel_after(std::chrono::seconds(30), 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
            log_error(
                "Uncaught exception: ",
                err.what(),
                "\nServer diagnostics: ",
                err.get_diagnostics().server_message()
            );
            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.
            log_error("Uncaught exception: ", err.what());
            return error_response(http::status::internal_server_error, "Internal error");
        }
    }
};

}  // namespace

// External interface
boost::beast::http::response<boost::beast::http::string_body> notes::handle_request(
    const boost::beast::http::request<boost::beast::http::string_body>& request,
    note_repository repo,
    boost::asio::yield_context yield
)
{
    return request_handler(request, repo).handle_request(yield);
}
//
// File: server.hpp
//

#include <boost/mysql/connection_pool.hpp>

#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/system/error_code.hpp>

#include <memory>

namespace notes {

// State shared by all sessions created by our server.
// For this application, we only need a connection_pool object.
// Place here any other singleton objects your application may need.
// We will use std::shared_ptr<shared_state> to ensure that objects
// are kept alive until all sessions are terminated.
struct shared_state
{
    boost::mysql::connection_pool pool;

    shared_state(boost::mysql::connection_pool pool) : pool(std::move(pool)) {}
};

// 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 aleady in use),
// returns a non-zero error code. ex should identify the io_context or thread_pool
// where the server should run. The server is run until the underlying execution
// context is stopped.
boost::system::error_code launch_server(
    boost::asio::any_io_executor ex,
    std::shared_ptr<shared_state> state,
    unsigned short port
);

}  // namespace notes
//
// File: server.cpp
//

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

#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/strand.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/write.hpp>

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

#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. Functions here end up invoking handle_request.

namespace asio = boost::asio;
namespace http = boost::beast::http;
using boost::mysql::error_code;
using namespace notes;

namespace {

static void run_http_session(
    boost::asio::ip::tcp::socket sock,
    std::shared_ptr<shared_state> st,
    boost::asio::yield_context yield
)
{
    error_code ec;

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

    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
        http::async_read(sock, buff, parser.get(), yield[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
                log_error("Error reading HTTP request: ", ec);
            }
            return;
        }

        // Process the request to generate a response.
        // This invokes the business logic, which will need to access MySQL data
        auto response = handle_request(parser.get(), note_repository(st->pool), yield);

        // Determine if we should close the connection
        bool keep_alive = response.keep_alive();

        // Send the response
        http::async_write(sock, response, yield[ec]);
        if (ec)
            return log_error("Error writing HTTP response: ", ec);

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

// Implements the server's accept loop. The server will
// listen for connections until stopped.
static void do_accept(
    asio::any_io_executor executor,  // The original executor (without strands)
    std::shared_ptr<asio::ip::tcp::acceptor> acceptor,
    std::shared_ptr<shared_state> st
)
{
    acceptor->async_accept([executor, st, acceptor](error_code ec, asio::ip::tcp::socket sock) {
        // If there was an error accepting the connection, exit our loop
        if (ec)
            return log_error("Error while accepting connection", ec);

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

            // The actual coroutine
            [st, socket = std::move(sock)](boost::asio::yield_context yield) mutable {
                run_http_session(std::move(socket), std::move(st), yield);
            },

            // All errors in the session are handled via error codes or by catching
            // exceptions explicitly. An unhandled exception here means an error.
            // Rethrowing it will propagate the exception, making io_context::run()
            // to throw and terminate the program.
            [](std::exception_ptr ex) {
                if (ex)
                    std::rethrow_exception(ex);
            }
        );

        // Accept a new connection
        do_accept(executor, acceptor, st);
    });
}

}  // namespace

error_code notes::launch_server(
    boost::asio::any_io_executor ex,
    std::shared_ptr<shared_state> st,
    unsigned short port
)
{
    error_code ec;

    // An object that allows us to accept incoming TCP connections.
    // Since we're in a multi-threaded environment, we create a strand for the acceptor,
    // so all accept handlers are run serialized
    auto acceptor = std::make_shared<asio::ip::tcp::acceptor>(asio::make_strand(ex));

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

    // Open the acceptor
    acceptor->open(listening_endpoint.protocol(), ec);
    if (ec)
        return ec;

    // Allow address reuse
    acceptor->set_option(asio::socket_base::reuse_address(true), ec);
    if (ec)
        return ec;

    // Bind to the server address
    acceptor->bind(listening_endpoint, ec);
    if (ec)
        return ec;

    // Start listening for connections
    acceptor->listen(asio::socket_base::max_listen_connections, ec);
    if (ec)
        return ec;

    std::cout << "Server listening at " << acceptor->local_endpoint() << std::endl;

    // Launch the acceptor loop
    do_accept(std::move(ex), std::move(acceptor), std::move(st));

    // Done
    return error_code();
}
//
// File: log_error.hpp
//

#include <iostream>
#include <mutex>

// Helper function to safely write diagnostics to std::cerr.
// Since we're in a multi-threaded environment, directly writing to std::cerr
// can lead to interleaved output, so we should synchronize calls with a mutex.
// This function is only called in rare cases (e.g. unhandled exceptions),
// so we can afford the synchronization overhead.

namespace notes {

// If you're in C++17+, you can write this using fold expressions
// instead of recursion.
inline void log_error_impl() {}

template <class Arg1, class... Tail>
void log_error_impl(const Arg1& arg, const Tail&... tail)
{
    std::cerr << arg;
    log_error_impl(tail...);
}

template <class... Args>
void log_error(const Args&... args)
{
    static std::mutex mtx;

    // Acquire the mutex, then write the passed arguments to std::cerr.
    std::unique_lock<std::mutex> lock(mtx);
    log_error_impl(args...);
    std::cerr << std::endl;
}

}  // namespace notes

PrevUpHomeNext