Last active
June 18, 2018 00:23
-
-
Save DragonOsman/7bbdb049ea2a77173c5dafbecd5f1be2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Osman Zakir | |
| // 3 / 16 / 2018 | |
| // Currency Converter Web Application | |
| // This application uses a Google Map as its UI. The user's geolocation is taken if the browser has permission, and an info window is opened at that location | |
| // which displays a HTML form. The user can then enter the money amount to convert into a text box that accepts only numeric input, | |
| // and choose the currency to convert to from a drop down menu | |
| // (note: with the current currency API plan, changing the base currency (USD by default) isn't allowed). | |
| #include <boost/beast/core.hpp> | |
| #include <boost/beast/http.hpp> | |
| #include <boost/beast/version.hpp> | |
| #include <boost/asio/ip/tcp.hpp> | |
| #include <boost/asio/connect.hpp> | |
| #include <cstdlib> | |
| #include <map> | |
| #include <cctype> | |
| #include <iostream> | |
| #include <memory> | |
| #include <string> | |
| #include <thread> | |
| #include "json.hpp" | |
| #include <jinja2cpp/template.h> | |
| #include <jinja2cpp/value.h> | |
| #include <jinja2cpp/template_env.h.h> | |
| using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp> | |
| using nlohmann::json; | |
| namespace http = boost::beast::http; // from <boost/beast/http.hpp> | |
| //------------------------------------------------------------------------------ | |
| // Return a reasonable mime type based on the extension of a file. | |
| boost::beast::string_view mime_type(boost::beast::string_view path); | |
| // This class represents a cache for storing results from the | |
| // currency exchange API used by currencylayer.com | |
| class cache_storage | |
| { | |
| public: | |
| cache_storage(const std::chrono::seconds &duration) | |
| : m_cache{}, m_duration{ duration } | |
| { | |
| } | |
| // This function queries the currency API after making sure | |
| // that the stored result(s) is/are old enough | |
| // It also makes a new query to the API if needed | |
| const json &query(const std::string &query_data, const char *accesskey); | |
| private: | |
| std::map<std::string, std::pair<std::chrono::time_point<std::chrono::steady_clock>, json>> m_cache; | |
| std::chrono::seconds m_duration; | |
| }; | |
| // Parse POST body | |
| std::map<std::string, std::string> parse(const std::string &data); | |
| // Perform currency conversion | |
| double convert(const std::string &to_currency, const double money_amount, const char *accesskey); | |
| // Append an HTTP rel-path to a local filesystem path. | |
| // The returned path is normalized for the platform. | |
| std::string path_cat(boost::beast::string_view base, boost::beast::string_view path); | |
| // This function produces an HTTP response for the given | |
| // request. The type of the response object depends on the | |
| // contents of the request, so the interface requires the | |
| // caller to pass a generic lambda for receiving the response. | |
| template<class Body, class Allocator, class Send> | |
| void handle_request(boost::beast::string_view doc_root, http::request<Body, http::basic_fields<Allocator>>&& req, | |
| Send&& send, const char *accesskey, const char *apikey); | |
| //------------------------------------------------------------------------------ | |
| // Report a failure | |
| void fail(boost::system::error_code ec, char const* what); | |
| // This is the C++11 equivalent of a generic lambda. | |
| // The function object is used to send an HTTP message. | |
| template<class Stream> | |
| struct send_lambda | |
| { | |
| Stream& stream_; | |
| bool& close_; | |
| boost::system::error_code& ec_; | |
| explicit | |
| send_lambda(Stream& stream, bool& close, boost::system::error_code& ec) | |
| : stream_{ stream }, close_{ close }, ec_{ ec } | |
| { | |
| } | |
| template<bool isRequest, class Body, class Fields> | |
| void operator()(http::message<isRequest, Body, Fields>&& msg) const; | |
| }; | |
| // Handles an HTTP server connection | |
| void do_session(tcp::socket socket, std::string const& doc_root, const char *accesskey, const char *apikey); | |
| //------------------------------------------------------------------------------ | |
| int main(int argc, char* argv[]) | |
| { | |
| try | |
| { | |
| // Check command line arguments. | |
| if (argc != 4) | |
| { | |
| std::cerr << | |
| "Usage: currency_converter <address> <port> <doc_root>\n" << | |
| "Example:\n" << | |
| " currency_converter 0.0.0.0 8080 .\n"; | |
| return EXIT_FAILURE; | |
| } | |
| auto const address = boost::asio::ip::make_address(argv[1]); | |
| auto const port = static_cast<unsigned short>(std::atoi(argv[2])); | |
| std::string const doc_root = argv[3]; | |
| // The io_context is required for all I/O | |
| boost::asio::io_context ioc{ 1 }; | |
| // Google API Key | |
| char *apikey = std::getenv("apikey"); | |
| // Access key for currencylayer.com's currency exchange API | |
| char *accesskey = std::getenv("accesskey"); | |
| // The acceptor receives incoming connections | |
| tcp::acceptor acceptor{ ioc, { address, port } }; | |
| for (;;) | |
| { | |
| // This will receive the new connection | |
| tcp::socket socket{ ioc }; | |
| // Block until we get a connection | |
| acceptor.accept(socket); | |
| // Launch the session, transferring ownership of the socket | |
| std::thread([=, socket = std::move(socket)]() mutable { | |
| do_session(std::move(socket), doc_root, accesskey, apikey); | |
| }).detach(); | |
| } | |
| } | |
| catch (const std::runtime_error &e) | |
| { | |
| std::cerr << "Line 146: Error: " << e.what() << '\n'; | |
| return EXIT_FAILURE; | |
| } | |
| catch (const std::exception &e) | |
| { | |
| std::cerr << "Line 151: Error: " << e.what() << '\n'; | |
| return EXIT_FAILURE + 1; | |
| } | |
| } | |
| boost::beast::string_view mime_type(boost::beast::string_view path) | |
| { | |
| using boost::beast::iequals; | |
| const auto ext = [&path] | |
| { | |
| auto const pos = path.rfind("."); | |
| if (pos == boost::beast::string_view::npos) | |
| { | |
| return boost::beast::string_view{}; | |
| } | |
| return path.substr(pos); | |
| }(); | |
| if (iequals(ext, ".htm")) | |
| { | |
| return "text/html"; | |
| } | |
| if (iequals(ext, ".html")) | |
| { | |
| return "text/html"; | |
| } | |
| if (iequals(ext, ".php")) | |
| { | |
| return "text/html"; | |
| } | |
| if (iequals(ext, ".css")) | |
| { | |
| return "text/css"; | |
| } | |
| if (iequals(ext, ".txt")) | |
| { | |
| return "text/plain"; | |
| } | |
| if (iequals(ext, ".js")) | |
| { | |
| return "application/javascript"; | |
| } | |
| if (iequals(ext, ".json")) | |
| { | |
| return "application/json"; | |
| } | |
| if (iequals(ext, ".xml")) | |
| { | |
| return "application/xml"; | |
| } | |
| if (iequals(ext, ".swf")) | |
| { | |
| return "application/x-shockwave-flash"; | |
| } | |
| if (iequals(ext, ".flv")) | |
| { | |
| return "video/x-flv"; | |
| } | |
| if (iequals(ext, ".png")) | |
| { | |
| return "image/png"; | |
| } | |
| if (iequals(ext, ".jpe")) | |
| { | |
| return "image/jpeg"; | |
| } | |
| if (iequals(ext, ".jpeg")) | |
| { | |
| return "image/jpeg"; | |
| } | |
| if (iequals(ext, ".jpg")) | |
| { | |
| return "image/jpeg"; | |
| } | |
| if (iequals(ext, ".gif")) | |
| { | |
| return "image/gif"; | |
| } | |
| if (iequals(ext, ".bmp")) | |
| { | |
| return "image/bmp"; | |
| } | |
| if (iequals(ext, ".ico")) | |
| { | |
| return "image/vnd.microsoft.icon"; | |
| } | |
| if (iequals(ext, ".tiff")) | |
| { | |
| return "image/tiff"; | |
| } | |
| if (iequals(ext, ".tif")) | |
| { | |
| return "image/tiff"; | |
| } | |
| if (iequals(ext, ".svg")) | |
| { | |
| return "image/svg+xml"; | |
| } | |
| if (iequals(ext, ".svgz")) | |
| { | |
| return "image/svg+xml"; | |
| } | |
| return "application/text"; | |
| } | |
| std::string path_cat(boost::beast::string_view base, boost::beast::string_view path) | |
| { | |
| if (base.empty()) | |
| { | |
| return path.to_string(); | |
| } | |
| std::string result = base.to_string(); | |
| #if BOOST_MSVC | |
| char constexpr path_separator = '\\'; | |
| if (result.back() == path_separator) | |
| { | |
| result.resize(result.size() - 1); | |
| } | |
| result.append(path.data(), path.size()); | |
| for (auto& c : result) | |
| { | |
| if (c == '/') | |
| { | |
| c = path_separator; | |
| } | |
| } | |
| #else | |
| char constexpr path_separator = '/'; | |
| if (result.back() == path_separator) | |
| { | |
| result.resize(result.size() - 1); | |
| } | |
| result.append(path.data(), path.size()); | |
| #endif | |
| return result; | |
| } | |
| std::map<std::string, std::string> parse(const std::string &data) | |
| { | |
| enum class States | |
| { | |
| Start, | |
| Name, | |
| Ignore, | |
| Value | |
| }; | |
| std::map<std::string, std::string> parsed_values; | |
| std::string name; | |
| std::string value; | |
| States state = States::Start; | |
| for (char c : data) | |
| { | |
| switch (state) | |
| { | |
| case States::Start: | |
| if (c == '"') | |
| { | |
| state = States::Name; | |
| } | |
| break; | |
| case States::Name: | |
| if (c != '"') | |
| { | |
| name += c; | |
| } | |
| else | |
| { | |
| state = States::Ignore; | |
| } | |
| break; | |
| case States::Ignore: | |
| if (!isspace(c)) | |
| { | |
| state = States::Value; | |
| value += c; | |
| } | |
| break; | |
| case States::Value: | |
| if (c != '\n') | |
| { | |
| value += c; | |
| } | |
| else | |
| { | |
| parsed_values.insert(std::make_pair(name, value)); | |
| name = ""; | |
| value = ""; | |
| state = States::Start; | |
| } | |
| break; | |
| } | |
| } | |
| return parsed_values; | |
| } | |
| template<class Body, class Allocator, class Send> | |
| void handle_request(boost::beast::string_view doc_root, http::request<Body, http::basic_fields<Allocator>>&& req, | |
| Send&& send, const char *accesskey, const char *apikey) | |
| { | |
| // Returns a bad request response | |
| const auto bad_request = [&req](boost::beast::string_view why) | |
| { | |
| http::response<http::string_body> res{ http::status::bad_request, req.version() }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, "text/html"); | |
| res.keep_alive(req.keep_alive()); | |
| res.body() = why.to_string(); | |
| res.prepare_payload(); | |
| return res; | |
| }; | |
| // Returns a not found response | |
| const auto not_found = [&req](boost::beast::string_view target) | |
| { | |
| http::response<http::string_body> res{ http::status::not_found, req.version() }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, "text/html"); | |
| res.keep_alive(req.keep_alive()); | |
| res.body() = "The resource '" + target.to_string() + "' was not found."; | |
| res.prepare_payload(); | |
| return res; | |
| }; | |
| // Returns a server error response | |
| const auto server_error = [&req](boost::beast::string_view what) | |
| { | |
| http::response<http::string_body> res{ http::status::internal_server_error, req.version() }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, "text/html"); | |
| res.keep_alive(req.keep_alive()); | |
| res.body() = "An error occurred: '" + what.to_string() + "'"; | |
| res.prepare_payload(); | |
| return res; | |
| }; | |
| // Make sure we can handle the method | |
| if (req.method() != http::verb::get && | |
| req.method() != http::verb::head && | |
| req.method() != http::verb::post) | |
| { | |
| return send(bad_request("Unknown HTTP-method")); | |
| } | |
| // Request path must be absolute and not contain "..". | |
| if (req.target().empty() || | |
| req.target()[0] != '/' || | |
| req.target().find("..") != boost::beast::string_view::npos) | |
| { | |
| return send(bad_request("Illegal request-target")); | |
| } | |
| // Build the path to the requested file | |
| std::string path; | |
| if (req.target() != "/?q=accesskey") | |
| { | |
| path = path_cat(doc_root, req.target()); | |
| if (req.target().back() == '/') | |
| { | |
| path.append("index.html"); | |
| } | |
| } | |
| // Attempt to open the file | |
| boost::beast::error_code ec; | |
| http::file_body::value_type body; | |
| if (req.target() != "/?q=accesskey" && req.target() != "/") | |
| { | |
| body.open(path.c_str(), boost::beast::file_mode::scan, ec); | |
| } | |
| // Handle the case where the file doesn't exist | |
| if (ec == boost::system::errc::no_such_file_or_directory) | |
| { | |
| return send(not_found(req.target())); | |
| } | |
| // Handle an unknown error | |
| if (ec) | |
| { | |
| return send(server_error(ec.message())); | |
| } | |
| // Respond to HEAD request | |
| if (req.method() == http::verb::head) | |
| { | |
| http::response<http::empty_body> res{ http::status::ok, req.version() }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, mime_type(path)); | |
| res.content_length(body.size()); | |
| res.keep_alive(req.keep_alive()); | |
| return send(std::move(res)); | |
| } | |
| // Respond to GET request | |
| else if (req.method() == http::verb::get) | |
| { | |
| if (req.target() == "/?q=accesskey") | |
| { | |
| http::response<http::string_body> res{ | |
| std::piecewise_construct, | |
| std::make_tuple(std::move(std::string{ accesskey })), | |
| std::make_tuple(http::status::ok, req.version()) }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, "plain/text"); | |
| res.content_length(res.body().size()); | |
| res.keep_alive(req.keep_alive()); | |
| return send(std::move(res)); | |
| } | |
| else if (req.target() == "/") | |
| { | |
| jinja2::Template tpl; | |
| tpl.LoadFromFile(path.c_str()); | |
| jinja2::ValuesMap params = { { "apikey", std::string(apikey) } }; | |
| http::response<http::string_body> res{ | |
| std::piecewise_construct, | |
| std::make_tuple(std::move(tpl.RenderAsString(params))), | |
| std::make_tuple(http::status::ok, req.version()) }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, "text/html"); | |
| res.set(http::field::content_length, tpl.RenderAsString(params).size()); | |
| res.keep_alive(req.keep_alive()); | |
| return send(std::move(res)); | |
| } | |
| else | |
| { | |
| http::response<http::file_body> res{ | |
| std::piecewise_construct, | |
| std::make_tuple(std::move(body)), | |
| std::make_tuple(http::status::ok, req.version()) }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, mime_type(path)); | |
| res.content_length(body.size()); | |
| res.keep_alive(req.keep_alive()); | |
| return send(std::move(res)); | |
| } | |
| } | |
| // Respond to POST request | |
| else if (req.method() == http::verb::post) | |
| { | |
| boost::beast::string_view content_type = req[http::field::content_type]; | |
| if (content_type.find("multipart/form-data") == std::string::npos && | |
| content_type.find("application/x-www-form-urlencoded") == std::string::npos) | |
| { | |
| return send(bad_request("Bad request")); | |
| } | |
| std::map<std::string, std::string> parsed_value = parse(req.body()); | |
| std::cout << "line 501: POST body example:\n" << req.body() << '\n'; | |
| std::cout << "Line 502: parsed_value map:\n"; | |
| for (const auto &x : parsed_value) | |
| { | |
| std::cout << x.first << ": " << x.second << '\n'; | |
| } | |
| double money_amount = std::stod(parsed_value["currency_amount"]); | |
| std::string to_currency = parsed_value["to_currency"]; | |
| std::cout << "Line 509: to_currency is: " << to_currency << '\n'; | |
| std::string currency_abbr = to_currency.substr(0, 3); | |
| double conversion_result = convert(currency_abbr, money_amount, accesskey); | |
| http::response<http::string_body> res{ | |
| std::piecewise_construct, | |
| std::make_tuple(std::move(std::to_string(conversion_result))), | |
| std::make_tuple(http::status::ok, req.version()) }; | |
| res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
| res.set(http::field::content_type, "text/plain"); | |
| res.content_length(std::to_string(conversion_result).size()); | |
| res.keep_alive(req.keep_alive()); | |
| std::cout << "Line 521: response to client is: " << res << '\n'; | |
| return send(std::move(res)); | |
| } | |
| } | |
| void fail(boost::system::error_code ec, char const* what) | |
| { | |
| std::cerr << what << ": " << ec.message() << "\n"; | |
| } | |
| template<class Stream> | |
| template<bool isRequest, class Body, class Fields> | |
| void send_lambda<Stream>::operator()(http::message<isRequest, Body, Fields>&& msg) const | |
| { | |
| // Determine if we should close the connection after | |
| close_ = msg.need_eof(); | |
| // We need the serializer here because the serializer requires | |
| // a non-const file_body, and the message oriented version of | |
| // http::write only works with const messages. | |
| http::serializer<isRequest, Body, Fields> sr{ msg }; | |
| http::write(stream_, sr, ec_); | |
| } | |
| void do_session(tcp::socket socket, std::string const& doc_root, const char *accesskey, const char *apikey) | |
| { | |
| bool close = false; | |
| boost::system::error_code ec; | |
| // This buffer is required to persist across reads | |
| boost::beast::flat_buffer buffer; | |
| // This lambda is used to send messages | |
| send_lambda<tcp::socket> lambda{ socket, close, ec }; | |
| for (;;) | |
| { | |
| // Read a request | |
| http::request<http::string_body> req; | |
| http::read(socket, buffer, req, ec); | |
| if (ec == http::error::end_of_stream) | |
| { | |
| break; | |
| } | |
| if (ec) | |
| { | |
| std::cerr << "Lines 567 and 568:\n"; | |
| return fail(ec, "read"); | |
| } | |
| // Send the response | |
| handle_request(doc_root, std::move(req), lambda, accesskey, apikey); | |
| if (ec) | |
| { | |
| std::cerr << "Lines 575 and 576:\n"; | |
| return fail(ec, "write"); | |
| } | |
| if (close) | |
| { | |
| // This means we should close the connection, usually because | |
| // the response indicated the "Connection: close" semantic. | |
| break; | |
| } | |
| } | |
| // Send a TCP shutdown | |
| socket.shutdown(tcp::socket::shutdown_send, ec); | |
| // At this point the connection is closed gracefully | |
| } | |
| double convert(const std::string &to_currency, const double money_amount, const char *accesskey) | |
| { | |
| using namespace std::chrono_literals; | |
| std::vector<std::string> currencies{ | |
| "AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTC","BTN","BWP", | |
| "BYN","BYR","BZD","CAD","CDF","CHF","CLF","CLP","CNY","COP","CRC","CUC","CUP","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD", | |
| "FKP","GBP","GEL","GGP","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF","IDR","ILS","IMP","INR","IQD","IRR","ISK","JEP","JMD", | |
| "JOD","JPY","KES","KGS","KHR","KMF","KPW","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LTL","LVL","LYD","MAD","MDL","MGA","MKD","MMK", | |
| "MNT","MOP","MRO","MUR","MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR", | |
| "RON","RSD","RUB","RWF","SAR","SBD","SCR","SDG","SEK","SGD","SHP","SLL","SOS","SRD","STD","SVC","SYP","SZL","THB","TJS","TMT","TND","TOP","TRY", | |
| "TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VEF","VND","VUV","WST","XAF","XAG","XAU","XCD","XDR","XOF","XPF","YER","ZAR","ZMK","ZMW","ZWL" | |
| }; | |
| cache_storage cache{ 1h }; | |
| json j_res; | |
| try | |
| { | |
| j_res = cache.query(to_currency, accesskey); | |
| } | |
| catch (const std::exception &e) | |
| { | |
| std::cerr << "Line 615: Error: " << e.what() << '\n'; | |
| } | |
| catch (...) | |
| { | |
| std::cerr << "Line 619: Did something become NULL?\n"; | |
| } | |
| std::cout << "Hello from line 621, in function 'convert'\n"; | |
| std::cout << "Line 622: currencies:\n" << "j_res:\n"; | |
| for (auto const x : j_res) | |
| { | |
| std::cout << x << '\n'; | |
| } | |
| for (auto const x : currencies) | |
| { | |
| std::cout << x << '\n'; | |
| } | |
| const std::string from_currency = "USD"; | |
| double result = 0, rate = std::stod(j_res["quotes"][from_currency + to_currency].get<json::string_t>()); | |
| std::cout << "Lines 634 to 644: Rate is: " << rate << '\n' | |
| << "To currency is: " << to_currency << '\n' | |
| << "currencies:\n" << "j_res:\n"; | |
| for (auto const x : j_res) | |
| { | |
| std::cout << x << '\n'; | |
| } | |
| for (auto const x : currencies) | |
| { | |
| std::cout << x << '\n'; | |
| } | |
| if (std::find(currencies.begin(), currencies.end(), to_currency) != currencies.end()) | |
| { | |
| std::cout << "Match found\n"; | |
| result = money_amount * rate; | |
| } | |
| return result; | |
| } | |
| const json &cache_storage::query(const std::string &query_data, const char *accesskey) | |
| { | |
| auto found = m_cache.find(query_data); | |
| boost::beast::error_code ec; | |
| try | |
| { | |
| if (found == m_cache.end() || (std::chrono::steady_clock::now() - found->second.first) > m_duration) | |
| { | |
| std::string host{ "apilayer.net" }, api_endpoint{ "/api/live" }, | |
| key{ accesskey }, currencies_param{ query_data }, | |
| target = api_endpoint + "?access_key=" + accesskey + "¤cies=" + currencies_param, | |
| port{ "80" }; | |
| int version = 11; | |
| std::cout << "Line 668: target is: " << target << '\n'; | |
| // The io_context is required for all IO | |
| boost::asio::io_context ioc; | |
| // These objects perform our IO | |
| tcp::resolver resolver{ ioc }; | |
| tcp::socket socket{ ioc }; | |
| // Look up the domain name | |
| const auto results = resolver.resolve(host, port); | |
| // Make the connection on the IP address we get from a lookup | |
| boost::asio::connect(socket, results.begin(), results.end()); | |
| // Set up an HTTP GET request message | |
| http::request<http::string_body> req{ http::verb::get, target, version }; | |
| req.set(http::field::host, host); | |
| req.set(http::field::content_type, "application/json"); | |
| req.set(http::field::accept, "application/json"); | |
| req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); | |
| // Send the HTTP request to the remote host | |
| http::write(socket, req); | |
| // This buffer is used for reading and must be persisted | |
| boost::beast::flat_buffer buffer; | |
| // Declare a container to hold the response | |
| http::response<http::string_body> res; | |
| // Receive the HTTP response | |
| http::read(socket, buffer, res, ec); | |
| std::cout << "Line: 701: API response body:\n" | |
| << res.body() << '\n'; | |
| found = m_cache.insert_or_assign(found, query_data, std::make_pair(std::chrono::steady_clock::now(), json::parse(res.body()))); | |
| } | |
| return found->second.second; | |
| } | |
| catch (const std::exception &e) | |
| { | |
| std::cerr << "Line 709: Error: " << e.what() << '\n'; | |
| } | |
| catch (const boost::beast::error_code &ec) | |
| { | |
| std::cerr << "Line 713: Error: " << ec.message() << '\n'; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment