From 52b3cd46c76fd4be18aeb8422a08a073484b9fad Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Sat, 29 Aug 2026 12:01:42 +0200 Subject: More module implementation partition units --- server/src/api.cpp | 12 +- server/src/api.cppm | 3 +- server/src/database.cpp | 34 +++ server/src/database.cppm | 27 +-- server/src/datex2.cpp | 377 ++++++++++++++++++++++++++++++++ server/src/datex2.cppm | 365 +------------------------------ server/src/geo.cpp | 42 ++++ server/src/geo.cppm | 29 +-- server/src/http_client.cpp | 20 ++ server/src/http_client.cppm | 10 +- server/src/http_common.cpp | 108 +++++++++ server/src/http_common.cppm | 110 +--------- server/src/http_server.cpp | 188 ++++++++++++++++ server/src/http_server.cppm | 522 ++++++++++++++++++-------------------------- server/src/locale.cpp | 232 ++++++++++++++++++++ server/src/locale.cppm | 227 +------------------ server/src/log.cpp | 99 +++++++++ server/src/log.cppm | 128 ++--------- server/src/problem.cpp | 52 +++++ server/src/problem.cppm | 40 +--- server/src/req_ctx.cppm | 48 ---- server/src/rwgps.cpp | 150 +++++++++++++ server/src/rwgps.cppm | 112 +--------- server/src/sqlite3.cpp | 218 ++++++++++++++++++ server/src/sqlite3.cppm | 248 +++++---------------- server/src/srv.cpp | 258 ++++++++++++++++++++++ server/src/srv.cppm | 251 +-------------------- server/src/time.cpp | 173 +++++++++++++++ server/src/time.cppm | 169 +------------- server/src/trace.cpp | 75 +++++++ server/src/trace.cppm | 72 +----- server/src/util.cpp | 75 +++++++ server/src/util.cppm | 109 +++------ server/src/xml.cppm | 23 +- 34 files changed, 2506 insertions(+), 2100 deletions(-) create mode 100644 server/src/database.cpp create mode 100644 server/src/datex2.cpp create mode 100644 server/src/geo.cpp create mode 100644 server/src/http_client.cpp create mode 100644 server/src/http_common.cpp create mode 100644 server/src/http_server.cpp create mode 100644 server/src/locale.cpp create mode 100644 server/src/log.cpp create mode 100644 server/src/problem.cpp delete mode 100644 server/src/req_ctx.cppm create mode 100644 server/src/rwgps.cpp create mode 100644 server/src/sqlite3.cpp create mode 100644 server/src/srv.cpp create mode 100644 server/src/time.cpp create mode 100644 server/src/trace.cpp create mode 100644 server/src/util.cpp (limited to 'server/src') diff --git a/server/src/api.cpp b/server/src/api.cpp index 9317f1b..e671f8a 100644 --- a/server/src/api.cpp +++ b/server/src/api.cpp @@ -5,15 +5,7 @@ module; module routemon:api$impl; -import std; import :api; -import :datex2; -import :geo; -import :gpx; -import :log; -import :req_ctx; -import :time; -import :trace; namespace { @@ -255,8 +247,8 @@ auto handler::process_gpx(gpx::file&& gpx_file) | views::transform( [](auto const& lsp) -> geo::linestring { return *lsp; }) - | std::ranges::to< - std::vector>(), + | std::ranges:: + to>(), }; }) | std::ranges::to>(), diff --git a/server/src/api.cppm b/server/src/api.cppm index 66e8f45..c93e65e 100644 --- a/server/src/api.cppm +++ b/server/src/api.cppm @@ -77,7 +77,8 @@ class handler { using lse_index_value = std::tuple< geo::box, std::shared_ptr, - std::shared_ptr>; + std::shared_ptr + >; using p_index_value = std::pair>; using lse_index = diff --git a/server/src/database.cpp b/server/src/database.cpp new file mode 100644 index 0000000..54e5f34 --- /dev/null +++ b/server/src/database.cpp @@ -0,0 +1,34 @@ +module routemon:database$impl; + +import :database; + +namespace routemon::database { + +constexpr std::int64_t expected_database_version = 1; + +auto open(std::string const& filename) -> std::shared_ptr +{ + auto dbc = sqlite3::open(filename); + try + { + auto version = std::optional{}; + dbc.query("SELECT version FROM migration;").scan_single(version); + if (!version) + throw std::runtime_error{"failed to fetch database migration version"}; + if (version != expected_database_version) + { + throw std::runtime_error{std::format( + "database migration version ({}) does not match expected " + "version ({}), consider running migrations", + *version, expected_database_version)}; + } + } + catch (std::exception const& e) + { + throw std::runtime_error{std::format( + "failed to query database version: {}", e.what())}; + } + return std::shared_ptr{new connection{std::move(dbc)}}; +} + +} // namespace routemon::database diff --git a/server/src/database.cppm b/server/src/database.cppm index 86ed0d7..ff5cae4 100644 --- a/server/src/database.cppm +++ b/server/src/database.cppm @@ -5,8 +5,6 @@ import :sqlite3; namespace routemon::database { -static constexpr std::int64_t expected_database_version = 1; - export class connection { sqlite3::connection dbc_; @@ -19,29 +17,6 @@ public: // Nothing here yet }; -export auto open(std::string const& filename) -> std::shared_ptr -{ - auto dbc = sqlite3::open(filename); - try - { - auto version = std::optional{}; - dbc.query("SELECT version FROM migration;").scan_single(version); - if (!version) - throw std::runtime_error{"failed to fetch database migration version"}; - if (version != expected_database_version) - { - throw std::runtime_error{std::format( - "database migration version ({}) does not match expected " - "version ({}), consider running migrations", - *version, expected_database_version)}; - } - } - catch (std::exception const& e) - { - throw std::runtime_error{std::format( - "failed to query database version: {}", e.what())}; - } - return std::shared_ptr{new connection{std::move(dbc)}}; -} +export auto open(std::string const& filename) -> std::shared_ptr; } // namespace routemon::database diff --git a/server/src/datex2.cpp b/server/src/datex2.cpp new file mode 100644 index 0000000..4c8000b --- /dev/null +++ b/server/src/datex2.cpp @@ -0,0 +1,377 @@ +module; + +#include +#include +#include + +#include + +module routemon:datex2$impl; + +import :datex2; + +using namespace std::literals::string_view_literals; + +namespace routemon::datex2 { + +auto parse_timestamp(char const* in) -> std::optional +{ + auto res = time::timestamp{}; + auto is = std::istringstream{in}; + is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); + return is.fail() ? std::nullopt : std::make_optional(res); +} + +auto loader::add_location_from_xml( + road_closure& rc, pugi::xml_node const& loc_xml) -> void +{ + auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; + if (loc_xml_type == "loc:ItineraryByIndexedLocations") + { + for (auto const loc_cont_xml : + loc_xml.children("loc:locationContainedInItinerary")) + { + add_location_from_xml(rc, loc_cont_xml.child("loc:location")); + } + } + else if (loc_xml_type == "loc:LinearLocation" + || loc_xml_type == "loc:SingleRoadLinearLocation") + { + auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); + if (!loc_gml_xml) + return; + + auto const srs_name = + std::string_view{loc_gml_xml.attribute("srsName").value()}; + if (srs_name != "WGS 84"sv) + { + warnings_.insert( + std::format("don't now how to handle the CRS {}", srs_name)); + return; + } + auto const pos_list_str = + std::string_view{loc_gml_xml.child_value("loc:posList")}; + // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn + + auto ls = std::make_shared(); + + auto lat_set = false; + auto lat = 0.0; + for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) + { + auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); + if (!mlat_or_long) + { + warnings_.insert( + std::format( + "failed to parse coordinate {:?}", + std::string_view{lat_or_long_str})); + return; + } + + if (!lat_set) + { + lat = *mlat_or_long; + lat_set = true; + } + else + { + bgeo::append(*ls, geo::point{*mlat_or_long, lat}); + lat = 0; + lat_set = false; + } + } + + if (bgeo::is_empty(*ls)) + { + warnings_.emplace("empty line string in data set"); + return; + } + + rc.relevant_line_strings.push_back(ls); + } + else if (loc_xml_type == "loc:PointLocation") + { + auto const& coords_xml = + loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); + if (!coords_xml) + return; + + auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); + auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); + if (!mlat || !mlon) + { + warnings_.emplace("failed to parse PointLocation coordinates"); + return; + } + + // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: + // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ + // maar heeft het UML-model van DATEX II v3 het over ETRS 89: + // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm + + auto const coords_etrs89 = geo::point{*mlon, *mlat}; + auto coords_wgs84 = geo::point{}; + etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); + + rc.relevant_points.push_back(coords_wgs84); + } + else + { + warnings_.insert( + std::format( + "don't know how to hande location of type {}, ignoring", + loc_xml.attribute("xsi:type").value())); + return; + } +} + +auto loader::handle_road_or_carriageway_or_lane_management( + pugi::xml_node const& record_xml, std::weak_ptr parent) + -> std::optional> +{ + auto const type = std::string_view{ + record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value() + }; + if (type != "carriagewayClosures" && type != "roadClosed") + // TODO: checken of er nog andere types fietsers de doorgang zouden + // kunnen blokkeren? + return std::nullopt; + + auto const& restricted_vehicle_types_xml = + record_xml.child("sit:forVehiclesWithCharacteristicsOf"); + bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); + for (auto const vehicle_type_xml : + restricted_vehicle_types_xml.children("com:vehicleType")) + { + auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; + if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" + || vehicle_type == "unknown" || vehicle_type == "other") + { + likely_restriction_for_bikes = true; + } + } + if (!likely_restriction_for_bikes) + return std::nullopt; + + //---- Check if within the defined validity period + + auto validity = std::optional{}; + auto const& validity_xml = record_xml.child("sit:validity"); + if (validity_xml + && validity_xml.child_value("com:validityStatus") + == "definedByValidityTimeSpec"sv) + { + auto const& validity_spec_xml = + validity_xml.child("com:validityTimeSpecification"); + + auto valid_periods = std::vector{}; + auto exception_periods = std::vector{}; + + // TODO: com:overallEndTime may be missing (according to the DATEX + // II v3 data model) + auto const overall_start_time = + parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); + auto const overall_end_time = + parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); + if (overall_start_time && overall_end_time + && *overall_start_time < *overall_end_time) + { + valid_periods.emplace_back(*overall_start_time, *overall_end_time); + + for (auto const valid_period_xml : + validity_xml.children("com:validPeriod")) + { + auto const start_of_period = + parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); + auto const end_of_period = + parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); + if (start_of_period && end_of_period + && *start_of_period < *end_of_period) + { + valid_periods.emplace_back(*start_of_period, *end_of_period); + } + } + for (auto const exception_period_xml : + validity_xml.children("com:exceptionPeriod")) + { + auto const start_of_period = parse_timestamp( + exception_period_xml.child_value("com:startOfPeriod")); + auto const end_of_period = parse_timestamp( + exception_period_xml.child_value("com:endOfPeriod")); + if (start_of_period && end_of_period + && *start_of_period < *end_of_period) + { + exception_periods.emplace_back(*start_of_period, *end_of_period); + } + } + + validity = + time::period_seq{valid_periods.begin(), valid_periods.end()}.except( + time::period_seq{ + exception_periods.begin(), exception_periods.end() + }); + } + else + { + warnings_.insert( + std::format( + "invalid overall start / end time (start time: {}, end " + "time: {})", + validity_spec_xml.child_value("com:overallStartTime"), + validity_spec_xml.child_value("com:overallEndTime"))); + return std::nullopt; + } + } + + //---- Try to extract the location info + + auto rc = std::make_shared(std::move(parent), validity); + add_location_from_xml(*rc, record_xml.child("sit:locationReference")); + return rc; +} + +auto loader::load_situation_publication(std::string const& filename) + -> situation_publication +{ + auto doc = pugi::xml_document{}; + if (auto result = doc.load_file(filename.c_str()); !result) + { + throw std::runtime_error{result.description()}; + } + auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); + auto mpublication_time = + parse_timestamp(payload_xml.child_value("com:publicationTime")); + if (!mpublication_time) + throw std::runtime_error{ + "provided publication does not name publication time" + }; + + auto situations = std::vector>{}; + for (auto const sit_xml : payload_xml.children("sit:situation")) + { + auto id = std::string_view{sit_xml.attribute("id").value()}; + + auto const sit = std::make_shared(std::string{id}); + situations.push_back(sit); + + auto const& header_info_xml = sit_xml.child("sit:headerInformation"); + if (header_info_xml.child_value("com:informationStatus") != "real"sv) + continue; + + for (auto const record_xml : sit_xml.children("sit:situationRecord")) + { + auto const record_type = + std::string_view{record_xml.attribute("xsi:type").value()}; + auto const primary_record_types = std::unordered_set{ + "sit:Roadworks", + /* { */ "sit:MaintenanceWorks", + /* | */ "sit:ConstructionWorks", + /* } */ + "sit:Obstruction", + /* { */ "sit:EnvironmentalObstruction", + /* | */ "sit:GeneralObstruction", + /* | */ "sit:InfrastructureDamageObstruction", + /* } */ + "sit:Activity", + /* { */ "sit:PublicEvent", + /* } */ + }; + + if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") + { + if (auto rc = + handle_road_or_carriageway_or_lane_management(record_xml, sit)) + { + sit->road_closures.push_back(*rc); + } + } + else if (primary_record_types.contains(record_type)) + { + for (auto const comment_xml : + record_xml.children("sit:generalPublicComment")) + { + // if + // (comment_xml.child_value("sit:commentType") + // == "internalNote"sv) { + auto candidate = std::optional< + std::pair + >{}; // (text, language) + for (auto const comment_value_xml : comment_xml.child("sit:comment") + .child("com:values") + .children("com:value")) + { + if (!candidate + || comment_value_xml.attribute("lang").value() == "nl"sv + || (candidate->second != "nl"sv + && comment_value_xml.attribute("lang").value() == "nl"sv)) + { + candidate = std::make_pair( + comment_value_xml.child_value(), + comment_value_xml.attribute("lang").value()); + } + } + if (candidate) + { + auto already_present = false; + for (auto const& comment : sit->comments) + already_present = already_present || comment == candidate->first; + if (!already_present) + { + sit->comments.emplace_back(candidate->first); + } + } + // } + } + + if (auto const location_ref_xml = + record_xml.child("sit:locationReference")) + { + if (location_ref_xml.attribute("xsi:type").value() + == "loc:PointLocation"sv) + { + if (auto const coords_xml = + location_ref_xml.child("loc:pointByCoordinates") + .child("loc:pointCoordinates")) + { + auto const mlat = + util::parse_double(coords_xml.child_value("loc:latitude")); + auto const mlon = + util::parse_double(coords_xml.child_value("loc:longitude")); + if (mlat && mlon) + { + // Vaag genoeg zegt NDW dat het hier om WGS + // 84 gaat: + // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ + // maar heeft het UML-model van DATEX II v3 + // het over ETRS 89: + // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm + + auto const coords_etrs89 = geo::point{*mlon, *mlat}; + auto coords_wgs84 = geo::point{}; + etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); + + if (!sit->location) + { + sit->location = coords_wgs84; + } + } + } + } + } + } + } + } + + return { + .publication_time = *mpublication_time, + .situations = situations, + }; +} + +auto loader::warnings() const -> std::multiset const& +{ + return warnings_; +} + +} // namespace routemon::datex2 diff --git a/server/src/datex2.cppm b/server/src/datex2.cppm index d7b4921..9c75013 100644 --- a/server/src/datex2.cppm +++ b/server/src/datex2.cppm @@ -42,377 +42,26 @@ export struct situation_publication std::vector> situations; }; -auto parse_timestamp(char const* in) -> std::optional -{ - auto res = time::timestamp{}; - auto is = std::istringstream{in}; - is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); - return is.fail() ? std::nullopt : std::make_optional(res); -} - export class loader { // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) - bgeo::srs::transformation< - bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> - etrs89_to_wgs84_{}; + bgeo::srs:: + transformation, bgeo::srs::static_epsg<4326>> + etrs89_to_wgs84_{}; std::multiset warnings_; auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) - -> void - { - auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; - if (loc_xml_type == "loc:ItineraryByIndexedLocations") - { - for (auto const loc_cont_xml : - loc_xml.children("loc:locationContainedInItinerary")) - { - add_location_from_xml(rc, loc_cont_xml.child("loc:location")); - } - } - else if (loc_xml_type == "loc:LinearLocation" - || loc_xml_type == "loc:SingleRoadLinearLocation") - { - auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); - if (!loc_gml_xml) - return; - - auto const srs_name = - std::string_view{loc_gml_xml.attribute("srsName").value()}; - if (srs_name != "WGS 84"sv) - { - warnings_.insert( - std::format("don't now how to handle the CRS {}", srs_name)); - return; - } - auto const pos_list_str = - std::string_view{loc_gml_xml.child_value("loc:posList")}; - // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn - - auto ls = std::make_shared(); - - auto lat_set = false; - auto lat = 0.0; - for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) - { - auto mlat_or_long = - util::parse_double(std::string_view{lat_or_long_str}); - if (!mlat_or_long) - { - warnings_.insert( - std::format( - "failed to parse coordinate {:?}", - std::string_view{lat_or_long_str})); - return; - } - - if (!lat_set) - { - lat = *mlat_or_long; - lat_set = true; - } - else - { - bgeo::append(*ls, geo::point{*mlat_or_long, lat}); - lat = 0; - lat_set = false; - } - } - - if (bgeo::is_empty(*ls)) - { - warnings_.emplace("empty line string in data set"); - return; - } - - rc.relevant_line_strings.push_back(ls); - } - else if (loc_xml_type == "loc:PointLocation") - { - auto const& coords_xml = - loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); - if (!coords_xml) - return; - - auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); - auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); - if (!mlat || !mlon) - { - warnings_.emplace("failed to parse PointLocation coordinates"); - return; - } - - // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: - // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ - // maar heeft het UML-model van DATEX II v3 het over ETRS 89: - // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm - - auto const coords_etrs89 = geo::point{*mlon, *mlat}; - auto coords_wgs84 = geo::point{}; - etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); - - rc.relevant_points.push_back(coords_wgs84); - } - else - { - warnings_.insert( - std::format( - "don't know how to hande location of type {}, ignoring", - loc_xml.attribute("xsi:type").value())); - return; - } - } + -> void; auto handle_road_or_carriageway_or_lane_management( pugi::xml_node const& record_xml, std::weak_ptr parent) - -> std::optional> - { - auto const type = - std::string_view{record_xml - .child("sit:roadOrCarriagewayOrLaneManagementType") - .child_value()}; - if (type != "carriagewayClosures" && type != "roadClosed") - // TODO: checken of er nog andere types fietsers de doorgang zouden - // kunnen blokkeren? - return std::nullopt; - - auto const& restricted_vehicle_types_xml = - record_xml.child("sit:forVehiclesWithCharacteristicsOf"); - bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); - for (auto const vehicle_type_xml : - restricted_vehicle_types_xml.children("com:vehicleType")) - { - auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; - if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" - || vehicle_type == "unknown" || vehicle_type == "other") - { - likely_restriction_for_bikes = true; - } - } - if (!likely_restriction_for_bikes) - return std::nullopt; - - //---- Check if within the defined validity period - - auto validity = std::optional{}; - auto const& validity_xml = record_xml.child("sit:validity"); - if (validity_xml - && validity_xml.child_value("com:validityStatus") - == "definedByValidityTimeSpec"sv) - { - auto const& validity_spec_xml = - validity_xml.child("com:validityTimeSpecification"); - - auto valid_periods = std::vector{}; - auto exception_periods = std::vector{}; - - // TODO: com:overallEndTime may be missing (according to the DATEX - // II v3 data model) - auto const overall_start_time = parse_timestamp( - validity_spec_xml.child_value("com:overallStartTime")); - auto const overall_end_time = - parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); - if (overall_start_time && overall_end_time - && *overall_start_time < *overall_end_time) - { - valid_periods.emplace_back(*overall_start_time, *overall_end_time); - - for (auto const valid_period_xml : - validity_xml.children("com:validPeriod")) - { - auto const start_of_period = parse_timestamp( - valid_period_xml.child_value("com:startOfPeriod")); - auto const end_of_period = - parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); - if (start_of_period && end_of_period - && *start_of_period < *end_of_period) - { - valid_periods.emplace_back(*start_of_period, *end_of_period); - } - } - for (auto const exception_period_xml : - validity_xml.children("com:exceptionPeriod")) - { - auto const start_of_period = parse_timestamp( - exception_period_xml.child_value("com:startOfPeriod")); - auto const end_of_period = parse_timestamp( - exception_period_xml.child_value("com:endOfPeriod")); - if (start_of_period && end_of_period - && *start_of_period < *end_of_period) - { - exception_periods.emplace_back(*start_of_period, *end_of_period); - } - } - - validity = - time::period_seq{valid_periods.begin(), valid_periods.end()}.except( - time::period_seq{ - exception_periods.begin(), exception_periods.end() - }); - } - else - { - warnings_.insert( - std::format( - "invalid overall start / end time (start time: {}, end " - "time: {})", - validity_spec_xml.child_value("com:overallStartTime"), - validity_spec_xml.child_value("com:overallEndTime"))); - return std::nullopt; - } - } - - //---- Try to extract the location info - - auto rc = std::make_shared(std::move(parent), validity); - add_location_from_xml(*rc, record_xml.child("sit:locationReference")); - return rc; - } + -> std::optional>; public: [[nodiscard]] auto load_situation_publication(std::string const& filename) - -> situation_publication - { - auto doc = pugi::xml_document{}; - if (auto result = doc.load_file(filename.c_str()); !result) - { - throw std::runtime_error{result.description()}; - } - auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); - auto mpublication_time = - parse_timestamp(payload_xml.child_value("com:publicationTime")); - if (!mpublication_time) - throw std::runtime_error{ - "provided publication does not name publication time" - }; - - auto situations = std::vector>{}; - for (auto const sit_xml : payload_xml.children("sit:situation")) - { - auto id = std::string_view{sit_xml.attribute("id").value()}; - - auto const sit = std::make_shared(std::string{id}); - situations.push_back(sit); - - auto const& header_info_xml = sit_xml.child("sit:headerInformation"); - if (header_info_xml.child_value("com:informationStatus") != "real"sv) - continue; - - for (auto const record_xml : sit_xml.children("sit:situationRecord")) - { - auto const record_type = - std::string_view{record_xml.attribute("xsi:type").value()}; - auto const primary_record_types = std::unordered_set{ - "sit:Roadworks", - /* { */ "sit:MaintenanceWorks", - /* | */ "sit:ConstructionWorks", - /* } */ - "sit:Obstruction", - /* { */ "sit:EnvironmentalObstruction", - /* | */ "sit:GeneralObstruction", - /* | */ "sit:InfrastructureDamageObstruction", - /* } */ - "sit:Activity", - /* { */ "sit:PublicEvent", - /* } */ - }; - - if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") - { - if (auto rc = handle_road_or_carriageway_or_lane_management( - record_xml, sit)) - { - sit->road_closures.push_back(*rc); - } - } - else if (primary_record_types.contains(record_type)) - { - for (auto const comment_xml : - record_xml.children("sit:generalPublicComment")) - { - // if - // (comment_xml.child_value("sit:commentType") - // == "internalNote"sv) { - auto candidate = std::optional>{}; // (text, language) - for (auto const comment_value_xml : comment_xml.child("sit:comment") - .child("com:values") - .children("com:value")) - { - if (!candidate - || comment_value_xml.attribute("lang").value() == "nl"sv - || (candidate->second != "nl"sv - && comment_value_xml.attribute("lang").value() == "nl"sv)) - { - candidate = std::make_pair( - comment_value_xml.child_value(), - comment_value_xml.attribute("lang").value()); - } - } - if (candidate) - { - auto already_present = false; - for (auto const& comment : sit->comments) - already_present = - already_present || comment == candidate->first; - if (!already_present) - { - sit->comments.emplace_back(candidate->first); - } - } - // } - } - - if (auto const location_ref_xml = - record_xml.child("sit:locationReference")) - { - if (location_ref_xml.attribute("xsi:type").value() - == "loc:PointLocation"sv) - { - if (auto const coords_xml = - location_ref_xml.child("loc:pointByCoordinates") - .child("loc:pointCoordinates")) - { - auto const mlat = - util::parse_double(coords_xml.child_value("loc:latitude")); - auto const mlon = - util::parse_double(coords_xml.child_value("loc:longitude")); - if (mlat && mlon) - { - // Vaag genoeg zegt NDW dat het hier om WGS - // 84 gaat: - // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ - // maar heeft het UML-model van DATEX II v3 - // het over ETRS 89: - // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm - - auto const coords_etrs89 = geo::point{*mlon, *mlat}; - auto coords_wgs84 = geo::point{}; - etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); - - if (!sit->location) - { - sit->location = coords_wgs84; - } - } - } - } - } - } - } - } - - return { - .publication_time = *mpublication_time, - .situations = situations, - }; - } - - [[nodiscard]] auto warnings() const -> std::multiset const& - { - return warnings_; - } + -> situation_publication; + [[nodiscard]] auto warnings() const -> std::multiset const&; }; } // namespace routemon::datex2 diff --git a/server/src/geo.cpp b/server/src/geo.cpp new file mode 100644 index 0000000..1776f3c --- /dev/null +++ b/server/src/geo.cpp @@ -0,0 +1,42 @@ +module; + +#include + +module routemon:geo$impl; + +import :geo; + +namespace routemon::geo { + +auto split_linestring_with_overlap_segments( + linestring const& ls, double max_split_distance_m, + std::vector& append_to) -> void +{ + if (bgeo::is_empty(ls)) + return; + + auto current_ls = linestring{}; + auto current_ls_length = 0.0; + auto previous = std::optional{}; + bgeo::for_each_point( + ls, + [&](point p) -> void + { + bgeo::append(current_ls, p); + if (previous) + { + auto d = bgeo::distance(*previous, p, vincenty_strategy()); + current_ls_length += d; + if (current_ls_length > max_split_distance_m) + { + append_to.push_back(std::move(current_ls)); + current_ls = linestring{*previous, p}; + current_ls_length = d; + } + } + previous = p; + }); + append_to.emplace_back(std::move(current_ls)); +} + +} // namespace routemon::geo diff --git a/server/src/geo.cppm b/server/src/geo.cppm index b599ad4..4f8b840 100644 --- a/server/src/geo.cppm +++ b/server/src/geo.cppm @@ -17,33 +17,6 @@ using vincenty_strategy = bgeo::strategy::distance::vincenty; auto split_linestring_with_overlap_segments( linestring const& ls, double max_split_distance_m, - std::vector& append_to) -> void -{ - if (bgeo::is_empty(ls)) - return; - - auto current_ls = linestring{}; - auto current_ls_length = 0.0; - auto previous = std::optional{}; - bgeo::for_each_point( - ls, - [&](point p) -> void - { - bgeo::append(current_ls, p); - if (previous) - { - auto d = bgeo::distance(*previous, p, vincenty_strategy()); - current_ls_length += d; - if (current_ls_length > max_split_distance_m) - { - append_to.push_back(std::move(current_ls)); - current_ls = linestring{*previous, p}; - current_ls_length = d; - } - } - previous = p; - }); - append_to.emplace_back(std::move(current_ls)); -} + std::vector& append_to) -> void; } // namespace routemon::geo diff --git a/server/src/http_client.cpp b/server/src/http_client.cpp new file mode 100644 index 0000000..0f3d9e6 --- /dev/null +++ b/server/src/http_client.cpp @@ -0,0 +1,20 @@ +module; + +#include +#include +#include + +module routemon:http.client$impl; + +import :http.client; + +namespace routemon::http { + +client::client(net::io_context& ioc) : ioc_{ioc}, resolver_{ioc} +{ + sslc_.set_default_verify_paths(); + sslc_.set_verify_mode( + net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); +} + +} // namespace routemon::http diff --git a/server/src/http_client.cppm b/server/src/http_client.cppm index bea5384..2047b01 100644 --- a/server/src/http_client.cppm +++ b/server/src/http_client.cppm @@ -23,17 +23,15 @@ export class client tcp::resolver resolver_; public: - explicit client(net::io_context& ioc) : ioc_{ioc}, resolver_{ioc} - { - sslc_.set_default_verify_paths(); - sslc_.set_verify_mode( - net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); - } + explicit client(net::io_context& ioc); template auto do_request(bhttp::request& req) -> bhttp::response { + // TODO: call into non-template member ASAP. + // Consider using beast::message_generator. + auto stream = ssl::stream{ioc_, sslc_}; auto host = std::string{req.at(bhttp::field::host)}; diff --git a/server/src/http_common.cpp b/server/src/http_common.cpp new file mode 100644 index 0000000..1c60d60 --- /dev/null +++ b/server/src/http_common.cpp @@ -0,0 +1,108 @@ +module; + +#include +#include + +module routemon:http.common$impl; + +import :http.common; + +namespace routemon::http { + +supported_verb::supported_verb(supported_verb_t value) : value{value} {} + +auto supported_verb::from(bhttp::verb v) -> std::optional +{ + switch (v) + { + case bhttp::verb::options: + return supported_verb::options; + case bhttp::verb::delete_: + return supported_verb::delete_; + case bhttp::verb::get: + return supported_verb::get; + case bhttp::verb::head: + return supported_verb::head; + case bhttp::verb::post: + return supported_verb::post; + case bhttp::verb::put: + return supported_verb::put; + default: + return std::nullopt; + } +} + +supported_verb::operator bhttp::verb() const +{ + switch (value) + { + case supported_verb::options: + return bhttp::verb::options; + case supported_verb::delete_: + return bhttp::verb::delete_; + case supported_verb::get: + return bhttp::verb::get; + case supported_verb::head: + return bhttp::verb::head; + case supported_verb::post: + return bhttp::verb::post; + case supported_verb::put: + return bhttp::verb::put; + } +} + +auto verb_set::with(supported_verb v) const noexcept -> verb_set +{ + auto set = verb_set{*this}; + switch (v.value) + { + case supported_verb::delete_: + set.delete_ = true; + return set; + case supported_verb::get: + set.get = true; + return set; + case supported_verb::head: + set.head = true; + return set; + case supported_verb::post: + set.post = true; + return set; + case supported_verb::put: + set.put = true; + return set; + case supported_verb::options: + set.options = true; + return set; + } +} + +auto verb_set::empty() const noexcept -> bool { return *this == verb_set{}; } + +auto verb_set::to_string() const -> std::string +{ + std::ostringstream ss; + bool wrote = false; + auto write = [&](bhttp::verb v) + { + if (wrote) + ss << ", "; + ss << v; + wrote = true; + }; + if (delete_) + write(bhttp::verb::delete_); + if (get) + write(bhttp::verb::get); + if (head) + write(bhttp::verb::head); + if (post) + write(bhttp::verb::post); + if (put) + write(bhttp::verb::put); + if (options) + write(bhttp::verb::options); + return ss.str(); +} + +} // namespace routemon::http diff --git a/server/src/http_common.cppm b/server/src/http_common.cppm index 606a5d5..f56c92b 100644 --- a/server/src/http_common.cppm +++ b/server/src/http_common.cppm @@ -51,119 +51,29 @@ struct supported_verb supported_verb_t value; - supported_verb(supported_verb_t value) : value{value} {} + supported_verb(supported_verb_t value); - static auto from(bhttp::verb v) -> std::optional - { - switch (v) - { - case bhttp::verb::options: - return supported_verb::options; - case bhttp::verb::delete_: - return supported_verb::delete_; - case bhttp::verb::get: - return supported_verb::get; - case bhttp::verb::head: - return supported_verb::head; - case bhttp::verb::post: - return supported_verb::post; - case bhttp::verb::put: - return supported_verb::put; - default: - return std::nullopt; - } - } - - operator bhttp::verb() const - { - switch (value) - { - case supported_verb::options: - return bhttp::verb::options; - case supported_verb::delete_: - return bhttp::verb::delete_; - case supported_verb::get: - return bhttp::verb::get; - case supported_verb::head: - return bhttp::verb::head; - case supported_verb::post: - return bhttp::verb::post; - case supported_verb::put: - return bhttp::verb::put; - } - } + static auto from(bhttp::verb v) -> std::optional; + + operator bhttp::verb() const; }; export struct verb_set { + bool options : 1 = false; bool delete_ : 1 = false; bool get : 1 = false; bool head : 1 = false; bool post : 1 = false; bool put : 1 = false; - bool options : 1 = false; - auto enable(supported_verb v) -> void - { - switch (v.value) - { - case supported_verb::delete_: - delete_ = true; - break; - case supported_verb::get: - get = true; - break; - case supported_verb::head: - head = true; - break; - case supported_verb::post: - post = true; - break; - case supported_verb::put: - put = true; - break; - case supported_verb::options: - options = true; - break; - default:; - } - } + [[nodiscard]] auto with(supported_verb v) const noexcept -> verb_set; + [[nodiscard]] auto empty() const noexcept -> bool; + [[nodiscard]] auto to_string() const -> std::string; auto operator==(verb_set const& rhs) const noexcept -> bool = default; - - auto empty() const -> bool { return *this == verb_set{}; } - - verb_set(std::initializer_list vs) - { - for (auto const v : vs) - enable(v); - } - - auto to_string() const -> std::string - { - std::ostringstream ss; - bool wrote = false; - auto write = [&](bhttp::verb v) - { - if (wrote) - ss << ", "; - ss << v; - wrote = true; - }; - if (delete_) - write(bhttp::verb::delete_); - if (get) - write(bhttp::verb::get); - if (head) - write(bhttp::verb::head); - if (post) - write(bhttp::verb::post); - if (put) - write(bhttp::verb::put); - if (options) - write(bhttp::verb::options); - return ss.str(); - } }; +static_assert(sizeof(verb_set) == 1); +static_assert(alignof(verb_set) == 1); } // namespace routemon::http diff --git a/server/src/http_server.cpp b/server/src/http_server.cpp new file mode 100644 index 0000000..da8e6f0 --- /dev/null +++ b/server/src/http_server.cpp @@ -0,0 +1,188 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +module routemon:http.server$impl; + +import :http.server; + +namespace routemon::http { + +auto presponse::header() -> bhttp::response_header& +{ + return impl_->header(); +} + +auto presponse::header() const -> bhttp::response_header const& +{ + return impl_->header(); +} + +auto presponse::is_done() const -> bool { return impl_->is_done(); } + +auto presponse::prepare(beast::error_code& ec) -> const_buffers_type +{ + return impl_->prepare(ec); +} + +auto presponse::consume(std::size_t n) -> void { return impl_->consume(n); } + +auto presponse::keep_alive() const noexcept -> bool +{ + return impl_->keep_alive(); +} + +keep_alive::keep_alive(bool value) : value{value} {} + +auto problem_rsp( + base_ctx const& ctx, problem::details const& problem, keep_alive ka) + -> presponse +{ + auto rsp = make_rsp(problem.status, ka); + rsp.set(bhttp::field::content_type, "application/problem+json"); + rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); + rsp.prepare_payload(); + return presponse{std::move(rsp)}; +} + +auto make_preflight_rsp(preflight_response res, keep_alive ka) + -> bhttp::response +{ + auto rsp = make_rsp(bhttp::status::no_content, ka); + auto allow_headers_str = res.allow_headers + | std::views::transform( + [](auto const& field) -> std::string_view + { return bhttp::to_string(field); }) + | std::views::join_with(std::string_view{", "}) + | std::ranges::to(); + rsp.set( + bhttp::field::access_control_allow_methods, + res.allow_methods.to_string()); + rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); + rsp.prepare_payload(); + return rsp; +} + +auto global_options_handler(base_ctx const& ctx, readable_request r) + -> net::awaitable +{ + // TODO: switch to "small (4KB) discarded" body type, similar to what Go + // does? Same goes for default_options_handler? Not sure. + if (auto res = co_await read_request(ctx, std::move(r)); + !res) + co_return std::move(res.error()); + auto req = r.p->release(); + auto rsp = make_rsp( + bhttp::status::no_content, keep_alive{req.keep_alive()}); + rsp.prepare_payload(); + co_return std::move(rsp); +} + +auto router::handle_request(readable_request r) const + -> net::awaitable +{ + return impl_->handle_request(r); +} + +server::server(log::logger const& l, router&& r) + : l_{l.sub("http_server")}, r_{std::move(r)} +{ +} + +auto server::do_session(beast::tcp_stream strm) -> net::awaitable +{ + auto buf = beast::flat_buffer{}; + + while (true) + { + auto p0 = bhttp::request_parser{}; + p0.body_limit(boost::none); + auto [ec, _] = + co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); + if (ec == bhttp::error::end_of_stream) + break; + else if (ec) + throw boost::system::system_error{ec}; + + auto http_version = p0.get().version(); + auto&& rsp = co_await r_.handle_request( + readable_request{ + .p = util::not_null{&p0}, + .strm = util::not_null{&strm}, + .buf = util::not_null{&buf}, + }); + rsp.header().version(http_version); + bool keep_alive = rsp.keep_alive(); + co_await beast::async_write(strm, std::move(rsp)); + if (!keep_alive) + { + break; + } + } + + strm.socket().shutdown(tcp::socket::shutdown_send); +} + +auto server::do_listen(tcp::endpoint endpoint) -> net::awaitable +{ + auto executor = co_await net::this_coro::executor; + auto acceptor = tcp::acceptor{executor, endpoint}; + + l_.with("endpoint", endpoint.address().to_string()) + .with("port", std::to_string(endpoint.port())) + .info("Serving"); + while (true) + { + net::co_spawn( + executor, + do_session(beast::tcp_stream{co_await acceptor.async_accept()}), + [this](std::exception_ptr e) + { + if (e) + { + try + { + std::rethrow_exception(e); + } + catch (std::exception const& e) + { + l_.error("Error in session: {}", e.what()); + } + } + }); + } +} + +auto server::spawn(net::io_context& ioc) -> void +{ + auto const addr = net::ip::make_address("0.0.0.0"); + auto const endpoint = tcp::endpoint{addr, 8284}; + + // TODO: make exception handling as nice as in srv.cpp + net::co_spawn( + ioc, do_listen(endpoint), + [this](std::exception_ptr e) + { + if (e) + { + try + { + std::rethrow_exception(e); + } + catch (std::exception const& e) + { + l_.error("Error: {}", e.what()); + } + } + }); +} + +} // namespace routemon::http diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index f5b4c3e..dc8c183 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -137,25 +137,12 @@ public: { } - auto header() -> bhttp::response_header& - { - return impl_->header(); - } - auto header() const -> bhttp::response_header const& - { - return impl_->header(); - } - - auto is_done() const -> bool { return impl_->is_done(); } - - auto prepare(beast::error_code& ec) -> const_buffers_type - { - return impl_->prepare(ec); - } - - auto consume(std::size_t n) -> void { return impl_->consume(n); } - - auto keep_alive() const noexcept -> bool { return impl_->keep_alive(); } + auto header() -> bhttp::response_header&; + auto header() const -> bhttp::response_header const&; + auto is_done() const -> bool; + auto prepare(beast::error_code& ec) -> const_buffers_type; + auto consume(std::size_t n) -> void; + auto keep_alive() const noexcept -> bool; }; static_assert(beast::concepts::buffers_generator); @@ -207,15 +194,17 @@ struct base_ctx }; template -using basic_route_handler_fn_t = std::function< - auto(Ctx, readable_request, std::vector const& matches) - ->net::awaitable>; +using basic_route_handler_fn_t = + std::function const& matches) + ->net::awaitable>; struct keep_alive { bool value; - explicit keep_alive(bool value) : value{value} {} + explicit keep_alive(bool value); }; template @@ -229,37 +218,16 @@ auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response auto problem_rsp( base_ctx const& ctx, problem::details const& problem, keep_alive ka) - -> presponse -{ - auto rsp = make_rsp(problem.status, ka); - rsp.set(bhttp::field::content_type, "application/problem+json"); - rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); - rsp.prepare_payload(); - return presponse{std::move(rsp)}; -} + -> presponse; struct preflight_response { verb_set allow_methods; std::vector allow_headers; }; + auto make_preflight_rsp(preflight_response res, keep_alive ka) - -> bhttp::response -{ - auto rsp = make_rsp(bhttp::status::no_content, ka); - auto allow_headers_str = res.allow_headers - | std::views::transform( - [](auto const& field) -> std::string_view - { return bhttp::to_string(field); }) - | std::views::join_with(std::string_view{", "}) - | std::ranges::to(); - rsp.set( - bhttp::field::access_control_allow_methods, - res.allow_methods.to_string()); - rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); - rsp.prepare_payload(); - return rsp; -} + -> bhttp::response; // Using base_ctx instead of a template here since that saves you // typing on invocation (and we do not care about the context type @@ -276,8 +244,8 @@ auto read_request(base_ctx const& ctx, readable_request&& r) template <> auto read_request(base_ctx const& ctx, readable_request&& r) - -> net::awaitable< - std::expected, presponse>> + -> net:: + awaitable, presponse>> { auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); @@ -340,19 +308,7 @@ auto default_options_handler( } auto global_options_handler(base_ctx const& ctx, readable_request r) - -> net::awaitable -{ - // TODO: switch to "small (4KB) discarded" body type, similar to what Go - // does? Same goes for default_options_handler? Not sure. - if (auto res = co_await read_request(ctx, std::move(r)); - !res) - co_return std::move(res.error()); - auto req = r.p->release(); - auto rsp = make_rsp( - bhttp::status::no_content, keep_alive{req.keep_alive()}); - rsp.prepare_payload(); - co_return std::move(rsp); -} + -> net::awaitable; template auto id_middleware( @@ -427,28 +383,22 @@ struct handler_map auto verbs() const -> verb_set { - auto set = verb_set{}; - if (static_cast(options)) - set.enable(supported_verb::options); - if (static_cast(delete_)) - set.enable(supported_verb::delete_); - if (static_cast(get)) - set.enable(supported_verb::get); - if (static_cast(head)) - set.enable(supported_verb::head); - if (static_cast(post)) - set.enable(supported_verb::post); - if (static_cast(put)) - set.enable(supported_verb::put); - return set; + return verb_set{ + .options = static_cast(options), + .delete_ = static_cast(delete_), + .get = static_cast(get), + .head = static_cast(head), + .post = static_cast(post), + .put = static_cast(put), + }; } auto empty() const -> bool { return verbs().empty(); } template auto map(std::invocable auto f) const -> handler_map - requires std::assignable_from< - U&, std::invoke_result_t> + requires std:: + assignable_from> { return { .options = static_cast(options) ? f(options) : U{}, @@ -508,8 +458,9 @@ template concept match_arg = std::constructible_from; template -using route_handler_fn_t = std::function< - auto(Ctx, readable_request, MatchArgs...)->net::awaitable>; +using route_handler_fn_t = + std::functionnet::awaitable>; template auto degen_route_handler(route_handler_fn_t fn) @@ -588,271 +539,226 @@ struct dtree : handler_map> } }; -template PreRouteCtx> -class server +class router { - log::logger l_; - locale::selector lsel_; - middleware_t global_middleware_; - route_tree> routes_; - -public: - explicit server( - log::logger const& l, locale::selector&& lsel, - middleware_t global_middleware, - route_tree> routes) - : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, - global_middleware_{std::move(global_middleware)}, - routes_{std::move(routes)} - { - } - - struct match_result + struct impl_base { - util::not_null< - handler_map>> const*> - route_handlers; - std::vector wildcard_matches; - - auto allowed_methods() const -> verb_set { return route_handlers->verbs(); } + virtual ~impl_base() = default; + virtual auto handle_request(readable_request r) const + -> net::awaitable = 0; }; - auto match(boost::urls::segments_view segments) const - -> std::optional + std::unique_ptr impl_; + + template PreRouteCtx> + class impl : public impl_base { - auto const* tree = &routes_; - auto wildcard_matches = std::vector{}; - for (auto const& seg : segments) + locale::selector lsel_; + middleware_t global_middleware_; + route_tree> routes_; + + public: + explicit impl( + locale::selector&& lsel, + middleware_t global_middleware, + route_tree> routes) + : lsel_{std::move(lsel)}, + global_middleware_{std::move(global_middleware)}, + routes_{std::move(routes)} { - tree->sub.visit( - util::overloaded{ - [&](route_tree>::named_subtrees const& - subtrees) - { - auto it = subtrees.find(seg); - tree = it == subtrees.end() ? nullptr : &it->second; - }, - [&](route_tree>::wildcard_subtree const& - wildcard_subtree) - { - wildcard_matches.push_back(seg); - tree = &*wildcard_subtree; - }, - }); - if (!tree) - return std::nullopt; } - if (tree->here.empty()) - return std::nullopt; - return match_result{ - .route_handlers = util::not_null{&tree->here}, - .wildcard_matches = wildcard_matches, - }; - } - auto route_request(PreRouteCtx ctx, readable_request r) const - -> net::awaitable - { - auto req_base = r.p->get().base(); + struct match_result + { + util::not_null< + handler_map>> const* + > + route_handlers; + std::vector wildcard_matches; - auto const bad_request_tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("Bad request"), - .type_uri = "https://routemon.fautchen.eu/problems/bad-request", + auto allowed_methods() const -> verb_set + { + return route_handlers->verbs(); + } }; - if (req_base.target() == "*") + auto match(boost::urls::segments_view segments) const + -> std::optional { - // request-target is in asterisk-form (RFC 9112, § 3.2.4), - // so the request must be a server-wide OPTIONS request. - - if (req_base.method() != bhttp::verb::options) + auto const* tree = &routes_; + auto wildcard_matches = std::vector{}; + for (auto const& seg : segments) { - auto tpl = problem::tpl{ - .status = bhttp::status::method_not_allowed, - .title = translate("Method not allowed"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "method-not-allowed", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + tree->sub.visit( + util::overloaded{ + [&](route_tree>::named_subtrees const& + subtrees) + { + auto it = subtrees.find(seg); + tree = it == subtrees.end() ? nullptr : &it->second; + }, + [&](route_tree>::wildcard_subtree const& + wildcard_subtree) + { + wildcard_matches.push_back(seg); + tree = &*wildcard_subtree; + }, + }); + if (!tree) + return std::nullopt; } - - co_return co_await global_options_handler(ctx, r); + if (tree->here.empty()) + return std::nullopt; + return match_result{ + .route_handlers = util::not_null{&tree->here}, + .wildcard_matches = wildcard_matches, + }; } - else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) + + auto route_request(PreRouteCtx ctx, readable_request r) const + -> net::awaitable { - // request-target is in origin-form (RFC 9112, § 3.2.1), - // so it must be a normal request (not a CONNECT or - // server-wide OPTIONS request). + auto req_base = r.p->get().base(); - auto req_url = boost::urls::url{*mreq_url0}; - req_url.normalize(); - if (!req_url.is_path_absolute()) - { - auto problem = bad_request_tpl.instantiate().set_detail(translate( - "Path of normalized (RFC 3986, § 6) " - "origin-form request-target (RFC " - "9112, § 3.2.1) should be " - "absolute")); - co_return problem_rsp(ctx, problem, keep_alive{false}); - } + auto const bad_request_tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Bad request"), + .type_uri = "https://routemon.fautchen.eu/problems/bad-request", + }; - auto mres = match(req_url.segments()); - if (!mres) + if (req_base.target() == "*") { - auto tpl = problem::tpl{ - .status = bhttp::status::not_found, - .title = translate("Not found"), - .type_uri = "https://routemon.fautchen.eu/problems/not-found", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + // request-target is in asterisk-form (RFC 9112, § 3.2.4), + // so the request must be a server-wide OPTIONS request. - auto mverb = supported_verb::from(req_base.method()); - if (!mverb) - { - // Method not implemented. - auto tpl = problem::tpl{ - .status = bhttp::status::not_implemented, - .title = translate("Method not implemented"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "method-not-implemented", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + if (req_base.method() != bhttp::verb::options) + { + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-allowed", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } - if (auto mhdl = mres->route_handlers->lookup(*mverb)) + co_return co_await global_options_handler(ctx, r); + } + else if (auto mreq_url0 = + boost::urls::parse_origin_form(req_base.target())) { - auto new_ctx = - routed_ctx{std::move(ctx), mres->allowed_methods()}; - co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); + // request-target is in origin-form (RFC 9112, § 3.2.1), + // so it must be a normal request (not a CONNECT or + // server-wide OPTIONS request). + + auto req_url = boost::urls::url{*mreq_url0}; + req_url.normalize(); + if (!req_url.is_path_absolute()) + { + auto problem = bad_request_tpl.instantiate().set_detail(translate( + "Path of normalized (RFC 3986, § 6) " + "origin-form request-target (RFC " + "9112, § 3.2.1) should be " + "absolute")); + co_return problem_rsp(ctx, problem, keep_alive{false}); + } + + auto mres = match(req_url.segments()); + if (!mres) + { + auto tpl = problem::tpl{ + .status = bhttp::status::not_found, + .title = translate("Not found"), + .type_uri = "https://routemon.fautchen.eu/problems/not-found", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + auto mverb = supported_verb::from(req_base.method()); + if (!mverb) + { + // Method not implemented. + auto tpl = problem::tpl{ + .status = bhttp::status::not_implemented, + .title = translate("Method not implemented"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-implemented", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } + + if (auto mhdl = mres->route_handlers->lookup(*mverb)) + { + auto new_ctx = + routed_ctx{std::move(ctx), mres->allowed_methods()}; + co_return co_await mhdl( + std::move(new_ctx), r, mres->wildcard_matches); + } + else + { + // Path recognized, but method not allowed. + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-allowed", + }; + auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + rsp.header().set( + bhttp::field::allow, mres->allowed_methods().to_string()); + co_return std::move(rsp); + } } else { - // Path recognized, but method not allowed. - auto tpl = problem::tpl{ - .status = bhttp::status::method_not_allowed, - .title = translate("Method not allowed"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "method-not-allowed", - }; - auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - rsp.header().set( - bhttp::field::allow, mres->allowed_methods().to_string()); - co_return std::move(rsp); + // We do not accept any other request-target forms. + + auto problem = bad_request_tpl.instantiate().set_detail(translate( + "Invalid request-target, expected " + "asterisk-form or origin-form " + "(see RFC 9112, § 3.2)")); + co_return problem_rsp(ctx, problem, keep_alive{false}); } } - else - { - // We do not accept any other request-target forms. - auto problem = bad_request_tpl.instantiate().set_detail(translate( - "Invalid request-target, expected " - "asterisk-form or origin-form " - "(see RFC 9112, § 3.2)")); - co_return problem_rsp(ctx, problem, keep_alive{false}); + auto handle_request(readable_request r) const + -> net::awaitable override + { + auto header = r.p->get().base(); + auto locale = lsel_.select(header[bhttp::field::accept_language]); + co_return co_await global_middleware_( + base_ctx{.locale = locale}, header, + [&](PreRouteCtx ctx) -> net::awaitable + { co_return co_await route_request(std::move(ctx), std::move(r)); }); } - } + }; - auto handle_request(readable_request r) const -> net::awaitable +public: + template PreRouteCtx> + explicit router( + locale::selector&& lsel, + middleware_t global_middleware, + route_tree> routes) + : impl_{std::make_unique>( + std::move(lsel), std::move(global_middleware), std::move(routes))} { - auto header = r.p->get().base(); - auto locale = lsel_.select(header[bhttp::field::accept_language]); - auto ctx0 = base_ctx{.locale = locale}; - - co_return co_await global_middleware_( - std::move(ctx0), header, - [&](PreRouteCtx ctx) -> net::awaitable - { co_return co_await route_request(std::move(ctx), std::move(r)); }); } - auto do_session(beast::tcp_stream strm) -> net::awaitable - { - auto buf = beast::flat_buffer{}; - - while (true) - { - auto p0 = bhttp::request_parser{}; - p0.body_limit(boost::none); - auto [ec, _] = - co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); - if (ec == bhttp::error::end_of_stream) - break; - else if (ec) - throw boost::system::system_error{ec}; - - auto http_version = p0.get().version(); - auto&& rsp = co_await handle_request( - readable_request{ - .p = util::not_null{&p0}, - .strm = util::not_null{&strm}, - .buf = util::not_null{&buf}, - }); - rsp.header().version(http_version); - bool keep_alive = rsp.keep_alive(); - co_await beast::async_write(strm, std::move(rsp)); - if (!keep_alive) - { - break; - } - } - - strm.socket().shutdown(tcp::socket::shutdown_send); - } + auto handle_request(readable_request r) const -> net::awaitable; +}; - auto do_listen(tcp::endpoint endpoint) -> net::awaitable - { - auto executor = co_await net::this_coro::executor; - auto acceptor = tcp::acceptor{executor, endpoint}; +class server +{ + log::logger l_; + router r_; - l_.with("endpoint", endpoint.address().to_string()) - .with("port", std::to_string(endpoint.port())) - .info("Serving"); - while (true) - { - net::co_spawn( - executor, - do_session(beast::tcp_stream{co_await acceptor.async_accept()}), - [this](std::exception_ptr e) - { - if (e) - { - try - { - std::rethrow_exception(e); - } - catch (std::exception const& e) - { - l_.error("Error in session: {}", e.what()); - } - } - }); - } - } + auto do_session(beast::tcp_stream strm) -> net::awaitable; + auto do_listen(tcp::endpoint endpoint) -> net::awaitable; - auto spawn(net::io_context& ioc) -> void - { - auto const addr = net::ip::make_address("0.0.0.0"); - auto const endpoint = tcp::endpoint{addr, 8284}; +public: + explicit server(log::logger const& l, router&& r); - // TODO: make exception handling as nice as in srv.cpp - net::co_spawn( - ioc, do_listen(endpoint), - [this](std::exception_ptr e) - { - if (e) - { - try - { - std::rethrow_exception(e); - } - catch (std::exception const& e) - { - l_.error("Error: {}", e.what()); - } - } - }); - } + auto spawn(net::io_context& ioc) -> void; }; } // namespace routemon::http diff --git a/server/src/locale.cpp b/server/src/locale.cpp new file mode 100644 index 0000000..652349c --- /dev/null +++ b/server/src/locale.cpp @@ -0,0 +1,232 @@ +module; + +#include +#include + +module routemon:locale$impl; + +import :locale; + +namespace routemon::locale { + +struct locale_priority +{ + float weight; + std::size_t original_index; +}; + +auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool +{ + if (lhs.weight != rhs.weight) + return lhs.weight > rhs.weight; + return lhs.original_index < rhs.original_index; +} + +struct icu_locale_hash +{ + std::size_t operator()(icu::Locale const& l) const noexcept + { + static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); + std::int32_t hash = l.hashCode(); + if (hash < 0) + { + return static_cast(std::numeric_limits::max()) + + static_cast(-hash) + 1; + } + else + { + return static_cast(hash); + } + } +}; + +using icu_locale_priority_map = + std::unordered_map; +using icu_priority_locale = std::pair; + +auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) + -> bool +{ + return lhs.second < rhs.second; +} + +class icu_priority_locale_vec_iterator : public icu::Locale::Iterator +{ + std::size_t i_ = 0uz; + std::vector ls_; + +public: + explicit icu_priority_locale_vec_iterator( + std::vector&& ls) + : ls_(std::move(ls)) + { + } + + auto hasNext() const -> UBool override { return i_ < ls_.size(); } + + auto next() -> icu::Locale const& override { return ls_[i_++].first; } + + ~icu_priority_locale_vec_iterator() override = default; +}; + +// Trimming optional whitespace as defined in RFC 9110, § 12.4.2. +auto selector::ltrim_ows(std::string_view s) -> std::string_view +{ + if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) + s.remove_prefix(i); + return s; +} +auto selector::rtrim_ows(std::string_view s) -> std::string_view +{ + if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) + return s.substr(0, i + 1); + return s; +} +auto selector::trim_ows(std::string_view s) -> std::string_view +{ + return rtrim_ows(ltrim_ows(s)); +} + +auto selector::from_icu_locale(icu::Locale const& l) const -> std::locale +{ + auto posix_name = std::string{l.getLanguage()}; + if (l.getScript() && std::strlen(l.getScript()) > 0) + { + posix_name += "_"; + posix_name += l.getScript(); + } + if (l.getCountry() && std::strlen(l.getCountry()) > 0) + { + posix_name += "_"; + posix_name += l.getCountry(); + } + posix_name += ".UTF-8"; + auto added_at = false; + if (l.getVariant() && std::strlen(l.getVariant()) > 0) + { + added_at = true; + posix_name += "@"; + posix_name += l.getVariant(); + } + auto ec = UErrorCode::U_ZERO_ERROR; + auto* keywords = l.createKeywords(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to create keywords"}; + if (keywords) + { + std::int32_t kw_len = 0; + char const* kw = nullptr; + while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) + { + auto value = + l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); + if (!added_at) + { + posix_name += "@"; + added_at = true; + } + else + { + posix_name += ";"; + } + posix_name += kw; + posix_name += "="; + posix_name += value; + } + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to iterate over keywords"}; + delete keywords; + } + return lgen_->generate(posix_name); +} + +auto selector::select(std::string_view accept_language) const -> std::locale +{ + using namespace std::literals::string_view_literals; + // NOTE: can also contain a *;q=0.1 + // q should have at most 3 digits after period + auto dlpm = icu_locale_priority_map{}; + for (auto const [i, lang_prio] : + accept_language | std::views::split(","sv) | std::views::enumerate) + { + auto [lang_range_ut, mweight_ut] = + util::split_on(std::string_view{lang_prio}, ';'); + auto lang_range_str = trim_ows(lang_range_ut); + auto mweight_str = mweight_ut.transform(trim_ows); + if (lang_range_str == "*") + break; + + auto ec = UErrorCode::U_ZERO_ERROR; + auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); + if (U_FAILURE(ec) || icu_locale.isBogus()) + continue; // ignore this locale + + auto weight = 1.0f; + if (mweight_str && mweight_str->starts_with("q=")) + { + auto weight_str = mweight_str->substr(2, 4); + if (auto mweight = + util::parse_float(weight_str, std::chars_format::fixed); + mweight && 0.0f < *mweight && *mweight < 1.0f) + { + weight = *mweight; + } + } + + if (weight > 0.0f) + { + dlpm[icu_locale] = { + .weight = weight, + .original_index = static_cast(i), + }; + } + else + { + dlpm.erase(icu_locale); + } + } + + auto desired_locales = + std::vector{dlpm.begin(), dlpm.end()}; + std::sort(desired_locales.begin(), desired_locales.end()); + auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; + auto ec = UErrorCode::U_ZERO_ERROR; + auto res = matcher_.getBestMatchResult(it, ec); + if (U_FAILURE(ec)) + return default_; + auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? + if (U_FAILURE(ec)) + return from_icu_locale(*res.getSupportedLocale()); + return from_icu_locale(resolved); +} + +auto to_bcp47_lang_tag(std::locale locale) -> std::optional +{ + auto const& locale_info = std::use_facet(locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto bcp47_lang_tag = + icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); + if (U_FAILURE(ec)) + return std::nullopt; + return bcp47_lang_tag; +} + +#ifdef LOCALEDIR +#define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) +#define LOCALEDIR_AUX_STR(s) #s +constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; +#undef LOCALEDIR_AUX_STR +#undef LOCALEDIR_AUX_XSTR +#else // ifdef LOCALEDIR +constexpr auto messages_path = std::string_view{"locale/dev"}; +#endif // ifdef LOCALEDIR + +export auto make_generator() -> std::shared_ptr +{ + auto lgen = std::make_shared(); + lgen->add_messages_path(std::string{messages_path}); + lgen->add_messages_domain("routemon"); + return std::static_pointer_cast(lgen); +} + +} // namespace routemon::locale diff --git a/server/src/locale.cppm b/server/src/locale.cppm index da80f76..263de1d 100644 --- a/server/src/locale.cppm +++ b/server/src/locale.cppm @@ -11,78 +11,20 @@ import :util; export namespace blocale = boost::locale; export namespace routemon { + using lformat = blocale::format; using blocale::gettext; using blocale::translate; + } // namespace routemon namespace routemon::locale { -struct locale_priority -{ - float weight; - std::size_t original_index; -}; - -auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool -{ - if (lhs.weight != rhs.weight) - return lhs.weight > rhs.weight; - return lhs.original_index < rhs.original_index; -} - -struct icu_locale_hash -{ - std::size_t operator()(icu::Locale const& l) const noexcept - { - static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); - std::int32_t hash = l.hashCode(); - if (hash < 0) - { - return static_cast(std::numeric_limits::max()) - + static_cast(-hash) + 1; - } - else - { - return static_cast(hash); - } - } -}; - -using icu_locale_priority_map = - std::unordered_map; -using icu_priority_locale = std::pair; - -auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) - -> bool -{ - return lhs.second < rhs.second; -} - -class icu_priority_locale_vec_iterator : public icu::Locale::Iterator -{ - std::size_t i_ = 0uz; - std::vector ls_; - -public: - explicit icu_priority_locale_vec_iterator( - std::vector&& ls) - : ls_(std::move(ls)) - { - } - - auto hasNext() const -> UBool override { return i_ < ls_.size(); } - - auto next() -> icu::Locale const& override { return ls_[i_++].first; } - - ~icu_priority_locale_vec_iterator() override = default; -}; - export template concept locale_input_range = std::ranges::input_range - && std::same_as< - std::locale const&, std::ranges::range_const_reference_t>; + && std:: + same_as>; // Helps select a locale based on the Accept-Language header in an // HTTP request. @@ -122,75 +64,11 @@ export class selector } // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. - static auto ltrim_ows(std::string_view s) -> std::string_view - { - if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) - s.remove_prefix(i); - return s; - } - static auto rtrim_ows(std::string_view s) -> std::string_view - { - if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) - return s.substr(0, i + 1); - return s; - } - static auto trim_ows(std::string_view s) -> std::string_view - { - return rtrim_ows(ltrim_ows(s)); - } + static auto ltrim_ows(std::string_view s) -> std::string_view; + static auto rtrim_ows(std::string_view s) -> std::string_view; + static auto trim_ows(std::string_view s) -> std::string_view; - auto from_icu_locale(icu::Locale const& l) const -> std::locale - { - auto posix_name = std::string{l.getLanguage()}; - if (l.getScript() && std::strlen(l.getScript()) > 0) - { - posix_name += "_"; - posix_name += l.getScript(); - } - if (l.getCountry() && std::strlen(l.getCountry()) > 0) - { - posix_name += "_"; - posix_name += l.getCountry(); - } - posix_name += ".UTF-8"; - auto added_at = false; - if (l.getVariant() && std::strlen(l.getVariant()) > 0) - { - added_at = true; - posix_name += "@"; - posix_name += l.getVariant(); - } - auto ec = UErrorCode::U_ZERO_ERROR; - auto* keywords = l.createKeywords(ec); - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to create keywords"}; - if (keywords) - { - std::int32_t kw_len = 0; - char const* kw = nullptr; - while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) - { - auto value = - l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); - if (!added_at) - { - posix_name += "@"; - added_at = true; - } - else - { - posix_name += ";"; - } - posix_name += kw; - posix_name += "="; - posix_name += value; - } - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to iterate over keywords"}; - delete keywords; - } - return lgen_->generate(posix_name); - } + auto from_icu_locale(icu::Locale const& l) const -> std::locale; public: // Note: lgen must live at least as long as the selector constructed here! @@ -202,94 +80,11 @@ public: { } - auto select(std::string_view accept_language) const -> std::locale - { - using namespace std::literals::string_view_literals; - // NOTE: can also contain a *;q=0.1 - // q should have at most 3 digits after period - auto dlpm = icu_locale_priority_map{}; - for (auto const [i, lang_prio] : - accept_language | std::views::split(","sv) | std::views::enumerate) - { - auto [lang_range_ut, mweight_ut] = - util::split_on(std::string_view{lang_prio}, ';'); - auto lang_range_str = trim_ows(lang_range_ut); - auto mweight_str = mweight_ut.transform(trim_ows); - if (lang_range_str == "*") - break; - - auto ec = UErrorCode::U_ZERO_ERROR; - auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); - if (U_FAILURE(ec) || icu_locale.isBogus()) - continue; // ignore this locale - - auto weight = 1.0f; - if (mweight_str && mweight_str->starts_with("q=")) - { - auto weight_str = mweight_str->substr(2, 4); - if (auto mweight = - util::parse_float(weight_str, std::chars_format::fixed); - mweight && 0.0f < *mweight && *mweight < 1.0f) - { - weight = *mweight; - } - } - - if (weight > 0.0f) - { - dlpm[icu_locale] = { - .weight = weight, - .original_index = static_cast(i), - }; - } - else - { - dlpm.erase(icu_locale); - } - } - - auto desired_locales = - std::vector{dlpm.begin(), dlpm.end()}; - std::sort(desired_locales.begin(), desired_locales.end()); - auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; - auto ec = UErrorCode::U_ZERO_ERROR; - auto res = matcher_.getBestMatchResult(it, ec); - if (U_FAILURE(ec)) - return default_; - auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? - if (U_FAILURE(ec)) - return from_icu_locale(*res.getSupportedLocale()); - return from_icu_locale(resolved); - } + auto select(std::string_view accept_language) const -> std::locale; }; -export auto to_bcp47_lang_tag(std::locale locale) -> std::optional -{ - auto const& locale_info = std::use_facet(locale); - auto ec = UErrorCode::U_ZERO_ERROR; - auto bcp47_lang_tag = - icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); - if (U_FAILURE(ec)) - return std::nullopt; - return bcp47_lang_tag; -} - -#ifdef LOCALEDIR -#define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) -#define LOCALEDIR_AUX_STR(s) #s -constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; -#undef LOCALEDIR_AUX_STR -#undef LOCALEDIR_AUX_XSTR -#else // ifdef LOCALEDIR -constexpr auto messages_path = std::string_view{"locale/dev"}; -#endif // ifdef LOCALEDIR +auto to_bcp47_lang_tag(std::locale locale) -> std::optional; -export auto make_generator() -> std::shared_ptr -{ - auto lgen = std::make_shared(); - lgen->add_messages_path(std::string{messages_path}); - lgen->add_messages_domain("routemon"); - return std::static_pointer_cast(lgen); -} +export auto make_generator() -> std::shared_ptr; } // namespace routemon::locale diff --git a/server/src/log.cpp b/server/src/log.cpp new file mode 100644 index 0000000..7fe725f --- /dev/null +++ b/server/src/log.cpp @@ -0,0 +1,99 @@ +module routemon:log$impl; + +import :log; + +namespace routemon::log { + +auto operator<<(std::ostream& os, level lvl) -> std::ostream& +{ + switch (lvl) + { + case level::debug: + os << "dbg"; + break; + case level::info: + os << "inf"; + break; + case level::warn: + os << "wrn"; + break; + case level::error: + os << "err"; + break; + } + return os; +} + +sink::sink(enum level lvl) : lvl_{lvl} {} + +auto sink::level() const -> enum level { return lvl_; } + +auto sink::set_level(enum level lvl) -> void { lvl_ = lvl; } + +auto sink::write(tmp_message msg) -> void +{ + auto sos = std::osyncstream{os_}; + sos << "[" << msg.lvl; + if (!msg.component.empty()) + sos << " " << msg.component; + sos << "] " << msg.txt; + for (auto const& [k, v] : msg.attrs) + sos << " " << k << "=" << std::quoted(v); + sos << '\n'; +} + +auto make_sink(level lvl) -> std::shared_ptr +{ + return std::shared_ptr{new sink{lvl}}; +} + +auto logger::log_at(log::level lvl, std::string_view fmt, std::format_args args) + -> logger& +{ + if (sink_->level() <= lvl) + sink_->write( + sink::tmp_message{ + .lvl = lvl, + .component = component_, + .txt = std::vformat(fmt, args), + .attrs = attrs_, + }); + return *this; +} + +logger::logger(std::shared_ptr const& sink) : sink_{sink} +{ + if (!sink) + { + throw std::invalid_argument{"logger sink may not be null"}; + } +} + +auto logger::sub(std::string_view component) const -> logger +{ + auto l = *this; + if (l.component_.empty()) + { + l.component_ = component; + } + else + { + l.component_ += "."; + l.component_ += component; + } + return l; +} + +auto logger::with(std::string const& k, std::string&& v) const -> logger +{ + auto l = *this; + l.attrs_[k] = std::move(v); + return l; +} + +auto logger::with(std::string const& k, std::string_view v) const -> logger +{ + return with(k, std::string{v}); +} + +} // namespace routemon::log diff --git a/server/src/log.cppm b/server/src/log.cppm index 97552b6..dc8c2e1 100644 --- a/server/src/log.cppm +++ b/server/src/log.cppm @@ -1,11 +1,3 @@ -module; - -// Seems like ADL for std::quoted is broken with -// import std; -// Might be because the _Quoted_string object is defined in -// std::__detail, which is not exported by the module. -#include - export module routemon:log; import std; @@ -19,35 +11,16 @@ export enum class level : std::uint8_t { error, }; -namespace { - -auto operator<<(std::ostream& os, level lvl) -> std::ostream& -{ - switch (lvl) - { - case level::debug: - os << "dbg"; - break; - case level::info: - os << "inf"; - break; - case level::warn: - os << "wrn"; - break; - case level::error: - os << "err"; - break; - } - return os; -} - -} // namespace - export class sink { std::atomic lvl_; std::ostream& os_ = std::cout; + explicit sink(level lvl); + + friend auto make_sink(level lvl) -> std::shared_ptr; + +public: struct tmp_message { level lvl; @@ -56,35 +29,13 @@ export class sink std::map const& attrs; }; - auto write(tmp_message msg) -> void - { - auto sos = std::osyncstream{os_}; - sos << "[" << msg.lvl; - if (!msg.component.empty()) - sos << " " << msg.component; - sos << "] " << msg.txt; - for (auto const& [k, v] : msg.attrs) - { - sos << " " << k << "=" << std::quoted(v); - } - sos << '\n'; - } - - explicit sink(level lvl) : lvl_{lvl} {} - - friend auto make_sink(level lvl) -> std::shared_ptr; - friend class logger; - -public: - [[nodiscard]] auto level() const -> enum level { return lvl_; } + [[nodiscard]] auto level() const -> enum level; + auto set_level(enum level lvl) -> void; - auto set_level(enum level lvl) -> void { lvl_ = lvl; } + auto write(tmp_message msg) -> void; }; -export auto make_sink(level lvl) -> std::shared_ptr -{ - return std::shared_ptr{new sink{lvl}}; -} +export auto make_sink(level lvl) -> std::shared_ptr; export class logger { @@ -92,79 +43,40 @@ export class logger std::string component_; std::map attrs_; - template - auto log_at(std::string_view fmt, std::format_args args) -> logger& - { - if (sink_->level() <= lvl) - sink_->write( - sink::tmp_message{ - .lvl = lvl, - .component = component_, - .txt = std::vformat(fmt, args), - .attrs = attrs_, - }); - return *this; - } + auto log_at(log::level lvl, std::string_view fmt, std::format_args args) + -> logger&; public: - explicit logger(std::shared_ptr const& sink) : sink_{sink} - { - if (!sink) - { - throw std::invalid_argument{"logger sink may not be null"}; - } - } - - [[nodiscard]] auto sub(std::string_view component) const -> logger - { - auto l = *this; - if (l.component_.empty()) - { - l.component_ = component; - } - else - { - l.component_ += "."; - l.component_ += component; - } - return l; - } - - [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger - { - auto l = *this; - l.attrs_[k] = std::move(v); - return l; - } + explicit logger(std::shared_ptr const& sink); + [[nodiscard]] auto sub(std::string_view component) const -> logger; + [[nodiscard]] auto with(std::string const& k, std::string&& v) const + -> logger; [[nodiscard]] auto with(std::string const& k, std::string_view v) const - -> logger - { - return with(k, std::string{v}); - } + -> logger; template auto debug(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); + return log_at(level::debug, fmt.get(), std::make_format_args(args...)); } template auto info(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); + return log_at(level::info, fmt.get(), std::make_format_args(args...)); } template auto warn(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); + return log_at(level::warn, fmt.get(), std::make_format_args(args...)); } template auto error(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); + return log_at(level::error, fmt.get(), std::make_format_args(args...)); } }; diff --git a/server/src/problem.cpp b/server/src/problem.cpp new file mode 100644 index 0000000..3608f38 --- /dev/null +++ b/server/src/problem.cpp @@ -0,0 +1,52 @@ +module; + +#include +#include +#include + +module routemon:problem$impl; + +import :problem; + +namespace routemon::problem { + +auto details::set_detail(blocale::message detail) -> details& +{ + this->detail = detail; + return *this; +} + +auto details::set_instance(std::string&& instance) -> details& +{ + this->instance = instance; + return *this; +} + +auto details::set_instance(std::string_view instance) -> details& +{ + this->instance = std::string{instance}; + return *this; +} + +auto tag_invoke( + json::value_from_tag, json::value& jv, details const& details, + std::locale locale) -> void +{ + auto obj = json::object{ + {"type", details.type_uri}, + {"title", details.title.str(locale)}, + {"status", static_cast(details.status)}, + }; + if (details.detail) + obj["detail"] = details.detail->str(locale); + if (details.instance) + obj["instance"] = *details.instance; + jv = obj; +} + +auto tpl::instantiate() const -> details +{ + return details{status, title, type_uri}; +} + +} // namespace routemon::problem diff --git a/server/src/problem.cppm b/server/src/problem.cppm index 37e6257..18c2fe2 100644 --- a/server/src/problem.cppm +++ b/server/src/problem.cppm @@ -22,39 +22,14 @@ export struct details std::optional detail = std::nullopt; std::optional instance = std::nullopt; - auto set_detail(blocale::message detail) -> details& - { - this->detail = detail; - return *this; - } - - auto set_instance(std::string&& instance) -> details& - { - this->instance = instance; - return *this; - } - auto set_instance(std::string_view instance) -> details& - { - this->instance = std::string{instance}; - return *this; - } + auto set_detail(blocale::message detail) -> details&; + auto set_instance(std::string&& instance) -> details&; + auto set_instance(std::string_view instance) -> details&; }; -export auto tag_invoke( +auto tag_invoke( json::value_from_tag, json::value& jv, details const& details, - std::locale locale) -> void -{ - auto obj = json::object{ - {"type", details.type_uri}, - {"title", details.title.str(locale)}, - {"status", static_cast(details.status)}, - }; - if (details.detail) - obj["detail"] = details.detail->str(locale); - if (details.instance) - obj["instance"] = *details.instance; - jv = obj; -} + std::locale locale) -> void; export struct tpl { @@ -62,10 +37,7 @@ export struct tpl blocale::message title; std::string_view type_uri; - auto instantiate() const -> details - { - return details{status, title, type_uri}; - } + auto instantiate() const -> details; }; } // namespace routemon::problem diff --git a/server/src/req_ctx.cppm b/server/src/req_ctx.cppm deleted file mode 100644 index 4a64d7d..0000000 --- a/server/src/req_ctx.cppm +++ /dev/null @@ -1,48 +0,0 @@ -module; - -#include -#include - -export module routemon:req_ctx; - -import :http.common; -import :trace; - -namespace routemon { - -export class req_ctx -{ - trace::id tid_; - std::locale locale_; - http::verb_set route_verbs_; - bool keep_alive_; - bhttp::request_header const& req_header_; - -public: - explicit req_ctx( - trace::id tid, std::locale locale, http::verb_set route_verbs, - bool keep_alive, bhttp::request_header const& req_header) - : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, - keep_alive_{keep_alive}, req_header_{req_header} - { - } - - template - explicit req_ctx( - trace::id tid, std::locale locale, http::verb_set route_verbs, - bhttp::request const& req) - : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} - { - } - - auto trace_id() const -> trace::id { return tid_; } - auto locale() const -> std::locale { return locale_; } - auto route_verbs() const -> http::verb_set { return route_verbs_; } - auto keep_alive() const -> bool { return keep_alive_; } - auto req_header() const -> bhttp::request_header const& - { - return req_header_; - } -}; - -} // namespace routemon diff --git a/server/src/rwgps.cpp b/server/src/rwgps.cpp new file mode 100644 index 0000000..b8ebf11 --- /dev/null +++ b/server/src/rwgps.cpp @@ -0,0 +1,150 @@ +module; + +#include +#include +#include + +module routemon:rwgps$impl; + +import :rwgps; + +namespace beast = boost::beast; +namespace bhttp = beast::http; +namespace json = boost::json; + +namespace routemon::rwgps { + +export struct route_summary +{ + std::int64_t id; + std::int64_t user_id; + std::string url; + std::string name; + std::string description; +}; + +struct pagination +{ + std::size_t record_count; + std::size_t page_count; + std::size_t page_size; + std::optional next_page_url; +}; + +struct get_routes_meta +{ + pagination pagination; +}; + +struct get_routes_response +{ + std::vector routes; + get_routes_meta meta; +}; + +auto tag_invoke(json::value_to_tag const&, json::value const& jv) + -> route_summary +{ + return { + .id = json::value_to(jv.at("id")), + .user_id = json::value_to(jv.at("user_id")), + .url = json::value_to(jv.at("url")), + .name = json::value_to(jv.at("name")), + .description = json::value_to(jv.at("description")), + }; +} + +auto tag_invoke(json::value_to_tag const&, json::value const& jv) + -> pagination +{ + return { + .record_count = json::value_to(jv.at("record_count")), + .page_count = json::value_to(jv.at("page_count")), + .page_size = json::value_to(jv.at("page_size")), + .next_page_url = + json::value_to>(jv.at("next_page_url")), + }; +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv) + -> get_routes_meta +{ + return { + .pagination = json::value_to(jv.at("pagination")), + }; +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv) + -> get_routes_response +{ + return { + .routes = json::value_to>(jv.at("routes")), + .meta = json::value_to(jv.at("meta")), + }; +} + +auto json_value_to_get_routes_response(json::value const& jv) + -> get_routes_response +{ + return json::value_to(jv); +} + +constexpr std::string host = "ridewithgps.com"; + +// TODO: handle failure appropriately +auto client::get_routes_page(std::size_t page) -> get_routes_response +{ + auto req = bhttp::request{ + bhttp::verb::get, + std::format("/api/v1/routes.json?page_size=200?page={}", page), + 11, // HTTP 1.1 + }; + req.set(bhttp::field::host, host); + req.set("x-rwgps-api-key", api_key_); + req.set("x-rwgps-auth-token", auth_token_); + + auto rsp = hc_.do_request(req); + auto p = json::stream_parser{}; + for (auto const frag : rsp.body().cdata()) + p.write(static_cast(frag.data()), frag.size()); + assert(p.done()); + return json_value_to_get_routes_response(p.release()); +} + +client::client( + net::io_context& ioc, log::logger const& l, std::string api_key, + std::string auth_token) + : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, + auth_token_{std::move(auth_token)} +{ +} + +auto client::get_all_routes() -> std::vector +{ + // TODO: make sure that there are no duplicates here. + // What does RWGPS sort on, by default? + // Consider using an associative container instead of a vector. + auto record_count = 0uz; + auto current_page = 0uz; + auto routes = std::vector{}; + + while (true) + { + auto rsp = get_routes_page(current_page); + if (rsp.meta.pagination.next_page_url) + l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); + routes.append_range(rsp.routes); + if (rsp.meta.pagination.record_count > 0) + record_count = rsp.meta.pagination.record_count; + if (rsp.routes.empty() || routes.size() >= record_count) + { + break; + } + } + + return routes; +} + +} // namespace routemon::rwgps diff --git a/server/src/rwgps.cppm b/server/src/rwgps.cppm index 3b01e6c..2ed124e 100644 --- a/server/src/rwgps.cppm +++ b/server/src/rwgps.cppm @@ -1,8 +1,6 @@ module; -#include -#include -#include +#include export module routemon:rwgps; @@ -10,10 +8,7 @@ import std; import :http.client; import :log; -namespace beast = boost::beast; -namespace bhttp = beast::http; namespace net = boost::asio; -namespace json = boost::json; namespace routemon::rwgps { @@ -45,55 +40,6 @@ struct get_routes_response get_routes_meta meta; }; -auto tag_invoke(json::value_to_tag const&, json::value const& jv) - -> route_summary -{ - return { - .id = json::value_to(jv.at("id")), - .user_id = json::value_to(jv.at("user_id")), - .url = json::value_to(jv.at("url")), - .name = json::value_to(jv.at("name")), - .description = json::value_to(jv.at("description")), - }; -} - -auto tag_invoke(json::value_to_tag const&, json::value const& jv) - -> pagination -{ - return { - .record_count = json::value_to(jv.at("record_count")), - .page_count = json::value_to(jv.at("page_count")), - .page_size = json::value_to(jv.at("page_size")), - .next_page_url = - json::value_to>(jv.at("next_page_url")), - }; -} - -auto tag_invoke( - json::value_to_tag const&, json::value const& jv) - -> get_routes_meta -{ - return { - .pagination = json::value_to(jv.at("pagination")), - }; -} - -auto tag_invoke( - json::value_to_tag const&, json::value const& jv) - -> get_routes_response -{ - return { - .routes = json::value_to>(jv.at("routes")), - .meta = json::value_to(jv.at("meta")), - }; -} - -auto json_value_to_get_routes_response(json::value const& jv) - -> get_routes_response -{ - return json::value_to(jv); -} - export class client { log::logger l_; @@ -101,64 +47,14 @@ export class client std::string api_key_; std::string auth_token_; - static constexpr std::string host = "ridewithgps.com"; - - // TODO: handle failure appropriately - auto get_routes_page(std::size_t page) -> get_routes_response - { - auto req = bhttp::request{ - bhttp::verb::get, - std::format("/api/v1/routes.json?page_size=200?page={}", page), - 11, // HTTP 1.1 - }; - req.set(bhttp::field::host, host); - req.set("x-rwgps-api-key", api_key_); - req.set("x-rwgps-auth-token", auth_token_); - - auto rsp = hc_.do_request(req); - auto p = json::stream_parser{}; - for (auto const frag : rsp.body().cdata()) - { - p.write(static_cast(frag.data()), frag.size()); - } - assert(p.done()); - return json_value_to_get_routes_response(p.release()); - } + auto get_routes_page(std::size_t page) -> get_routes_response; public: explicit client( net::io_context& ioc, log::logger const& l, std::string api_key, - std::string auth_token) - : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, - auth_token_{std::move(auth_token)} - { - } - - auto get_all_routes() -> std::vector - { - // TODO: make sure that there are no duplicates here. - // What does RWGPS sort on, by default? - // Consider using an associative container instead of a vector. - auto record_count = 0uz; - auto current_page = 0uz; - auto routes = std::vector{}; - - while (true) - { - auto rsp = get_routes_page(current_page); - if (rsp.meta.pagination.next_page_url) - l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); - routes.append_range(rsp.routes); - if (rsp.meta.pagination.record_count > 0) - record_count = rsp.meta.pagination.record_count; - if (rsp.routes.empty() || routes.size() >= record_count) - { - break; - } - } + std::string auth_token); - return routes; - } + auto get_all_routes() -> std::vector; }; } // namespace routemon::rwgps diff --git a/server/src/sqlite3.cpp b/server/src/sqlite3.cpp new file mode 100644 index 0000000..3e0d64a --- /dev/null +++ b/server/src/sqlite3.cpp @@ -0,0 +1,218 @@ +module; + +#include + +module routemon:sqlite3$impl; + +import :sqlite3; + +namespace routemon::sqlite3 { + +mutex_guard::mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} {} + +mutex_guard::~mutex_guard() { ::sqlite3_mutex_leave(mut_); } + +auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) + -> decltype(f(std::declval())) +{ + return f(mutex_guard{mut}); +} + +auto do_guarded(::sqlite3* dbc, std::invocable auto f) + -> decltype(f(std::declval())) +{ + return do_guarded(::sqlite3_db_mutex(dbc), f); +} + +error::error(mutex_guard const&, int code, ::sqlite3* dbc) + : code_{code}, message_{::sqlite3_errmsg(dbc)} +{ +} + +error::error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} + +[[nodiscard]] auto error::what() const noexcept -> char const* +{ + return message_.c_str(); +} + +[[nodiscard]] auto error::code() const noexcept -> int { return code_; } + +statement::statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} +statement::statement(statement&& s) noexcept +{ + stmt_ = s.stmt_; + s.stmt_ = nullptr; +} +statement::~statement() { ::sqlite3_finalize(stmt_); } +auto statement::get() -> ::sqlite3_stmt* { return stmt_; } + +row_reader::row_reader(statement stmt) : stmt_{std::move(stmt)} {} + +auto row_reader::is_null(int col) -> bool +{ + return ::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL; +} + +auto row_reader::ncols() -> std::size_t +{ + auto const mncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); + if (!mncols.has_value()) + throw std::logic_error{"got unexpected negative amount of columns"}; + return *mncols; +} + +auto row_reader::scan(int col, std::string& s) -> void +{ + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) + throw std::invalid_argument{"invalid type for scan"}; + unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); + auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); + if (!size.has_value()) + throw std::logic_error{"unexpected negative amount of bytes in column"}; + s = std::string{reinterpret_cast(chs), *size}; +} + +auto row_reader::scan(int col, double& v) -> void +{ + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) + throw std::invalid_argument{"invalid type for scan"}; + v = ::sqlite3_column_double(stmt_.get(), col); +} + +auto row_reader::scan(int col, std::int64_t& v) -> void +{ + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) + throw std::invalid_argument{"invalid type for scan"}; + v = ::sqlite3_column_int64(stmt_.get(), col); +} + +auto row_reader::next() -> bool +{ + ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); + return do_guarded( + dbc, + [&](auto const& guard) -> bool + { + auto const s = ::sqlite3_step(stmt_.get()); + if (s == SQLITE_ROW) + return true; + if (s == SQLITE_DONE) + return false; + throw error{guard, s, dbc}; + }); +} + +binder::binder(statement& stmt) : stmt_{stmt} {} + +auto binder::text(std::string const& param_name, std::string_view str) -> void +{ + int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); + if (i == 0) + throw std::invalid_argument{std::format( + "bind: no parameter with name {} found", param_name)}; + auto str_size = util::int_from_size(str.size()); + if (!str_size.has_value()) + throw std::invalid_argument{"bind: provided text is too long"}; + if (auto s = ::sqlite3_bind_text( + stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); + s != SQLITE_OK) + { + throw error{s}; + } +} + +auto binder::noop(binder&) -> void {} + +connection::connection(::sqlite3* dbc) + : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} +{ +} + +connection::connection(connection const&) = delete; +connection::connection(connection&& c) noexcept +{ + dbc_ = c.dbc_; + mut_ = c.mut_; + c.dbc_ = nullptr; + c.mut_ = nullptr; +} + +auto connection::query( + std::string const& sql, std::function const& bf) + -> row_reader +{ + ::sqlite3_stmt* pstmt = nullptr; + char const* sql_tail = nullptr; + auto sql_size = util::int_from_size(sql.size()); + if (!sql_size.has_value() || *sql_size >= std::numeric_limits::max() - 1) + throw std::invalid_argument{"provided input text too large"}; + do_guarded( + mut_, + [&](auto const& guard) -> void + { + if (auto s = ::sqlite3_prepare_v2( + dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); + s != SQLITE_OK) + { + if (pstmt != nullptr) + { + // Use contract_assert when having a compiler with + // contracts available + ::sqlite3_finalize(pstmt); + throw std::logic_error{ + "expected stmt to be null after failed preparation" + }; + } + throw error{guard, s, dbc_}; + } + }); + if (!pstmt) + throw std::invalid_argument{"provided input text contains no SQL"}; + auto stmt = statement{pstmt}; + if (sql_tail && std::strlen(sql_tail) > 0) + throw std::invalid_argument{ + "provided input text contains more than one SQL statement" + }; + auto b = binder{stmt}; + bf(b); + return row_reader{std::move(stmt)}; +} + +auto connection::exec( + std::string const& sql, std::function const& bf) -> void +{ + auto reader = query(sql, bf); + while (reader.next()) + ; +} + +connection::~connection() +{ + std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); +} + +auto open(std::string const& filename) -> connection +{ + ::sqlite3* dbc = nullptr; + auto s = ::sqlite3_open_v2( + filename.c_str(), &dbc, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX + | SQLITE_OPEN_EXRESCODE, + nullptr); + if (s != SQLITE_OK) + { + if (dbc) + { + do_guarded( + dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); + } + else + { + throw error{s}; + } + } + return connection{dbc}; +} + +} // namespace routemon::sqlite3 diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm index 4623cdc..b771c49 100644 --- a/server/src/sqlite3.cppm +++ b/server/src/sqlite3.cppm @@ -1,20 +1,34 @@ -module; - -#include - export module routemon:sqlite3; import std; import :util; +extern "C" +{ + using sqlite3 = struct sqlite3; + using sqlite3_mutex = struct sqlite3_mutex; + using sqlite3_stmt = struct sqlite3_stmt; +} + namespace routemon::sqlite3 { +template concept C> +concept optional_of = requires { + typename T::value_type; + requires std::same_as>; + requires C; +}; + +template +concept scannable_prim = std::same_as || std::same_as + || std::same_as; + +template +concept scannable = scannable_prim || optional_of; + class mutex_guard { - explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} - { - ::sqlite3_mutex_enter(mut_); - } + explicit mutex_guard(::sqlite3_mutex* mut) noexcept; friend auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) @@ -22,111 +36,54 @@ class mutex_guard public: mutex_guard(mutex_guard const&) = delete; - ~mutex_guard() { ::sqlite3_mutex_leave(mut_); } + ~mutex_guard(); private: ::sqlite3_mutex* mut_; }; -auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) - -> decltype(f(std::declval())) -{ - return f(mutex_guard{mut}); -} - -auto do_guarded(::sqlite3* dbc, std::invocable auto f) - -> decltype(f(std::declval())) -{ - return do_guarded(::sqlite3_db_mutex(dbc), f); -} - class error : public std::exception { int code_; std::string message_; public: - explicit error(mutex_guard const&, int code, ::sqlite3* dbc) - : code_{code}, message_{::sqlite3_errmsg(dbc)} - { - } - - explicit error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} - - [[nodiscard]] auto what() const noexcept -> char const* override - { - return message_.c_str(); - } + explicit error(mutex_guard const&, int code, ::sqlite3* dbc); + explicit error(int code); - [[nodiscard]] auto code() const noexcept -> int { return code_; } -}; - -template concept C> -concept optional_of = requires { - typename T::value_type; - requires std::same_as>; - requires C; + [[nodiscard]] auto what() const noexcept -> char const* override; + [[nodiscard]] auto code() const noexcept -> int; }; -template -concept scannable_prim = std::same_as || std::same_as - || std::same_as; - -template -concept scannable = scannable_prim || optional_of; - class statement { ::sqlite3_stmt* stmt_; public: - explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} + explicit statement(::sqlite3_stmt* stmt); statement(statement const&) = delete; - statement(statement&& s) noexcept - { - stmt_ = s.stmt_; - s.stmt_ = nullptr; - } - ~statement() { ::sqlite3_finalize(stmt_); } - auto get() -> ::sqlite3_stmt* { return stmt_; } + statement(statement&& s) noexcept; + ~statement(); + auto get() -> ::sqlite3_stmt*; }; class row_reader { statement stmt_; - explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} + explicit row_reader(statement stmt); friend class connection; - void scan(int col, std::string& s) - { - if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) - throw std::invalid_argument{"invalid type for scan"}; - unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); - auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); - if (!size.has_value()) - throw std::logic_error{"unexpected negative amount of bytes in column"}; - s = std::string{reinterpret_cast(chs), *size}; - } + auto is_null(int col) -> bool; + auto ncols() -> std::size_t; - void scan(int col, double& v) + auto scan(int col, std::string& s) -> void; + auto scan(int col, double& v) -> void; + auto scan(int col, std::int64_t& v) -> void; + auto scan(int col, optional_of auto& v) -> void { - if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) - throw std::invalid_argument{"invalid type for scan"}; - v = ::sqlite3_column_double(stmt_.get(), col); - } - - void scan(int col, std::int64_t& v) - { - if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) - throw std::invalid_argument{"invalid type for scan"}; - v = ::sqlite3_column_int64(stmt_.get(), col); - } - - void scan(int col, optional_of auto& v) - { - if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) + if (is_null(col)) { v.reset(); } @@ -139,28 +96,11 @@ class row_reader } public: - auto next() -> bool - { - ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); - return do_guarded( - dbc, - [&](auto const& guard) -> bool - { - auto const s = ::sqlite3_step(stmt_.get()); - if (s == SQLITE_ROW) - return true; - if (s == SQLITE_DONE) - return false; - throw error{guard, s, dbc}; - }); - } + auto next() -> bool; auto scan(scannable auto&... args) -> void { - auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); - if (!ncols.has_value()) - throw std::logic_error{"got unexpected negative amount of columns"}; - if (sizeof...(args) > *ncols) + if (sizeof...(args) > ncols()) throw std::invalid_argument{ "more scanning arguments provided than columns in result set" }; @@ -184,30 +124,14 @@ class binder { statement& stmt_; - explicit binder(statement& stmt) : stmt_{stmt} {} + explicit binder(statement& stmt); friend class connection; public: - auto text(std::string const& param_name, std::string_view str) -> void - { - int const i = - ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); - if (i == 0) - throw std::invalid_argument{std::format( - "bind: no parameter with name {} found", param_name)}; - auto str_size = util::int_from_size(str.size()); - if (!str_size.has_value()) - throw std::invalid_argument{"bind: provided text is too long"}; - if (auto s = ::sqlite3_bind_text( - stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); - s != SQLITE_OK) - { - throw error{s}; - } - } + auto text(std::string const& param_name, std::string_view str) -> void; - static auto noop(binder&) -> void {} + static auto noop(binder&) -> void; }; export class connection @@ -215,97 +139,25 @@ export class connection ::sqlite3* dbc_; ::sqlite3_mutex* mut_; - explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} - { - } + explicit connection(::sqlite3* dbc); friend auto open(std::string const& filename) -> connection; public: connection(connection const&) = delete; - connection(connection&& c) noexcept - { - dbc_ = c.dbc_; - mut_ = c.mut_; - c.dbc_ = nullptr; - c.mut_ = nullptr; - } + connection(connection&& c) noexcept; [[nodiscard]] auto query( std::string const& sql, - std::function const& bf = binder::noop) -> row_reader - { - ::sqlite3_stmt* pstmt = nullptr; - char const* sql_tail = nullptr; - auto sql_size = util::int_from_size(sql.size()); - if (!sql_size.has_value() - || *sql_size >= std::numeric_limits::max() - 1) - throw std::invalid_argument{"provided input text too large"}; - do_guarded( - mut_, - [&](auto const& guard) -> void - { - if (auto s = ::sqlite3_prepare_v2( - dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); - s != SQLITE_OK) - { - if (pstmt != nullptr) - { - // Use contract_assert when having a compiler with - // contracts available - ::sqlite3_finalize(pstmt); - throw std::logic_error{ - "expected stmt to be null after failed preparation" - }; - } - throw error{guard, s, dbc_}; - } - }); - if (!pstmt) - throw std::invalid_argument{"provided input text contains no SQL"}; - auto stmt = statement{pstmt}; - if (sql_tail && std::strlen(sql_tail) > 0) - throw std::invalid_argument{ - "provided input text contains more than one SQL statement" - }; - auto b = binder{stmt}; - bf(b); - return row_reader{std::move(stmt)}; - } + std::function const& bf = binder::noop) -> row_reader; auto exec( std::string const& sql, - std::function const& bf = binder::noop) -> void - { - auto reader = query(sql, bf); - while (reader.next()) - ; - } + std::function const& bf = binder::noop) -> void; - ~connection() { std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); } + ~connection(); }; -export auto open(std::string const& filename) -> connection -{ - ::sqlite3* dbc = nullptr; - auto s = ::sqlite3_open_v2( - filename.c_str(), &dbc, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX - | SQLITE_OPEN_EXRESCODE, - nullptr); - if (s != SQLITE_OK) - { - if (dbc) - { - do_guarded( - dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); - } - else - { - throw error{s}; - } - } - return connection{dbc}; -} +export auto open(std::string const& filename) -> connection; } // namespace routemon::sqlite3 diff --git a/server/src/srv.cpp b/server/src/srv.cpp new file mode 100644 index 0000000..00a2bb8 --- /dev/null +++ b/server/src/srv.cpp @@ -0,0 +1,258 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +module routemon:srv$impl; + +import std; +import :gpx; +import :srv; + +namespace beast = boost::beast; +namespace json = boost::json; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; + +namespace routemon::srv { + +class gpx_parse_error_category_impl : public std::error_category +{ +public: + char const* name() const noexcept override { return "gpx_parse"; } + + auto message(int condition) const noexcept -> std::string override + { + std::ignore = condition; + return "failed to parse GPX file"; + } +}; + +auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& +{ + static auto const inst = gpx_parse_error_category_impl{}; + return inst; +} + +auto gpx_parse_error() noexcept -> std::error_code +{ + return std::error_code{1, gpx_parse_error_category()}; +} + +class gpx_parse_result +{ + std::variant res_; + +public: + auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } + auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } + + auto unwrap() -> gpx::file&& + { + return std::visit( + util::overloaded{ + [](std::exception_ptr ex) -> gpx::file&& + { + if (ex) + std::rethrow_exception(ex); + else + throw std::runtime_error{"no GPX file parse result available"}; + }, + [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, + }, + std::move(res_)); + } +}; + +struct readable_gpx_body +{ + using value_type = gpx_parse_result; + + class reader + { + gpx::reader r_; + util::not_null res_; + + public: + template + explicit reader(bhttp::header&, value_type& v) : res_{&v} + { + } + + // The following methods (which are called by Beast) are marked + // noexcept, since Beast does not ensure that exceptions thrown + // here are appropriately directed to the caller of + // (async_)read(_some), so throwing here might cause the program + // to crash. + + auto + init(boost::optional /* n */, beast::error_code& ec) noexcept + -> void + { + try + { + r_.init(); + ec = {}; + } + catch (std::exception& ex) + { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); + } + } + + auto + put(beast::concepts::const_buffer_sequence auto b, + beast::error_code& ec) noexcept -> std::size_t + { + auto total = 0uz; + try + { + for (auto it = net::buffer_sequence_begin(b); + it != net::buffer_sequence_end(b); it++) + { + r_.put( + std::string_view{ + static_cast(it->data()), it->size() + }); + total += it->size(); + } + ec = {}; + } + catch (std::exception& ex) + { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); + } + return total; + } + + auto finish(beast::error_code& ec) noexcept + { + try + { + res_->set_gpx_file(r_.finish()); + ec = {}; + } + catch (std::exception& ex) + { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); + } + } + }; +}; +static_assert(bhttp::concepts::body); +static_assert(bhttp::concepts::body_reader); + +auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) + -> net::awaitable +{ + auto gpx_file = gpx::file{}; + try + { + auto req = + co_await http::read_request(ctx, std::move(r)); + gpx_file = std::move(req->body().unwrap()); + } + catch (std::exception& ex) + { + // TODO: more detailed problem reporting + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Failed to parse GPX file"), + .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + + // TODO: catch handler exceptions and return 500 when raised? + // (keep-alive depends on whether whole request was read) + auto mres = inner_.process_gpx(std::move(gpx_file)); + if (!mres) + { + auto tpl = problem::tpl{ + .status = bhttp::status::internal_server_error, + .title = translate("Internal server error"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "internal-server-error", + }; + co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true}); + } + + auto rsp = http::make_rsp( + bhttp::status::ok, http::keep_alive{true}); + rsp.set(bhttp::field::content_type, "application/json"); + rsp.body() = json::serialize(json::value_from(*mres)); + rsp.prepare_payload(); + co_return rsp; +} + +auto handler::handle_sysinfo(l0_ctx ctx, http::readable_request r) + -> net::awaitable +{ + auto req = co_await http::read_request(ctx, std::move(r)); + auto info = inner_.sysinfo(); + + auto rsp = http::make_rsp( + bhttp::status::ok, http::keep_alive{true}); + rsp.set(bhttp::field::content_type, "application/json"); + rsp.body() = json::serialize(json::value_from(info)); + rsp.prepare_payload(); + co_return rsp; +} + +handler::handler(api::handler&& inner) : inner_{std::move(inner)} {} + +auto handler::make_routes() -> http::route_tree> +{ + auto handler = [this](MemFn member) + { return std::bind_front(member, this); }; + + return http::dtree>{}.named_subtrees({ + {"gpx", http::dtree{{ + .post = handler(&handler::handle_process_gpx), + }} + .no_subtrees()}, + {"sysinfo", http::dtree{ + { + .get = handler(&handler::handle_sysinfo), + } + }.no_subtrees()}, + }); +} + +auto server::make_global_middleware() + -> http::middleware_t +{ + return http::middleware_compose< + http::base_ctx, http::trace_id_ctx, + http::trace_id_ctx + >(http::trace_id_middleware, + http::lax_cors_middleware>); +} + +server::server( + log::logger const& l, locale::selector&& lsel, api::handler&& inner) + : handler_{std::move(inner)}, + srv_{ + l, http::router{ + std::move(lsel), make_global_middleware(), handler_.make_routes() + } + } +{ +} + +auto server::spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } + +} // namespace routemon::srv diff --git a/server/src/srv.cppm b/server/src/srv.cppm index 904bf1a..1d082fb 100644 --- a/server/src/srv.cppm +++ b/server/src/srv.cppm @@ -1,166 +1,14 @@ module; -#include #include -#include -#include -#include -#include -#include -#include -#include - -#include export module routemon:srv; -import std; import :api; -import :config; -import :gpx; import :http.server; -import :locale; -import :log; -import :problem; -import :req_ctx; -import :util; - -namespace beast = boost::beast; -namespace json = boost::json; -namespace net = boost::asio; -using tcp = boost::asio::ip::tcp; namespace routemon::srv { -class gpx_parse_error_category_impl : public std::error_category -{ -public: - char const* name() const noexcept override { return "gpx_parse"; } - - auto message(int condition) const noexcept -> std::string override - { - std::ignore = condition; - return "failed to parse GPX file"; - } -}; - -auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& -{ - static auto const inst = gpx_parse_error_category_impl{}; - return inst; -} - -auto gpx_parse_error() noexcept -> std::error_code -{ - return std::error_code{1, gpx_parse_error_category()}; -} - -class gpx_parse_result -{ - std::variant res_; - -public: - auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } - auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } - - auto unwrap() -> gpx::file&& - { - return std::visit( - util::overloaded{ - [](std::exception_ptr ex) -> gpx::file&& - { - if (ex) - std::rethrow_exception(ex); - else - throw std::runtime_error{"no GPX file parse result available"}; - }, - [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, - }, - std::move(res_)); - } -}; - -struct readable_gpx_body -{ - using value_type = gpx_parse_result; - - class reader - { - gpx::reader r_; - util::not_null res_; - - public: - template - explicit reader(bhttp::header&, value_type& v) : res_{&v} - { - } - - // The following methods (which are called by Beast) are marked - // noexcept, since Beast does not ensure that exceptions thrown - // here are appropriately directed to the caller of - // (async_)read(_some), so throwing here might cause the program - // to crash. - - auto - init(boost::optional /* n */, beast::error_code& ec) noexcept - -> void - { - try - { - r_.init(); - ec = {}; - } - catch (std::exception& ex) - { - res_->set_exception(std::current_exception()); - ec = gpx_parse_error(); - } - } - - auto - put(beast::concepts::const_buffer_sequence auto b, - beast::error_code& ec) noexcept -> std::size_t - { - auto total = 0uz; - try - { - for (auto it = net::buffer_sequence_begin(b); - it != net::buffer_sequence_end(b); it++) - { - r_.put( - std::string_view{ - static_cast(it->data()), it->size() - }); - total += it->size(); - } - ec = {}; - } - catch (std::exception& ex) - { - res_->set_exception(std::current_exception()); - ec = gpx_parse_error(); - } - return total; - } - - auto finish(beast::error_code& ec) noexcept - { - try - { - res_->set_gpx_file(r_.finish()); - ec = {}; - } - catch (std::exception& ex) - { - res_->set_exception(std::current_exception()); - ec = gpx_parse_error(); - } - } - }; -}; -static_assert(bhttp::concepts::body); -static_assert(bhttp::concepts::body_reader); - class handler { api::handler inner_; @@ -171,112 +19,29 @@ public: private: auto handle_process_gpx(l0_ctx ctx, http::readable_request r) - -> net::awaitable - { - auto gpx_file = gpx::file{}; - try - { - auto req = - co_await http::read_request(ctx, std::move(r)); - gpx_file = std::move(req->body().unwrap()); - } - catch (std::exception& ex) - { - // TODO: more detailed problem reporting - auto tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("Failed to parse GPX file"), - .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", - }; - co_return http::problem_rsp( - ctx, tpl.instantiate(), http::keep_alive{false}); - } - - // TODO: catch handler exceptions and return 500 when raised? - // (keep-alive depends on whether whole request was read) - auto mres = inner_.process_gpx(std::move(gpx_file)); - if (!mres) - { - auto tpl = problem::tpl{ - .status = bhttp::status::internal_server_error, - .title = translate("Internal server error"), - .type_uri = "https://routemon.fautchen.eu/problems/" - "internal-server-error", - }; - co_return http::problem_rsp( - ctx, tpl.instantiate(), http::keep_alive{true}); - } - - auto rsp = http::make_rsp( - bhttp::status::ok, http::keep_alive{true}); - rsp.set(bhttp::field::content_type, "application/json"); - rsp.body() = json::serialize(json::value_from(*mres)); - rsp.prepare_payload(); - co_return rsp; - } + -> net::awaitable; auto handle_sysinfo(l0_ctx ctx, http::readable_request r) - -> net::awaitable - { - auto req = - co_await http::read_request(ctx, std::move(r)); - auto info = inner_.sysinfo(); - - auto rsp = http::make_rsp( - bhttp::status::ok, http::keep_alive{true}); - rsp.set(bhttp::field::content_type, "application/json"); - rsp.body() = json::serialize(json::value_from(info)); - rsp.prepare_payload(); - co_return rsp; - } + -> net::awaitable; public: - handler(api::handler&& inner) : inner_{std::move(inner)} {} - - auto make_routes() -> http::route_tree> - { - auto handler = [this](MemFn member) - { return std::bind_front(member, this); }; + handler(api::handler&& inner); - return http::dtree>{}.named_subtrees({ - {"gpx", - http::dtree{ - { - .post = handler(&handler::handle_process_gpx), - } - }.no_subtrees()}, - {"sysinfo", http::dtree{ - { - .get = handler(&handler::handle_sysinfo), - } - }.no_subtrees()}, - }); - } + auto make_routes() -> http::route_tree>; }; export class server { handler handler_; - http::server srv_; + http::server srv_; static auto make_global_middleware() - -> http::middleware_t - { - return http::middleware_compose< - http::base_ctx, http::trace_id_ctx, - http::trace_id_ctx>( - http::trace_id_middleware, - http::lax_cors_middleware>); - } + -> http::middleware_t; public: - server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) - : handler_{std::move(inner)}, - srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} - { - } + server(log::logger const& l, locale::selector&& lsel, api::handler&& inner); - auto spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } + auto spawn(net::io_context& ioc) -> void; }; } // namespace routemon::srv diff --git a/server/src/time.cpp b/server/src/time.cpp new file mode 100644 index 0000000..8f99dd2 --- /dev/null +++ b/server/src/time.cpp @@ -0,0 +1,173 @@ +module routemon:time$impl; + +import :time; + +export namespace routemon::time { + +using timestamp = std::chrono::time_point; + +period::period(timestamp start, timestamp end) : start_{start}, end_{end} +{ + if (start >= end) + { + throw std::invalid_argument("period: start should be before end"); + } +} + +auto period::intersect(period other) const -> std::optional +{ + auto const new_start = start_ < other.start() ? other.start() : start_; + auto const new_end = other.end() < end_ ? other.end() : end_; + return new_start < new_end ? std::make_optional(period{new_start, new_end}) + : std::nullopt; +} + +auto period::except(period other) const + -> std::pair, std::optional> +{ + auto const before_start = start_; + auto const before_end = other.start(); + auto const after_start = end_; + auto const after_end = other.end(); + std::optional before, after; + if (before_start < before_end) + before = period{before_start, before_end}; + if (after_start < after_end) + after = period{after_end, after_start}; + return std::make_pair(before, after); +} + +auto period::start() const -> timestamp { return start_; } +auto period::end() const -> timestamp { return end_; } + +period_seq::period_seq(std::vector periods) + : periods_{std::move(periods)} +{ + for (auto i = 0uz; i < periods_.size(); i++) + { + if (i + 1 < periods_.size()) + { + if (periods_[i].end() >= periods_[i + 1].start()) + { + throw std::logic_error{"period_seq: vector provided to private " + "constructor not ordered properly"}; + } + } + } +} + +period_seq::period_seq(period singleton) : periods_{singleton} {} + +auto period_seq::intersect(period_seq const& other) const -> period_seq +{ + auto it1 = periods_.begin(); + auto end1 = periods_.end(); + auto it2 = other.periods_.begin(); + auto end2 = other.periods_.end(); + + auto res = std::vector{}; + while (it1 != end1 && it2 != end2) + { + auto overlap = it1->intersect(*it2); + if (overlap) + { + res.push_back(*overlap); + if (it1->end() < it2->end()) + { + it1++; + } + else + { + it2++; + } + } + else + { + if (it1->end() < it2->start()) + { + it1++; + } + else + { + it2++; + } + } + } + + return period_seq{res}; +} + +auto period_seq::except(period_seq const& other) const -> period_seq +{ + // This code was pretty tricky to write, I wouldn't be surprised if it + // has some bugs in it. + + auto it1 = periods_.begin(); + auto end1 = periods_.end(); + auto it2 = other.periods_.begin(); + auto end2 = other.periods_.end(); + + auto res = std::vector{}; + if (it1 == end1) + return period_seq{res}; + if (it2 == end2) + return period_seq{periods_}; + auto period1 = period{*it1++}; + + while (it1 != end1 && it2 != end2) + { + if (period1.end() <= it2->start()) + { + res.push_back(period1); + period1 = *it1++; + } + else if (it2->end() <= period1.start()) + { + it2++; + } + else /* period1.begin() < it2->end() && it2->begin() < + period1.end() */ + { + auto const [mbefore, mafter] = period1.except(*it2); + if (mbefore) + res.push_back(*mbefore); + if (mafter) + { + period1 = *mafter; + } + else + { + period1 = *it1++; + } + } + } + + return period_seq{res}; +} + +auto period_seq::periods() const -> std::vector const& +{ + return periods_; +} + +auto operator<<(std::ostream& os, period const& p) -> std::ostream& +{ + return os << "[" << p.start() << ", " << p.end() << ")"; +} + +auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream& +{ + os << "{"; + auto it = ps.periods().begin(); + while (it != ps.periods().end()) + { + os << " " << *it; + if (++it != ps.periods().end()) + { + os << ","; + } + } + return os << " }"; +} + +} // namespace routemon::time diff --git a/server/src/time.cppm b/server/src/time.cppm index 8f53bd9..0007839 100644 --- a/server/src/time.cppm +++ b/server/src/time.cppm @@ -14,39 +14,13 @@ class period timestamp end_; public: - explicit period(timestamp start, timestamp end) : start_{start}, end_{end} - { - if (start >= end) - { - throw std::invalid_argument("period: start should be before end"); - } - } - - [[nodiscard]] auto intersect(period other) const -> std::optional - { - auto const new_start = start_ < other.start() ? other.start() : start_; - auto const new_end = other.end() < end_ ? other.end() : end_; - return new_start < new_end ? std::make_optional(period{new_start, new_end}) - : std::nullopt; - } + explicit period(timestamp start, timestamp end); + [[nodiscard]] auto intersect(period other) const -> std::optional; [[nodiscard]] auto except(period other) const - -> std::pair, std::optional> - { - auto const before_start = start_; - auto const before_end = other.start(); - auto const after_start = end_; - auto const after_end = other.end(); - std::optional before, after; - if (before_start < before_end) - before = period{before_start, before_end}; - if (after_start < after_end) - after = period{after_end, after_start}; - return std::make_pair(before, after); - } - - [[nodiscard]] auto start() const -> timestamp { return start_; } - [[nodiscard]] auto end() const -> timestamp { return end_; } + -> std::pair, std::optional>; + [[nodiscard]] auto start() const -> timestamp; + [[nodiscard]] auto end() const -> timestamp; }; class period_seq @@ -115,21 +89,7 @@ class period_seq return periods; } - explicit period_seq(std::vector periods) - : periods_{std::move(periods)} - { - for (auto i = 0uz; i < periods_.size(); i++) - { - if (i + 1 < periods_.size()) - { - if (periods_[i].end() >= periods_[i + 1].start()) - { - throw std::logic_error{"period_seq: vector provided to private " - "constructor not ordered properly"}; - } - } - } - } + explicit period_seq(std::vector periods); public: template S> @@ -138,119 +98,14 @@ public: { } - explicit period_seq(period singleton) : periods_{singleton} {} - - [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq - { - auto it1 = periods_.begin(); - auto end1 = periods_.end(); - auto it2 = other.periods_.begin(); - auto end2 = other.periods_.end(); - - auto res = std::vector{}; - while (it1 != end1 && it2 != end2) - { - auto overlap = it1->intersect(*it2); - if (overlap) - { - res.push_back(*overlap); - if (it1->end() < it2->end()) - { - it1++; - } - else - { - it2++; - } - } - else - { - if (it1->end() < it2->start()) - { - it1++; - } - else - { - it2++; - } - } - } - - return period_seq{res}; - } - - [[nodiscard]] auto except(period_seq const& other) const -> period_seq - { - // This code was pretty tricky to write, I wouldn't be surprised if it - // has some bugs in it. - - auto it1 = periods_.begin(); - auto end1 = periods_.end(); - auto it2 = other.periods_.begin(); - auto end2 = other.periods_.end(); - - auto res = std::vector{}; - if (it1 == end1) - return period_seq{res}; - if (it2 == end2) - return period_seq{periods_}; - auto period1 = period{*it1++}; - - while (it1 != end1 && it2 != end2) - { - if (period1.end() <= it2->start()) - { - res.push_back(period1); - period1 = *it1++; - } - else if (it2->end() <= period1.start()) - { - it2++; - } - else /* period1.begin() < it2->end() && it2->begin() < - period1.end() */ - { - auto const [mbefore, mafter] = period1.except(*it2); - if (mbefore) - res.push_back(*mbefore); - if (mafter) - { - period1 = *mafter; - } - else - { - period1 = *it1++; - } - } - } - - return period_seq{res}; - } + explicit period_seq(period singleton); - [[nodiscard]] auto periods() const -> std::vector const& - { - return periods_; - } + [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq; + [[nodiscard]] auto except(period_seq const& other) const -> period_seq; + [[nodiscard]] auto periods() const -> std::vector const&; }; -auto operator<<(std::ostream& os, period const& p) -> std::ostream& -{ - return os << "[" << p.start() << ", " << p.end() << ")"; -} - -auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream& -{ - os << "{"; - auto it = ps.periods().begin(); - while (it != ps.periods().end()) - { - os << " " << *it; - if (++it != ps.periods().end()) - { - os << ","; - } - } - return os << " }"; -} +auto operator<<(std::ostream& os, period const& p) -> std::ostream&; +auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream&; } // namespace routemon::time diff --git a/server/src/trace.cpp b/server/src/trace.cpp new file mode 100644 index 0000000..e7c00d0 --- /dev/null +++ b/server/src/trace.cpp @@ -0,0 +1,75 @@ +module; + +// Might as well since we're using OpenSSL +#include +#include + +module routemon:trace$impl; + +import :trace; + +namespace routemon::trace { + +uuid7::uuid7() +{ + namespace chrono = std::chrono; + auto const unix_time_ms_signed = static_cast( + chrono::duration_cast( + chrono::system_clock::now().time_since_epoch()) + .count()); + if (unix_time_ms_signed < 0) + throw std::runtime_error{"system time before UNIX epoch"}; + auto const unix_time_ms = static_cast(unix_time_ms_signed); + if (std::countl_zero(unix_time_ms) < 16) + throw std::runtime_error{"system time too great"}; + + auto rand = std::array{}; + int s = RAND_bytes(rand.data(), static_cast(rand.size())); + if (s != 1) + { + unsigned long e = ERR_get_error(); + throw std::runtime_error{std::format( + "failed to generate UUID(v7): {} ({}, code {})", + ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; + } + + auto version = std::uint64_t{0b0111}; + auto variant = std::uint64_t{0b10}; + + hi_ |= unix_time_ms << 16; + hi_ |= version << 12; + hi_ |= std::uint64_t{rand[0]} << 4; + hi_ |= std::uint64_t{rand[1]}; + lo_ |= variant << 62; + lo_ |= std::uint64_t{rand[2]} << 54; + lo_ |= std::uint64_t{rand[3]} << 48; + lo_ |= std::uint64_t{rand[4]} << 40; + lo_ |= std::uint64_t{rand[5]} << 32; + lo_ |= std::uint64_t{rand[6]} << 24; + lo_ |= std::uint64_t{rand[7]} << 16; + lo_ |= std::uint64_t{rand[8]} << 8; + lo_ |= std::uint64_t{rand[9]}; +} + +auto uuid7::format(std::array& target) -> void +{ + auto p____hi_hi = (hi_ & 0xffff'ffff'0000'0000) >> 32; + auto p_hi_lo_hi = (hi_ & 0x0000'0000'ffff'0000) >> 16; + auto p_lo_lo_hi = (hi_ & 0x0000'0000'0000'ffff) >> 0; + auto p____hi_lo = (lo_ & 0xffff'0000'0000'0000) >> 48; + auto p____lo_lo = (lo_ & 0x0000'ffff'ffff'ffff) >> 0; + + std::format_to( + target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", p____hi_hi, + p_hi_lo_hi, p_lo_lo_hi, p____hi_lo, p____lo_lo); + target.back() = '\0'; +} + +id::id() { uuid7{}.format(chars_); } + +auto id::as_string() const -> util::zstring_view +{ + return util::zstring_view{chars_.data(), chars_.size() - 1}; +} + +} // namespace routemon::trace diff --git a/server/src/trace.cppm b/server/src/trace.cppm index 35d32f3..deb84a0 100644 --- a/server/src/trace.cppm +++ b/server/src/trace.cppm @@ -1,9 +1,3 @@ -module; - -// Might as well since we're using OpenSSL -#include -#include - export module routemon:trace; import std; @@ -13,64 +7,13 @@ namespace routemon::trace { class uuid7 { - std::uint64_t high_ = 0; - std::uint64_t low_ = 0; + std::uint64_t hi_ = 0; + std::uint64_t lo_ = 0; public: - uuid7() - { - namespace chrono = std::chrono; - auto const unix_time_ms_signed = static_cast( - chrono::duration_cast( - chrono::system_clock::now().time_since_epoch()) - .count()); - if (unix_time_ms_signed < 0) - throw std::runtime_error{"system time before UNIX epoch"}; - auto const unix_time_ms = static_cast(unix_time_ms_signed); - if (std::countl_zero(unix_time_ms) < 16) - throw std::runtime_error{"system time too great"}; - - auto rand = std::array{}; - int s = RAND_bytes(rand.data(), static_cast(rand.size())); - if (s != 1) - { - unsigned long e = ERR_get_error(); - throw std::runtime_error{std::format( - "failed to generate UUID(v7): {} ({}, code {})", - ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; - } - - auto version = std::uint64_t{0b0111}; - auto variant = std::uint64_t{0b10}; - - high_ |= unix_time_ms << 16; - high_ |= version << 12; - high_ |= std::uint64_t{rand[0]} << 4; - high_ |= std::uint64_t{rand[1]}; - low_ |= variant << 62; - low_ |= std::uint64_t{rand[2]} << 54; - low_ |= std::uint64_t{rand[3]} << 48; - low_ |= std::uint64_t{rand[4]} << 40; - low_ |= std::uint64_t{rand[5]} << 32; - low_ |= std::uint64_t{rand[6]} << 24; - low_ |= std::uint64_t{rand[7]} << 16; - low_ |= std::uint64_t{rand[8]} << 8; - low_ |= std::uint64_t{rand[9]}; - } - - auto format(std::array& target) -> void - { - auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; - auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; - auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; - auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; - auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; + uuid7(); - std::format_to( - target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", high_high, - high_low_high, low_low_high, high_low, low_low); - target.back() = '\0'; - } + auto format(std::array& target) -> void; }; export class id @@ -78,12 +21,9 @@ export class id std::array chars_; public: - id() { uuid7{}.format(chars_); } + id(); - auto as_string() const -> util::zstring_view - { - return util::zstring_view{chars_.data(), chars_.size() - 1}; - } + auto as_string() const -> util::zstring_view; }; } // namespace routemon::trace diff --git a/server/src/util.cpp b/server/src/util.cpp new file mode 100644 index 0000000..54040a6 --- /dev/null +++ b/server/src/util.cpp @@ -0,0 +1,75 @@ +module routemon:util$impl; + +import :util; + +namespace routemon::util { + +lazy_zstring_view::lazy_zstring_view(lazy_zstring_view const& sv) noexcept + : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} +{ +} + +lazy_zstring_view::lazy_zstring_view(lazy_zstring_view&& sv) noexcept + : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} +{ +} + +auto lazy_zstring_view::operator=(lazy_zstring_view const& rhs) noexcept + -> lazy_zstring_view& +{ + if (this != &rhs) + { + s_ = rhs.s_; + length_.store( + rhs.length_.load(std::memory_order_acquire), std::memory_order_release); + } + return *this; +} + +auto lazy_zstring_view::operator=(lazy_zstring_view&& rhs) noexcept + -> lazy_zstring_view& +{ + return *this = rhs; // use the copy assignment operator +} + +auto lazy_zstring_view::length() const noexcept -> std::size_t +{ + if (auto v = length_.load(std::memory_order_acquire); v != unset_length) + return v; + auto l = std::char_traits::length(s_); + length_.store(l, std::memory_order_release); + return l; +} + +auto lazy_zstring_view::c_str() const noexcept -> char const* { return s_; } + +lazy_zstring_view::operator std::string_view() const noexcept +{ + return std::string_view{s_, length()}; +} + +lazy_zstring_view::operator char const*() const noexcept { return s_; } + +auto lazy_zstring_view::operator==(std::string_view sv) const noexcept -> bool +{ + if (auto v = length_.load(std::memory_order_acquire); v != unset_length) + if (sv.length() != v) + return false; + auto res = std::char_traits::compare(s_, sv.data(), sv.length()); + if (res != 0) + return false; + // Strings are equal for sv.length() characters. + if (s_[sv.length()] != '\0') + return false; + // Strings are actually equal, and we have just found out the + // length of this string, so we might as well set it. + length_.store(sv.length(), std::memory_order_release); + return true; +} + +auto operator==(zstring_view lhs, zstring_view rhs) -> bool +{ + return std::string_view{lhs} == std::string_view{rhs}; +} + +} // namespace routemon::util diff --git a/server/src/util.cppm b/server/src/util.cppm index bdccf10..2857fe5 100644 --- a/server/src/util.cppm +++ b/server/src/util.cppm @@ -13,7 +13,7 @@ struct overloaded : Ts... using Ts::operator()...; }; -export constexpr auto parse_double( +constexpr auto parse_double( std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional @@ -30,7 +30,7 @@ export constexpr auto parse_double( } } -export constexpr auto parse_float( +constexpr auto parse_float( std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional @@ -47,7 +47,7 @@ export constexpr auto parse_float( } } -export template +template class aolist : public std::enable_shared_from_this> { T v_; @@ -72,7 +72,7 @@ public: auto value() const noexcept -> T const& { return v_; } }; -export constexpr auto size_from_int(int x) -> std::optional +constexpr auto size_from_int(int x) -> std::optional { static_assert( sizeof(int) <= sizeof(std::size_t), @@ -82,7 +82,7 @@ export constexpr auto size_from_int(int x) -> std::optional return static_cast(x); } -export constexpr auto int_from_size(std::size_t x) -> std::optional +constexpr auto int_from_size(std::size_t x) -> std::optional { constexpr auto int_max = size_from_int(std::numeric_limits::max()); static_assert(int_max.has_value()); @@ -91,7 +91,7 @@ export constexpr auto int_from_size(std::size_t x) -> std::optional return static_cast(x); } -export class zstring_view +class zstring_view { char const* s_; std::size_t length_; @@ -107,13 +107,16 @@ public: { } - auto length() const -> std::size_t { return length_; } + constexpr auto length() const -> std::size_t { return length_; } - auto c_str() const -> char const* { return s_; } + constexpr auto c_str() const -> char const* { return s_; } - operator std::string_view() const { return std::string_view{s_, length_}; } + constexpr operator std::string_view() const + { + return std::string_view{s_, length_}; + } - operator char const*() const { return s_; } + constexpr operator char const*() const { return s_; } }; // View for null-terminated strings for which we might not @@ -123,7 +126,7 @@ public: // // It is undefined behavior to assign to a lazy_zstring_view when // it is in use by other threads. -export class lazy_zstring_view +class lazy_zstring_view { static constexpr auto unset_length = std::numeric_limits::max(); @@ -136,70 +139,21 @@ public: : s_{s}, length_{s ? unset_length : 0} { } + lazy_zstring_view(lazy_zstring_view const& sv) noexcept; + lazy_zstring_view(lazy_zstring_view&& sv) noexcept; ~lazy_zstring_view() = default; - lazy_zstring_view(lazy_zstring_view const& sv) noexcept - : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} - { - } - - lazy_zstring_view(lazy_zstring_view&& sv) noexcept - : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} - { - } - - auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& - { - if (this != &rhs) - { - s_ = rhs.s_; - length_.store( - rhs.length_.load(std::memory_order_acquire), - std::memory_order_release); - } - return *this; - } - - auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& - { - return *this = rhs; // use the copy assignment operator - } - - auto length() const noexcept -> std::size_t - { - if (auto v = length_.load(std::memory_order_acquire); v != unset_length) - return v; - auto l = std::char_traits::length(s_); - length_.store(l, std::memory_order_release); - return l; - } - - auto c_str() const noexcept -> char const* { return s_; } + auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view&; + auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view&; - operator std::string_view() const noexcept - { - return std::string_view{s_, length()}; - } + auto length() const noexcept -> std::size_t; + auto c_str() const noexcept -> char const*; - operator char const*() const noexcept { return s_; } + operator std::string_view() const noexcept; + operator char const*() const noexcept; - auto operator==(std::string_view sv) const noexcept -> bool - { - if (auto v = length_.load(std::memory_order_acquire); v != unset_length) - if (sv.length() != v) - return false; - auto res = std::char_traits::compare(s_, sv.data(), sv.length()); - if (res != 0) - return false; - // Strings are equal for sv.length() characters. - if (s_[sv.length()] != '\0') - return false; - // Strings are actually equal, and we have just found out the - // length of this string, so we might as well set it. - length_.store(sv.length(), std::memory_order_release); - return true; - } + auto operator==(std::string_view sv) const noexcept -> bool; }; constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept @@ -208,12 +162,9 @@ constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept return zstring_view{s, length}; } -auto operator==(zstring_view lhs, zstring_view rhs) -> bool -{ - return std::string_view{lhs} == std::string_view{rhs}; -} +auto operator==(zstring_view lhs, zstring_view rhs) -> bool; -export constexpr auto split_on(std::string_view s, char c) +constexpr auto split_on(std::string_view s, char c) -> std::pair> { if (auto i = s.find(c); i != std::string_view::npos) @@ -221,10 +172,10 @@ export constexpr auto split_on(std::string_view s, char c) return std::make_pair(s, std::nullopt); } -export template +template class not_null; -export template +template class not_null { T* p_; @@ -264,10 +215,10 @@ public: auto operator->() const noexcept -> T* { return p_; } }; -export template +template explicit not_null(T*) -> not_null; -export template <> +template <> class not_null { lazy_zstring_view s_; @@ -285,6 +236,6 @@ public: operator std::string_view() const noexcept { return s_; } operator char const*() const noexcept { return s_; } }; -export explicit not_null(lazy_zstring_view s) -> not_null; +explicit not_null(lazy_zstring_view s) -> not_null; } // namespace routemon::util diff --git a/server/src/xml.cppm b/server/src/xml.cppm index 4648047..ffaf159 100644 --- a/server/src/xml.cppm +++ b/server/src/xml.cppm @@ -201,7 +201,8 @@ struct eof_event using event = std::variant< start_element_event, end_element_event, character_data_event, - processing_instructions_event, xml_decl_event, eof_event>; + processing_instructions_event, xml_decl_event, eof_event +>; template concept event_type = requires(event ev) { std::get(ev); }; @@ -522,6 +523,7 @@ public: } constexpr auto await_resume() const noexcept -> void { return; } }; + if (this->continuation()) { return awaiter{this->continuation()}; @@ -548,16 +550,19 @@ public: { return !executor_->advance_flag(); } + auto await_suspend(std::coroutine_handle> h) -> void { executor_->set_continuation(h.promise().base_handle()); } + [[nodiscard]] auto await_resume() const -> event { assert(executor_->event()); return executor_->event().value(); } }; + return awaiter{req.executor}; } @@ -572,6 +577,7 @@ public: { return false; } + auto await_suspend(std::coroutine_handle> h) -> std::coroutine_handle<> { @@ -580,6 +586,7 @@ public: next_->set_continuation(h.promise().base_handle()); return next_->handle(); } + auto await_resume() -> U { // Promise is still valid since coroutine frame is still alive @@ -599,6 +606,7 @@ public: } } }; + return awaiter{util::not_null{&coro.promise()}}; } }; @@ -717,14 +725,16 @@ auto expect_element( co_await expect_end_element(e, want); co_await ignore_whitespace(e); co_return std::forward< - parser_invoke_result_t>(res); + parser_invoke_result_t + >(res); } auto allow_element( executor_ref e, qname_view want, parser_invocable auto p) -> parser>> + parser_invoke_result_t + >> { co_await ignore_whitespace(e); if (auto mattrs = co_await allow_start_element(e, want)) @@ -734,8 +744,8 @@ auto allow_element( co_await ignore_whitespace(e); co_return std::make_optional( std::forward< - parser_invoke_result_t>( - res)); + parser_invoke_result_t + >(res)); } co_return std::nullopt; } @@ -744,7 +754,8 @@ auto allow_element( executor_ref e, qname_view want, parser_invocable auto p) -> parser requires std::is_void_v< - parser_invoke_result_t> + parser_invoke_result_t + > { co_await ignore_whitespace(e); if (auto mattrs = co_await allow_start_element(e, want)) -- cgit v1.3