diff options
| author | Rutger Broekhoff | 2026-08-29 12:01:42 +0200 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-08-29 12:01:42 +0200 |
| commit | 52b3cd46c76fd4be18aeb8422a08a073484b9fad (patch) | |
| tree | b4f23957c096e6bce5369bb94a4e8e23de71761a /server/src | |
| parent | 35878b76049b9c3e752480e9f298b7e2da6fdf1f (diff) | |
| download | routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.tar.gz routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.zip | |
More module implementation partition units
Diffstat (limited to 'server/src')
34 files changed, 2505 insertions, 2099 deletions
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; | |||
| 5 | 5 | ||
| 6 | module routemon:api$impl; | 6 | module routemon:api$impl; |
| 7 | 7 | ||
| 8 | import std; | ||
| 9 | import :api; | 8 | import :api; |
| 10 | import :datex2; | ||
| 11 | import :geo; | ||
| 12 | import :gpx; | ||
| 13 | import :log; | ||
| 14 | import :req_ctx; | ||
| 15 | import :time; | ||
| 16 | import :trace; | ||
| 17 | 9 | ||
| 18 | namespace { | 10 | namespace { |
| 19 | 11 | ||
| @@ -255,8 +247,8 @@ auto handler::process_gpx(gpx::file&& gpx_file) | |||
| 255 | | views::transform( | 247 | | views::transform( |
| 256 | [](auto const& lsp) -> geo::linestring | 248 | [](auto const& lsp) -> geo::linestring |
| 257 | { return *lsp; }) | 249 | { return *lsp; }) |
| 258 | | std::ranges::to< | 250 | | std::ranges:: |
| 259 | std::vector<geo::linestring>>(), | 251 | to<std::vector<geo::linestring>>(), |
| 260 | }; | 252 | }; |
| 261 | }) | 253 | }) |
| 262 | | std::ranges::to<std::vector<relevant_road_closure>>(), | 254 | | std::ranges::to<std::vector<relevant_road_closure>>(), |
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 | |||
| 77 | { | 77 | { |
| 78 | using lse_index_value = std::tuple< | 78 | using lse_index_value = std::tuple< |
| 79 | geo::box, std::shared_ptr<geo::linestring>, | 79 | geo::box, std::shared_ptr<geo::linestring>, |
| 80 | std::shared_ptr<datex2::road_closure>>; | 80 | std::shared_ptr<datex2::road_closure> |
| 81 | >; | ||
| 81 | using p_index_value = | 82 | using p_index_value = |
| 82 | std::pair<geo::point, std::shared_ptr<datex2::road_closure>>; | 83 | std::pair<geo::point, std::shared_ptr<datex2::road_closure>>; |
| 83 | using lse_index = | 84 | 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 @@ | |||
| 1 | module routemon:database$impl; | ||
| 2 | |||
| 3 | import :database; | ||
| 4 | |||
| 5 | namespace routemon::database { | ||
| 6 | |||
| 7 | constexpr std::int64_t expected_database_version = 1; | ||
| 8 | |||
| 9 | auto open(std::string const& filename) -> std::shared_ptr<connection> | ||
| 10 | { | ||
| 11 | auto dbc = sqlite3::open(filename); | ||
| 12 | try | ||
| 13 | { | ||
| 14 | auto version = std::optional<std::int64_t>{}; | ||
| 15 | dbc.query("SELECT version FROM migration;").scan_single(version); | ||
| 16 | if (!version) | ||
| 17 | throw std::runtime_error{"failed to fetch database migration version"}; | ||
| 18 | if (version != expected_database_version) | ||
| 19 | { | ||
| 20 | throw std::runtime_error{std::format( | ||
| 21 | "database migration version ({}) does not match expected " | ||
| 22 | "version ({}), consider running migrations", | ||
| 23 | *version, expected_database_version)}; | ||
| 24 | } | ||
| 25 | } | ||
| 26 | catch (std::exception const& e) | ||
| 27 | { | ||
| 28 | throw std::runtime_error{std::format( | ||
| 29 | "failed to query database version: {}", e.what())}; | ||
| 30 | } | ||
| 31 | return std::shared_ptr<connection>{new connection{std::move(dbc)}}; | ||
| 32 | } | ||
| 33 | |||
| 34 | } // 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; | |||
| 5 | 5 | ||
| 6 | namespace routemon::database { | 6 | namespace routemon::database { |
| 7 | 7 | ||
| 8 | static constexpr std::int64_t expected_database_version = 1; | ||
| 9 | |||
| 10 | export class connection | 8 | export class connection |
| 11 | { | 9 | { |
| 12 | sqlite3::connection dbc_; | 10 | sqlite3::connection dbc_; |
| @@ -19,29 +17,6 @@ public: | |||
| 19 | // Nothing here yet | 17 | // Nothing here yet |
| 20 | }; | 18 | }; |
| 21 | 19 | ||
| 22 | export auto open(std::string const& filename) -> std::shared_ptr<connection> | 20 | export auto open(std::string const& filename) -> std::shared_ptr<connection>; |
| 23 | { | ||
| 24 | auto dbc = sqlite3::open(filename); | ||
| 25 | try | ||
| 26 | { | ||
| 27 | auto version = std::optional<std::int64_t>{}; | ||
| 28 | dbc.query("SELECT version FROM migration;").scan_single(version); | ||
| 29 | if (!version) | ||
| 30 | throw std::runtime_error{"failed to fetch database migration version"}; | ||
| 31 | if (version != expected_database_version) | ||
| 32 | { | ||
| 33 | throw std::runtime_error{std::format( | ||
| 34 | "database migration version ({}) does not match expected " | ||
| 35 | "version ({}), consider running migrations", | ||
| 36 | *version, expected_database_version)}; | ||
| 37 | } | ||
| 38 | } | ||
| 39 | catch (std::exception const& e) | ||
| 40 | { | ||
| 41 | throw std::runtime_error{std::format( | ||
| 42 | "failed to query database version: {}", e.what())}; | ||
| 43 | } | ||
| 44 | return std::shared_ptr<connection>{new connection{std::move(dbc)}}; | ||
| 45 | } | ||
| 46 | 21 | ||
| 47 | } // namespace routemon::database | 22 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry/algorithms/is_empty.hpp> | ||
| 4 | #include <boost/geometry/srs/epsg.hpp> | ||
| 5 | #include <boost/geometry/srs/transformation.hpp> | ||
| 6 | |||
| 7 | #include <pugixml.hpp> | ||
| 8 | |||
| 9 | module routemon:datex2$impl; | ||
| 10 | |||
| 11 | import :datex2; | ||
| 12 | |||
| 13 | using namespace std::literals::string_view_literals; | ||
| 14 | |||
| 15 | namespace routemon::datex2 { | ||
| 16 | |||
| 17 | auto parse_timestamp(char const* in) -> std::optional<time::timestamp> | ||
| 18 | { | ||
| 19 | auto res = time::timestamp{}; | ||
| 20 | auto is = std::istringstream{in}; | ||
| 21 | is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); | ||
| 22 | return is.fail() ? std::nullopt : std::make_optional(res); | ||
| 23 | } | ||
| 24 | |||
| 25 | auto loader::add_location_from_xml( | ||
| 26 | road_closure& rc, pugi::xml_node const& loc_xml) -> void | ||
| 27 | { | ||
| 28 | auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; | ||
| 29 | if (loc_xml_type == "loc:ItineraryByIndexedLocations") | ||
| 30 | { | ||
| 31 | for (auto const loc_cont_xml : | ||
| 32 | loc_xml.children("loc:locationContainedInItinerary")) | ||
| 33 | { | ||
| 34 | add_location_from_xml(rc, loc_cont_xml.child("loc:location")); | ||
| 35 | } | ||
| 36 | } | ||
| 37 | else if (loc_xml_type == "loc:LinearLocation" | ||
| 38 | || loc_xml_type == "loc:SingleRoadLinearLocation") | ||
| 39 | { | ||
| 40 | auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); | ||
| 41 | if (!loc_gml_xml) | ||
| 42 | return; | ||
| 43 | |||
| 44 | auto const srs_name = | ||
| 45 | std::string_view{loc_gml_xml.attribute("srsName").value()}; | ||
| 46 | if (srs_name != "WGS 84"sv) | ||
| 47 | { | ||
| 48 | warnings_.insert( | ||
| 49 | std::format("don't now how to handle the CRS {}", srs_name)); | ||
| 50 | return; | ||
| 51 | } | ||
| 52 | auto const pos_list_str = | ||
| 53 | std::string_view{loc_gml_xml.child_value("loc:posList")}; | ||
| 54 | // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn | ||
| 55 | |||
| 56 | auto ls = std::make_shared<geo::linestring>(); | ||
| 57 | |||
| 58 | auto lat_set = false; | ||
| 59 | auto lat = 0.0; | ||
| 60 | for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) | ||
| 61 | { | ||
| 62 | auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); | ||
| 63 | if (!mlat_or_long) | ||
| 64 | { | ||
| 65 | warnings_.insert( | ||
| 66 | std::format( | ||
| 67 | "failed to parse coordinate {:?}", | ||
| 68 | std::string_view{lat_or_long_str})); | ||
| 69 | return; | ||
| 70 | } | ||
| 71 | |||
| 72 | if (!lat_set) | ||
| 73 | { | ||
| 74 | lat = *mlat_or_long; | ||
| 75 | lat_set = true; | ||
| 76 | } | ||
| 77 | else | ||
| 78 | { | ||
| 79 | bgeo::append(*ls, geo::point{*mlat_or_long, lat}); | ||
| 80 | lat = 0; | ||
| 81 | lat_set = false; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | if (bgeo::is_empty(*ls)) | ||
| 86 | { | ||
| 87 | warnings_.emplace("empty line string in data set"); | ||
| 88 | return; | ||
| 89 | } | ||
| 90 | |||
| 91 | rc.relevant_line_strings.push_back(ls); | ||
| 92 | } | ||
| 93 | else if (loc_xml_type == "loc:PointLocation") | ||
| 94 | { | ||
| 95 | auto const& coords_xml = | ||
| 96 | loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); | ||
| 97 | if (!coords_xml) | ||
| 98 | return; | ||
| 99 | |||
| 100 | auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 101 | auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 102 | if (!mlat || !mlon) | ||
| 103 | { | ||
| 104 | warnings_.emplace("failed to parse PointLocation coordinates"); | ||
| 105 | return; | ||
| 106 | } | ||
| 107 | |||
| 108 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | ||
| 109 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 110 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | ||
| 111 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 112 | |||
| 113 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 114 | auto coords_wgs84 = geo::point{}; | ||
| 115 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 116 | |||
| 117 | rc.relevant_points.push_back(coords_wgs84); | ||
| 118 | } | ||
| 119 | else | ||
| 120 | { | ||
| 121 | warnings_.insert( | ||
| 122 | std::format( | ||
| 123 | "don't know how to hande location of type {}, ignoring", | ||
| 124 | loc_xml.attribute("xsi:type").value())); | ||
| 125 | return; | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | auto loader::handle_road_or_carriageway_or_lane_management( | ||
| 130 | pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) | ||
| 131 | -> std::optional<std::shared_ptr<road_closure>> | ||
| 132 | { | ||
| 133 | auto const type = std::string_view{ | ||
| 134 | record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value() | ||
| 135 | }; | ||
| 136 | if (type != "carriagewayClosures" && type != "roadClosed") | ||
| 137 | // TODO: checken of er nog andere types fietsers de doorgang zouden | ||
| 138 | // kunnen blokkeren? | ||
| 139 | return std::nullopt; | ||
| 140 | |||
| 141 | auto const& restricted_vehicle_types_xml = | ||
| 142 | record_xml.child("sit:forVehiclesWithCharacteristicsOf"); | ||
| 143 | bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); | ||
| 144 | for (auto const vehicle_type_xml : | ||
| 145 | restricted_vehicle_types_xml.children("com:vehicleType")) | ||
| 146 | { | ||
| 147 | auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; | ||
| 148 | if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" | ||
| 149 | || vehicle_type == "unknown" || vehicle_type == "other") | ||
| 150 | { | ||
| 151 | likely_restriction_for_bikes = true; | ||
| 152 | } | ||
| 153 | } | ||
| 154 | if (!likely_restriction_for_bikes) | ||
| 155 | return std::nullopt; | ||
| 156 | |||
| 157 | //---- Check if within the defined validity period | ||
| 158 | |||
| 159 | auto validity = std::optional<time::period_seq>{}; | ||
| 160 | auto const& validity_xml = record_xml.child("sit:validity"); | ||
| 161 | if (validity_xml | ||
| 162 | && validity_xml.child_value("com:validityStatus") | ||
| 163 | == "definedByValidityTimeSpec"sv) | ||
| 164 | { | ||
| 165 | auto const& validity_spec_xml = | ||
| 166 | validity_xml.child("com:validityTimeSpecification"); | ||
| 167 | |||
| 168 | auto valid_periods = std::vector<time::period>{}; | ||
| 169 | auto exception_periods = std::vector<time::period>{}; | ||
| 170 | |||
| 171 | // TODO: com:overallEndTime may be missing (according to the DATEX | ||
| 172 | // II v3 data model) | ||
| 173 | auto const overall_start_time = | ||
| 174 | parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); | ||
| 175 | auto const overall_end_time = | ||
| 176 | parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); | ||
| 177 | if (overall_start_time && overall_end_time | ||
| 178 | && *overall_start_time < *overall_end_time) | ||
| 179 | { | ||
| 180 | valid_periods.emplace_back(*overall_start_time, *overall_end_time); | ||
| 181 | |||
| 182 | for (auto const valid_period_xml : | ||
| 183 | validity_xml.children("com:validPeriod")) | ||
| 184 | { | ||
| 185 | auto const start_of_period = | ||
| 186 | parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); | ||
| 187 | auto const end_of_period = | ||
| 188 | parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); | ||
| 189 | if (start_of_period && end_of_period | ||
| 190 | && *start_of_period < *end_of_period) | ||
| 191 | { | ||
| 192 | valid_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | for (auto const exception_period_xml : | ||
| 196 | validity_xml.children("com:exceptionPeriod")) | ||
| 197 | { | ||
| 198 | auto const start_of_period = parse_timestamp( | ||
| 199 | exception_period_xml.child_value("com:startOfPeriod")); | ||
| 200 | auto const end_of_period = parse_timestamp( | ||
| 201 | exception_period_xml.child_value("com:endOfPeriod")); | ||
| 202 | if (start_of_period && end_of_period | ||
| 203 | && *start_of_period < *end_of_period) | ||
| 204 | { | ||
| 205 | exception_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | validity = | ||
| 210 | time::period_seq{valid_periods.begin(), valid_periods.end()}.except( | ||
| 211 | time::period_seq{ | ||
| 212 | exception_periods.begin(), exception_periods.end() | ||
| 213 | }); | ||
| 214 | } | ||
| 215 | else | ||
| 216 | { | ||
| 217 | warnings_.insert( | ||
| 218 | std::format( | ||
| 219 | "invalid overall start / end time (start time: {}, end " | ||
| 220 | "time: {})", | ||
| 221 | validity_spec_xml.child_value("com:overallStartTime"), | ||
| 222 | validity_spec_xml.child_value("com:overallEndTime"))); | ||
| 223 | return std::nullopt; | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | //---- Try to extract the location info | ||
| 228 | |||
| 229 | auto rc = std::make_shared<road_closure>(std::move(parent), validity); | ||
| 230 | add_location_from_xml(*rc, record_xml.child("sit:locationReference")); | ||
| 231 | return rc; | ||
| 232 | } | ||
| 233 | |||
| 234 | auto loader::load_situation_publication(std::string const& filename) | ||
| 235 | -> situation_publication | ||
| 236 | { | ||
| 237 | auto doc = pugi::xml_document{}; | ||
| 238 | if (auto result = doc.load_file(filename.c_str()); !result) | ||
| 239 | { | ||
| 240 | throw std::runtime_error{result.description()}; | ||
| 241 | } | ||
| 242 | auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); | ||
| 243 | auto mpublication_time = | ||
| 244 | parse_timestamp(payload_xml.child_value("com:publicationTime")); | ||
| 245 | if (!mpublication_time) | ||
| 246 | throw std::runtime_error{ | ||
| 247 | "provided publication does not name publication time" | ||
| 248 | }; | ||
| 249 | |||
| 250 | auto situations = std::vector<std::shared_ptr<situation>>{}; | ||
| 251 | for (auto const sit_xml : payload_xml.children("sit:situation")) | ||
| 252 | { | ||
| 253 | auto id = std::string_view{sit_xml.attribute("id").value()}; | ||
| 254 | |||
| 255 | auto const sit = std::make_shared<situation>(std::string{id}); | ||
| 256 | situations.push_back(sit); | ||
| 257 | |||
| 258 | auto const& header_info_xml = sit_xml.child("sit:headerInformation"); | ||
| 259 | if (header_info_xml.child_value("com:informationStatus") != "real"sv) | ||
| 260 | continue; | ||
| 261 | |||
| 262 | for (auto const record_xml : sit_xml.children("sit:situationRecord")) | ||
| 263 | { | ||
| 264 | auto const record_type = | ||
| 265 | std::string_view{record_xml.attribute("xsi:type").value()}; | ||
| 266 | auto const primary_record_types = std::unordered_set<std::string_view>{ | ||
| 267 | "sit:Roadworks", | ||
| 268 | /* { */ "sit:MaintenanceWorks", | ||
| 269 | /* | */ "sit:ConstructionWorks", | ||
| 270 | /* } */ | ||
| 271 | "sit:Obstruction", | ||
| 272 | /* { */ "sit:EnvironmentalObstruction", | ||
| 273 | /* | */ "sit:GeneralObstruction", | ||
| 274 | /* | */ "sit:InfrastructureDamageObstruction", | ||
| 275 | /* } */ | ||
| 276 | "sit:Activity", | ||
| 277 | /* { */ "sit:PublicEvent", | ||
| 278 | /* } */ | ||
| 279 | }; | ||
| 280 | |||
| 281 | if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") | ||
| 282 | { | ||
| 283 | if (auto rc = | ||
| 284 | handle_road_or_carriageway_or_lane_management(record_xml, sit)) | ||
| 285 | { | ||
| 286 | sit->road_closures.push_back(*rc); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | else if (primary_record_types.contains(record_type)) | ||
| 290 | { | ||
| 291 | for (auto const comment_xml : | ||
| 292 | record_xml.children("sit:generalPublicComment")) | ||
| 293 | { | ||
| 294 | // if | ||
| 295 | // (comment_xml.child_value("sit:commentType") | ||
| 296 | // == "internalNote"sv) { | ||
| 297 | auto candidate = std::optional< | ||
| 298 | std::pair<std::string_view, std::string_view> | ||
| 299 | >{}; // (text, language) | ||
| 300 | for (auto const comment_value_xml : comment_xml.child("sit:comment") | ||
| 301 | .child("com:values") | ||
| 302 | .children("com:value")) | ||
| 303 | { | ||
| 304 | if (!candidate | ||
| 305 | || comment_value_xml.attribute("lang").value() == "nl"sv | ||
| 306 | || (candidate->second != "nl"sv | ||
| 307 | && comment_value_xml.attribute("lang").value() == "nl"sv)) | ||
| 308 | { | ||
| 309 | candidate = std::make_pair( | ||
| 310 | comment_value_xml.child_value(), | ||
| 311 | comment_value_xml.attribute("lang").value()); | ||
| 312 | } | ||
| 313 | } | ||
| 314 | if (candidate) | ||
| 315 | { | ||
| 316 | auto already_present = false; | ||
| 317 | for (auto const& comment : sit->comments) | ||
| 318 | already_present = already_present || comment == candidate->first; | ||
| 319 | if (!already_present) | ||
| 320 | { | ||
| 321 | sit->comments.emplace_back(candidate->first); | ||
| 322 | } | ||
| 323 | } | ||
| 324 | // } | ||
| 325 | } | ||
| 326 | |||
| 327 | if (auto const location_ref_xml = | ||
| 328 | record_xml.child("sit:locationReference")) | ||
| 329 | { | ||
| 330 | if (location_ref_xml.attribute("xsi:type").value() | ||
| 331 | == "loc:PointLocation"sv) | ||
| 332 | { | ||
| 333 | if (auto const coords_xml = | ||
| 334 | location_ref_xml.child("loc:pointByCoordinates") | ||
| 335 | .child("loc:pointCoordinates")) | ||
| 336 | { | ||
| 337 | auto const mlat = | ||
| 338 | util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 339 | auto const mlon = | ||
| 340 | util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 341 | if (mlat && mlon) | ||
| 342 | { | ||
| 343 | // Vaag genoeg zegt NDW dat het hier om WGS | ||
| 344 | // 84 gaat: | ||
| 345 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 346 | // maar heeft het UML-model van DATEX II v3 | ||
| 347 | // het over ETRS 89: | ||
| 348 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 349 | |||
| 350 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 351 | auto coords_wgs84 = geo::point{}; | ||
| 352 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 353 | |||
| 354 | if (!sit->location) | ||
| 355 | { | ||
| 356 | sit->location = coords_wgs84; | ||
| 357 | } | ||
| 358 | } | ||
| 359 | } | ||
| 360 | } | ||
| 361 | } | ||
| 362 | } | ||
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 366 | return { | ||
| 367 | .publication_time = *mpublication_time, | ||
| 368 | .situations = situations, | ||
| 369 | }; | ||
| 370 | } | ||
| 371 | |||
| 372 | auto loader::warnings() const -> std::multiset<std::string> const& | ||
| 373 | { | ||
| 374 | return warnings_; | ||
| 375 | } | ||
| 376 | |||
| 377 | } // 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 | |||
| 42 | std::vector<std::shared_ptr<situation>> situations; | 42 | std::vector<std::shared_ptr<situation>> situations; |
| 43 | }; | 43 | }; |
| 44 | 44 | ||
| 45 | auto parse_timestamp(char const* in) -> std::optional<time::timestamp> | ||
| 46 | { | ||
| 47 | auto res = time::timestamp{}; | ||
| 48 | auto is = std::istringstream{in}; | ||
| 49 | is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); | ||
| 50 | return is.fail() ? std::nullopt : std::make_optional(res); | ||
| 51 | } | ||
| 52 | |||
| 53 | export class loader | 45 | export class loader |
| 54 | { | 46 | { |
| 55 | // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) | 47 | // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) |
| 56 | bgeo::srs::transformation< | 48 | bgeo::srs:: |
| 57 | bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> | 49 | transformation<bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> |
| 58 | etrs89_to_wgs84_{}; | 50 | etrs89_to_wgs84_{}; |
| 59 | 51 | ||
| 60 | std::multiset<std::string> warnings_; | 52 | std::multiset<std::string> warnings_; |
| 61 | 53 | ||
| 62 | auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) | 54 | auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) |
| 63 | -> void | 55 | -> void; |
| 64 | { | ||
| 65 | auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; | ||
| 66 | if (loc_xml_type == "loc:ItineraryByIndexedLocations") | ||
| 67 | { | ||
| 68 | for (auto const loc_cont_xml : | ||
| 69 | loc_xml.children("loc:locationContainedInItinerary")) | ||
| 70 | { | ||
| 71 | add_location_from_xml(rc, loc_cont_xml.child("loc:location")); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | else if (loc_xml_type == "loc:LinearLocation" | ||
| 75 | || loc_xml_type == "loc:SingleRoadLinearLocation") | ||
| 76 | { | ||
| 77 | auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); | ||
| 78 | if (!loc_gml_xml) | ||
| 79 | return; | ||
| 80 | |||
| 81 | auto const srs_name = | ||
| 82 | std::string_view{loc_gml_xml.attribute("srsName").value()}; | ||
| 83 | if (srs_name != "WGS 84"sv) | ||
| 84 | { | ||
| 85 | warnings_.insert( | ||
| 86 | std::format("don't now how to handle the CRS {}", srs_name)); | ||
| 87 | return; | ||
| 88 | } | ||
| 89 | auto const pos_list_str = | ||
| 90 | std::string_view{loc_gml_xml.child_value("loc:posList")}; | ||
| 91 | // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn | ||
| 92 | |||
| 93 | auto ls = std::make_shared<geo::linestring>(); | ||
| 94 | |||
| 95 | auto lat_set = false; | ||
| 96 | auto lat = 0.0; | ||
| 97 | for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) | ||
| 98 | { | ||
| 99 | auto mlat_or_long = | ||
| 100 | util::parse_double(std::string_view{lat_or_long_str}); | ||
| 101 | if (!mlat_or_long) | ||
| 102 | { | ||
| 103 | warnings_.insert( | ||
| 104 | std::format( | ||
| 105 | "failed to parse coordinate {:?}", | ||
| 106 | std::string_view{lat_or_long_str})); | ||
| 107 | return; | ||
| 108 | } | ||
| 109 | |||
| 110 | if (!lat_set) | ||
| 111 | { | ||
| 112 | lat = *mlat_or_long; | ||
| 113 | lat_set = true; | ||
| 114 | } | ||
| 115 | else | ||
| 116 | { | ||
| 117 | bgeo::append(*ls, geo::point{*mlat_or_long, lat}); | ||
| 118 | lat = 0; | ||
| 119 | lat_set = false; | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | if (bgeo::is_empty(*ls)) | ||
| 124 | { | ||
| 125 | warnings_.emplace("empty line string in data set"); | ||
| 126 | return; | ||
| 127 | } | ||
| 128 | |||
| 129 | rc.relevant_line_strings.push_back(ls); | ||
| 130 | } | ||
| 131 | else if (loc_xml_type == "loc:PointLocation") | ||
| 132 | { | ||
| 133 | auto const& coords_xml = | ||
| 134 | loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); | ||
| 135 | if (!coords_xml) | ||
| 136 | return; | ||
| 137 | |||
| 138 | auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 139 | auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 140 | if (!mlat || !mlon) | ||
| 141 | { | ||
| 142 | warnings_.emplace("failed to parse PointLocation coordinates"); | ||
| 143 | return; | ||
| 144 | } | ||
| 145 | |||
| 146 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | ||
| 147 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 148 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | ||
| 149 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 150 | |||
| 151 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 152 | auto coords_wgs84 = geo::point{}; | ||
| 153 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 154 | |||
| 155 | rc.relevant_points.push_back(coords_wgs84); | ||
| 156 | } | ||
| 157 | else | ||
| 158 | { | ||
| 159 | warnings_.insert( | ||
| 160 | std::format( | ||
| 161 | "don't know how to hande location of type {}, ignoring", | ||
| 162 | loc_xml.attribute("xsi:type").value())); | ||
| 163 | return; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | 56 | ||
| 167 | auto handle_road_or_carriageway_or_lane_management( | 57 | auto handle_road_or_carriageway_or_lane_management( |
| 168 | pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) | 58 | pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) |
| 169 | -> std::optional<std::shared_ptr<road_closure>> | 59 | -> std::optional<std::shared_ptr<road_closure>>; |
| 170 | { | ||
| 171 | auto const type = | ||
| 172 | std::string_view{record_xml | ||
| 173 | .child("sit:roadOrCarriagewayOrLaneManagementType") | ||
| 174 | .child_value()}; | ||
| 175 | if (type != "carriagewayClosures" && type != "roadClosed") | ||
| 176 | // TODO: checken of er nog andere types fietsers de doorgang zouden | ||
| 177 | // kunnen blokkeren? | ||
| 178 | return std::nullopt; | ||
| 179 | |||
| 180 | auto const& restricted_vehicle_types_xml = | ||
| 181 | record_xml.child("sit:forVehiclesWithCharacteristicsOf"); | ||
| 182 | bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); | ||
| 183 | for (auto const vehicle_type_xml : | ||
| 184 | restricted_vehicle_types_xml.children("com:vehicleType")) | ||
| 185 | { | ||
| 186 | auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; | ||
| 187 | if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" | ||
| 188 | || vehicle_type == "unknown" || vehicle_type == "other") | ||
| 189 | { | ||
| 190 | likely_restriction_for_bikes = true; | ||
| 191 | } | ||
| 192 | } | ||
| 193 | if (!likely_restriction_for_bikes) | ||
| 194 | return std::nullopt; | ||
| 195 | |||
| 196 | //---- Check if within the defined validity period | ||
| 197 | |||
| 198 | auto validity = std::optional<time::period_seq>{}; | ||
| 199 | auto const& validity_xml = record_xml.child("sit:validity"); | ||
| 200 | if (validity_xml | ||
| 201 | && validity_xml.child_value("com:validityStatus") | ||
| 202 | == "definedByValidityTimeSpec"sv) | ||
| 203 | { | ||
| 204 | auto const& validity_spec_xml = | ||
| 205 | validity_xml.child("com:validityTimeSpecification"); | ||
| 206 | |||
| 207 | auto valid_periods = std::vector<time::period>{}; | ||
| 208 | auto exception_periods = std::vector<time::period>{}; | ||
| 209 | |||
| 210 | // TODO: com:overallEndTime may be missing (according to the DATEX | ||
| 211 | // II v3 data model) | ||
| 212 | auto const overall_start_time = parse_timestamp( | ||
| 213 | validity_spec_xml.child_value("com:overallStartTime")); | ||
| 214 | auto const overall_end_time = | ||
| 215 | parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); | ||
| 216 | if (overall_start_time && overall_end_time | ||
| 217 | && *overall_start_time < *overall_end_time) | ||
| 218 | { | ||
| 219 | valid_periods.emplace_back(*overall_start_time, *overall_end_time); | ||
| 220 | |||
| 221 | for (auto const valid_period_xml : | ||
| 222 | validity_xml.children("com:validPeriod")) | ||
| 223 | { | ||
| 224 | auto const start_of_period = parse_timestamp( | ||
| 225 | valid_period_xml.child_value("com:startOfPeriod")); | ||
| 226 | auto const end_of_period = | ||
| 227 | parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); | ||
| 228 | if (start_of_period && end_of_period | ||
| 229 | && *start_of_period < *end_of_period) | ||
| 230 | { | ||
| 231 | valid_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 232 | } | ||
| 233 | } | ||
| 234 | for (auto const exception_period_xml : | ||
| 235 | validity_xml.children("com:exceptionPeriod")) | ||
| 236 | { | ||
| 237 | auto const start_of_period = parse_timestamp( | ||
| 238 | exception_period_xml.child_value("com:startOfPeriod")); | ||
| 239 | auto const end_of_period = parse_timestamp( | ||
| 240 | exception_period_xml.child_value("com:endOfPeriod")); | ||
| 241 | if (start_of_period && end_of_period | ||
| 242 | && *start_of_period < *end_of_period) | ||
| 243 | { | ||
| 244 | exception_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | validity = | ||
| 249 | time::period_seq{valid_periods.begin(), valid_periods.end()}.except( | ||
| 250 | time::period_seq{ | ||
| 251 | exception_periods.begin(), exception_periods.end() | ||
| 252 | }); | ||
| 253 | } | ||
| 254 | else | ||
| 255 | { | ||
| 256 | warnings_.insert( | ||
| 257 | std::format( | ||
| 258 | "invalid overall start / end time (start time: {}, end " | ||
| 259 | "time: {})", | ||
| 260 | validity_spec_xml.child_value("com:overallStartTime"), | ||
| 261 | validity_spec_xml.child_value("com:overallEndTime"))); | ||
| 262 | return std::nullopt; | ||
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | //---- Try to extract the location info | ||
| 267 | |||
| 268 | auto rc = std::make_shared<road_closure>(std::move(parent), validity); | ||
| 269 | add_location_from_xml(*rc, record_xml.child("sit:locationReference")); | ||
| 270 | return rc; | ||
| 271 | } | ||
| 272 | 60 | ||
| 273 | public: | 61 | public: |
| 274 | [[nodiscard]] auto load_situation_publication(std::string const& filename) | 62 | [[nodiscard]] auto load_situation_publication(std::string const& filename) |
| 275 | -> situation_publication | 63 | -> situation_publication; |
| 276 | { | 64 | [[nodiscard]] auto warnings() const -> std::multiset<std::string> const&; |
| 277 | auto doc = pugi::xml_document{}; | ||
| 278 | if (auto result = doc.load_file(filename.c_str()); !result) | ||
| 279 | { | ||
| 280 | throw std::runtime_error{result.description()}; | ||
| 281 | } | ||
| 282 | auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); | ||
| 283 | auto mpublication_time = | ||
| 284 | parse_timestamp(payload_xml.child_value("com:publicationTime")); | ||
| 285 | if (!mpublication_time) | ||
| 286 | throw std::runtime_error{ | ||
| 287 | "provided publication does not name publication time" | ||
| 288 | }; | ||
| 289 | |||
| 290 | auto situations = std::vector<std::shared_ptr<situation>>{}; | ||
| 291 | for (auto const sit_xml : payload_xml.children("sit:situation")) | ||
| 292 | { | ||
| 293 | auto id = std::string_view{sit_xml.attribute("id").value()}; | ||
| 294 | |||
| 295 | auto const sit = std::make_shared<situation>(std::string{id}); | ||
| 296 | situations.push_back(sit); | ||
| 297 | |||
| 298 | auto const& header_info_xml = sit_xml.child("sit:headerInformation"); | ||
| 299 | if (header_info_xml.child_value("com:informationStatus") != "real"sv) | ||
| 300 | continue; | ||
| 301 | |||
| 302 | for (auto const record_xml : sit_xml.children("sit:situationRecord")) | ||
| 303 | { | ||
| 304 | auto const record_type = | ||
| 305 | std::string_view{record_xml.attribute("xsi:type").value()}; | ||
| 306 | auto const primary_record_types = std::unordered_set<std::string_view>{ | ||
| 307 | "sit:Roadworks", | ||
| 308 | /* { */ "sit:MaintenanceWorks", | ||
| 309 | /* | */ "sit:ConstructionWorks", | ||
| 310 | /* } */ | ||
| 311 | "sit:Obstruction", | ||
| 312 | /* { */ "sit:EnvironmentalObstruction", | ||
| 313 | /* | */ "sit:GeneralObstruction", | ||
| 314 | /* | */ "sit:InfrastructureDamageObstruction", | ||
| 315 | /* } */ | ||
| 316 | "sit:Activity", | ||
| 317 | /* { */ "sit:PublicEvent", | ||
| 318 | /* } */ | ||
| 319 | }; | ||
| 320 | |||
| 321 | if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") | ||
| 322 | { | ||
| 323 | if (auto rc = handle_road_or_carriageway_or_lane_management( | ||
| 324 | record_xml, sit)) | ||
| 325 | { | ||
| 326 | sit->road_closures.push_back(*rc); | ||
| 327 | } | ||
| 328 | } | ||
| 329 | else if (primary_record_types.contains(record_type)) | ||
| 330 | { | ||
| 331 | for (auto const comment_xml : | ||
| 332 | record_xml.children("sit:generalPublicComment")) | ||
| 333 | { | ||
| 334 | // if | ||
| 335 | // (comment_xml.child_value("sit:commentType") | ||
| 336 | // == "internalNote"sv) { | ||
| 337 | auto candidate = std::optional<std::pair< | ||
| 338 | std::string_view, std::string_view>>{}; // (text, language) | ||
| 339 | for (auto const comment_value_xml : comment_xml.child("sit:comment") | ||
| 340 | .child("com:values") | ||
| 341 | .children("com:value")) | ||
| 342 | { | ||
| 343 | if (!candidate | ||
| 344 | || comment_value_xml.attribute("lang").value() == "nl"sv | ||
| 345 | || (candidate->second != "nl"sv | ||
| 346 | && comment_value_xml.attribute("lang").value() == "nl"sv)) | ||
| 347 | { | ||
| 348 | candidate = std::make_pair( | ||
| 349 | comment_value_xml.child_value(), | ||
| 350 | comment_value_xml.attribute("lang").value()); | ||
| 351 | } | ||
| 352 | } | ||
| 353 | if (candidate) | ||
| 354 | { | ||
| 355 | auto already_present = false; | ||
| 356 | for (auto const& comment : sit->comments) | ||
| 357 | already_present = | ||
| 358 | already_present || comment == candidate->first; | ||
| 359 | if (!already_present) | ||
| 360 | { | ||
| 361 | sit->comments.emplace_back(candidate->first); | ||
| 362 | } | ||
| 363 | } | ||
| 364 | // } | ||
| 365 | } | ||
| 366 | |||
| 367 | if (auto const location_ref_xml = | ||
| 368 | record_xml.child("sit:locationReference")) | ||
| 369 | { | ||
| 370 | if (location_ref_xml.attribute("xsi:type").value() | ||
| 371 | == "loc:PointLocation"sv) | ||
| 372 | { | ||
| 373 | if (auto const coords_xml = | ||
| 374 | location_ref_xml.child("loc:pointByCoordinates") | ||
| 375 | .child("loc:pointCoordinates")) | ||
| 376 | { | ||
| 377 | auto const mlat = | ||
| 378 | util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 379 | auto const mlon = | ||
| 380 | util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 381 | if (mlat && mlon) | ||
| 382 | { | ||
| 383 | // Vaag genoeg zegt NDW dat het hier om WGS | ||
| 384 | // 84 gaat: | ||
| 385 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 386 | // maar heeft het UML-model van DATEX II v3 | ||
| 387 | // het over ETRS 89: | ||
| 388 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 389 | |||
| 390 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 391 | auto coords_wgs84 = geo::point{}; | ||
| 392 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 393 | |||
| 394 | if (!sit->location) | ||
| 395 | { | ||
| 396 | sit->location = coords_wgs84; | ||
| 397 | } | ||
| 398 | } | ||
| 399 | } | ||
| 400 | } | ||
| 401 | } | ||
| 402 | } | ||
| 403 | } | ||
| 404 | } | ||
| 405 | |||
| 406 | return { | ||
| 407 | .publication_time = *mpublication_time, | ||
| 408 | .situations = situations, | ||
| 409 | }; | ||
| 410 | } | ||
| 411 | |||
| 412 | [[nodiscard]] auto warnings() const -> std::multiset<std::string> const& | ||
| 413 | { | ||
| 414 | return warnings_; | ||
| 415 | } | ||
| 416 | }; | 65 | }; |
| 417 | 66 | ||
| 418 | } // namespace routemon::datex2 | 67 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry.hpp> | ||
| 4 | |||
| 5 | module routemon:geo$impl; | ||
| 6 | |||
| 7 | import :geo; | ||
| 8 | |||
| 9 | namespace routemon::geo { | ||
| 10 | |||
| 11 | auto split_linestring_with_overlap_segments( | ||
| 12 | linestring const& ls, double max_split_distance_m, | ||
| 13 | std::vector<linestring>& append_to) -> void | ||
| 14 | { | ||
| 15 | if (bgeo::is_empty(ls)) | ||
| 16 | return; | ||
| 17 | |||
| 18 | auto current_ls = linestring{}; | ||
| 19 | auto current_ls_length = 0.0; | ||
| 20 | auto previous = std::optional<point>{}; | ||
| 21 | bgeo::for_each_point( | ||
| 22 | ls, | ||
| 23 | [&](point p) -> void | ||
| 24 | { | ||
| 25 | bgeo::append(current_ls, p); | ||
| 26 | if (previous) | ||
| 27 | { | ||
| 28 | auto d = bgeo::distance(*previous, p, vincenty_strategy()); | ||
| 29 | current_ls_length += d; | ||
| 30 | if (current_ls_length > max_split_distance_m) | ||
| 31 | { | ||
| 32 | append_to.push_back(std::move(current_ls)); | ||
| 33 | current_ls = linestring{*previous, p}; | ||
| 34 | current_ls_length = d; | ||
| 35 | } | ||
| 36 | } | ||
| 37 | previous = p; | ||
| 38 | }); | ||
| 39 | append_to.emplace_back(std::move(current_ls)); | ||
| 40 | } | ||
| 41 | |||
| 42 | } // 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<stype>; | |||
| 17 | 17 | ||
| 18 | auto split_linestring_with_overlap_segments( | 18 | auto split_linestring_with_overlap_segments( |
| 19 | linestring const& ls, double max_split_distance_m, | 19 | linestring const& ls, double max_split_distance_m, |
| 20 | std::vector<linestring>& append_to) -> void | 20 | std::vector<linestring>& append_to) -> void; |
| 21 | { | ||
| 22 | if (bgeo::is_empty(ls)) | ||
| 23 | return; | ||
| 24 | |||
| 25 | auto current_ls = linestring{}; | ||
| 26 | auto current_ls_length = 0.0; | ||
| 27 | auto previous = std::optional<point>{}; | ||
| 28 | bgeo::for_each_point( | ||
| 29 | ls, | ||
| 30 | [&](point p) -> void | ||
| 31 | { | ||
| 32 | bgeo::append(current_ls, p); | ||
| 33 | if (previous) | ||
| 34 | { | ||
| 35 | auto d = bgeo::distance(*previous, p, vincenty_strategy()); | ||
| 36 | current_ls_length += d; | ||
| 37 | if (current_ls_length > max_split_distance_m) | ||
| 38 | { | ||
| 39 | append_to.push_back(std::move(current_ls)); | ||
| 40 | current_ls = linestring{*previous, p}; | ||
| 41 | current_ls_length = d; | ||
| 42 | } | ||
| 43 | } | ||
| 44 | previous = p; | ||
| 45 | }); | ||
| 46 | append_to.emplace_back(std::move(current_ls)); | ||
| 47 | } | ||
| 48 | 21 | ||
| 49 | } // namespace routemon::geo | 22 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/asio/ssl.hpp> | ||
| 4 | #include <boost/beast/core.hpp> | ||
| 5 | #include <boost/beast/http.hpp> | ||
| 6 | |||
| 7 | module routemon:http.client$impl; | ||
| 8 | |||
| 9 | import :http.client; | ||
| 10 | |||
| 11 | namespace routemon::http { | ||
| 12 | |||
| 13 | client::client(net::io_context& ioc) : ioc_{ioc}, resolver_{ioc} | ||
| 14 | { | ||
| 15 | sslc_.set_default_verify_paths(); | ||
| 16 | sslc_.set_verify_mode( | ||
| 17 | net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); | ||
| 18 | } | ||
| 19 | |||
| 20 | } // 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 | |||
| 23 | tcp::resolver resolver_; | 23 | tcp::resolver resolver_; |
| 24 | 24 | ||
| 25 | public: | 25 | public: |
| 26 | explicit client(net::io_context& ioc) : ioc_{ioc}, resolver_{ioc} | 26 | explicit client(net::io_context& ioc); |
| 27 | { | ||
| 28 | sslc_.set_default_verify_paths(); | ||
| 29 | sslc_.set_verify_mode( | ||
| 30 | net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); | ||
| 31 | } | ||
| 32 | 27 | ||
| 33 | template <class ReqBody> | 28 | template <class ReqBody> |
| 34 | auto do_request(bhttp::request<ReqBody>& req) | 29 | auto do_request(bhttp::request<ReqBody>& req) |
| 35 | -> bhttp::response<bhttp::dynamic_body> | 30 | -> bhttp::response<bhttp::dynamic_body> |
| 36 | { | 31 | { |
| 32 | // TODO: call into non-template member ASAP. | ||
| 33 | // Consider using beast::message_generator. | ||
| 34 | |||
| 37 | auto stream = ssl::stream<beast::tcp_stream>{ioc_, sslc_}; | 35 | auto stream = ssl::stream<beast::tcp_stream>{ioc_, sslc_}; |
| 38 | 36 | ||
| 39 | auto host = std::string{req.at(bhttp::field::host)}; | 37 | 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/core.hpp> | ||
| 4 | #include <boost/beast/http.hpp> | ||
| 5 | |||
| 6 | module routemon:http.common$impl; | ||
| 7 | |||
| 8 | import :http.common; | ||
| 9 | |||
| 10 | namespace routemon::http { | ||
| 11 | |||
| 12 | supported_verb::supported_verb(supported_verb_t value) : value{value} {} | ||
| 13 | |||
| 14 | auto supported_verb::from(bhttp::verb v) -> std::optional<supported_verb> | ||
| 15 | { | ||
| 16 | switch (v) | ||
| 17 | { | ||
| 18 | case bhttp::verb::options: | ||
| 19 | return supported_verb::options; | ||
| 20 | case bhttp::verb::delete_: | ||
| 21 | return supported_verb::delete_; | ||
| 22 | case bhttp::verb::get: | ||
| 23 | return supported_verb::get; | ||
| 24 | case bhttp::verb::head: | ||
| 25 | return supported_verb::head; | ||
| 26 | case bhttp::verb::post: | ||
| 27 | return supported_verb::post; | ||
| 28 | case bhttp::verb::put: | ||
| 29 | return supported_verb::put; | ||
| 30 | default: | ||
| 31 | return std::nullopt; | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | supported_verb::operator bhttp::verb() const | ||
| 36 | { | ||
| 37 | switch (value) | ||
| 38 | { | ||
| 39 | case supported_verb::options: | ||
| 40 | return bhttp::verb::options; | ||
| 41 | case supported_verb::delete_: | ||
| 42 | return bhttp::verb::delete_; | ||
| 43 | case supported_verb::get: | ||
| 44 | return bhttp::verb::get; | ||
| 45 | case supported_verb::head: | ||
| 46 | return bhttp::verb::head; | ||
| 47 | case supported_verb::post: | ||
| 48 | return bhttp::verb::post; | ||
| 49 | case supported_verb::put: | ||
| 50 | return bhttp::verb::put; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | auto verb_set::with(supported_verb v) const noexcept -> verb_set | ||
| 55 | { | ||
| 56 | auto set = verb_set{*this}; | ||
| 57 | switch (v.value) | ||
| 58 | { | ||
| 59 | case supported_verb::delete_: | ||
| 60 | set.delete_ = true; | ||
| 61 | return set; | ||
| 62 | case supported_verb::get: | ||
| 63 | set.get = true; | ||
| 64 | return set; | ||
| 65 | case supported_verb::head: | ||
| 66 | set.head = true; | ||
| 67 | return set; | ||
| 68 | case supported_verb::post: | ||
| 69 | set.post = true; | ||
| 70 | return set; | ||
| 71 | case supported_verb::put: | ||
| 72 | set.put = true; | ||
| 73 | return set; | ||
| 74 | case supported_verb::options: | ||
| 75 | set.options = true; | ||
| 76 | return set; | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | auto verb_set::empty() const noexcept -> bool { return *this == verb_set{}; } | ||
| 81 | |||
| 82 | auto verb_set::to_string() const -> std::string | ||
| 83 | { | ||
| 84 | std::ostringstream ss; | ||
| 85 | bool wrote = false; | ||
| 86 | auto write = [&](bhttp::verb v) | ||
| 87 | { | ||
| 88 | if (wrote) | ||
| 89 | ss << ", "; | ||
| 90 | ss << v; | ||
| 91 | wrote = true; | ||
| 92 | }; | ||
| 93 | if (delete_) | ||
| 94 | write(bhttp::verb::delete_); | ||
| 95 | if (get) | ||
| 96 | write(bhttp::verb::get); | ||
| 97 | if (head) | ||
| 98 | write(bhttp::verb::head); | ||
| 99 | if (post) | ||
| 100 | write(bhttp::verb::post); | ||
| 101 | if (put) | ||
| 102 | write(bhttp::verb::put); | ||
| 103 | if (options) | ||
| 104 | write(bhttp::verb::options); | ||
| 105 | return ss.str(); | ||
| 106 | } | ||
| 107 | |||
| 108 | } // 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 | |||
| 51 | 51 | ||
| 52 | supported_verb_t value; | 52 | supported_verb_t value; |
| 53 | 53 | ||
| 54 | supported_verb(supported_verb_t value) : value{value} {} | 54 | supported_verb(supported_verb_t value); |
| 55 | 55 | ||
| 56 | static auto from(bhttp::verb v) -> std::optional<supported_verb> | 56 | static auto from(bhttp::verb v) -> std::optional<supported_verb>; |
| 57 | { | ||
| 58 | switch (v) | ||
| 59 | { | ||
| 60 | case bhttp::verb::options: | ||
| 61 | return supported_verb::options; | ||
| 62 | case bhttp::verb::delete_: | ||
| 63 | return supported_verb::delete_; | ||
| 64 | case bhttp::verb::get: | ||
| 65 | return supported_verb::get; | ||
| 66 | case bhttp::verb::head: | ||
| 67 | return supported_verb::head; | ||
| 68 | case bhttp::verb::post: | ||
| 69 | return supported_verb::post; | ||
| 70 | case bhttp::verb::put: | ||
| 71 | return supported_verb::put; | ||
| 72 | default: | ||
| 73 | return std::nullopt; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | 57 | ||
| 77 | operator bhttp::verb() const | 58 | operator bhttp::verb() const; |
| 78 | { | ||
| 79 | switch (value) | ||
| 80 | { | ||
| 81 | case supported_verb::options: | ||
| 82 | return bhttp::verb::options; | ||
| 83 | case supported_verb::delete_: | ||
| 84 | return bhttp::verb::delete_; | ||
| 85 | case supported_verb::get: | ||
| 86 | return bhttp::verb::get; | ||
| 87 | case supported_verb::head: | ||
| 88 | return bhttp::verb::head; | ||
| 89 | case supported_verb::post: | ||
| 90 | return bhttp::verb::post; | ||
| 91 | case supported_verb::put: | ||
| 92 | return bhttp::verb::put; | ||
| 93 | } | ||
| 94 | } | ||
| 95 | }; | 59 | }; |
| 96 | 60 | ||
| 97 | export struct verb_set | 61 | export struct verb_set |
| 98 | { | 62 | { |
| 63 | bool options : 1 = false; | ||
| 99 | bool delete_ : 1 = false; | 64 | bool delete_ : 1 = false; |
| 100 | bool get : 1 = false; | 65 | bool get : 1 = false; |
| 101 | bool head : 1 = false; | 66 | bool head : 1 = false; |
| 102 | bool post : 1 = false; | 67 | bool post : 1 = false; |
| 103 | bool put : 1 = false; | 68 | bool put : 1 = false; |
| 104 | bool options : 1 = false; | ||
| 105 | 69 | ||
| 106 | auto enable(supported_verb v) -> void | 70 | [[nodiscard]] auto with(supported_verb v) const noexcept -> verb_set; |
| 107 | { | 71 | [[nodiscard]] auto empty() const noexcept -> bool; |
| 108 | switch (v.value) | 72 | [[nodiscard]] auto to_string() const -> std::string; |
| 109 | { | ||
| 110 | case supported_verb::delete_: | ||
| 111 | delete_ = true; | ||
| 112 | break; | ||
| 113 | case supported_verb::get: | ||
| 114 | get = true; | ||
| 115 | break; | ||
| 116 | case supported_verb::head: | ||
| 117 | head = true; | ||
| 118 | break; | ||
| 119 | case supported_verb::post: | ||
| 120 | post = true; | ||
| 121 | break; | ||
| 122 | case supported_verb::put: | ||
| 123 | put = true; | ||
| 124 | break; | ||
| 125 | case supported_verb::options: | ||
| 126 | options = true; | ||
| 127 | break; | ||
| 128 | default:; | ||
| 129 | } | ||
| 130 | } | ||
| 131 | 73 | ||
| 132 | auto operator==(verb_set const& rhs) const noexcept -> bool = default; | 74 | auto operator==(verb_set const& rhs) const noexcept -> bool = default; |
| 133 | |||
| 134 | auto empty() const -> bool { return *this == verb_set{}; } | ||
| 135 | |||
| 136 | verb_set(std::initializer_list<supported_verb> vs) | ||
| 137 | { | ||
| 138 | for (auto const v : vs) | ||
| 139 | enable(v); | ||
| 140 | } | ||
| 141 | |||
| 142 | auto to_string() const -> std::string | ||
| 143 | { | ||
| 144 | std::ostringstream ss; | ||
| 145 | bool wrote = false; | ||
| 146 | auto write = [&](bhttp::verb v) | ||
| 147 | { | ||
| 148 | if (wrote) | ||
| 149 | ss << ", "; | ||
| 150 | ss << v; | ||
| 151 | wrote = true; | ||
| 152 | }; | ||
| 153 | if (delete_) | ||
| 154 | write(bhttp::verb::delete_); | ||
| 155 | if (get) | ||
| 156 | write(bhttp::verb::get); | ||
| 157 | if (head) | ||
| 158 | write(bhttp::verb::head); | ||
| 159 | if (post) | ||
| 160 | write(bhttp::verb::post); | ||
| 161 | if (put) | ||
| 162 | write(bhttp::verb::put); | ||
| 163 | if (options) | ||
| 164 | write(bhttp::verb::options); | ||
| 165 | return ss.str(); | ||
| 166 | } | ||
| 167 | }; | 75 | }; |
| 76 | static_assert(sizeof(verb_set) == 1); | ||
| 77 | static_assert(alignof(verb_set) == 1); | ||
| 168 | 78 | ||
| 169 | } // namespace routemon::http | 79 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/asio/as_tuple.hpp> | ||
| 4 | #include <boost/asio/awaitable.hpp> | ||
| 5 | #include <boost/asio/co_spawn.hpp> | ||
| 6 | #include <boost/asio/ip/tcp.hpp> | ||
| 7 | #include <boost/beast/core.hpp> | ||
| 8 | #include <boost/beast/http.hpp> | ||
| 9 | #include <boost/config.hpp> | ||
| 10 | #include <boost/json/serialize.hpp> | ||
| 11 | #include <boost/url.hpp> | ||
| 12 | |||
| 13 | module routemon:http.server$impl; | ||
| 14 | |||
| 15 | import :http.server; | ||
| 16 | |||
| 17 | namespace routemon::http { | ||
| 18 | |||
| 19 | auto presponse::header() -> bhttp::response_header<bhttp::fields>& | ||
| 20 | { | ||
| 21 | return impl_->header(); | ||
| 22 | } | ||
| 23 | |||
| 24 | auto presponse::header() const -> bhttp::response_header<bhttp::fields> const& | ||
| 25 | { | ||
| 26 | return impl_->header(); | ||
| 27 | } | ||
| 28 | |||
| 29 | auto presponse::is_done() const -> bool { return impl_->is_done(); } | ||
| 30 | |||
| 31 | auto presponse::prepare(beast::error_code& ec) -> const_buffers_type | ||
| 32 | { | ||
| 33 | return impl_->prepare(ec); | ||
| 34 | } | ||
| 35 | |||
| 36 | auto presponse::consume(std::size_t n) -> void { return impl_->consume(n); } | ||
| 37 | |||
| 38 | auto presponse::keep_alive() const noexcept -> bool | ||
| 39 | { | ||
| 40 | return impl_->keep_alive(); | ||
| 41 | } | ||
| 42 | |||
| 43 | keep_alive::keep_alive(bool value) : value{value} {} | ||
| 44 | |||
| 45 | auto problem_rsp( | ||
| 46 | base_ctx const& ctx, problem::details const& problem, keep_alive ka) | ||
| 47 | -> presponse | ||
| 48 | { | ||
| 49 | auto rsp = make_rsp<bhttp::string_body>(problem.status, ka); | ||
| 50 | rsp.set(bhttp::field::content_type, "application/problem+json"); | ||
| 51 | rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); | ||
| 52 | rsp.prepare_payload(); | ||
| 53 | return presponse{std::move(rsp)}; | ||
| 54 | } | ||
| 55 | |||
| 56 | auto make_preflight_rsp(preflight_response res, keep_alive ka) | ||
| 57 | -> bhttp::response<bhttp::empty_body> | ||
| 58 | { | ||
| 59 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka); | ||
| 60 | auto allow_headers_str = res.allow_headers | ||
| 61 | | std::views::transform( | ||
| 62 | [](auto const& field) -> std::string_view | ||
| 63 | { return bhttp::to_string(field); }) | ||
| 64 | | std::views::join_with(std::string_view{", "}) | ||
| 65 | | std::ranges::to<std::string>(); | ||
| 66 | rsp.set( | ||
| 67 | bhttp::field::access_control_allow_methods, | ||
| 68 | res.allow_methods.to_string()); | ||
| 69 | rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); | ||
| 70 | rsp.prepare_payload(); | ||
| 71 | return rsp; | ||
| 72 | } | ||
| 73 | |||
| 74 | auto global_options_handler(base_ctx const& ctx, readable_request r) | ||
| 75 | -> net::awaitable<presponse> | ||
| 76 | { | ||
| 77 | // TODO: switch to "small (4KB) discarded" body type, similar to what Go | ||
| 78 | // does? Same goes for default_options_handler? Not sure. | ||
| 79 | if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 80 | !res) | ||
| 81 | co_return std::move(res.error()); | ||
| 82 | auto req = r.p->release(); | ||
| 83 | auto rsp = make_rsp<bhttp::empty_body>( | ||
| 84 | bhttp::status::no_content, keep_alive{req.keep_alive()}); | ||
| 85 | rsp.prepare_payload(); | ||
| 86 | co_return std::move(rsp); | ||
| 87 | } | ||
| 88 | |||
| 89 | auto router::handle_request(readable_request r) const | ||
| 90 | -> net::awaitable<presponse> | ||
| 91 | { | ||
| 92 | return impl_->handle_request(r); | ||
| 93 | } | ||
| 94 | |||
| 95 | server::server(log::logger const& l, router&& r) | ||
| 96 | : l_{l.sub("http_server")}, r_{std::move(r)} | ||
| 97 | { | ||
| 98 | } | ||
| 99 | |||
| 100 | auto server::do_session(beast::tcp_stream strm) -> net::awaitable<void> | ||
| 101 | { | ||
| 102 | auto buf = beast::flat_buffer{}; | ||
| 103 | |||
| 104 | while (true) | ||
| 105 | { | ||
| 106 | auto p0 = bhttp::request_parser<bhttp::empty_body>{}; | ||
| 107 | p0.body_limit(boost::none); | ||
| 108 | auto [ec, _] = | ||
| 109 | co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); | ||
| 110 | if (ec == bhttp::error::end_of_stream) | ||
| 111 | break; | ||
| 112 | else if (ec) | ||
| 113 | throw boost::system::system_error{ec}; | ||
| 114 | |||
| 115 | auto http_version = p0.get().version(); | ||
| 116 | auto&& rsp = co_await r_.handle_request( | ||
| 117 | readable_request{ | ||
| 118 | .p = util::not_null{&p0}, | ||
| 119 | .strm = util::not_null{&strm}, | ||
| 120 | .buf = util::not_null{&buf}, | ||
| 121 | }); | ||
| 122 | rsp.header().version(http_version); | ||
| 123 | bool keep_alive = rsp.keep_alive(); | ||
| 124 | co_await beast::async_write(strm, std::move(rsp)); | ||
| 125 | if (!keep_alive) | ||
| 126 | { | ||
| 127 | break; | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | strm.socket().shutdown(tcp::socket::shutdown_send); | ||
| 132 | } | ||
| 133 | |||
| 134 | auto server::do_listen(tcp::endpoint endpoint) -> net::awaitable<void> | ||
| 135 | { | ||
| 136 | auto executor = co_await net::this_coro::executor; | ||
| 137 | auto acceptor = tcp::acceptor{executor, endpoint}; | ||
| 138 | |||
| 139 | l_.with("endpoint", endpoint.address().to_string()) | ||
| 140 | .with("port", std::to_string(endpoint.port())) | ||
| 141 | .info("Serving"); | ||
| 142 | while (true) | ||
| 143 | { | ||
| 144 | net::co_spawn( | ||
| 145 | executor, | ||
| 146 | do_session(beast::tcp_stream{co_await acceptor.async_accept()}), | ||
| 147 | [this](std::exception_ptr e) | ||
| 148 | { | ||
| 149 | if (e) | ||
| 150 | { | ||
| 151 | try | ||
| 152 | { | ||
| 153 | std::rethrow_exception(e); | ||
| 154 | } | ||
| 155 | catch (std::exception const& e) | ||
| 156 | { | ||
| 157 | l_.error("Error in session: {}", e.what()); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | }); | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | auto server::spawn(net::io_context& ioc) -> void | ||
| 165 | { | ||
| 166 | auto const addr = net::ip::make_address("0.0.0.0"); | ||
| 167 | auto const endpoint = tcp::endpoint{addr, 8284}; | ||
| 168 | |||
| 169 | // TODO: make exception handling as nice as in srv.cpp | ||
| 170 | net::co_spawn( | ||
| 171 | ioc, do_listen(endpoint), | ||
| 172 | [this](std::exception_ptr e) | ||
| 173 | { | ||
| 174 | if (e) | ||
| 175 | { | ||
| 176 | try | ||
| 177 | { | ||
| 178 | std::rethrow_exception(e); | ||
| 179 | } | ||
| 180 | catch (std::exception const& e) | ||
| 181 | { | ||
| 182 | l_.error("Error: {}", e.what()); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | }); | ||
| 186 | } | ||
| 187 | |||
| 188 | } // 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: | |||
| 137 | { | 137 | { |
| 138 | } | 138 | } |
| 139 | 139 | ||
| 140 | auto header() -> bhttp::response_header<bhttp::fields>& | 140 | auto header() -> bhttp::response_header<bhttp::fields>&; |
| 141 | { | 141 | auto header() const -> bhttp::response_header<bhttp::fields> const&; |
| 142 | return impl_->header(); | 142 | auto is_done() const -> bool; |
| 143 | } | 143 | auto prepare(beast::error_code& ec) -> const_buffers_type; |
| 144 | auto header() const -> bhttp::response_header<bhttp::fields> const& | 144 | auto consume(std::size_t n) -> void; |
| 145 | { | 145 | auto keep_alive() const noexcept -> bool; |
| 146 | return impl_->header(); | ||
| 147 | } | ||
| 148 | |||
| 149 | auto is_done() const -> bool { return impl_->is_done(); } | ||
| 150 | |||
| 151 | auto prepare(beast::error_code& ec) -> const_buffers_type | ||
| 152 | { | ||
| 153 | return impl_->prepare(ec); | ||
| 154 | } | ||
| 155 | |||
| 156 | auto consume(std::size_t n) -> void { return impl_->consume(n); } | ||
| 157 | |||
| 158 | auto keep_alive() const noexcept -> bool { return impl_->keep_alive(); } | ||
| 159 | }; | 146 | }; |
| 160 | static_assert(beast::concepts::buffers_generator<presponse>); | 147 | static_assert(beast::concepts::buffers_generator<presponse>); |
| 161 | 148 | ||
| @@ -207,15 +194,17 @@ struct base_ctx | |||
| 207 | }; | 194 | }; |
| 208 | 195 | ||
| 209 | template <class Ctx> | 196 | template <class Ctx> |
| 210 | using basic_route_handler_fn_t = std::function< | 197 | using basic_route_handler_fn_t = |
| 211 | auto(Ctx, readable_request, std::vector<std::string> const& matches) | 198 | std::function<auto( |
| 212 | ->net::awaitable<presponse>>; | 199 | Ctx, readable_request, |
| 200 | std::vector<std::string> const& matches) | ||
| 201 | ->net::awaitable<presponse>>; | ||
| 213 | 202 | ||
| 214 | struct keep_alive | 203 | struct keep_alive |
| 215 | { | 204 | { |
| 216 | bool value; | 205 | bool value; |
| 217 | 206 | ||
| 218 | explicit keep_alive(bool value) : value{value} {} | 207 | explicit keep_alive(bool value); |
| 219 | }; | 208 | }; |
| 220 | 209 | ||
| 221 | template <bhttp::concepts::body Body> | 210 | template <bhttp::concepts::body Body> |
| @@ -229,37 +218,16 @@ auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> | |||
| 229 | 218 | ||
| 230 | auto problem_rsp( | 219 | auto problem_rsp( |
| 231 | base_ctx const& ctx, problem::details const& problem, keep_alive ka) | 220 | base_ctx const& ctx, problem::details const& problem, keep_alive ka) |
| 232 | -> presponse | 221 | -> presponse; |
| 233 | { | ||
| 234 | auto rsp = make_rsp<bhttp::string_body>(problem.status, ka); | ||
| 235 | rsp.set(bhttp::field::content_type, "application/problem+json"); | ||
| 236 | rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); | ||
| 237 | rsp.prepare_payload(); | ||
| 238 | return presponse{std::move(rsp)}; | ||
| 239 | } | ||
| 240 | 222 | ||
| 241 | struct preflight_response | 223 | struct preflight_response |
| 242 | { | 224 | { |
| 243 | verb_set allow_methods; | 225 | verb_set allow_methods; |
| 244 | std::vector<bhttp::field> allow_headers; | 226 | std::vector<bhttp::field> allow_headers; |
| 245 | }; | 227 | }; |
| 228 | |||
| 246 | auto make_preflight_rsp(preflight_response res, keep_alive ka) | 229 | auto make_preflight_rsp(preflight_response res, keep_alive ka) |
| 247 | -> bhttp::response<bhttp::empty_body> | 230 | -> bhttp::response<bhttp::empty_body>; |
| 248 | { | ||
| 249 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka); | ||
| 250 | auto allow_headers_str = res.allow_headers | ||
| 251 | | std::views::transform( | ||
| 252 | [](auto const& field) -> std::string_view | ||
| 253 | { return bhttp::to_string(field); }) | ||
| 254 | | std::views::join_with(std::string_view{", "}) | ||
| 255 | | std::ranges::to<std::string>(); | ||
| 256 | rsp.set( | ||
| 257 | bhttp::field::access_control_allow_methods, | ||
| 258 | res.allow_methods.to_string()); | ||
| 259 | rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); | ||
| 260 | rsp.prepare_payload(); | ||
| 261 | return rsp; | ||
| 262 | } | ||
| 263 | 231 | ||
| 264 | // Using base_ctx instead of a template here since that saves you | 232 | // Using base_ctx instead of a template here since that saves you |
| 265 | // typing on invocation (and we do not care about the context type | 233 | // 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) | |||
| 276 | 244 | ||
| 277 | template <> | 245 | template <> |
| 278 | auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) | 246 | auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) |
| 279 | -> net::awaitable< | 247 | -> net:: |
| 280 | std::expected<bhttp::request<bhttp::empty_body>, presponse>> | 248 | awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>> |
| 281 | { | 249 | { |
| 282 | auto [ec, _] = | 250 | auto [ec, _] = |
| 283 | co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); | 251 | co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); |
| @@ -340,19 +308,7 @@ auto default_options_handler( | |||
| 340 | } | 308 | } |
| 341 | 309 | ||
| 342 | auto global_options_handler(base_ctx const& ctx, readable_request r) | 310 | auto global_options_handler(base_ctx const& ctx, readable_request r) |
| 343 | -> net::awaitable<presponse> | 311 | -> net::awaitable<presponse>; |
| 344 | { | ||
| 345 | // TODO: switch to "small (4KB) discarded" body type, similar to what Go | ||
| 346 | // does? Same goes for default_options_handler? Not sure. | ||
| 347 | if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 348 | !res) | ||
| 349 | co_return std::move(res.error()); | ||
| 350 | auto req = r.p->release(); | ||
| 351 | auto rsp = make_rsp<bhttp::empty_body>( | ||
| 352 | bhttp::status::no_content, keep_alive{req.keep_alive()}); | ||
| 353 | rsp.prepare_payload(); | ||
| 354 | co_return std::move(rsp); | ||
| 355 | } | ||
| 356 | 312 | ||
| 357 | template <class Ctx> | 313 | template <class Ctx> |
| 358 | auto id_middleware( | 314 | auto id_middleware( |
| @@ -427,28 +383,22 @@ struct handler_map | |||
| 427 | 383 | ||
| 428 | auto verbs() const -> verb_set | 384 | auto verbs() const -> verb_set |
| 429 | { | 385 | { |
| 430 | auto set = verb_set{}; | 386 | return verb_set{ |
| 431 | if (static_cast<bool>(options)) | 387 | .options = static_cast<bool>(options), |
| 432 | set.enable(supported_verb::options); | 388 | .delete_ = static_cast<bool>(delete_), |
| 433 | if (static_cast<bool>(delete_)) | 389 | .get = static_cast<bool>(get), |
| 434 | set.enable(supported_verb::delete_); | 390 | .head = static_cast<bool>(head), |
| 435 | if (static_cast<bool>(get)) | 391 | .post = static_cast<bool>(post), |
| 436 | set.enable(supported_verb::get); | 392 | .put = static_cast<bool>(put), |
| 437 | if (static_cast<bool>(head)) | 393 | }; |
| 438 | set.enable(supported_verb::head); | ||
| 439 | if (static_cast<bool>(post)) | ||
| 440 | set.enable(supported_verb::post); | ||
| 441 | if (static_cast<bool>(put)) | ||
| 442 | set.enable(supported_verb::put); | ||
| 443 | return set; | ||
| 444 | } | 394 | } |
| 445 | 395 | ||
| 446 | auto empty() const -> bool { return verbs().empty(); } | 396 | auto empty() const -> bool { return verbs().empty(); } |
| 447 | 397 | ||
| 448 | template <std::default_initializable U> | 398 | template <std::default_initializable U> |
| 449 | auto map(std::invocable<V const&> auto f) const -> handler_map<U> | 399 | auto map(std::invocable<V const&> auto f) const -> handler_map<U> |
| 450 | requires std::assignable_from< | 400 | requires std:: |
| 451 | U&, std::invoke_result_t<decltype(f), V const&>> | 401 | assignable_from<U&, std::invoke_result_t<decltype(f), V const&>> |
| 452 | { | 402 | { |
| 453 | return { | 403 | return { |
| 454 | .options = static_cast<bool>(options) ? f(options) : U{}, | 404 | .options = static_cast<bool>(options) ? f(options) : U{}, |
| @@ -508,8 +458,9 @@ template <class T> | |||
| 508 | concept match_arg = std::constructible_from<T, std::string const&>; | 458 | concept match_arg = std::constructible_from<T, std::string const&>; |
| 509 | 459 | ||
| 510 | template <class Ctx, match_arg... MatchArgs> | 460 | template <class Ctx, match_arg... MatchArgs> |
| 511 | using route_handler_fn_t = std::function< | 461 | using route_handler_fn_t = |
| 512 | auto(Ctx, readable_request, MatchArgs...)->net::awaitable<presponse>>; | 462 | std::function<auto(Ctx, readable_request, MatchArgs...) |
| 463 | ->net::awaitable<presponse>>; | ||
| 513 | 464 | ||
| 514 | template <class Ctx, match_arg... MatchArgs> | 465 | template <class Ctx, match_arg... MatchArgs> |
| 515 | auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) | 466 | auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) |
| @@ -588,271 +539,226 @@ struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> | |||
| 588 | } | 539 | } |
| 589 | }; | 540 | }; |
| 590 | 541 | ||
| 591 | template <std::derived_from<base_ctx> PreRouteCtx> | 542 | class router |
| 592 | class server | ||
| 593 | { | 543 | { |
| 594 | log::logger l_; | 544 | struct impl_base |
| 595 | locale::selector lsel_; | ||
| 596 | middleware_t<base_ctx, PreRouteCtx> global_middleware_; | ||
| 597 | route_tree<routed_ctx<PreRouteCtx>> routes_; | ||
| 598 | |||
| 599 | public: | ||
| 600 | explicit server( | ||
| 601 | log::logger const& l, locale::selector&& lsel, | ||
| 602 | middleware_t<base_ctx, PreRouteCtx> global_middleware, | ||
| 603 | route_tree<routed_ctx<PreRouteCtx>> routes) | ||
| 604 | : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, | ||
| 605 | global_middleware_{std::move(global_middleware)}, | ||
| 606 | routes_{std::move(routes)} | ||
| 607 | { | ||
| 608 | } | ||
| 609 | |||
| 610 | struct match_result | ||
| 611 | { | 545 | { |
| 612 | util::not_null< | 546 | virtual ~impl_base() = default; |
| 613 | handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const*> | 547 | virtual auto handle_request(readable_request r) const |
| 614 | route_handlers; | 548 | -> net::awaitable<presponse> = 0; |
| 615 | std::vector<std::string> wildcard_matches; | ||
| 616 | |||
| 617 | auto allowed_methods() const -> verb_set { return route_handlers->verbs(); } | ||
| 618 | }; | 549 | }; |
| 619 | 550 | ||
| 620 | auto match(boost::urls::segments_view segments) const | 551 | std::unique_ptr<impl_base> impl_; |
| 621 | -> std::optional<match_result> | 552 | |
| 553 | template <std::derived_from<base_ctx> PreRouteCtx> | ||
| 554 | class impl : public impl_base | ||
| 622 | { | 555 | { |
| 623 | auto const* tree = &routes_; | 556 | locale::selector lsel_; |
| 624 | auto wildcard_matches = std::vector<std::string>{}; | 557 | middleware_t<base_ctx, PreRouteCtx> global_middleware_; |
| 625 | for (auto const& seg : segments) | 558 | route_tree<routed_ctx<PreRouteCtx>> routes_; |
| 559 | |||
| 560 | public: | ||
| 561 | explicit impl( | ||
| 562 | locale::selector&& lsel, | ||
| 563 | middleware_t<base_ctx, PreRouteCtx> global_middleware, | ||
| 564 | route_tree<routed_ctx<PreRouteCtx>> routes) | ||
| 565 | : lsel_{std::move(lsel)}, | ||
| 566 | global_middleware_{std::move(global_middleware)}, | ||
| 567 | routes_{std::move(routes)} | ||
| 626 | { | 568 | { |
| 627 | tree->sub.visit( | ||
| 628 | util::overloaded{ | ||
| 629 | [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& | ||
| 630 | subtrees) | ||
| 631 | { | ||
| 632 | auto it = subtrees.find(seg); | ||
| 633 | tree = it == subtrees.end() ? nullptr : &it->second; | ||
| 634 | }, | ||
| 635 | [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& | ||
| 636 | wildcard_subtree) | ||
| 637 | { | ||
| 638 | wildcard_matches.push_back(seg); | ||
| 639 | tree = &*wildcard_subtree; | ||
| 640 | }, | ||
| 641 | }); | ||
| 642 | if (!tree) | ||
| 643 | return std::nullopt; | ||
| 644 | } | 569 | } |
| 645 | if (tree->here.empty()) | ||
| 646 | return std::nullopt; | ||
| 647 | return match_result{ | ||
| 648 | .route_handlers = util::not_null{&tree->here}, | ||
| 649 | .wildcard_matches = wildcard_matches, | ||
| 650 | }; | ||
| 651 | } | ||
| 652 | 570 | ||
| 653 | auto route_request(PreRouteCtx ctx, readable_request r) const | 571 | struct match_result |
| 654 | -> net::awaitable<presponse> | 572 | { |
| 655 | { | 573 | util::not_null< |
| 656 | auto req_base = r.p->get().base(); | 574 | handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const* |
| 575 | > | ||
| 576 | route_handlers; | ||
| 577 | std::vector<std::string> wildcard_matches; | ||
| 657 | 578 | ||
| 658 | auto const bad_request_tpl = problem::tpl{ | 579 | auto allowed_methods() const -> verb_set |
| 659 | .status = bhttp::status::bad_request, | 580 | { |
| 660 | .title = translate("Bad request"), | 581 | return route_handlers->verbs(); |
| 661 | .type_uri = "https://routemon.fautchen.eu/problems/bad-request", | 582 | } |
| 662 | }; | 583 | }; |
| 663 | 584 | ||
| 664 | if (req_base.target() == "*") | 585 | auto match(boost::urls::segments_view segments) const |
| 586 | -> std::optional<match_result> | ||
| 665 | { | 587 | { |
| 666 | // request-target is in asterisk-form (RFC 9112, § 3.2.4), | 588 | auto const* tree = &routes_; |
| 667 | // so the request must be a server-wide OPTIONS request. | 589 | auto wildcard_matches = std::vector<std::string>{}; |
| 668 | 590 | for (auto const& seg : segments) | |
| 669 | if (req_base.method() != bhttp::verb::options) | ||
| 670 | { | 591 | { |
| 671 | auto tpl = problem::tpl{ | 592 | tree->sub.visit( |
| 672 | .status = bhttp::status::method_not_allowed, | 593 | util::overloaded{ |
| 673 | .title = translate("Method not allowed"), | 594 | [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& |
| 674 | .type_uri = "https://routemon.fautchen.eu/problems/" | 595 | subtrees) |
| 675 | "method-not-allowed", | 596 | { |
| 676 | }; | 597 | auto it = subtrees.find(seg); |
| 677 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | 598 | tree = it == subtrees.end() ? nullptr : &it->second; |
| 599 | }, | ||
| 600 | [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& | ||
| 601 | wildcard_subtree) | ||
| 602 | { | ||
| 603 | wildcard_matches.push_back(seg); | ||
| 604 | tree = &*wildcard_subtree; | ||
| 605 | }, | ||
| 606 | }); | ||
| 607 | if (!tree) | ||
| 608 | return std::nullopt; | ||
| 678 | } | 609 | } |
| 679 | 610 | if (tree->here.empty()) | |
| 680 | co_return co_await global_options_handler(ctx, r); | 611 | return std::nullopt; |
| 612 | return match_result{ | ||
| 613 | .route_handlers = util::not_null{&tree->here}, | ||
| 614 | .wildcard_matches = wildcard_matches, | ||
| 615 | }; | ||
| 681 | } | 616 | } |
| 682 | else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) | 617 | |
| 618 | auto route_request(PreRouteCtx ctx, readable_request r) const | ||
| 619 | -> net::awaitable<presponse> | ||
| 683 | { | 620 | { |
| 684 | // request-target is in origin-form (RFC 9112, § 3.2.1), | 621 | auto req_base = r.p->get().base(); |
| 685 | // so it must be a normal request (not a CONNECT or | ||
| 686 | // server-wide OPTIONS request). | ||
| 687 | 622 | ||
| 688 | auto req_url = boost::urls::url{*mreq_url0}; | 623 | auto const bad_request_tpl = problem::tpl{ |
| 689 | req_url.normalize(); | 624 | .status = bhttp::status::bad_request, |
| 690 | if (!req_url.is_path_absolute()) | 625 | .title = translate("Bad request"), |
| 691 | { | 626 | .type_uri = "https://routemon.fautchen.eu/problems/bad-request", |
| 692 | auto problem = bad_request_tpl.instantiate().set_detail(translate( | 627 | }; |
| 693 | "Path of normalized (RFC 3986, § 6) " | ||
| 694 | "origin-form request-target (RFC " | ||
| 695 | "9112, § 3.2.1) should be " | ||
| 696 | "absolute")); | ||
| 697 | co_return problem_rsp(ctx, problem, keep_alive{false}); | ||
| 698 | } | ||
| 699 | 628 | ||
| 700 | auto mres = match(req_url.segments()); | 629 | if (req_base.target() == "*") |
| 701 | if (!mres) | ||
| 702 | { | 630 | { |
| 703 | auto tpl = problem::tpl{ | 631 | // request-target is in asterisk-form (RFC 9112, § 3.2.4), |
| 704 | .status = bhttp::status::not_found, | 632 | // so the request must be a server-wide OPTIONS request. |
| 705 | .title = translate("Not found"), | ||
| 706 | .type_uri = "https://routemon.fautchen.eu/problems/not-found", | ||
| 707 | }; | ||
| 708 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 709 | } | ||
| 710 | 633 | ||
| 711 | auto mverb = supported_verb::from(req_base.method()); | 634 | if (req_base.method() != bhttp::verb::options) |
| 712 | if (!mverb) | 635 | { |
| 713 | { | 636 | auto tpl = problem::tpl{ |
| 714 | // Method not implemented. | 637 | .status = bhttp::status::method_not_allowed, |
| 715 | auto tpl = problem::tpl{ | 638 | .title = translate("Method not allowed"), |
| 716 | .status = bhttp::status::not_implemented, | 639 | .type_uri = "https://routemon.fautchen.eu/problems/" |
| 717 | .title = translate("Method not implemented"), | 640 | "method-not-allowed", |
| 718 | .type_uri = "https://routemon.fautchen.eu/problems/" | 641 | }; |
| 719 | "method-not-implemented", | 642 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); |
| 720 | }; | 643 | } |
| 721 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 722 | } | ||
| 723 | 644 | ||
| 724 | if (auto mhdl = mres->route_handlers->lookup(*mverb)) | 645 | co_return co_await global_options_handler(ctx, r); |
| 646 | } | ||
| 647 | else if (auto mreq_url0 = | ||
| 648 | boost::urls::parse_origin_form(req_base.target())) | ||
| 725 | { | 649 | { |
| 726 | auto new_ctx = | 650 | // request-target is in origin-form (RFC 9112, § 3.2.1), |
| 727 | routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()}; | 651 | // so it must be a normal request (not a CONNECT or |
| 728 | co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); | 652 | // server-wide OPTIONS request). |
| 653 | |||
| 654 | auto req_url = boost::urls::url{*mreq_url0}; | ||
| 655 | req_url.normalize(); | ||
| 656 | if (!req_url.is_path_absolute()) | ||
| 657 | { | ||
| 658 | auto problem = bad_request_tpl.instantiate().set_detail(translate( | ||
| 659 | "Path of normalized (RFC 3986, § 6) " | ||
| 660 | "origin-form request-target (RFC " | ||
| 661 | "9112, § 3.2.1) should be " | ||
| 662 | "absolute")); | ||
| 663 | co_return problem_rsp(ctx, problem, keep_alive{false}); | ||
| 664 | } | ||
| 665 | |||
| 666 | auto mres = match(req_url.segments()); | ||
| 667 | if (!mres) | ||
| 668 | { | ||
| 669 | auto tpl = problem::tpl{ | ||
| 670 | .status = bhttp::status::not_found, | ||
| 671 | .title = translate("Not found"), | ||
| 672 | .type_uri = "https://routemon.fautchen.eu/problems/not-found", | ||
| 673 | }; | ||
| 674 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 675 | } | ||
| 676 | |||
| 677 | auto mverb = supported_verb::from(req_base.method()); | ||
| 678 | if (!mverb) | ||
| 679 | { | ||
| 680 | // Method not implemented. | ||
| 681 | auto tpl = problem::tpl{ | ||
| 682 | .status = bhttp::status::not_implemented, | ||
| 683 | .title = translate("Method not implemented"), | ||
| 684 | .type_uri = "https://routemon.fautchen.eu/problems/" | ||
| 685 | "method-not-implemented", | ||
| 686 | }; | ||
| 687 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 688 | } | ||
| 689 | |||
| 690 | if (auto mhdl = mres->route_handlers->lookup(*mverb)) | ||
| 691 | { | ||
| 692 | auto new_ctx = | ||
| 693 | routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()}; | ||
| 694 | co_return co_await mhdl( | ||
| 695 | std::move(new_ctx), r, mres->wildcard_matches); | ||
| 696 | } | ||
| 697 | else | ||
| 698 | { | ||
| 699 | // Path recognized, but method not allowed. | ||
| 700 | auto tpl = problem::tpl{ | ||
| 701 | .status = bhttp::status::method_not_allowed, | ||
| 702 | .title = translate("Method not allowed"), | ||
| 703 | .type_uri = "https://routemon.fautchen.eu/problems/" | ||
| 704 | "method-not-allowed", | ||
| 705 | }; | ||
| 706 | auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 707 | rsp.header().set( | ||
| 708 | bhttp::field::allow, mres->allowed_methods().to_string()); | ||
| 709 | co_return std::move(rsp); | ||
| 710 | } | ||
| 729 | } | 711 | } |
| 730 | else | 712 | else |
| 731 | { | 713 | { |
| 732 | // Path recognized, but method not allowed. | 714 | // We do not accept any other request-target forms. |
| 733 | auto tpl = problem::tpl{ | 715 | |
| 734 | .status = bhttp::status::method_not_allowed, | 716 | auto problem = bad_request_tpl.instantiate().set_detail(translate( |
| 735 | .title = translate("Method not allowed"), | 717 | "Invalid request-target, expected " |
| 736 | .type_uri = "https://routemon.fautchen.eu/problems/" | 718 | "asterisk-form or origin-form " |
| 737 | "method-not-allowed", | 719 | "(see RFC 9112, § 3.2)")); |
| 738 | }; | 720 | co_return problem_rsp(ctx, problem, keep_alive{false}); |
| 739 | auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 740 | rsp.header().set( | ||
| 741 | bhttp::field::allow, mres->allowed_methods().to_string()); | ||
| 742 | co_return std::move(rsp); | ||
| 743 | } | 721 | } |
| 744 | } | 722 | } |
| 745 | else | ||
| 746 | { | ||
| 747 | // We do not accept any other request-target forms. | ||
| 748 | 723 | ||
| 749 | auto problem = bad_request_tpl.instantiate().set_detail(translate( | 724 | auto handle_request(readable_request r) const |
| 750 | "Invalid request-target, expected " | 725 | -> net::awaitable<presponse> override |
| 751 | "asterisk-form or origin-form " | 726 | { |
| 752 | "(see RFC 9112, § 3.2)")); | 727 | auto header = r.p->get().base(); |
| 753 | co_return problem_rsp(ctx, problem, keep_alive{false}); | 728 | auto locale = lsel_.select(header[bhttp::field::accept_language]); |
| 729 | co_return co_await global_middleware_( | ||
| 730 | base_ctx{.locale = locale}, header, | ||
| 731 | [&](PreRouteCtx ctx) -> net::awaitable<presponse> | ||
| 732 | { co_return co_await route_request(std::move(ctx), std::move(r)); }); | ||
| 754 | } | 733 | } |
| 755 | } | 734 | }; |
| 756 | 735 | ||
| 757 | auto handle_request(readable_request r) const -> net::awaitable<presponse> | 736 | public: |
| 737 | template <std::derived_from<base_ctx> PreRouteCtx> | ||
| 738 | explicit router( | ||
| 739 | locale::selector&& lsel, | ||
| 740 | middleware_t<base_ctx, PreRouteCtx> global_middleware, | ||
| 741 | route_tree<routed_ctx<PreRouteCtx>> routes) | ||
| 742 | : impl_{std::make_unique<impl<PreRouteCtx>>( | ||
| 743 | std::move(lsel), std::move(global_middleware), std::move(routes))} | ||
| 758 | { | 744 | { |
| 759 | auto header = r.p->get().base(); | ||
| 760 | auto locale = lsel_.select(header[bhttp::field::accept_language]); | ||
| 761 | auto ctx0 = base_ctx{.locale = locale}; | ||
| 762 | |||
| 763 | co_return co_await global_middleware_( | ||
| 764 | std::move(ctx0), header, | ||
| 765 | [&](PreRouteCtx ctx) -> net::awaitable<presponse> | ||
| 766 | { co_return co_await route_request(std::move(ctx), std::move(r)); }); | ||
| 767 | } | 745 | } |
| 768 | 746 | ||
| 769 | auto do_session(beast::tcp_stream strm) -> net::awaitable<void> | 747 | auto handle_request(readable_request r) const -> net::awaitable<presponse>; |
| 770 | { | 748 | }; |
| 771 | auto buf = beast::flat_buffer{}; | ||
| 772 | |||
| 773 | while (true) | ||
| 774 | { | ||
| 775 | auto p0 = bhttp::request_parser<bhttp::empty_body>{}; | ||
| 776 | p0.body_limit(boost::none); | ||
| 777 | auto [ec, _] = | ||
| 778 | co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); | ||
| 779 | if (ec == bhttp::error::end_of_stream) | ||
| 780 | break; | ||
| 781 | else if (ec) | ||
| 782 | throw boost::system::system_error{ec}; | ||
| 783 | |||
| 784 | auto http_version = p0.get().version(); | ||
| 785 | auto&& rsp = co_await handle_request( | ||
| 786 | readable_request{ | ||
| 787 | .p = util::not_null{&p0}, | ||
| 788 | .strm = util::not_null{&strm}, | ||
| 789 | .buf = util::not_null{&buf}, | ||
| 790 | }); | ||
| 791 | rsp.header().version(http_version); | ||
| 792 | bool keep_alive = rsp.keep_alive(); | ||
| 793 | co_await beast::async_write(strm, std::move(rsp)); | ||
| 794 | if (!keep_alive) | ||
| 795 | { | ||
| 796 | break; | ||
| 797 | } | ||
| 798 | } | ||
| 799 | |||
| 800 | strm.socket().shutdown(tcp::socket::shutdown_send); | ||
| 801 | } | ||
| 802 | 749 | ||
| 803 | auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void> | 750 | class server |
| 804 | { | 751 | { |
| 805 | auto executor = co_await net::this_coro::executor; | 752 | log::logger l_; |
| 806 | auto acceptor = tcp::acceptor{executor, endpoint}; | 753 | router r_; |
| 807 | 754 | ||
| 808 | l_.with("endpoint", endpoint.address().to_string()) | 755 | auto do_session(beast::tcp_stream strm) -> net::awaitable<void>; |
| 809 | .with("port", std::to_string(endpoint.port())) | 756 | auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void>; |
| 810 | .info("Serving"); | ||
| 811 | while (true) | ||
| 812 | { | ||
| 813 | net::co_spawn( | ||
| 814 | executor, | ||
| 815 | do_session(beast::tcp_stream{co_await acceptor.async_accept()}), | ||
| 816 | [this](std::exception_ptr e) | ||
| 817 | { | ||
| 818 | if (e) | ||
| 819 | { | ||
| 820 | try | ||
| 821 | { | ||
| 822 | std::rethrow_exception(e); | ||
| 823 | } | ||
| 824 | catch (std::exception const& e) | ||
| 825 | { | ||
| 826 | l_.error("Error in session: {}", e.what()); | ||
| 827 | } | ||
| 828 | } | ||
| 829 | }); | ||
| 830 | } | ||
| 831 | } | ||
| 832 | 757 | ||
| 833 | auto spawn(net::io_context& ioc) -> void | 758 | public: |
| 834 | { | 759 | explicit server(log::logger const& l, router&& r); |
| 835 | auto const addr = net::ip::make_address("0.0.0.0"); | ||
| 836 | auto const endpoint = tcp::endpoint{addr, 8284}; | ||
| 837 | 760 | ||
| 838 | // TODO: make exception handling as nice as in srv.cpp | 761 | auto spawn(net::io_context& ioc) -> void; |
| 839 | net::co_spawn( | ||
| 840 | ioc, do_listen(endpoint), | ||
| 841 | [this](std::exception_ptr e) | ||
| 842 | { | ||
| 843 | if (e) | ||
| 844 | { | ||
| 845 | try | ||
| 846 | { | ||
| 847 | std::rethrow_exception(e); | ||
| 848 | } | ||
| 849 | catch (std::exception const& e) | ||
| 850 | { | ||
| 851 | l_.error("Error: {}", e.what()); | ||
| 852 | } | ||
| 853 | } | ||
| 854 | }); | ||
| 855 | } | ||
| 856 | }; | 762 | }; |
| 857 | 763 | ||
| 858 | } // namespace routemon::http | 764 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/locale.hpp> | ||
| 4 | #include <unicode/localematcher.h> | ||
| 5 | |||
| 6 | module routemon:locale$impl; | ||
| 7 | |||
| 8 | import :locale; | ||
| 9 | |||
| 10 | namespace routemon::locale { | ||
| 11 | |||
| 12 | struct locale_priority | ||
| 13 | { | ||
| 14 | float weight; | ||
| 15 | std::size_t original_index; | ||
| 16 | }; | ||
| 17 | |||
| 18 | auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool | ||
| 19 | { | ||
| 20 | if (lhs.weight != rhs.weight) | ||
| 21 | return lhs.weight > rhs.weight; | ||
| 22 | return lhs.original_index < rhs.original_index; | ||
| 23 | } | ||
| 24 | |||
| 25 | struct icu_locale_hash | ||
| 26 | { | ||
| 27 | std::size_t operator()(icu::Locale const& l) const noexcept | ||
| 28 | { | ||
| 29 | static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); | ||
| 30 | std::int32_t hash = l.hashCode(); | ||
| 31 | if (hash < 0) | ||
| 32 | { | ||
| 33 | return static_cast<std::size_t>(std::numeric_limits<int>::max()) | ||
| 34 | + static_cast<std::size_t>(-hash) + 1; | ||
| 35 | } | ||
| 36 | else | ||
| 37 | { | ||
| 38 | return static_cast<std::size_t>(hash); | ||
| 39 | } | ||
| 40 | } | ||
| 41 | }; | ||
| 42 | |||
| 43 | using icu_locale_priority_map = | ||
| 44 | std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>; | ||
| 45 | using icu_priority_locale = std::pair<icu::Locale, locale_priority>; | ||
| 46 | |||
| 47 | auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) | ||
| 48 | -> bool | ||
| 49 | { | ||
| 50 | return lhs.second < rhs.second; | ||
| 51 | } | ||
| 52 | |||
| 53 | class icu_priority_locale_vec_iterator : public icu::Locale::Iterator | ||
| 54 | { | ||
| 55 | std::size_t i_ = 0uz; | ||
| 56 | std::vector<icu_priority_locale> ls_; | ||
| 57 | |||
| 58 | public: | ||
| 59 | explicit icu_priority_locale_vec_iterator( | ||
| 60 | std::vector<icu_priority_locale>&& ls) | ||
| 61 | : ls_(std::move(ls)) | ||
| 62 | { | ||
| 63 | } | ||
| 64 | |||
| 65 | auto hasNext() const -> UBool override { return i_ < ls_.size(); } | ||
| 66 | |||
| 67 | auto next() -> icu::Locale const& override { return ls_[i_++].first; } | ||
| 68 | |||
| 69 | ~icu_priority_locale_vec_iterator() override = default; | ||
| 70 | }; | ||
| 71 | |||
| 72 | // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. | ||
| 73 | auto selector::ltrim_ows(std::string_view s) -> std::string_view | ||
| 74 | { | ||
| 75 | if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) | ||
| 76 | s.remove_prefix(i); | ||
| 77 | return s; | ||
| 78 | } | ||
| 79 | auto selector::rtrim_ows(std::string_view s) -> std::string_view | ||
| 80 | { | ||
| 81 | if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) | ||
| 82 | return s.substr(0, i + 1); | ||
| 83 | return s; | ||
| 84 | } | ||
| 85 | auto selector::trim_ows(std::string_view s) -> std::string_view | ||
| 86 | { | ||
| 87 | return rtrim_ows(ltrim_ows(s)); | ||
| 88 | } | ||
| 89 | |||
| 90 | auto selector::from_icu_locale(icu::Locale const& l) const -> std::locale | ||
| 91 | { | ||
| 92 | auto posix_name = std::string{l.getLanguage()}; | ||
| 93 | if (l.getScript() && std::strlen(l.getScript()) > 0) | ||
| 94 | { | ||
| 95 | posix_name += "_"; | ||
| 96 | posix_name += l.getScript(); | ||
| 97 | } | ||
| 98 | if (l.getCountry() && std::strlen(l.getCountry()) > 0) | ||
| 99 | { | ||
| 100 | posix_name += "_"; | ||
| 101 | posix_name += l.getCountry(); | ||
| 102 | } | ||
| 103 | posix_name += ".UTF-8"; | ||
| 104 | auto added_at = false; | ||
| 105 | if (l.getVariant() && std::strlen(l.getVariant()) > 0) | ||
| 106 | { | ||
| 107 | added_at = true; | ||
| 108 | posix_name += "@"; | ||
| 109 | posix_name += l.getVariant(); | ||
| 110 | } | ||
| 111 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 112 | auto* keywords = l.createKeywords(ec); | ||
| 113 | if (U_FAILURE(ec)) | ||
| 114 | throw std::runtime_error{"failed to create keywords"}; | ||
| 115 | if (keywords) | ||
| 116 | { | ||
| 117 | std::int32_t kw_len = 0; | ||
| 118 | char const* kw = nullptr; | ||
| 119 | while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) | ||
| 120 | { | ||
| 121 | auto value = | ||
| 122 | l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec); | ||
| 123 | if (!added_at) | ||
| 124 | { | ||
| 125 | posix_name += "@"; | ||
| 126 | added_at = true; | ||
| 127 | } | ||
| 128 | else | ||
| 129 | { | ||
| 130 | posix_name += ";"; | ||
| 131 | } | ||
| 132 | posix_name += kw; | ||
| 133 | posix_name += "="; | ||
| 134 | posix_name += value; | ||
| 135 | } | ||
| 136 | if (U_FAILURE(ec)) | ||
| 137 | throw std::runtime_error{"failed to iterate over keywords"}; | ||
| 138 | delete keywords; | ||
| 139 | } | ||
| 140 | return lgen_->generate(posix_name); | ||
| 141 | } | ||
| 142 | |||
| 143 | auto selector::select(std::string_view accept_language) const -> std::locale | ||
| 144 | { | ||
| 145 | using namespace std::literals::string_view_literals; | ||
| 146 | // NOTE: can also contain a *;q=0.1 | ||
| 147 | // q should have at most 3 digits after period | ||
| 148 | auto dlpm = icu_locale_priority_map{}; | ||
| 149 | for (auto const [i, lang_prio] : | ||
| 150 | accept_language | std::views::split(","sv) | std::views::enumerate) | ||
| 151 | { | ||
| 152 | auto [lang_range_ut, mweight_ut] = | ||
| 153 | util::split_on(std::string_view{lang_prio}, ';'); | ||
| 154 | auto lang_range_str = trim_ows(lang_range_ut); | ||
| 155 | auto mweight_str = mweight_ut.transform(trim_ows); | ||
| 156 | if (lang_range_str == "*") | ||
| 157 | break; | ||
| 158 | |||
| 159 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 160 | auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); | ||
| 161 | if (U_FAILURE(ec) || icu_locale.isBogus()) | ||
| 162 | continue; // ignore this locale | ||
| 163 | |||
| 164 | auto weight = 1.0f; | ||
| 165 | if (mweight_str && mweight_str->starts_with("q=")) | ||
| 166 | { | ||
| 167 | auto weight_str = mweight_str->substr(2, 4); | ||
| 168 | if (auto mweight = | ||
| 169 | util::parse_float(weight_str, std::chars_format::fixed); | ||
| 170 | mweight && 0.0f < *mweight && *mweight < 1.0f) | ||
| 171 | { | ||
| 172 | weight = *mweight; | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | if (weight > 0.0f) | ||
| 177 | { | ||
| 178 | dlpm[icu_locale] = { | ||
| 179 | .weight = weight, | ||
| 180 | .original_index = static_cast<std::size_t>(i), | ||
| 181 | }; | ||
| 182 | } | ||
| 183 | else | ||
| 184 | { | ||
| 185 | dlpm.erase(icu_locale); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | auto desired_locales = | ||
| 190 | std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()}; | ||
| 191 | std::sort(desired_locales.begin(), desired_locales.end()); | ||
| 192 | auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; | ||
| 193 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 194 | auto res = matcher_.getBestMatchResult(it, ec); | ||
| 195 | if (U_FAILURE(ec)) | ||
| 196 | return default_; | ||
| 197 | auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? | ||
| 198 | if (U_FAILURE(ec)) | ||
| 199 | return from_icu_locale(*res.getSupportedLocale()); | ||
| 200 | return from_icu_locale(resolved); | ||
| 201 | } | ||
| 202 | |||
| 203 | auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> | ||
| 204 | { | ||
| 205 | auto const& locale_info = std::use_facet<blocale::info>(locale); | ||
| 206 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 207 | auto bcp47_lang_tag = | ||
| 208 | icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec); | ||
| 209 | if (U_FAILURE(ec)) | ||
| 210 | return std::nullopt; | ||
| 211 | return bcp47_lang_tag; | ||
| 212 | } | ||
| 213 | |||
| 214 | #ifdef LOCALEDIR | ||
| 215 | #define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) | ||
| 216 | #define LOCALEDIR_AUX_STR(s) #s | ||
| 217 | constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; | ||
| 218 | #undef LOCALEDIR_AUX_STR | ||
| 219 | #undef LOCALEDIR_AUX_XSTR | ||
| 220 | #else // ifdef LOCALEDIR | ||
| 221 | constexpr auto messages_path = std::string_view{"locale/dev"}; | ||
| 222 | #endif // ifdef LOCALEDIR | ||
| 223 | |||
| 224 | export auto make_generator() -> std::shared_ptr<blocale::generator const> | ||
| 225 | { | ||
| 226 | auto lgen = std::make_shared<blocale::generator>(); | ||
| 227 | lgen->add_messages_path(std::string{messages_path}); | ||
| 228 | lgen->add_messages_domain("routemon"); | ||
| 229 | return std::static_pointer_cast<blocale::generator const>(lgen); | ||
| 230 | } | ||
| 231 | |||
| 232 | } // 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; | |||
| 11 | export namespace blocale = boost::locale; | 11 | export namespace blocale = boost::locale; |
| 12 | 12 | ||
| 13 | export namespace routemon { | 13 | export namespace routemon { |
| 14 | |||
| 14 | using lformat = blocale::format; | 15 | using lformat = blocale::format; |
| 15 | using blocale::gettext; | 16 | using blocale::gettext; |
| 16 | using blocale::translate; | 17 | using blocale::translate; |
| 18 | |||
| 17 | } // namespace routemon | 19 | } // namespace routemon |
| 18 | 20 | ||
| 19 | namespace routemon::locale { | 21 | namespace routemon::locale { |
| 20 | 22 | ||
| 21 | struct locale_priority | ||
| 22 | { | ||
| 23 | float weight; | ||
| 24 | std::size_t original_index; | ||
| 25 | }; | ||
| 26 | |||
| 27 | auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool | ||
| 28 | { | ||
| 29 | if (lhs.weight != rhs.weight) | ||
| 30 | return lhs.weight > rhs.weight; | ||
| 31 | return lhs.original_index < rhs.original_index; | ||
| 32 | } | ||
| 33 | |||
| 34 | struct icu_locale_hash | ||
| 35 | { | ||
| 36 | std::size_t operator()(icu::Locale const& l) const noexcept | ||
| 37 | { | ||
| 38 | static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); | ||
| 39 | std::int32_t hash = l.hashCode(); | ||
| 40 | if (hash < 0) | ||
| 41 | { | ||
| 42 | return static_cast<std::size_t>(std::numeric_limits<int>::max()) | ||
| 43 | + static_cast<std::size_t>(-hash) + 1; | ||
| 44 | } | ||
| 45 | else | ||
| 46 | { | ||
| 47 | return static_cast<std::size_t>(hash); | ||
| 48 | } | ||
| 49 | } | ||
| 50 | }; | ||
| 51 | |||
| 52 | using icu_locale_priority_map = | ||
| 53 | std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>; | ||
| 54 | using icu_priority_locale = std::pair<icu::Locale, locale_priority>; | ||
| 55 | |||
| 56 | auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) | ||
| 57 | -> bool | ||
| 58 | { | ||
| 59 | return lhs.second < rhs.second; | ||
| 60 | } | ||
| 61 | |||
| 62 | class icu_priority_locale_vec_iterator : public icu::Locale::Iterator | ||
| 63 | { | ||
| 64 | std::size_t i_ = 0uz; | ||
| 65 | std::vector<icu_priority_locale> ls_; | ||
| 66 | |||
| 67 | public: | ||
| 68 | explicit icu_priority_locale_vec_iterator( | ||
| 69 | std::vector<icu_priority_locale>&& ls) | ||
| 70 | : ls_(std::move(ls)) | ||
| 71 | { | ||
| 72 | } | ||
| 73 | |||
| 74 | auto hasNext() const -> UBool override { return i_ < ls_.size(); } | ||
| 75 | |||
| 76 | auto next() -> icu::Locale const& override { return ls_[i_++].first; } | ||
| 77 | |||
| 78 | ~icu_priority_locale_vec_iterator() override = default; | ||
| 79 | }; | ||
| 80 | |||
| 81 | export template <class T> | 23 | export template <class T> |
| 82 | concept locale_input_range = | 24 | concept locale_input_range = |
| 83 | std::ranges::input_range<T> | 25 | std::ranges::input_range<T> |
| 84 | && std::same_as< | 26 | && std:: |
| 85 | std::locale const&, std::ranges::range_const_reference_t<T>>; | 27 | same_as<std::locale const&, std::ranges::range_const_reference_t<T>>; |
| 86 | 28 | ||
| 87 | // Helps select a locale based on the Accept-Language header in an | 29 | // Helps select a locale based on the Accept-Language header in an |
| 88 | // HTTP request. | 30 | // HTTP request. |
| @@ -122,75 +64,11 @@ export class selector | |||
| 122 | } | 64 | } |
| 123 | 65 | ||
| 124 | // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. | 66 | // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. |
| 125 | static auto ltrim_ows(std::string_view s) -> std::string_view | 67 | static auto ltrim_ows(std::string_view s) -> std::string_view; |
| 126 | { | 68 | static auto rtrim_ows(std::string_view s) -> std::string_view; |
| 127 | if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) | 69 | static auto trim_ows(std::string_view s) -> std::string_view; |
| 128 | s.remove_prefix(i); | ||
| 129 | return s; | ||
| 130 | } | ||
| 131 | static auto rtrim_ows(std::string_view s) -> std::string_view | ||
| 132 | { | ||
| 133 | if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) | ||
| 134 | return s.substr(0, i + 1); | ||
| 135 | return s; | ||
| 136 | } | ||
| 137 | static auto trim_ows(std::string_view s) -> std::string_view | ||
| 138 | { | ||
| 139 | return rtrim_ows(ltrim_ows(s)); | ||
| 140 | } | ||
| 141 | 70 | ||
| 142 | auto from_icu_locale(icu::Locale const& l) const -> std::locale | 71 | auto from_icu_locale(icu::Locale const& l) const -> std::locale; |
| 143 | { | ||
| 144 | auto posix_name = std::string{l.getLanguage()}; | ||
| 145 | if (l.getScript() && std::strlen(l.getScript()) > 0) | ||
| 146 | { | ||
| 147 | posix_name += "_"; | ||
| 148 | posix_name += l.getScript(); | ||
| 149 | } | ||
| 150 | if (l.getCountry() && std::strlen(l.getCountry()) > 0) | ||
| 151 | { | ||
| 152 | posix_name += "_"; | ||
| 153 | posix_name += l.getCountry(); | ||
| 154 | } | ||
| 155 | posix_name += ".UTF-8"; | ||
| 156 | auto added_at = false; | ||
| 157 | if (l.getVariant() && std::strlen(l.getVariant()) > 0) | ||
| 158 | { | ||
| 159 | added_at = true; | ||
| 160 | posix_name += "@"; | ||
| 161 | posix_name += l.getVariant(); | ||
| 162 | } | ||
| 163 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 164 | auto* keywords = l.createKeywords(ec); | ||
| 165 | if (U_FAILURE(ec)) | ||
| 166 | throw std::runtime_error{"failed to create keywords"}; | ||
| 167 | if (keywords) | ||
| 168 | { | ||
| 169 | std::int32_t kw_len = 0; | ||
| 170 | char const* kw = nullptr; | ||
| 171 | while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) | ||
| 172 | { | ||
| 173 | auto value = | ||
| 174 | l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec); | ||
| 175 | if (!added_at) | ||
| 176 | { | ||
| 177 | posix_name += "@"; | ||
| 178 | added_at = true; | ||
| 179 | } | ||
| 180 | else | ||
| 181 | { | ||
| 182 | posix_name += ";"; | ||
| 183 | } | ||
| 184 | posix_name += kw; | ||
| 185 | posix_name += "="; | ||
| 186 | posix_name += value; | ||
| 187 | } | ||
| 188 | if (U_FAILURE(ec)) | ||
| 189 | throw std::runtime_error{"failed to iterate over keywords"}; | ||
| 190 | delete keywords; | ||
| 191 | } | ||
| 192 | return lgen_->generate(posix_name); | ||
| 193 | } | ||
| 194 | 72 | ||
| 195 | public: | 73 | public: |
| 196 | // Note: lgen must live at least as long as the selector constructed here! | 74 | // Note: lgen must live at least as long as the selector constructed here! |
| @@ -202,94 +80,11 @@ public: | |||
| 202 | { | 80 | { |
| 203 | } | 81 | } |
| 204 | 82 | ||
| 205 | auto select(std::string_view accept_language) const -> std::locale | 83 | auto select(std::string_view accept_language) const -> std::locale; |
| 206 | { | ||
| 207 | using namespace std::literals::string_view_literals; | ||
| 208 | // NOTE: can also contain a *;q=0.1 | ||
| 209 | // q should have at most 3 digits after period | ||
| 210 | auto dlpm = icu_locale_priority_map{}; | ||
| 211 | for (auto const [i, lang_prio] : | ||
| 212 | accept_language | std::views::split(","sv) | std::views::enumerate) | ||
| 213 | { | ||
| 214 | auto [lang_range_ut, mweight_ut] = | ||
| 215 | util::split_on(std::string_view{lang_prio}, ';'); | ||
| 216 | auto lang_range_str = trim_ows(lang_range_ut); | ||
| 217 | auto mweight_str = mweight_ut.transform(trim_ows); | ||
| 218 | if (lang_range_str == "*") | ||
| 219 | break; | ||
| 220 | |||
| 221 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 222 | auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); | ||
| 223 | if (U_FAILURE(ec) || icu_locale.isBogus()) | ||
| 224 | continue; // ignore this locale | ||
| 225 | |||
| 226 | auto weight = 1.0f; | ||
| 227 | if (mweight_str && mweight_str->starts_with("q=")) | ||
| 228 | { | ||
| 229 | auto weight_str = mweight_str->substr(2, 4); | ||
| 230 | if (auto mweight = | ||
| 231 | util::parse_float(weight_str, std::chars_format::fixed); | ||
| 232 | mweight && 0.0f < *mweight && *mweight < 1.0f) | ||
| 233 | { | ||
| 234 | weight = *mweight; | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | if (weight > 0.0f) | ||
| 239 | { | ||
| 240 | dlpm[icu_locale] = { | ||
| 241 | .weight = weight, | ||
| 242 | .original_index = static_cast<std::size_t>(i), | ||
| 243 | }; | ||
| 244 | } | ||
| 245 | else | ||
| 246 | { | ||
| 247 | dlpm.erase(icu_locale); | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | auto desired_locales = | ||
| 252 | std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()}; | ||
| 253 | std::sort(desired_locales.begin(), desired_locales.end()); | ||
| 254 | auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; | ||
| 255 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 256 | auto res = matcher_.getBestMatchResult(it, ec); | ||
| 257 | if (U_FAILURE(ec)) | ||
| 258 | return default_; | ||
| 259 | auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? | ||
| 260 | if (U_FAILURE(ec)) | ||
| 261 | return from_icu_locale(*res.getSupportedLocale()); | ||
| 262 | return from_icu_locale(resolved); | ||
| 263 | } | ||
| 264 | }; | 84 | }; |
| 265 | 85 | ||
| 266 | export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> | 86 | auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string>; |
| 267 | { | ||
| 268 | auto const& locale_info = std::use_facet<blocale::info>(locale); | ||
| 269 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 270 | auto bcp47_lang_tag = | ||
| 271 | icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec); | ||
| 272 | if (U_FAILURE(ec)) | ||
| 273 | return std::nullopt; | ||
| 274 | return bcp47_lang_tag; | ||
| 275 | } | ||
| 276 | |||
| 277 | #ifdef LOCALEDIR | ||
| 278 | #define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) | ||
| 279 | #define LOCALEDIR_AUX_STR(s) #s | ||
| 280 | constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; | ||
| 281 | #undef LOCALEDIR_AUX_STR | ||
| 282 | #undef LOCALEDIR_AUX_XSTR | ||
| 283 | #else // ifdef LOCALEDIR | ||
| 284 | constexpr auto messages_path = std::string_view{"locale/dev"}; | ||
| 285 | #endif // ifdef LOCALEDIR | ||
| 286 | 87 | ||
| 287 | export auto make_generator() -> std::shared_ptr<blocale::generator const> | 88 | export auto make_generator() -> std::shared_ptr<blocale::generator const>; |
| 288 | { | ||
| 289 | auto lgen = std::make_shared<blocale::generator>(); | ||
| 290 | lgen->add_messages_path(std::string{messages_path}); | ||
| 291 | lgen->add_messages_domain("routemon"); | ||
| 292 | return std::static_pointer_cast<blocale::generator const>(lgen); | ||
| 293 | } | ||
| 294 | 89 | ||
| 295 | } // namespace routemon::locale | 90 | } // 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 @@ | |||
| 1 | module routemon:log$impl; | ||
| 2 | |||
| 3 | import :log; | ||
| 4 | |||
| 5 | namespace routemon::log { | ||
| 6 | |||
| 7 | auto operator<<(std::ostream& os, level lvl) -> std::ostream& | ||
| 8 | { | ||
| 9 | switch (lvl) | ||
| 10 | { | ||
| 11 | case level::debug: | ||
| 12 | os << "dbg"; | ||
| 13 | break; | ||
| 14 | case level::info: | ||
| 15 | os << "inf"; | ||
| 16 | break; | ||
| 17 | case level::warn: | ||
| 18 | os << "wrn"; | ||
| 19 | break; | ||
| 20 | case level::error: | ||
| 21 | os << "err"; | ||
| 22 | break; | ||
| 23 | } | ||
| 24 | return os; | ||
| 25 | } | ||
| 26 | |||
| 27 | sink::sink(enum level lvl) : lvl_{lvl} {} | ||
| 28 | |||
| 29 | auto sink::level() const -> enum level { return lvl_; } | ||
| 30 | |||
| 31 | auto sink::set_level(enum level lvl) -> void { lvl_ = lvl; } | ||
| 32 | |||
| 33 | auto sink::write(tmp_message msg) -> void | ||
| 34 | { | ||
| 35 | auto sos = std::osyncstream{os_}; | ||
| 36 | sos << "[" << msg.lvl; | ||
| 37 | if (!msg.component.empty()) | ||
| 38 | sos << " " << msg.component; | ||
| 39 | sos << "] " << msg.txt; | ||
| 40 | for (auto const& [k, v] : msg.attrs) | ||
| 41 | sos << " " << k << "=" << std::quoted(v); | ||
| 42 | sos << '\n'; | ||
| 43 | } | ||
| 44 | |||
| 45 | auto make_sink(level lvl) -> std::shared_ptr<sink> | ||
| 46 | { | ||
| 47 | return std::shared_ptr<sink>{new sink{lvl}}; | ||
| 48 | } | ||
| 49 | |||
| 50 | auto logger::log_at(log::level lvl, std::string_view fmt, std::format_args args) | ||
| 51 | -> logger& | ||
| 52 | { | ||
| 53 | if (sink_->level() <= lvl) | ||
| 54 | sink_->write( | ||
| 55 | sink::tmp_message{ | ||
| 56 | .lvl = lvl, | ||
| 57 | .component = component_, | ||
| 58 | .txt = std::vformat(fmt, args), | ||
| 59 | .attrs = attrs_, | ||
| 60 | }); | ||
| 61 | return *this; | ||
| 62 | } | ||
| 63 | |||
| 64 | logger::logger(std::shared_ptr<sink> const& sink) : sink_{sink} | ||
| 65 | { | ||
| 66 | if (!sink) | ||
| 67 | { | ||
| 68 | throw std::invalid_argument{"logger sink may not be null"}; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | auto logger::sub(std::string_view component) const -> logger | ||
| 73 | { | ||
| 74 | auto l = *this; | ||
| 75 | if (l.component_.empty()) | ||
| 76 | { | ||
| 77 | l.component_ = component; | ||
| 78 | } | ||
| 79 | else | ||
| 80 | { | ||
| 81 | l.component_ += "."; | ||
| 82 | l.component_ += component; | ||
| 83 | } | ||
| 84 | return l; | ||
| 85 | } | ||
| 86 | |||
| 87 | auto logger::with(std::string const& k, std::string&& v) const -> logger | ||
| 88 | { | ||
| 89 | auto l = *this; | ||
| 90 | l.attrs_[k] = std::move(v); | ||
| 91 | return l; | ||
| 92 | } | ||
| 93 | |||
| 94 | auto logger::with(std::string const& k, std::string_view v) const -> logger | ||
| 95 | { | ||
| 96 | return with(k, std::string{v}); | ||
| 97 | } | ||
| 98 | |||
| 99 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | // Seems like ADL for std::quoted is broken with | ||
| 4 | // import std; | ||
| 5 | // Might be because the _Quoted_string object is defined in | ||
| 6 | // std::__detail, which is not exported by the module. | ||
| 7 | #include <iomanip> | ||
| 8 | |||
| 9 | export module routemon:log; | 1 | export module routemon:log; |
| 10 | 2 | ||
| 11 | import std; | 3 | import std; |
| @@ -19,35 +11,16 @@ export enum class level : std::uint8_t { | |||
| 19 | error, | 11 | error, |
| 20 | }; | 12 | }; |
| 21 | 13 | ||
| 22 | namespace { | ||
| 23 | |||
| 24 | auto operator<<(std::ostream& os, level lvl) -> std::ostream& | ||
| 25 | { | ||
| 26 | switch (lvl) | ||
| 27 | { | ||
| 28 | case level::debug: | ||
| 29 | os << "dbg"; | ||
| 30 | break; | ||
| 31 | case level::info: | ||
| 32 | os << "inf"; | ||
| 33 | break; | ||
| 34 | case level::warn: | ||
| 35 | os << "wrn"; | ||
| 36 | break; | ||
| 37 | case level::error: | ||
| 38 | os << "err"; | ||
| 39 | break; | ||
| 40 | } | ||
| 41 | return os; | ||
| 42 | } | ||
| 43 | |||
| 44 | } // namespace | ||
| 45 | |||
| 46 | export class sink | 14 | export class sink |
| 47 | { | 15 | { |
| 48 | std::atomic<enum level> lvl_; | 16 | std::atomic<enum level> lvl_; |
| 49 | std::ostream& os_ = std::cout; | 17 | std::ostream& os_ = std::cout; |
| 50 | 18 | ||
| 19 | explicit sink(level lvl); | ||
| 20 | |||
| 21 | friend auto make_sink(level lvl) -> std::shared_ptr<sink>; | ||
| 22 | |||
| 23 | public: | ||
| 51 | struct tmp_message | 24 | struct tmp_message |
| 52 | { | 25 | { |
| 53 | level lvl; | 26 | level lvl; |
| @@ -56,35 +29,13 @@ export class sink | |||
| 56 | std::map<std::string, std::string> const& attrs; | 29 | std::map<std::string, std::string> const& attrs; |
| 57 | }; | 30 | }; |
| 58 | 31 | ||
| 59 | auto write(tmp_message msg) -> void | 32 | [[nodiscard]] auto level() const -> enum level; |
| 60 | { | 33 | auto set_level(enum level lvl) -> void; |
| 61 | auto sos = std::osyncstream{os_}; | ||
| 62 | sos << "[" << msg.lvl; | ||
| 63 | if (!msg.component.empty()) | ||
| 64 | sos << " " << msg.component; | ||
| 65 | sos << "] " << msg.txt; | ||
| 66 | for (auto const& [k, v] : msg.attrs) | ||
| 67 | { | ||
| 68 | sos << " " << k << "=" << std::quoted(v); | ||
| 69 | } | ||
| 70 | sos << '\n'; | ||
| 71 | } | ||
| 72 | |||
| 73 | explicit sink(level lvl) : lvl_{lvl} {} | ||
| 74 | |||
| 75 | friend auto make_sink(level lvl) -> std::shared_ptr<sink>; | ||
| 76 | friend class logger; | ||
| 77 | |||
| 78 | public: | ||
| 79 | [[nodiscard]] auto level() const -> enum level { return lvl_; } | ||
| 80 | 34 | ||
| 81 | auto set_level(enum level lvl) -> void { lvl_ = lvl; } | 35 | auto write(tmp_message msg) -> void; |
| 82 | }; | 36 | }; |
| 83 | 37 | ||
| 84 | export auto make_sink(level lvl) -> std::shared_ptr<sink> | 38 | export auto make_sink(level lvl) -> std::shared_ptr<sink>; |
| 85 | { | ||
| 86 | return std::shared_ptr<sink>{new sink{lvl}}; | ||
| 87 | } | ||
| 88 | 39 | ||
| 89 | export class logger | 40 | export class logger |
| 90 | { | 41 | { |
| @@ -92,79 +43,40 @@ export class logger | |||
| 92 | std::string component_; | 43 | std::string component_; |
| 93 | std::map<std::string, std::string> attrs_; | 44 | std::map<std::string, std::string> attrs_; |
| 94 | 45 | ||
| 95 | template <log::level lvl> | 46 | auto log_at(log::level lvl, std::string_view fmt, std::format_args args) |
| 96 | auto log_at(std::string_view fmt, std::format_args args) -> logger& | 47 | -> logger&; |
| 97 | { | ||
| 98 | if (sink_->level() <= lvl) | ||
| 99 | sink_->write( | ||
| 100 | sink::tmp_message{ | ||
| 101 | .lvl = lvl, | ||
| 102 | .component = component_, | ||
| 103 | .txt = std::vformat(fmt, args), | ||
| 104 | .attrs = attrs_, | ||
| 105 | }); | ||
| 106 | return *this; | ||
| 107 | } | ||
| 108 | 48 | ||
| 109 | public: | 49 | public: |
| 110 | explicit logger(std::shared_ptr<sink> const& sink) : sink_{sink} | 50 | explicit logger(std::shared_ptr<sink> const& sink); |
| 111 | { | ||
| 112 | if (!sink) | ||
| 113 | { | ||
| 114 | throw std::invalid_argument{"logger sink may not be null"}; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | [[nodiscard]] auto sub(std::string_view component) const -> logger | ||
| 119 | { | ||
| 120 | auto l = *this; | ||
| 121 | if (l.component_.empty()) | ||
| 122 | { | ||
| 123 | l.component_ = component; | ||
| 124 | } | ||
| 125 | else | ||
| 126 | { | ||
| 127 | l.component_ += "."; | ||
| 128 | l.component_ += component; | ||
| 129 | } | ||
| 130 | return l; | ||
| 131 | } | ||
| 132 | |||
| 133 | [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger | ||
| 134 | { | ||
| 135 | auto l = *this; | ||
| 136 | l.attrs_[k] = std::move(v); | ||
| 137 | return l; | ||
| 138 | } | ||
| 139 | 51 | ||
| 52 | [[nodiscard]] auto sub(std::string_view component) const -> logger; | ||
| 53 | [[nodiscard]] auto with(std::string const& k, std::string&& v) const | ||
| 54 | -> logger; | ||
| 140 | [[nodiscard]] auto with(std::string const& k, std::string_view v) const | 55 | [[nodiscard]] auto with(std::string const& k, std::string_view v) const |
| 141 | -> logger | 56 | -> logger; |
| 142 | { | ||
| 143 | return with(k, std::string{v}); | ||
| 144 | } | ||
| 145 | 57 | ||
| 146 | template <class... Args> | 58 | template <class... Args> |
| 147 | auto debug(std::format_string<Args...> fmt, Args&&... args) -> logger& | 59 | auto debug(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 148 | { | 60 | { |
| 149 | return log_at<level::debug>(fmt.get(), std::make_format_args(args...)); | 61 | return log_at(level::debug, fmt.get(), std::make_format_args(args...)); |
| 150 | } | 62 | } |
| 151 | 63 | ||
| 152 | template <class... Args> | 64 | template <class... Args> |
| 153 | auto info(std::format_string<Args...> fmt, Args&&... args) -> logger& | 65 | auto info(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 154 | { | 66 | { |
| 155 | return log_at<level::info>(fmt.get(), std::make_format_args(args...)); | 67 | return log_at(level::info, fmt.get(), std::make_format_args(args...)); |
| 156 | } | 68 | } |
| 157 | 69 | ||
| 158 | template <class... Args> | 70 | template <class... Args> |
| 159 | auto warn(std::format_string<Args...> fmt, Args&&... args) -> logger& | 71 | auto warn(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 160 | { | 72 | { |
| 161 | return log_at<level::warn>(fmt.get(), std::make_format_args(args...)); | 73 | return log_at(level::warn, fmt.get(), std::make_format_args(args...)); |
| 162 | } | 74 | } |
| 163 | 75 | ||
| 164 | template <class... Args> | 76 | template <class... Args> |
| 165 | auto error(std::format_string<Args...> fmt, Args&&... args) -> logger& | 77 | auto error(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 166 | { | 78 | { |
| 167 | return log_at<level::error>(fmt.get(), std::make_format_args(args...)); | 79 | return log_at(level::error, fmt.get(), std::make_format_args(args...)); |
| 168 | } | 80 | } |
| 169 | }; | 81 | }; |
| 170 | 82 | ||
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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/http/message.hpp> | ||
| 4 | #include <boost/json.hpp> | ||
| 5 | #include <boost/locale/message.hpp> | ||
| 6 | |||
| 7 | module routemon:problem$impl; | ||
| 8 | |||
| 9 | import :problem; | ||
| 10 | |||
| 11 | namespace routemon::problem { | ||
| 12 | |||
| 13 | auto details::set_detail(blocale::message detail) -> details& | ||
| 14 | { | ||
| 15 | this->detail = detail; | ||
| 16 | return *this; | ||
| 17 | } | ||
| 18 | |||
| 19 | auto details::set_instance(std::string&& instance) -> details& | ||
| 20 | { | ||
| 21 | this->instance = instance; | ||
| 22 | return *this; | ||
| 23 | } | ||
| 24 | |||
| 25 | auto details::set_instance(std::string_view instance) -> details& | ||
| 26 | { | ||
| 27 | this->instance = std::string{instance}; | ||
| 28 | return *this; | ||
| 29 | } | ||
| 30 | |||
| 31 | auto tag_invoke( | ||
| 32 | json::value_from_tag, json::value& jv, details const& details, | ||
| 33 | std::locale locale) -> void | ||
| 34 | { | ||
| 35 | auto obj = json::object{ | ||
| 36 | {"type", details.type_uri}, | ||
| 37 | {"title", details.title.str(locale)}, | ||
| 38 | {"status", static_cast<unsigned>(details.status)}, | ||
| 39 | }; | ||
| 40 | if (details.detail) | ||
| 41 | obj["detail"] = details.detail->str(locale); | ||
| 42 | if (details.instance) | ||
| 43 | obj["instance"] = *details.instance; | ||
| 44 | jv = obj; | ||
| 45 | } | ||
| 46 | |||
| 47 | auto tpl::instantiate() const -> details | ||
| 48 | { | ||
| 49 | return details{status, title, type_uri}; | ||
| 50 | } | ||
| 51 | |||
| 52 | } // 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 | |||
| 22 | std::optional<blocale::message> detail = std::nullopt; | 22 | std::optional<blocale::message> detail = std::nullopt; |
| 23 | std::optional<std::string> instance = std::nullopt; | 23 | std::optional<std::string> instance = std::nullopt; |
| 24 | 24 | ||
| 25 | auto set_detail(blocale::message detail) -> details& | 25 | auto set_detail(blocale::message detail) -> details&; |
| 26 | { | 26 | auto set_instance(std::string&& instance) -> details&; |
| 27 | this->detail = detail; | 27 | auto set_instance(std::string_view instance) -> details&; |
| 28 | return *this; | ||
| 29 | } | ||
| 30 | |||
| 31 | auto set_instance(std::string&& instance) -> details& | ||
| 32 | { | ||
| 33 | this->instance = instance; | ||
| 34 | return *this; | ||
| 35 | } | ||
| 36 | auto set_instance(std::string_view instance) -> details& | ||
| 37 | { | ||
| 38 | this->instance = std::string{instance}; | ||
| 39 | return *this; | ||
| 40 | } | ||
| 41 | }; | 28 | }; |
| 42 | 29 | ||
| 43 | export auto tag_invoke( | 30 | auto tag_invoke( |
| 44 | json::value_from_tag, json::value& jv, details const& details, | 31 | json::value_from_tag, json::value& jv, details const& details, |
| 45 | std::locale locale) -> void | 32 | std::locale locale) -> void; |
| 46 | { | ||
| 47 | auto obj = json::object{ | ||
| 48 | {"type", details.type_uri}, | ||
| 49 | {"title", details.title.str(locale)}, | ||
| 50 | {"status", static_cast<unsigned>(details.status)}, | ||
| 51 | }; | ||
| 52 | if (details.detail) | ||
| 53 | obj["detail"] = details.detail->str(locale); | ||
| 54 | if (details.instance) | ||
| 55 | obj["instance"] = *details.instance; | ||
| 56 | jv = obj; | ||
| 57 | } | ||
| 58 | 33 | ||
| 59 | export struct tpl | 34 | export struct tpl |
| 60 | { | 35 | { |
| @@ -62,10 +37,7 @@ export struct tpl | |||
| 62 | blocale::message title; | 37 | blocale::message title; |
| 63 | std::string_view type_uri; | 38 | std::string_view type_uri; |
| 64 | 39 | ||
| 65 | auto instantiate() const -> details | 40 | auto instantiate() const -> details; |
| 66 | { | ||
| 67 | return details{status, title, type_uri}; | ||
| 68 | } | ||
| 69 | }; | 41 | }; |
| 70 | 42 | ||
| 71 | } // namespace routemon::problem | 43 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/http/message.hpp> | ||
| 4 | #include <boost/beast/http/verb.hpp> | ||
| 5 | |||
| 6 | export module routemon:req_ctx; | ||
| 7 | |||
| 8 | import :http.common; | ||
| 9 | import :trace; | ||
| 10 | |||
| 11 | namespace routemon { | ||
| 12 | |||
| 13 | export class req_ctx | ||
| 14 | { | ||
| 15 | trace::id tid_; | ||
| 16 | std::locale locale_; | ||
| 17 | http::verb_set route_verbs_; | ||
| 18 | bool keep_alive_; | ||
| 19 | bhttp::request_header<bhttp::fields> const& req_header_; | ||
| 20 | |||
| 21 | public: | ||
| 22 | explicit req_ctx( | ||
| 23 | trace::id tid, std::locale locale, http::verb_set route_verbs, | ||
| 24 | bool keep_alive, bhttp::request_header<bhttp::fields> const& req_header) | ||
| 25 | : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, | ||
| 26 | keep_alive_{keep_alive}, req_header_{req_header} | ||
| 27 | { | ||
| 28 | } | ||
| 29 | |||
| 30 | template <typename ReqBody> | ||
| 31 | explicit req_ctx( | ||
| 32 | trace::id tid, std::locale locale, http::verb_set route_verbs, | ||
| 33 | bhttp::request<ReqBody> const& req) | ||
| 34 | : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} | ||
| 35 | { | ||
| 36 | } | ||
| 37 | |||
| 38 | auto trace_id() const -> trace::id { return tid_; } | ||
| 39 | auto locale() const -> std::locale { return locale_; } | ||
| 40 | auto route_verbs() const -> http::verb_set { return route_verbs_; } | ||
| 41 | auto keep_alive() const -> bool { return keep_alive_; } | ||
| 42 | auto req_header() const -> bhttp::request_header<bhttp::fields> const& | ||
| 43 | { | ||
| 44 | return req_header_; | ||
| 45 | } | ||
| 46 | }; | ||
| 47 | |||
| 48 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/core.hpp> | ||
| 4 | #include <boost/beast/http.hpp> | ||
| 5 | #include <boost/json.hpp> | ||
| 6 | |||
| 7 | module routemon:rwgps$impl; | ||
| 8 | |||
| 9 | import :rwgps; | ||
| 10 | |||
| 11 | namespace beast = boost::beast; | ||
| 12 | namespace bhttp = beast::http; | ||
| 13 | namespace json = boost::json; | ||
| 14 | |||
| 15 | namespace routemon::rwgps { | ||
| 16 | |||
| 17 | export struct route_summary | ||
| 18 | { | ||
| 19 | std::int64_t id; | ||
| 20 | std::int64_t user_id; | ||
| 21 | std::string url; | ||
| 22 | std::string name; | ||
| 23 | std::string description; | ||
| 24 | }; | ||
| 25 | |||
| 26 | struct pagination | ||
| 27 | { | ||
| 28 | std::size_t record_count; | ||
| 29 | std::size_t page_count; | ||
| 30 | std::size_t page_size; | ||
| 31 | std::optional<std::string> next_page_url; | ||
| 32 | }; | ||
| 33 | |||
| 34 | struct get_routes_meta | ||
| 35 | { | ||
| 36 | pagination pagination; | ||
| 37 | }; | ||
| 38 | |||
| 39 | struct get_routes_response | ||
| 40 | { | ||
| 41 | std::vector<route_summary> routes; | ||
| 42 | get_routes_meta meta; | ||
| 43 | }; | ||
| 44 | |||
| 45 | auto tag_invoke(json::value_to_tag<route_summary> const&, json::value const& jv) | ||
| 46 | -> route_summary | ||
| 47 | { | ||
| 48 | return { | ||
| 49 | .id = json::value_to<std::int64_t>(jv.at("id")), | ||
| 50 | .user_id = json::value_to<std::int64_t>(jv.at("user_id")), | ||
| 51 | .url = json::value_to<std::string>(jv.at("url")), | ||
| 52 | .name = json::value_to<std::string>(jv.at("name")), | ||
| 53 | .description = json::value_to<std::string>(jv.at("description")), | ||
| 54 | }; | ||
| 55 | } | ||
| 56 | |||
| 57 | auto tag_invoke(json::value_to_tag<pagination> const&, json::value const& jv) | ||
| 58 | -> pagination | ||
| 59 | { | ||
| 60 | return { | ||
| 61 | .record_count = json::value_to<std::size_t>(jv.at("record_count")), | ||
| 62 | .page_count = json::value_to<std::size_t>(jv.at("page_count")), | ||
| 63 | .page_size = json::value_to<std::size_t>(jv.at("page_size")), | ||
| 64 | .next_page_url = | ||
| 65 | json::value_to<std::optional<std::string>>(jv.at("next_page_url")), | ||
| 66 | }; | ||
| 67 | } | ||
| 68 | |||
| 69 | auto tag_invoke( | ||
| 70 | json::value_to_tag<get_routes_meta> const&, json::value const& jv) | ||
| 71 | -> get_routes_meta | ||
| 72 | { | ||
| 73 | return { | ||
| 74 | .pagination = json::value_to<pagination>(jv.at("pagination")), | ||
| 75 | }; | ||
| 76 | } | ||
| 77 | |||
| 78 | auto tag_invoke( | ||
| 79 | json::value_to_tag<get_routes_response> const&, json::value const& jv) | ||
| 80 | -> get_routes_response | ||
| 81 | { | ||
| 82 | return { | ||
| 83 | .routes = json::value_to<std::vector<route_summary>>(jv.at("routes")), | ||
| 84 | .meta = json::value_to<get_routes_meta>(jv.at("meta")), | ||
| 85 | }; | ||
| 86 | } | ||
| 87 | |||
| 88 | auto json_value_to_get_routes_response(json::value const& jv) | ||
| 89 | -> get_routes_response | ||
| 90 | { | ||
| 91 | return json::value_to<get_routes_response>(jv); | ||
| 92 | } | ||
| 93 | |||
| 94 | constexpr std::string host = "ridewithgps.com"; | ||
| 95 | |||
| 96 | // TODO: handle failure appropriately | ||
| 97 | auto client::get_routes_page(std::size_t page) -> get_routes_response | ||
| 98 | { | ||
| 99 | auto req = bhttp::request<bhttp::string_body>{ | ||
| 100 | bhttp::verb::get, | ||
| 101 | std::format("/api/v1/routes.json?page_size=200?page={}", page), | ||
| 102 | 11, // HTTP 1.1 | ||
| 103 | }; | ||
| 104 | req.set(bhttp::field::host, host); | ||
| 105 | req.set("x-rwgps-api-key", api_key_); | ||
| 106 | req.set("x-rwgps-auth-token", auth_token_); | ||
| 107 | |||
| 108 | auto rsp = hc_.do_request(req); | ||
| 109 | auto p = json::stream_parser{}; | ||
| 110 | for (auto const frag : rsp.body().cdata()) | ||
| 111 | p.write(static_cast<char const*>(frag.data()), frag.size()); | ||
| 112 | assert(p.done()); | ||
| 113 | return json_value_to_get_routes_response(p.release()); | ||
| 114 | } | ||
| 115 | |||
| 116 | client::client( | ||
| 117 | net::io_context& ioc, log::logger const& l, std::string api_key, | ||
| 118 | std::string auth_token) | ||
| 119 | : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, | ||
| 120 | auth_token_{std::move(auth_token)} | ||
| 121 | { | ||
| 122 | } | ||
| 123 | |||
| 124 | auto client::get_all_routes() -> std::vector<route_summary> | ||
| 125 | { | ||
| 126 | // TODO: make sure that there are no duplicates here. | ||
| 127 | // What does RWGPS sort on, by default? | ||
| 128 | // Consider using an associative container instead of a vector. | ||
| 129 | auto record_count = 0uz; | ||
| 130 | auto current_page = 0uz; | ||
| 131 | auto routes = std::vector<route_summary>{}; | ||
| 132 | |||
| 133 | while (true) | ||
| 134 | { | ||
| 135 | auto rsp = get_routes_page(current_page); | ||
| 136 | if (rsp.meta.pagination.next_page_url) | ||
| 137 | l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); | ||
| 138 | routes.append_range(rsp.routes); | ||
| 139 | if (rsp.meta.pagination.record_count > 0) | ||
| 140 | record_count = rsp.meta.pagination.record_count; | ||
| 141 | if (rsp.routes.empty() || routes.size() >= record_count) | ||
| 142 | { | ||
| 143 | break; | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | return routes; | ||
| 148 | } | ||
| 149 | |||
| 150 | } // 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 @@ | |||
| 1 | module; | 1 | module; |
| 2 | 2 | ||
| 3 | #include <boost/beast/core.hpp> | 3 | #include <boost/asio/io_context.hpp> |
| 4 | #include <boost/beast/http.hpp> | ||
| 5 | #include <boost/json.hpp> | ||
| 6 | 4 | ||
| 7 | export module routemon:rwgps; | 5 | export module routemon:rwgps; |
| 8 | 6 | ||
| @@ -10,10 +8,7 @@ import std; | |||
| 10 | import :http.client; | 8 | import :http.client; |
| 11 | import :log; | 9 | import :log; |
| 12 | 10 | ||
| 13 | namespace beast = boost::beast; | ||
| 14 | namespace bhttp = beast::http; | ||
| 15 | namespace net = boost::asio; | 11 | namespace net = boost::asio; |
| 16 | namespace json = boost::json; | ||
| 17 | 12 | ||
| 18 | namespace routemon::rwgps { | 13 | namespace routemon::rwgps { |
| 19 | 14 | ||
| @@ -45,55 +40,6 @@ struct get_routes_response | |||
| 45 | get_routes_meta meta; | 40 | get_routes_meta meta; |
| 46 | }; | 41 | }; |
| 47 | 42 | ||
| 48 | auto tag_invoke(json::value_to_tag<route_summary> const&, json::value const& jv) | ||
| 49 | -> route_summary | ||
| 50 | { | ||
| 51 | return { | ||
| 52 | .id = json::value_to<std::int64_t>(jv.at("id")), | ||
| 53 | .user_id = json::value_to<std::int64_t>(jv.at("user_id")), | ||
| 54 | .url = json::value_to<std::string>(jv.at("url")), | ||
| 55 | .name = json::value_to<std::string>(jv.at("name")), | ||
| 56 | .description = json::value_to<std::string>(jv.at("description")), | ||
| 57 | }; | ||
| 58 | } | ||
| 59 | |||
| 60 | auto tag_invoke(json::value_to_tag<pagination> const&, json::value const& jv) | ||
| 61 | -> pagination | ||
| 62 | { | ||
| 63 | return { | ||
| 64 | .record_count = json::value_to<std::size_t>(jv.at("record_count")), | ||
| 65 | .page_count = json::value_to<std::size_t>(jv.at("page_count")), | ||
| 66 | .page_size = json::value_to<std::size_t>(jv.at("page_size")), | ||
| 67 | .next_page_url = | ||
| 68 | json::value_to<std::optional<std::string>>(jv.at("next_page_url")), | ||
| 69 | }; | ||
| 70 | } | ||
| 71 | |||
| 72 | auto tag_invoke( | ||
| 73 | json::value_to_tag<get_routes_meta> const&, json::value const& jv) | ||
| 74 | -> get_routes_meta | ||
| 75 | { | ||
| 76 | return { | ||
| 77 | .pagination = json::value_to<pagination>(jv.at("pagination")), | ||
| 78 | }; | ||
| 79 | } | ||
| 80 | |||
| 81 | auto tag_invoke( | ||
| 82 | json::value_to_tag<get_routes_response> const&, json::value const& jv) | ||
| 83 | -> get_routes_response | ||
| 84 | { | ||
| 85 | return { | ||
| 86 | .routes = json::value_to<std::vector<route_summary>>(jv.at("routes")), | ||
| 87 | .meta = json::value_to<get_routes_meta>(jv.at("meta")), | ||
| 88 | }; | ||
| 89 | } | ||
| 90 | |||
| 91 | auto json_value_to_get_routes_response(json::value const& jv) | ||
| 92 | -> get_routes_response | ||
| 93 | { | ||
| 94 | return json::value_to<get_routes_response>(jv); | ||
| 95 | } | ||
| 96 | |||
| 97 | export class client | 43 | export class client |
| 98 | { | 44 | { |
| 99 | log::logger l_; | 45 | log::logger l_; |
| @@ -101,64 +47,14 @@ export class client | |||
| 101 | std::string api_key_; | 47 | std::string api_key_; |
| 102 | std::string auth_token_; | 48 | std::string auth_token_; |
| 103 | 49 | ||
| 104 | static constexpr std::string host = "ridewithgps.com"; | 50 | auto get_routes_page(std::size_t page) -> get_routes_response; |
| 105 | |||
| 106 | // TODO: handle failure appropriately | ||
| 107 | auto get_routes_page(std::size_t page) -> get_routes_response | ||
| 108 | { | ||
| 109 | auto req = bhttp::request<bhttp::string_body>{ | ||
| 110 | bhttp::verb::get, | ||
| 111 | std::format("/api/v1/routes.json?page_size=200?page={}", page), | ||
| 112 | 11, // HTTP 1.1 | ||
| 113 | }; | ||
| 114 | req.set(bhttp::field::host, host); | ||
| 115 | req.set("x-rwgps-api-key", api_key_); | ||
| 116 | req.set("x-rwgps-auth-token", auth_token_); | ||
| 117 | |||
| 118 | auto rsp = hc_.do_request(req); | ||
| 119 | auto p = json::stream_parser{}; | ||
| 120 | for (auto const frag : rsp.body().cdata()) | ||
| 121 | { | ||
| 122 | p.write(static_cast<char const*>(frag.data()), frag.size()); | ||
| 123 | } | ||
| 124 | assert(p.done()); | ||
| 125 | return json_value_to_get_routes_response(p.release()); | ||
| 126 | } | ||
| 127 | 51 | ||
| 128 | public: | 52 | public: |
| 129 | explicit client( | 53 | explicit client( |
| 130 | net::io_context& ioc, log::logger const& l, std::string api_key, | 54 | net::io_context& ioc, log::logger const& l, std::string api_key, |
| 131 | std::string auth_token) | 55 | std::string auth_token); |
| 132 | : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, | ||
| 133 | auth_token_{std::move(auth_token)} | ||
| 134 | { | ||
| 135 | } | ||
| 136 | |||
| 137 | auto get_all_routes() -> std::vector<route_summary> | ||
| 138 | { | ||
| 139 | // TODO: make sure that there are no duplicates here. | ||
| 140 | // What does RWGPS sort on, by default? | ||
| 141 | // Consider using an associative container instead of a vector. | ||
| 142 | auto record_count = 0uz; | ||
| 143 | auto current_page = 0uz; | ||
| 144 | auto routes = std::vector<route_summary>{}; | ||
| 145 | |||
| 146 | while (true) | ||
| 147 | { | ||
| 148 | auto rsp = get_routes_page(current_page); | ||
| 149 | if (rsp.meta.pagination.next_page_url) | ||
| 150 | l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); | ||
| 151 | routes.append_range(rsp.routes); | ||
| 152 | if (rsp.meta.pagination.record_count > 0) | ||
| 153 | record_count = rsp.meta.pagination.record_count; | ||
| 154 | if (rsp.routes.empty() || routes.size() >= record_count) | ||
| 155 | { | ||
| 156 | break; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | 56 | ||
| 160 | return routes; | 57 | auto get_all_routes() -> std::vector<route_summary>; |
| 161 | } | ||
| 162 | }; | 58 | }; |
| 163 | 59 | ||
| 164 | } // namespace routemon::rwgps | 60 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <sqlite3.h> | ||
| 4 | |||
| 5 | module routemon:sqlite3$impl; | ||
| 6 | |||
| 7 | import :sqlite3; | ||
| 8 | |||
| 9 | namespace routemon::sqlite3 { | ||
| 10 | |||
| 11 | mutex_guard::mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} {} | ||
| 12 | |||
| 13 | mutex_guard::~mutex_guard() { ::sqlite3_mutex_leave(mut_); } | ||
| 14 | |||
| 15 | auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) | ||
| 16 | -> decltype(f(std::declval<mutex_guard const&>())) | ||
| 17 | { | ||
| 18 | return f(mutex_guard{mut}); | ||
| 19 | } | ||
| 20 | |||
| 21 | auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) | ||
| 22 | -> decltype(f(std::declval<mutex_guard const&>())) | ||
| 23 | { | ||
| 24 | return do_guarded(::sqlite3_db_mutex(dbc), f); | ||
| 25 | } | ||
| 26 | |||
| 27 | error::error(mutex_guard const&, int code, ::sqlite3* dbc) | ||
| 28 | : code_{code}, message_{::sqlite3_errmsg(dbc)} | ||
| 29 | { | ||
| 30 | } | ||
| 31 | |||
| 32 | error::error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} | ||
| 33 | |||
| 34 | [[nodiscard]] auto error::what() const noexcept -> char const* | ||
| 35 | { | ||
| 36 | return message_.c_str(); | ||
| 37 | } | ||
| 38 | |||
| 39 | [[nodiscard]] auto error::code() const noexcept -> int { return code_; } | ||
| 40 | |||
| 41 | statement::statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} | ||
| 42 | statement::statement(statement&& s) noexcept | ||
| 43 | { | ||
| 44 | stmt_ = s.stmt_; | ||
| 45 | s.stmt_ = nullptr; | ||
| 46 | } | ||
| 47 | statement::~statement() { ::sqlite3_finalize(stmt_); } | ||
| 48 | auto statement::get() -> ::sqlite3_stmt* { return stmt_; } | ||
| 49 | |||
| 50 | row_reader::row_reader(statement stmt) : stmt_{std::move(stmt)} {} | ||
| 51 | |||
| 52 | auto row_reader::is_null(int col) -> bool | ||
| 53 | { | ||
| 54 | return ::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL; | ||
| 55 | } | ||
| 56 | |||
| 57 | auto row_reader::ncols() -> std::size_t | ||
| 58 | { | ||
| 59 | auto const mncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); | ||
| 60 | if (!mncols.has_value()) | ||
| 61 | throw std::logic_error{"got unexpected negative amount of columns"}; | ||
| 62 | return *mncols; | ||
| 63 | } | ||
| 64 | |||
| 65 | auto row_reader::scan(int col, std::string& s) -> void | ||
| 66 | { | ||
| 67 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) | ||
| 68 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 69 | unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); | ||
| 70 | auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); | ||
| 71 | if (!size.has_value()) | ||
| 72 | throw std::logic_error{"unexpected negative amount of bytes in column"}; | ||
| 73 | s = std::string{reinterpret_cast<char const*>(chs), *size}; | ||
| 74 | } | ||
| 75 | |||
| 76 | auto row_reader::scan(int col, double& v) -> void | ||
| 77 | { | ||
| 78 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) | ||
| 79 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 80 | v = ::sqlite3_column_double(stmt_.get(), col); | ||
| 81 | } | ||
| 82 | |||
| 83 | auto row_reader::scan(int col, std::int64_t& v) -> void | ||
| 84 | { | ||
| 85 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) | ||
| 86 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 87 | v = ::sqlite3_column_int64(stmt_.get(), col); | ||
| 88 | } | ||
| 89 | |||
| 90 | auto row_reader::next() -> bool | ||
| 91 | { | ||
| 92 | ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); | ||
| 93 | return do_guarded( | ||
| 94 | dbc, | ||
| 95 | [&](auto const& guard) -> bool | ||
| 96 | { | ||
| 97 | auto const s = ::sqlite3_step(stmt_.get()); | ||
| 98 | if (s == SQLITE_ROW) | ||
| 99 | return true; | ||
| 100 | if (s == SQLITE_DONE) | ||
| 101 | return false; | ||
| 102 | throw error{guard, s, dbc}; | ||
| 103 | }); | ||
| 104 | } | ||
| 105 | |||
| 106 | binder::binder(statement& stmt) : stmt_{stmt} {} | ||
| 107 | |||
| 108 | auto binder::text(std::string const& param_name, std::string_view str) -> void | ||
| 109 | { | ||
| 110 | int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); | ||
| 111 | if (i == 0) | ||
| 112 | throw std::invalid_argument{std::format( | ||
| 113 | "bind: no parameter with name {} found", param_name)}; | ||
| 114 | auto str_size = util::int_from_size(str.size()); | ||
| 115 | if (!str_size.has_value()) | ||
| 116 | throw std::invalid_argument{"bind: provided text is too long"}; | ||
| 117 | if (auto s = ::sqlite3_bind_text( | ||
| 118 | stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); | ||
| 119 | s != SQLITE_OK) | ||
| 120 | { | ||
| 121 | throw error{s}; | ||
| 122 | } | ||
| 123 | } | ||
| 124 | |||
| 125 | auto binder::noop(binder&) -> void {} | ||
| 126 | |||
| 127 | connection::connection(::sqlite3* dbc) | ||
| 128 | : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} | ||
| 129 | { | ||
| 130 | } | ||
| 131 | |||
| 132 | connection::connection(connection const&) = delete; | ||
| 133 | connection::connection(connection&& c) noexcept | ||
| 134 | { | ||
| 135 | dbc_ = c.dbc_; | ||
| 136 | mut_ = c.mut_; | ||
| 137 | c.dbc_ = nullptr; | ||
| 138 | c.mut_ = nullptr; | ||
| 139 | } | ||
| 140 | |||
| 141 | auto connection::query( | ||
| 142 | std::string const& sql, std::function<void(binder&)> const& bf) | ||
| 143 | -> row_reader | ||
| 144 | { | ||
| 145 | ::sqlite3_stmt* pstmt = nullptr; | ||
| 146 | char const* sql_tail = nullptr; | ||
| 147 | auto sql_size = util::int_from_size(sql.size()); | ||
| 148 | if (!sql_size.has_value() || *sql_size >= std::numeric_limits<int>::max() - 1) | ||
| 149 | throw std::invalid_argument{"provided input text too large"}; | ||
| 150 | do_guarded( | ||
| 151 | mut_, | ||
| 152 | [&](auto const& guard) -> void | ||
| 153 | { | ||
| 154 | if (auto s = ::sqlite3_prepare_v2( | ||
| 155 | dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); | ||
| 156 | s != SQLITE_OK) | ||
| 157 | { | ||
| 158 | if (pstmt != nullptr) | ||
| 159 | { | ||
| 160 | // Use contract_assert when having a compiler with | ||
| 161 | // contracts available | ||
| 162 | ::sqlite3_finalize(pstmt); | ||
| 163 | throw std::logic_error{ | ||
| 164 | "expected stmt to be null after failed preparation" | ||
| 165 | }; | ||
| 166 | } | ||
| 167 | throw error{guard, s, dbc_}; | ||
| 168 | } | ||
| 169 | }); | ||
| 170 | if (!pstmt) | ||
| 171 | throw std::invalid_argument{"provided input text contains no SQL"}; | ||
| 172 | auto stmt = statement{pstmt}; | ||
| 173 | if (sql_tail && std::strlen(sql_tail) > 0) | ||
| 174 | throw std::invalid_argument{ | ||
| 175 | "provided input text contains more than one SQL statement" | ||
| 176 | }; | ||
| 177 | auto b = binder{stmt}; | ||
| 178 | bf(b); | ||
| 179 | return row_reader{std::move(stmt)}; | ||
| 180 | } | ||
| 181 | |||
| 182 | auto connection::exec( | ||
| 183 | std::string const& sql, std::function<void(binder&)> const& bf) -> void | ||
| 184 | { | ||
| 185 | auto reader = query(sql, bf); | ||
| 186 | while (reader.next()) | ||
| 187 | ; | ||
| 188 | } | ||
| 189 | |||
| 190 | connection::~connection() | ||
| 191 | { | ||
| 192 | std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); | ||
| 193 | } | ||
| 194 | |||
| 195 | auto open(std::string const& filename) -> connection | ||
| 196 | { | ||
| 197 | ::sqlite3* dbc = nullptr; | ||
| 198 | auto s = ::sqlite3_open_v2( | ||
| 199 | filename.c_str(), &dbc, | ||
| 200 | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX | ||
| 201 | | SQLITE_OPEN_EXRESCODE, | ||
| 202 | nullptr); | ||
| 203 | if (s != SQLITE_OK) | ||
| 204 | { | ||
| 205 | if (dbc) | ||
| 206 | { | ||
| 207 | do_guarded( | ||
| 208 | dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); | ||
| 209 | } | ||
| 210 | else | ||
| 211 | { | ||
| 212 | throw error{s}; | ||
| 213 | } | ||
| 214 | } | ||
| 215 | return connection{dbc}; | ||
| 216 | } | ||
| 217 | |||
| 218 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <sqlite3.h> | ||
| 4 | |||
| 5 | export module routemon:sqlite3; | 1 | export module routemon:sqlite3; |
| 6 | 2 | ||
| 7 | import std; | 3 | import std; |
| 8 | import :util; | 4 | import :util; |
| 9 | 5 | ||
| 6 | extern "C" | ||
| 7 | { | ||
| 8 | using sqlite3 = struct sqlite3; | ||
| 9 | using sqlite3_mutex = struct sqlite3_mutex; | ||
| 10 | using sqlite3_stmt = struct sqlite3_stmt; | ||
| 11 | } | ||
| 12 | |||
| 10 | namespace routemon::sqlite3 { | 13 | namespace routemon::sqlite3 { |
| 11 | 14 | ||
| 15 | template <class T, template <class U> concept C> | ||
| 16 | concept optional_of = requires { | ||
| 17 | typename T::value_type; | ||
| 18 | requires std::same_as<T, std::optional<typename T::value_type>>; | ||
| 19 | requires C<typename T::value_type>; | ||
| 20 | }; | ||
| 21 | |||
| 22 | template <class T> | ||
| 23 | concept scannable_prim = std::same_as<T, std::string> || std::same_as<T, double> | ||
| 24 | || std::same_as<T, std::int64_t>; | ||
| 25 | |||
| 26 | template <class T> | ||
| 27 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; | ||
| 28 | |||
| 12 | class mutex_guard | 29 | class mutex_guard |
| 13 | { | 30 | { |
| 14 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} | 31 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept; |
| 15 | { | ||
| 16 | ::sqlite3_mutex_enter(mut_); | ||
| 17 | } | ||
| 18 | 32 | ||
| 19 | friend auto | 33 | friend auto |
| 20 | do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) | 34 | do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) |
| @@ -22,111 +36,54 @@ class mutex_guard | |||
| 22 | 36 | ||
| 23 | public: | 37 | public: |
| 24 | mutex_guard(mutex_guard const&) = delete; | 38 | mutex_guard(mutex_guard const&) = delete; |
| 25 | ~mutex_guard() { ::sqlite3_mutex_leave(mut_); } | 39 | ~mutex_guard(); |
| 26 | 40 | ||
| 27 | private: | 41 | private: |
| 28 | ::sqlite3_mutex* mut_; | 42 | ::sqlite3_mutex* mut_; |
| 29 | }; | 43 | }; |
| 30 | 44 | ||
| 31 | auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) | ||
| 32 | -> decltype(f(std::declval<mutex_guard const&>())) | ||
| 33 | { | ||
| 34 | return f(mutex_guard{mut}); | ||
| 35 | } | ||
| 36 | |||
| 37 | auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) | ||
| 38 | -> decltype(f(std::declval<mutex_guard const&>())) | ||
| 39 | { | ||
| 40 | return do_guarded(::sqlite3_db_mutex(dbc), f); | ||
| 41 | } | ||
| 42 | |||
| 43 | class error : public std::exception | 45 | class error : public std::exception |
| 44 | { | 46 | { |
| 45 | int code_; | 47 | int code_; |
| 46 | std::string message_; | 48 | std::string message_; |
| 47 | 49 | ||
| 48 | public: | 50 | public: |
| 49 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc) | 51 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc); |
| 50 | : code_{code}, message_{::sqlite3_errmsg(dbc)} | 52 | explicit error(int code); |
| 51 | { | ||
| 52 | } | ||
| 53 | |||
| 54 | explicit error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} | ||
| 55 | |||
| 56 | [[nodiscard]] auto what() const noexcept -> char const* override | ||
| 57 | { | ||
| 58 | return message_.c_str(); | ||
| 59 | } | ||
| 60 | 53 | ||
| 61 | [[nodiscard]] auto code() const noexcept -> int { return code_; } | 54 | [[nodiscard]] auto what() const noexcept -> char const* override; |
| 62 | }; | 55 | [[nodiscard]] auto code() const noexcept -> int; |
| 63 | |||
| 64 | template <class T, template <class U> concept C> | ||
| 65 | concept optional_of = requires { | ||
| 66 | typename T::value_type; | ||
| 67 | requires std::same_as<T, std::optional<typename T::value_type>>; | ||
| 68 | requires C<typename T::value_type>; | ||
| 69 | }; | 56 | }; |
| 70 | 57 | ||
| 71 | template <class T> | ||
| 72 | concept scannable_prim = std::same_as<T, std::string> || std::same_as<T, double> | ||
| 73 | || std::same_as<T, std::int64_t>; | ||
| 74 | |||
| 75 | template <class T> | ||
| 76 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; | ||
| 77 | |||
| 78 | class statement | 58 | class statement |
| 79 | { | 59 | { |
| 80 | ::sqlite3_stmt* stmt_; | 60 | ::sqlite3_stmt* stmt_; |
| 81 | 61 | ||
| 82 | public: | 62 | public: |
| 83 | explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} | 63 | explicit statement(::sqlite3_stmt* stmt); |
| 84 | statement(statement const&) = delete; | 64 | statement(statement const&) = delete; |
| 85 | statement(statement&& s) noexcept | 65 | statement(statement&& s) noexcept; |
| 86 | { | 66 | ~statement(); |
| 87 | stmt_ = s.stmt_; | 67 | auto get() -> ::sqlite3_stmt*; |
| 88 | s.stmt_ = nullptr; | ||
| 89 | } | ||
| 90 | ~statement() { ::sqlite3_finalize(stmt_); } | ||
| 91 | auto get() -> ::sqlite3_stmt* { return stmt_; } | ||
| 92 | }; | 68 | }; |
| 93 | 69 | ||
| 94 | class row_reader | 70 | class row_reader |
| 95 | { | 71 | { |
| 96 | statement stmt_; | 72 | statement stmt_; |
| 97 | 73 | ||
| 98 | explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} | 74 | explicit row_reader(statement stmt); |
| 99 | 75 | ||
| 100 | friend class connection; | 76 | friend class connection; |
| 101 | 77 | ||
| 102 | void scan(int col, std::string& s) | 78 | auto is_null(int col) -> bool; |
| 103 | { | 79 | auto ncols() -> std::size_t; |
| 104 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) | ||
| 105 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 106 | unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); | ||
| 107 | auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); | ||
| 108 | if (!size.has_value()) | ||
| 109 | throw std::logic_error{"unexpected negative amount of bytes in column"}; | ||
| 110 | s = std::string{reinterpret_cast<char const*>(chs), *size}; | ||
| 111 | } | ||
| 112 | 80 | ||
| 113 | void scan(int col, double& v) | 81 | auto scan(int col, std::string& s) -> void; |
| 82 | auto scan(int col, double& v) -> void; | ||
| 83 | auto scan(int col, std::int64_t& v) -> void; | ||
| 84 | auto scan(int col, optional_of<scannable> auto& v) -> void | ||
| 114 | { | 85 | { |
| 115 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) | 86 | if (is_null(col)) |
| 116 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 117 | v = ::sqlite3_column_double(stmt_.get(), col); | ||
| 118 | } | ||
| 119 | |||
| 120 | void scan(int col, std::int64_t& v) | ||
| 121 | { | ||
| 122 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) | ||
| 123 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 124 | v = ::sqlite3_column_int64(stmt_.get(), col); | ||
| 125 | } | ||
| 126 | |||
| 127 | void scan(int col, optional_of<scannable> auto& v) | ||
| 128 | { | ||
| 129 | if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) | ||
| 130 | { | 87 | { |
| 131 | v.reset(); | 88 | v.reset(); |
| 132 | } | 89 | } |
| @@ -139,28 +96,11 @@ class row_reader | |||
| 139 | } | 96 | } |
| 140 | 97 | ||
| 141 | public: | 98 | public: |
| 142 | auto next() -> bool | 99 | auto next() -> bool; |
| 143 | { | ||
| 144 | ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); | ||
| 145 | return do_guarded( | ||
| 146 | dbc, | ||
| 147 | [&](auto const& guard) -> bool | ||
| 148 | { | ||
| 149 | auto const s = ::sqlite3_step(stmt_.get()); | ||
| 150 | if (s == SQLITE_ROW) | ||
| 151 | return true; | ||
| 152 | if (s == SQLITE_DONE) | ||
| 153 | return false; | ||
| 154 | throw error{guard, s, dbc}; | ||
| 155 | }); | ||
| 156 | } | ||
| 157 | 100 | ||
| 158 | auto scan(scannable auto&... args) -> void | 101 | auto scan(scannable auto&... args) -> void |
| 159 | { | 102 | { |
| 160 | auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); | 103 | if (sizeof...(args) > ncols()) |
| 161 | if (!ncols.has_value()) | ||
| 162 | throw std::logic_error{"got unexpected negative amount of columns"}; | ||
| 163 | if (sizeof...(args) > *ncols) | ||
| 164 | throw std::invalid_argument{ | 104 | throw std::invalid_argument{ |
| 165 | "more scanning arguments provided than columns in result set" | 105 | "more scanning arguments provided than columns in result set" |
| 166 | }; | 106 | }; |
| @@ -184,30 +124,14 @@ class binder | |||
| 184 | { | 124 | { |
| 185 | statement& stmt_; | 125 | statement& stmt_; |
| 186 | 126 | ||
| 187 | explicit binder(statement& stmt) : stmt_{stmt} {} | 127 | explicit binder(statement& stmt); |
| 188 | 128 | ||
| 189 | friend class connection; | 129 | friend class connection; |
| 190 | 130 | ||
| 191 | public: | 131 | public: |
| 192 | auto text(std::string const& param_name, std::string_view str) -> void | 132 | auto text(std::string const& param_name, std::string_view str) -> void; |
| 193 | { | ||
| 194 | int const i = | ||
| 195 | ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); | ||
| 196 | if (i == 0) | ||
| 197 | throw std::invalid_argument{std::format( | ||
| 198 | "bind: no parameter with name {} found", param_name)}; | ||
| 199 | auto str_size = util::int_from_size(str.size()); | ||
| 200 | if (!str_size.has_value()) | ||
| 201 | throw std::invalid_argument{"bind: provided text is too long"}; | ||
| 202 | if (auto s = ::sqlite3_bind_text( | ||
| 203 | stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); | ||
| 204 | s != SQLITE_OK) | ||
| 205 | { | ||
| 206 | throw error{s}; | ||
| 207 | } | ||
| 208 | } | ||
| 209 | 133 | ||
| 210 | static auto noop(binder&) -> void {} | 134 | static auto noop(binder&) -> void; |
| 211 | }; | 135 | }; |
| 212 | 136 | ||
| 213 | export class connection | 137 | export class connection |
| @@ -215,97 +139,25 @@ export class connection | |||
| 215 | ::sqlite3* dbc_; | 139 | ::sqlite3* dbc_; |
| 216 | ::sqlite3_mutex* mut_; | 140 | ::sqlite3_mutex* mut_; |
| 217 | 141 | ||
| 218 | explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} | 142 | explicit connection(::sqlite3* dbc); |
| 219 | { | ||
| 220 | } | ||
| 221 | 143 | ||
| 222 | friend auto open(std::string const& filename) -> connection; | 144 | friend auto open(std::string const& filename) -> connection; |
| 223 | 145 | ||
| 224 | public: | 146 | public: |
| 225 | connection(connection const&) = delete; | 147 | connection(connection const&) = delete; |
| 226 | connection(connection&& c) noexcept | 148 | connection(connection&& c) noexcept; |
| 227 | { | ||
| 228 | dbc_ = c.dbc_; | ||
| 229 | mut_ = c.mut_; | ||
| 230 | c.dbc_ = nullptr; | ||
| 231 | c.mut_ = nullptr; | ||
| 232 | } | ||
| 233 | 149 | ||
| 234 | [[nodiscard]] auto query( | 150 | [[nodiscard]] auto query( |
| 235 | std::string const& sql, | 151 | std::string const& sql, |
| 236 | std::function<void(binder&)> const& bf = binder::noop) -> row_reader | 152 | std::function<void(binder&)> const& bf = binder::noop) -> row_reader; |
| 237 | { | ||
| 238 | ::sqlite3_stmt* pstmt = nullptr; | ||
| 239 | char const* sql_tail = nullptr; | ||
| 240 | auto sql_size = util::int_from_size(sql.size()); | ||
| 241 | if (!sql_size.has_value() | ||
| 242 | || *sql_size >= std::numeric_limits<int>::max() - 1) | ||
| 243 | throw std::invalid_argument{"provided input text too large"}; | ||
| 244 | do_guarded( | ||
| 245 | mut_, | ||
| 246 | [&](auto const& guard) -> void | ||
| 247 | { | ||
| 248 | if (auto s = ::sqlite3_prepare_v2( | ||
| 249 | dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); | ||
| 250 | s != SQLITE_OK) | ||
| 251 | { | ||
| 252 | if (pstmt != nullptr) | ||
| 253 | { | ||
| 254 | // Use contract_assert when having a compiler with | ||
| 255 | // contracts available | ||
| 256 | ::sqlite3_finalize(pstmt); | ||
| 257 | throw std::logic_error{ | ||
| 258 | "expected stmt to be null after failed preparation" | ||
| 259 | }; | ||
| 260 | } | ||
| 261 | throw error{guard, s, dbc_}; | ||
| 262 | } | ||
| 263 | }); | ||
| 264 | if (!pstmt) | ||
| 265 | throw std::invalid_argument{"provided input text contains no SQL"}; | ||
| 266 | auto stmt = statement{pstmt}; | ||
| 267 | if (sql_tail && std::strlen(sql_tail) > 0) | ||
| 268 | throw std::invalid_argument{ | ||
| 269 | "provided input text contains more than one SQL statement" | ||
| 270 | }; | ||
| 271 | auto b = binder{stmt}; | ||
| 272 | bf(b); | ||
| 273 | return row_reader{std::move(stmt)}; | ||
| 274 | } | ||
| 275 | 153 | ||
| 276 | auto exec( | 154 | auto exec( |
| 277 | std::string const& sql, | 155 | std::string const& sql, |
| 278 | std::function<void(binder&)> const& bf = binder::noop) -> void | 156 | std::function<void(binder&)> const& bf = binder::noop) -> void; |
| 279 | { | ||
| 280 | auto reader = query(sql, bf); | ||
| 281 | while (reader.next()) | ||
| 282 | ; | ||
| 283 | } | ||
| 284 | 157 | ||
| 285 | ~connection() { std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); } | 158 | ~connection(); |
| 286 | }; | 159 | }; |
| 287 | 160 | ||
| 288 | export auto open(std::string const& filename) -> connection | 161 | export auto open(std::string const& filename) -> connection; |
| 289 | { | ||
| 290 | ::sqlite3* dbc = nullptr; | ||
| 291 | auto s = ::sqlite3_open_v2( | ||
| 292 | filename.c_str(), &dbc, | ||
| 293 | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX | ||
| 294 | | SQLITE_OPEN_EXRESCODE, | ||
| 295 | nullptr); | ||
| 296 | if (s != SQLITE_OK) | ||
| 297 | { | ||
| 298 | if (dbc) | ||
| 299 | { | ||
| 300 | do_guarded( | ||
| 301 | dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); | ||
| 302 | } | ||
| 303 | else | ||
| 304 | { | ||
| 305 | throw error{s}; | ||
| 306 | } | ||
| 307 | } | ||
| 308 | return connection{dbc}; | ||
| 309 | } | ||
| 310 | 162 | ||
| 311 | } // namespace routemon::sqlite3 | 163 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/asio/as_tuple.hpp> | ||
| 4 | #include <boost/asio/awaitable.hpp> | ||
| 5 | #include <boost/asio/co_spawn.hpp> | ||
| 6 | #include <boost/asio/ip/tcp.hpp> | ||
| 7 | #include <boost/beast/core.hpp> | ||
| 8 | #include <boost/beast/http.hpp> | ||
| 9 | #include <boost/config.hpp> | ||
| 10 | #include <boost/json.hpp> | ||
| 11 | #include <boost/locale/generator.hpp> | ||
| 12 | |||
| 13 | #include <expat.h> | ||
| 14 | |||
| 15 | module routemon:srv$impl; | ||
| 16 | |||
| 17 | import std; | ||
| 18 | import :gpx; | ||
| 19 | import :srv; | ||
| 20 | |||
| 21 | namespace beast = boost::beast; | ||
| 22 | namespace json = boost::json; | ||
| 23 | namespace net = boost::asio; | ||
| 24 | using tcp = boost::asio::ip::tcp; | ||
| 25 | |||
| 26 | namespace routemon::srv { | ||
| 27 | |||
| 28 | class gpx_parse_error_category_impl : public std::error_category | ||
| 29 | { | ||
| 30 | public: | ||
| 31 | char const* name() const noexcept override { return "gpx_parse"; } | ||
| 32 | |||
| 33 | auto message(int condition) const noexcept -> std::string override | ||
| 34 | { | ||
| 35 | std::ignore = condition; | ||
| 36 | return "failed to parse GPX file"; | ||
| 37 | } | ||
| 38 | }; | ||
| 39 | |||
| 40 | auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& | ||
| 41 | { | ||
| 42 | static auto const inst = gpx_parse_error_category_impl{}; | ||
| 43 | return inst; | ||
| 44 | } | ||
| 45 | |||
| 46 | auto gpx_parse_error() noexcept -> std::error_code | ||
| 47 | { | ||
| 48 | return std::error_code{1, gpx_parse_error_category()}; | ||
| 49 | } | ||
| 50 | |||
| 51 | class gpx_parse_result | ||
| 52 | { | ||
| 53 | std::variant<std::exception_ptr, gpx::file> res_; | ||
| 54 | |||
| 55 | public: | ||
| 56 | auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } | ||
| 57 | auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } | ||
| 58 | |||
| 59 | auto unwrap() -> gpx::file&& | ||
| 60 | { | ||
| 61 | return std::visit( | ||
| 62 | util::overloaded{ | ||
| 63 | [](std::exception_ptr ex) -> gpx::file&& | ||
| 64 | { | ||
| 65 | if (ex) | ||
| 66 | std::rethrow_exception(ex); | ||
| 67 | else | ||
| 68 | throw std::runtime_error{"no GPX file parse result available"}; | ||
| 69 | }, | ||
| 70 | [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, | ||
| 71 | }, | ||
| 72 | std::move(res_)); | ||
| 73 | } | ||
| 74 | }; | ||
| 75 | |||
| 76 | struct readable_gpx_body | ||
| 77 | { | ||
| 78 | using value_type = gpx_parse_result; | ||
| 79 | |||
| 80 | class reader | ||
| 81 | { | ||
| 82 | gpx::reader r_; | ||
| 83 | util::not_null<value_type*> res_; | ||
| 84 | |||
| 85 | public: | ||
| 86 | template <bool isRequest, bhttp::concepts::fields Fields> | ||
| 87 | explicit reader(bhttp::header<isRequest, Fields>&, value_type& v) : res_{&v} | ||
| 88 | { | ||
| 89 | } | ||
| 90 | |||
| 91 | // The following methods (which are called by Beast) are marked | ||
| 92 | // noexcept, since Beast does not ensure that exceptions thrown | ||
| 93 | // here are appropriately directed to the caller of | ||
| 94 | // (async_)read(_some), so throwing here might cause the program | ||
| 95 | // to crash. | ||
| 96 | |||
| 97 | auto | ||
| 98 | init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept | ||
| 99 | -> void | ||
| 100 | { | ||
| 101 | try | ||
| 102 | { | ||
| 103 | r_.init(); | ||
| 104 | ec = {}; | ||
| 105 | } | ||
| 106 | catch (std::exception& ex) | ||
| 107 | { | ||
| 108 | res_->set_exception(std::current_exception()); | ||
| 109 | ec = gpx_parse_error(); | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | auto | ||
| 114 | put(beast::concepts::const_buffer_sequence auto b, | ||
| 115 | beast::error_code& ec) noexcept -> std::size_t | ||
| 116 | { | ||
| 117 | auto total = 0uz; | ||
| 118 | try | ||
| 119 | { | ||
| 120 | for (auto it = net::buffer_sequence_begin(b); | ||
| 121 | it != net::buffer_sequence_end(b); it++) | ||
| 122 | { | ||
| 123 | r_.put( | ||
| 124 | std::string_view{ | ||
| 125 | static_cast<char const*>(it->data()), it->size() | ||
| 126 | }); | ||
| 127 | total += it->size(); | ||
| 128 | } | ||
| 129 | ec = {}; | ||
| 130 | } | ||
| 131 | catch (std::exception& ex) | ||
| 132 | { | ||
| 133 | res_->set_exception(std::current_exception()); | ||
| 134 | ec = gpx_parse_error(); | ||
| 135 | } | ||
| 136 | return total; | ||
| 137 | } | ||
| 138 | |||
| 139 | auto finish(beast::error_code& ec) noexcept | ||
| 140 | { | ||
| 141 | try | ||
| 142 | { | ||
| 143 | res_->set_gpx_file(r_.finish()); | ||
| 144 | ec = {}; | ||
| 145 | } | ||
| 146 | catch (std::exception& ex) | ||
| 147 | { | ||
| 148 | res_->set_exception(std::current_exception()); | ||
| 149 | ec = gpx_parse_error(); | ||
| 150 | } | ||
| 151 | } | ||
| 152 | }; | ||
| 153 | }; | ||
| 154 | static_assert(bhttp::concepts::body<readable_gpx_body>); | ||
| 155 | static_assert(bhttp::concepts::body_reader<readable_gpx_body>); | ||
| 156 | |||
| 157 | auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) | ||
| 158 | -> net::awaitable<http::presponse> | ||
| 159 | { | ||
| 160 | auto gpx_file = gpx::file{}; | ||
| 161 | try | ||
| 162 | { | ||
| 163 | auto req = | ||
| 164 | co_await http::read_request<readable_gpx_body>(ctx, std::move(r)); | ||
| 165 | gpx_file = std::move(req->body().unwrap()); | ||
| 166 | } | ||
| 167 | catch (std::exception& ex) | ||
| 168 | { | ||
| 169 | // TODO: more detailed problem reporting | ||
| 170 | auto tpl = problem::tpl{ | ||
| 171 | .status = bhttp::status::bad_request, | ||
| 172 | .title = translate("Failed to parse GPX file"), | ||
| 173 | .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", | ||
| 174 | }; | ||
| 175 | co_return http::problem_rsp( | ||
| 176 | ctx, tpl.instantiate(), http::keep_alive{false}); | ||
| 177 | } | ||
| 178 | |||
| 179 | // TODO: catch handler exceptions and return 500 when raised? | ||
| 180 | // (keep-alive depends on whether whole request was read) | ||
| 181 | auto mres = inner_.process_gpx(std::move(gpx_file)); | ||
| 182 | if (!mres) | ||
| 183 | { | ||
| 184 | auto tpl = problem::tpl{ | ||
| 185 | .status = bhttp::status::internal_server_error, | ||
| 186 | .title = translate("Internal server error"), | ||
| 187 | .type_uri = "https://routemon.fautchen.eu/problems/" | ||
| 188 | "internal-server-error", | ||
| 189 | }; | ||
| 190 | co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true}); | ||
| 191 | } | ||
| 192 | |||
| 193 | auto rsp = http::make_rsp<bhttp::string_body>( | ||
| 194 | bhttp::status::ok, http::keep_alive{true}); | ||
| 195 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 196 | rsp.body() = json::serialize(json::value_from(*mres)); | ||
| 197 | rsp.prepare_payload(); | ||
| 198 | co_return rsp; | ||
| 199 | } | ||
| 200 | |||
| 201 | auto handler::handle_sysinfo(l0_ctx ctx, http::readable_request r) | ||
| 202 | -> net::awaitable<http::presponse> | ||
| 203 | { | ||
| 204 | auto req = co_await http::read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 205 | auto info = inner_.sysinfo(); | ||
| 206 | |||
| 207 | auto rsp = http::make_rsp<bhttp::string_body>( | ||
| 208 | bhttp::status::ok, http::keep_alive{true}); | ||
| 209 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 210 | rsp.body() = json::serialize(json::value_from(info)); | ||
| 211 | rsp.prepare_payload(); | ||
| 212 | co_return rsp; | ||
| 213 | } | ||
| 214 | |||
| 215 | handler::handler(api::handler&& inner) : inner_{std::move(inner)} {} | ||
| 216 | |||
| 217 | auto handler::make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> | ||
| 218 | { | ||
| 219 | auto handler = [this]<class MemFn>(MemFn member) | ||
| 220 | { return std::bind_front(member, this); }; | ||
| 221 | |||
| 222 | return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({ | ||
| 223 | {"gpx", http::dtree<l0_ctx>{{ | ||
| 224 | .post = handler(&handler::handle_process_gpx), | ||
| 225 | }} | ||
| 226 | .no_subtrees()}, | ||
| 227 | {"sysinfo", http::dtree<l0_ctx>{ | ||
| 228 | { | ||
| 229 | .get = handler(&handler::handle_sysinfo), | ||
| 230 | } | ||
| 231 | }.no_subtrees()}, | ||
| 232 | }); | ||
| 233 | } | ||
| 234 | |||
| 235 | auto server::make_global_middleware() | ||
| 236 | -> http::middleware_t<http::base_ctx, handler::outer_ctx> | ||
| 237 | { | ||
| 238 | return http::middleware_compose< | ||
| 239 | http::base_ctx, http::trace_id_ctx<http::base_ctx>, | ||
| 240 | http::trace_id_ctx<http::base_ctx> | ||
| 241 | >(http::trace_id_middleware<http::base_ctx>, | ||
| 242 | http::lax_cors_middleware<http::trace_id_ctx<http::base_ctx>>); | ||
| 243 | } | ||
| 244 | |||
| 245 | server::server( | ||
| 246 | log::logger const& l, locale::selector&& lsel, api::handler&& inner) | ||
| 247 | : handler_{std::move(inner)}, | ||
| 248 | srv_{ | ||
| 249 | l, http::router{ | ||
| 250 | std::move(lsel), make_global_middleware(), handler_.make_routes() | ||
| 251 | } | ||
| 252 | } | ||
| 253 | { | ||
| 254 | } | ||
| 255 | |||
| 256 | auto server::spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } | ||
| 257 | |||
| 258 | } // 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 @@ | |||
| 1 | module; | 1 | module; |
| 2 | 2 | ||
| 3 | #include <boost/asio/as_tuple.hpp> | ||
| 4 | #include <boost/asio/awaitable.hpp> | 3 | #include <boost/asio/awaitable.hpp> |
| 5 | #include <boost/asio/co_spawn.hpp> | ||
| 6 | #include <boost/asio/ip/tcp.hpp> | ||
| 7 | #include <boost/beast/core.hpp> | ||
| 8 | #include <boost/beast/http.hpp> | ||
| 9 | #include <boost/config.hpp> | ||
| 10 | #include <boost/json.hpp> | ||
| 11 | #include <boost/locale/generator.hpp> | ||
| 12 | |||
| 13 | #include <expat.h> | ||
| 14 | 4 | ||
| 15 | export module routemon:srv; | 5 | export module routemon:srv; |
| 16 | 6 | ||
| 17 | import std; | ||
| 18 | import :api; | 7 | import :api; |
| 19 | import :config; | ||
| 20 | import :gpx; | ||
| 21 | import :http.server; | 8 | import :http.server; |
| 22 | import :locale; | ||
| 23 | import :log; | ||
| 24 | import :problem; | ||
| 25 | import :req_ctx; | ||
| 26 | import :util; | ||
| 27 | |||
| 28 | namespace beast = boost::beast; | ||
| 29 | namespace json = boost::json; | ||
| 30 | namespace net = boost::asio; | ||
| 31 | using tcp = boost::asio::ip::tcp; | ||
| 32 | 9 | ||
| 33 | namespace routemon::srv { | 10 | namespace routemon::srv { |
| 34 | 11 | ||
| 35 | class gpx_parse_error_category_impl : public std::error_category | ||
| 36 | { | ||
| 37 | public: | ||
| 38 | char const* name() const noexcept override { return "gpx_parse"; } | ||
| 39 | |||
| 40 | auto message(int condition) const noexcept -> std::string override | ||
| 41 | { | ||
| 42 | std::ignore = condition; | ||
| 43 | return "failed to parse GPX file"; | ||
| 44 | } | ||
| 45 | }; | ||
| 46 | |||
| 47 | auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& | ||
| 48 | { | ||
| 49 | static auto const inst = gpx_parse_error_category_impl{}; | ||
| 50 | return inst; | ||
| 51 | } | ||
| 52 | |||
| 53 | auto gpx_parse_error() noexcept -> std::error_code | ||
| 54 | { | ||
| 55 | return std::error_code{1, gpx_parse_error_category()}; | ||
| 56 | } | ||
| 57 | |||
| 58 | class gpx_parse_result | ||
| 59 | { | ||
| 60 | std::variant<std::exception_ptr, gpx::file> res_; | ||
| 61 | |||
| 62 | public: | ||
| 63 | auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } | ||
| 64 | auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } | ||
| 65 | |||
| 66 | auto unwrap() -> gpx::file&& | ||
| 67 | { | ||
| 68 | return std::visit( | ||
| 69 | util::overloaded{ | ||
| 70 | [](std::exception_ptr ex) -> gpx::file&& | ||
| 71 | { | ||
| 72 | if (ex) | ||
| 73 | std::rethrow_exception(ex); | ||
| 74 | else | ||
| 75 | throw std::runtime_error{"no GPX file parse result available"}; | ||
| 76 | }, | ||
| 77 | [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, | ||
| 78 | }, | ||
| 79 | std::move(res_)); | ||
| 80 | } | ||
| 81 | }; | ||
| 82 | |||
| 83 | struct readable_gpx_body | ||
| 84 | { | ||
| 85 | using value_type = gpx_parse_result; | ||
| 86 | |||
| 87 | class reader | ||
| 88 | { | ||
| 89 | gpx::reader r_; | ||
| 90 | util::not_null<value_type*> res_; | ||
| 91 | |||
| 92 | public: | ||
| 93 | template <bool isRequest, bhttp::concepts::fields Fields> | ||
| 94 | explicit reader(bhttp::header<isRequest, Fields>&, value_type& v) : res_{&v} | ||
| 95 | { | ||
| 96 | } | ||
| 97 | |||
| 98 | // The following methods (which are called by Beast) are marked | ||
| 99 | // noexcept, since Beast does not ensure that exceptions thrown | ||
| 100 | // here are appropriately directed to the caller of | ||
| 101 | // (async_)read(_some), so throwing here might cause the program | ||
| 102 | // to crash. | ||
| 103 | |||
| 104 | auto | ||
| 105 | init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept | ||
| 106 | -> void | ||
| 107 | { | ||
| 108 | try | ||
| 109 | { | ||
| 110 | r_.init(); | ||
| 111 | ec = {}; | ||
| 112 | } | ||
| 113 | catch (std::exception& ex) | ||
| 114 | { | ||
| 115 | res_->set_exception(std::current_exception()); | ||
| 116 | ec = gpx_parse_error(); | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | auto | ||
| 121 | put(beast::concepts::const_buffer_sequence auto b, | ||
| 122 | beast::error_code& ec) noexcept -> std::size_t | ||
| 123 | { | ||
| 124 | auto total = 0uz; | ||
| 125 | try | ||
| 126 | { | ||
| 127 | for (auto it = net::buffer_sequence_begin(b); | ||
| 128 | it != net::buffer_sequence_end(b); it++) | ||
| 129 | { | ||
| 130 | r_.put( | ||
| 131 | std::string_view{ | ||
| 132 | static_cast<char const*>(it->data()), it->size() | ||
| 133 | }); | ||
| 134 | total += it->size(); | ||
| 135 | } | ||
| 136 | ec = {}; | ||
| 137 | } | ||
| 138 | catch (std::exception& ex) | ||
| 139 | { | ||
| 140 | res_->set_exception(std::current_exception()); | ||
| 141 | ec = gpx_parse_error(); | ||
| 142 | } | ||
| 143 | return total; | ||
| 144 | } | ||
| 145 | |||
| 146 | auto finish(beast::error_code& ec) noexcept | ||
| 147 | { | ||
| 148 | try | ||
| 149 | { | ||
| 150 | res_->set_gpx_file(r_.finish()); | ||
| 151 | ec = {}; | ||
| 152 | } | ||
| 153 | catch (std::exception& ex) | ||
| 154 | { | ||
| 155 | res_->set_exception(std::current_exception()); | ||
| 156 | ec = gpx_parse_error(); | ||
| 157 | } | ||
| 158 | } | ||
| 159 | }; | ||
| 160 | }; | ||
| 161 | static_assert(bhttp::concepts::body<readable_gpx_body>); | ||
| 162 | static_assert(bhttp::concepts::body_reader<readable_gpx_body>); | ||
| 163 | |||
| 164 | class handler | 12 | class handler |
| 165 | { | 13 | { |
| 166 | api::handler inner_; | 14 | api::handler inner_; |
| @@ -171,112 +19,29 @@ public: | |||
| 171 | 19 | ||
| 172 | private: | 20 | private: |
| 173 | auto handle_process_gpx(l0_ctx ctx, http::readable_request r) | 21 | auto handle_process_gpx(l0_ctx ctx, http::readable_request r) |
| 174 | -> net::awaitable<http::presponse> | 22 | -> net::awaitable<http::presponse>; |
| 175 | { | ||
| 176 | auto gpx_file = gpx::file{}; | ||
| 177 | try | ||
| 178 | { | ||
| 179 | auto req = | ||
| 180 | co_await http::read_request<readable_gpx_body>(ctx, std::move(r)); | ||
| 181 | gpx_file = std::move(req->body().unwrap()); | ||
| 182 | } | ||
| 183 | catch (std::exception& ex) | ||
| 184 | { | ||
| 185 | // TODO: more detailed problem reporting | ||
| 186 | auto tpl = problem::tpl{ | ||
| 187 | .status = bhttp::status::bad_request, | ||
| 188 | .title = translate("Failed to parse GPX file"), | ||
| 189 | .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", | ||
| 190 | }; | ||
| 191 | co_return http::problem_rsp( | ||
| 192 | ctx, tpl.instantiate(), http::keep_alive{false}); | ||
| 193 | } | ||
| 194 | |||
| 195 | // TODO: catch handler exceptions and return 500 when raised? | ||
| 196 | // (keep-alive depends on whether whole request was read) | ||
| 197 | auto mres = inner_.process_gpx(std::move(gpx_file)); | ||
| 198 | if (!mres) | ||
| 199 | { | ||
| 200 | auto tpl = problem::tpl{ | ||
| 201 | .status = bhttp::status::internal_server_error, | ||
| 202 | .title = translate("Internal server error"), | ||
| 203 | .type_uri = "https://routemon.fautchen.eu/problems/" | ||
| 204 | "internal-server-error", | ||
| 205 | }; | ||
| 206 | co_return http::problem_rsp( | ||
| 207 | ctx, tpl.instantiate(), http::keep_alive{true}); | ||
| 208 | } | ||
| 209 | |||
| 210 | auto rsp = http::make_rsp<bhttp::string_body>( | ||
| 211 | bhttp::status::ok, http::keep_alive{true}); | ||
| 212 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 213 | rsp.body() = json::serialize(json::value_from(*mres)); | ||
| 214 | rsp.prepare_payload(); | ||
| 215 | co_return rsp; | ||
| 216 | } | ||
| 217 | 23 | ||
| 218 | auto handle_sysinfo(l0_ctx ctx, http::readable_request r) | 24 | auto handle_sysinfo(l0_ctx ctx, http::readable_request r) |
| 219 | -> net::awaitable<http::presponse> | 25 | -> net::awaitable<http::presponse>; |
| 220 | { | ||
| 221 | auto req = | ||
| 222 | co_await http::read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 223 | auto info = inner_.sysinfo(); | ||
| 224 | |||
| 225 | auto rsp = http::make_rsp<bhttp::string_body>( | ||
| 226 | bhttp::status::ok, http::keep_alive{true}); | ||
| 227 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 228 | rsp.body() = json::serialize(json::value_from(info)); | ||
| 229 | rsp.prepare_payload(); | ||
| 230 | co_return rsp; | ||
| 231 | } | ||
| 232 | 26 | ||
| 233 | public: | 27 | public: |
| 234 | handler(api::handler&& inner) : inner_{std::move(inner)} {} | 28 | handler(api::handler&& inner); |
| 235 | |||
| 236 | auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> | ||
| 237 | { | ||
| 238 | auto handler = [this]<class MemFn>(MemFn member) | ||
| 239 | { return std::bind_front(member, this); }; | ||
| 240 | 29 | ||
| 241 | return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({ | 30 | auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>>; |
| 242 | {"gpx", | ||
| 243 | http::dtree<l0_ctx>{ | ||
| 244 | { | ||
| 245 | .post = handler(&handler::handle_process_gpx), | ||
| 246 | } | ||
| 247 | }.no_subtrees()}, | ||
| 248 | {"sysinfo", http::dtree<l0_ctx>{ | ||
| 249 | { | ||
| 250 | .get = handler(&handler::handle_sysinfo), | ||
| 251 | } | ||
| 252 | }.no_subtrees()}, | ||
| 253 | }); | ||
| 254 | } | ||
| 255 | }; | 31 | }; |
| 256 | 32 | ||
| 257 | export class server | 33 | export class server |
| 258 | { | 34 | { |
| 259 | handler handler_; | 35 | handler handler_; |
| 260 | http::server<handler::outer_ctx> srv_; | 36 | http::server srv_; |
| 261 | 37 | ||
| 262 | static auto make_global_middleware() | 38 | static auto make_global_middleware() |
| 263 | -> http::middleware_t<http::base_ctx, handler::outer_ctx> | 39 | -> http::middleware_t<http::base_ctx, handler::outer_ctx>; |
| 264 | { | ||
| 265 | return http::middleware_compose< | ||
| 266 | http::base_ctx, http::trace_id_ctx<http::base_ctx>, | ||
| 267 | http::trace_id_ctx<http::base_ctx>>( | ||
| 268 | http::trace_id_middleware<http::base_ctx>, | ||
| 269 | http::lax_cors_middleware<http::trace_id_ctx<http::base_ctx>>); | ||
| 270 | } | ||
| 271 | 40 | ||
| 272 | public: | 41 | public: |
| 273 | server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) | 42 | server(log::logger const& l, locale::selector&& lsel, api::handler&& inner); |
| 274 | : handler_{std::move(inner)}, | ||
| 275 | srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} | ||
| 276 | { | ||
| 277 | } | ||
| 278 | 43 | ||
| 279 | auto spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } | 44 | auto spawn(net::io_context& ioc) -> void; |
| 280 | }; | 45 | }; |
| 281 | 46 | ||
| 282 | } // namespace routemon::srv | 47 | } // 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 @@ | |||
| 1 | module routemon:time$impl; | ||
| 2 | |||
| 3 | import :time; | ||
| 4 | |||
| 5 | export namespace routemon::time { | ||
| 6 | |||
| 7 | using timestamp = std::chrono::time_point<std::chrono::utc_clock>; | ||
| 8 | |||
| 9 | period::period(timestamp start, timestamp end) : start_{start}, end_{end} | ||
| 10 | { | ||
| 11 | if (start >= end) | ||
| 12 | { | ||
| 13 | throw std::invalid_argument("period: start should be before end"); | ||
| 14 | } | ||
| 15 | } | ||
| 16 | |||
| 17 | auto period::intersect(period other) const -> std::optional<period> | ||
| 18 | { | ||
| 19 | auto const new_start = start_ < other.start() ? other.start() : start_; | ||
| 20 | auto const new_end = other.end() < end_ ? other.end() : end_; | ||
| 21 | return new_start < new_end ? std::make_optional(period{new_start, new_end}) | ||
| 22 | : std::nullopt; | ||
| 23 | } | ||
| 24 | |||
| 25 | auto period::except(period other) const | ||
| 26 | -> std::pair<std::optional<period>, std::optional<period>> | ||
| 27 | { | ||
| 28 | auto const before_start = start_; | ||
| 29 | auto const before_end = other.start(); | ||
| 30 | auto const after_start = end_; | ||
| 31 | auto const after_end = other.end(); | ||
| 32 | std::optional<period> before, after; | ||
| 33 | if (before_start < before_end) | ||
| 34 | before = period{before_start, before_end}; | ||
| 35 | if (after_start < after_end) | ||
| 36 | after = period{after_end, after_start}; | ||
| 37 | return std::make_pair(before, after); | ||
| 38 | } | ||
| 39 | |||
| 40 | auto period::start() const -> timestamp { return start_; } | ||
| 41 | auto period::end() const -> timestamp { return end_; } | ||
| 42 | |||
| 43 | period_seq::period_seq(std::vector<period> periods) | ||
| 44 | : periods_{std::move(periods)} | ||
| 45 | { | ||
| 46 | for (auto i = 0uz; i < periods_.size(); i++) | ||
| 47 | { | ||
| 48 | if (i + 1 < periods_.size()) | ||
| 49 | { | ||
| 50 | if (periods_[i].end() >= periods_[i + 1].start()) | ||
| 51 | { | ||
| 52 | throw std::logic_error{"period_seq: vector provided to private " | ||
| 53 | "constructor not ordered properly"}; | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | period_seq::period_seq(period singleton) : periods_{singleton} {} | ||
| 60 | |||
| 61 | auto period_seq::intersect(period_seq const& other) const -> period_seq | ||
| 62 | { | ||
| 63 | auto it1 = periods_.begin(); | ||
| 64 | auto end1 = periods_.end(); | ||
| 65 | auto it2 = other.periods_.begin(); | ||
| 66 | auto end2 = other.periods_.end(); | ||
| 67 | |||
| 68 | auto res = std::vector<period>{}; | ||
| 69 | while (it1 != end1 && it2 != end2) | ||
| 70 | { | ||
| 71 | auto overlap = it1->intersect(*it2); | ||
| 72 | if (overlap) | ||
| 73 | { | ||
| 74 | res.push_back(*overlap); | ||
| 75 | if (it1->end() < it2->end()) | ||
| 76 | { | ||
| 77 | it1++; | ||
| 78 | } | ||
| 79 | else | ||
| 80 | { | ||
| 81 | it2++; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | else | ||
| 85 | { | ||
| 86 | if (it1->end() < it2->start()) | ||
| 87 | { | ||
| 88 | it1++; | ||
| 89 | } | ||
| 90 | else | ||
| 91 | { | ||
| 92 | it2++; | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | return period_seq{res}; | ||
| 98 | } | ||
| 99 | |||
| 100 | auto period_seq::except(period_seq const& other) const -> period_seq | ||
| 101 | { | ||
| 102 | // This code was pretty tricky to write, I wouldn't be surprised if it | ||
| 103 | // has some bugs in it. | ||
| 104 | |||
| 105 | auto it1 = periods_.begin(); | ||
| 106 | auto end1 = periods_.end(); | ||
| 107 | auto it2 = other.periods_.begin(); | ||
| 108 | auto end2 = other.periods_.end(); | ||
| 109 | |||
| 110 | auto res = std::vector<period>{}; | ||
| 111 | if (it1 == end1) | ||
| 112 | return period_seq{res}; | ||
| 113 | if (it2 == end2) | ||
| 114 | return period_seq{periods_}; | ||
| 115 | auto period1 = period{*it1++}; | ||
| 116 | |||
| 117 | while (it1 != end1 && it2 != end2) | ||
| 118 | { | ||
| 119 | if (period1.end() <= it2->start()) | ||
| 120 | { | ||
| 121 | res.push_back(period1); | ||
| 122 | period1 = *it1++; | ||
| 123 | } | ||
| 124 | else if (it2->end() <= period1.start()) | ||
| 125 | { | ||
| 126 | it2++; | ||
| 127 | } | ||
| 128 | else /* period1.begin() < it2->end() && it2->begin() < | ||
| 129 | period1.end() */ | ||
| 130 | { | ||
| 131 | auto const [mbefore, mafter] = period1.except(*it2); | ||
| 132 | if (mbefore) | ||
| 133 | res.push_back(*mbefore); | ||
| 134 | if (mafter) | ||
| 135 | { | ||
| 136 | period1 = *mafter; | ||
| 137 | } | ||
| 138 | else | ||
| 139 | { | ||
| 140 | period1 = *it1++; | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | return period_seq{res}; | ||
| 146 | } | ||
| 147 | |||
| 148 | auto period_seq::periods() const -> std::vector<period> const& | ||
| 149 | { | ||
| 150 | return periods_; | ||
| 151 | } | ||
| 152 | |||
| 153 | auto operator<<(std::ostream& os, period const& p) -> std::ostream& | ||
| 154 | { | ||
| 155 | return os << "[" << p.start() << ", " << p.end() << ")"; | ||
| 156 | } | ||
| 157 | |||
| 158 | auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream& | ||
| 159 | { | ||
| 160 | os << "{"; | ||
| 161 | auto it = ps.periods().begin(); | ||
| 162 | while (it != ps.periods().end()) | ||
| 163 | { | ||
| 164 | os << " " << *it; | ||
| 165 | if (++it != ps.periods().end()) | ||
| 166 | { | ||
| 167 | os << ","; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | return os << " }"; | ||
| 171 | } | ||
| 172 | |||
| 173 | } // 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 | |||
| 14 | timestamp end_; | 14 | timestamp end_; |
| 15 | 15 | ||
| 16 | public: | 16 | public: |
| 17 | explicit period(timestamp start, timestamp end) : start_{start}, end_{end} | 17 | explicit period(timestamp start, timestamp end); |
| 18 | { | ||
| 19 | if (start >= end) | ||
| 20 | { | ||
| 21 | throw std::invalid_argument("period: start should be before end"); | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | [[nodiscard]] auto intersect(period other) const -> std::optional<period> | ||
| 26 | { | ||
| 27 | auto const new_start = start_ < other.start() ? other.start() : start_; | ||
| 28 | auto const new_end = other.end() < end_ ? other.end() : end_; | ||
| 29 | return new_start < new_end ? std::make_optional(period{new_start, new_end}) | ||
| 30 | : std::nullopt; | ||
| 31 | } | ||
| 32 | 18 | ||
| 19 | [[nodiscard]] auto intersect(period other) const -> std::optional<period>; | ||
| 33 | [[nodiscard]] auto except(period other) const | 20 | [[nodiscard]] auto except(period other) const |
| 34 | -> std::pair<std::optional<period>, std::optional<period>> | 21 | -> std::pair<std::optional<period>, std::optional<period>>; |
| 35 | { | 22 | [[nodiscard]] auto start() const -> timestamp; |
| 36 | auto const before_start = start_; | 23 | [[nodiscard]] auto end() const -> timestamp; |
| 37 | auto const before_end = other.start(); | ||
| 38 | auto const after_start = end_; | ||
| 39 | auto const after_end = other.end(); | ||
| 40 | std::optional<period> before, after; | ||
| 41 | if (before_start < before_end) | ||
| 42 | before = period{before_start, before_end}; | ||
| 43 | if (after_start < after_end) | ||
| 44 | after = period{after_end, after_start}; | ||
| 45 | return std::make_pair(before, after); | ||
| 46 | } | ||
| 47 | |||
| 48 | [[nodiscard]] auto start() const -> timestamp { return start_; } | ||
| 49 | [[nodiscard]] auto end() const -> timestamp { return end_; } | ||
| 50 | }; | 24 | }; |
| 51 | 25 | ||
| 52 | class period_seq | 26 | class period_seq |
| @@ -115,21 +89,7 @@ class period_seq | |||
| 115 | return periods; | 89 | return periods; |
| 116 | } | 90 | } |
| 117 | 91 | ||
| 118 | explicit period_seq(std::vector<period> periods) | 92 | explicit period_seq(std::vector<period> periods); |
| 119 | : periods_{std::move(periods)} | ||
| 120 | { | ||
| 121 | for (auto i = 0uz; i < periods_.size(); i++) | ||
| 122 | { | ||
| 123 | if (i + 1 < periods_.size()) | ||
| 124 | { | ||
| 125 | if (periods_[i].end() >= periods_[i + 1].start()) | ||
| 126 | { | ||
| 127 | throw std::logic_error{"period_seq: vector provided to private " | ||
| 128 | "constructor not ordered properly"}; | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | 93 | ||
| 134 | public: | 94 | public: |
| 135 | template <std::input_iterator I, std::sentinel_for<I> S> | 95 | template <std::input_iterator I, std::sentinel_for<I> S> |
| @@ -138,119 +98,14 @@ public: | |||
| 138 | { | 98 | { |
| 139 | } | 99 | } |
| 140 | 100 | ||
| 141 | explicit period_seq(period singleton) : periods_{singleton} {} | 101 | explicit period_seq(period singleton); |
| 142 | |||
| 143 | [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq | ||
| 144 | { | ||
| 145 | auto it1 = periods_.begin(); | ||
| 146 | auto end1 = periods_.end(); | ||
| 147 | auto it2 = other.periods_.begin(); | ||
| 148 | auto end2 = other.periods_.end(); | ||
| 149 | |||
| 150 | auto res = std::vector<period>{}; | ||
| 151 | while (it1 != end1 && it2 != end2) | ||
| 152 | { | ||
| 153 | auto overlap = it1->intersect(*it2); | ||
| 154 | if (overlap) | ||
| 155 | { | ||
| 156 | res.push_back(*overlap); | ||
| 157 | if (it1->end() < it2->end()) | ||
| 158 | { | ||
| 159 | it1++; | ||
| 160 | } | ||
| 161 | else | ||
| 162 | { | ||
| 163 | it2++; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | else | ||
| 167 | { | ||
| 168 | if (it1->end() < it2->start()) | ||
| 169 | { | ||
| 170 | it1++; | ||
| 171 | } | ||
| 172 | else | ||
| 173 | { | ||
| 174 | it2++; | ||
| 175 | } | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | return period_seq{res}; | ||
| 180 | } | ||
| 181 | |||
| 182 | [[nodiscard]] auto except(period_seq const& other) const -> period_seq | ||
| 183 | { | ||
| 184 | // This code was pretty tricky to write, I wouldn't be surprised if it | ||
| 185 | // has some bugs in it. | ||
| 186 | |||
| 187 | auto it1 = periods_.begin(); | ||
| 188 | auto end1 = periods_.end(); | ||
| 189 | auto it2 = other.periods_.begin(); | ||
| 190 | auto end2 = other.periods_.end(); | ||
| 191 | |||
| 192 | auto res = std::vector<period>{}; | ||
| 193 | if (it1 == end1) | ||
| 194 | return period_seq{res}; | ||
| 195 | if (it2 == end2) | ||
| 196 | return period_seq{periods_}; | ||
| 197 | auto period1 = period{*it1++}; | ||
| 198 | |||
| 199 | while (it1 != end1 && it2 != end2) | ||
| 200 | { | ||
| 201 | if (period1.end() <= it2->start()) | ||
| 202 | { | ||
| 203 | res.push_back(period1); | ||
| 204 | period1 = *it1++; | ||
| 205 | } | ||
| 206 | else if (it2->end() <= period1.start()) | ||
| 207 | { | ||
| 208 | it2++; | ||
| 209 | } | ||
| 210 | else /* period1.begin() < it2->end() && it2->begin() < | ||
| 211 | period1.end() */ | ||
| 212 | { | ||
| 213 | auto const [mbefore, mafter] = period1.except(*it2); | ||
| 214 | if (mbefore) | ||
| 215 | res.push_back(*mbefore); | ||
| 216 | if (mafter) | ||
| 217 | { | ||
| 218 | period1 = *mafter; | ||
| 219 | } | ||
| 220 | else | ||
| 221 | { | ||
| 222 | period1 = *it1++; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | return period_seq{res}; | ||
| 228 | } | ||
| 229 | 102 | ||
| 230 | [[nodiscard]] auto periods() const -> std::vector<period> const& | 103 | [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq; |
| 231 | { | 104 | [[nodiscard]] auto except(period_seq const& other) const -> period_seq; |
| 232 | return periods_; | 105 | [[nodiscard]] auto periods() const -> std::vector<period> const&; |
| 233 | } | ||
| 234 | }; | 106 | }; |
| 235 | 107 | ||
| 236 | auto operator<<(std::ostream& os, period const& p) -> std::ostream& | 108 | auto operator<<(std::ostream& os, period const& p) -> std::ostream&; |
| 237 | { | 109 | auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream&; |
| 238 | return os << "[" << p.start() << ", " << p.end() << ")"; | ||
| 239 | } | ||
| 240 | |||
| 241 | auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream& | ||
| 242 | { | ||
| 243 | os << "{"; | ||
| 244 | auto it = ps.periods().begin(); | ||
| 245 | while (it != ps.periods().end()) | ||
| 246 | { | ||
| 247 | os << " " << *it; | ||
| 248 | if (++it != ps.periods().end()) | ||
| 249 | { | ||
| 250 | os << ","; | ||
| 251 | } | ||
| 252 | } | ||
| 253 | return os << " }"; | ||
| 254 | } | ||
| 255 | 110 | ||
| 256 | } // namespace routemon::time | 111 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | // Might as well since we're using OpenSSL | ||
| 4 | #include <openssl/err.h> | ||
| 5 | #include <openssl/rand.h> | ||
| 6 | |||
| 7 | module routemon:trace$impl; | ||
| 8 | |||
| 9 | import :trace; | ||
| 10 | |||
| 11 | namespace routemon::trace { | ||
| 12 | |||
| 13 | uuid7::uuid7() | ||
| 14 | { | ||
| 15 | namespace chrono = std::chrono; | ||
| 16 | auto const unix_time_ms_signed = static_cast<std::int64_t>( | ||
| 17 | chrono::duration_cast<chrono::milliseconds>( | ||
| 18 | chrono::system_clock::now().time_since_epoch()) | ||
| 19 | .count()); | ||
| 20 | if (unix_time_ms_signed < 0) | ||
| 21 | throw std::runtime_error{"system time before UNIX epoch"}; | ||
| 22 | auto const unix_time_ms = static_cast<std::uint64_t>(unix_time_ms_signed); | ||
| 23 | if (std::countl_zero(unix_time_ms) < 16) | ||
| 24 | throw std::runtime_error{"system time too great"}; | ||
| 25 | |||
| 26 | auto rand = std::array<unsigned char, 10>{}; | ||
| 27 | int s = RAND_bytes(rand.data(), static_cast<int>(rand.size())); | ||
| 28 | if (s != 1) | ||
| 29 | { | ||
| 30 | unsigned long e = ERR_get_error(); | ||
| 31 | throw std::runtime_error{std::format( | ||
| 32 | "failed to generate UUID(v7): {} ({}, code {})", | ||
| 33 | ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; | ||
| 34 | } | ||
| 35 | |||
| 36 | auto version = std::uint64_t{0b0111}; | ||
| 37 | auto variant = std::uint64_t{0b10}; | ||
| 38 | |||
| 39 | hi_ |= unix_time_ms << 16; | ||
| 40 | hi_ |= version << 12; | ||
| 41 | hi_ |= std::uint64_t{rand[0]} << 4; | ||
| 42 | hi_ |= std::uint64_t{rand[1]}; | ||
| 43 | lo_ |= variant << 62; | ||
| 44 | lo_ |= std::uint64_t{rand[2]} << 54; | ||
| 45 | lo_ |= std::uint64_t{rand[3]} << 48; | ||
| 46 | lo_ |= std::uint64_t{rand[4]} << 40; | ||
| 47 | lo_ |= std::uint64_t{rand[5]} << 32; | ||
| 48 | lo_ |= std::uint64_t{rand[6]} << 24; | ||
| 49 | lo_ |= std::uint64_t{rand[7]} << 16; | ||
| 50 | lo_ |= std::uint64_t{rand[8]} << 8; | ||
| 51 | lo_ |= std::uint64_t{rand[9]}; | ||
| 52 | } | ||
| 53 | |||
| 54 | auto uuid7::format(std::array<char, 37>& target) -> void | ||
| 55 | { | ||
| 56 | auto p____hi_hi = (hi_ & 0xffff'ffff'0000'0000) >> 32; | ||
| 57 | auto p_hi_lo_hi = (hi_ & 0x0000'0000'ffff'0000) >> 16; | ||
| 58 | auto p_lo_lo_hi = (hi_ & 0x0000'0000'0000'ffff) >> 0; | ||
| 59 | auto p____hi_lo = (lo_ & 0xffff'0000'0000'0000) >> 48; | ||
| 60 | auto p____lo_lo = (lo_ & 0x0000'ffff'ffff'ffff) >> 0; | ||
| 61 | |||
| 62 | std::format_to( | ||
| 63 | target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", p____hi_hi, | ||
| 64 | p_hi_lo_hi, p_lo_lo_hi, p____hi_lo, p____lo_lo); | ||
| 65 | target.back() = '\0'; | ||
| 66 | } | ||
| 67 | |||
| 68 | id::id() { uuid7{}.format(chars_); } | ||
| 69 | |||
| 70 | auto id::as_string() const -> util::zstring_view | ||
| 71 | { | ||
| 72 | return util::zstring_view{chars_.data(), chars_.size() - 1}; | ||
| 73 | } | ||
| 74 | |||
| 75 | } // 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 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | // Might as well since we're using OpenSSL | ||
| 4 | #include <openssl/err.h> | ||
| 5 | #include <openssl/rand.h> | ||
| 6 | |||
| 7 | export module routemon:trace; | 1 | export module routemon:trace; |
| 8 | 2 | ||
| 9 | import std; | 3 | import std; |
| @@ -13,64 +7,13 @@ namespace routemon::trace { | |||
| 13 | 7 | ||
| 14 | class uuid7 | 8 | class uuid7 |
| 15 | { | 9 | { |
| 16 | std::uint64_t high_ = 0; | 10 | std::uint64_t hi_ = 0; |
| 17 | std::uint64_t low_ = 0; | 11 | std::uint64_t lo_ = 0; |
| 18 | 12 | ||
| 19 | public: | 13 | public: |
| 20 | uuid7() | 14 | uuid7(); |
| 21 | { | ||
| 22 | namespace chrono = std::chrono; | ||
| 23 | auto const unix_time_ms_signed = static_cast<std::int64_t>( | ||
| 24 | chrono::duration_cast<chrono::milliseconds>( | ||
| 25 | chrono::system_clock::now().time_since_epoch()) | ||
| 26 | .count()); | ||
| 27 | if (unix_time_ms_signed < 0) | ||
| 28 | throw std::runtime_error{"system time before UNIX epoch"}; | ||
| 29 | auto const unix_time_ms = static_cast<std::uint64_t>(unix_time_ms_signed); | ||
| 30 | if (std::countl_zero(unix_time_ms) < 16) | ||
| 31 | throw std::runtime_error{"system time too great"}; | ||
| 32 | |||
| 33 | auto rand = std::array<unsigned char, 10>{}; | ||
| 34 | int s = RAND_bytes(rand.data(), static_cast<int>(rand.size())); | ||
| 35 | if (s != 1) | ||
| 36 | { | ||
| 37 | unsigned long e = ERR_get_error(); | ||
| 38 | throw std::runtime_error{std::format( | ||
| 39 | "failed to generate UUID(v7): {} ({}, code {})", | ||
| 40 | ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; | ||
| 41 | } | ||
| 42 | |||
| 43 | auto version = std::uint64_t{0b0111}; | ||
| 44 | auto variant = std::uint64_t{0b10}; | ||
| 45 | |||
| 46 | high_ |= unix_time_ms << 16; | ||
| 47 | high_ |= version << 12; | ||
| 48 | high_ |= std::uint64_t{rand[0]} << 4; | ||
| 49 | high_ |= std::uint64_t{rand[1]}; | ||
| 50 | low_ |= variant << 62; | ||
| 51 | low_ |= std::uint64_t{rand[2]} << 54; | ||
| 52 | low_ |= std::uint64_t{rand[3]} << 48; | ||
| 53 | low_ |= std::uint64_t{rand[4]} << 40; | ||
| 54 | low_ |= std::uint64_t{rand[5]} << 32; | ||
| 55 | low_ |= std::uint64_t{rand[6]} << 24; | ||
| 56 | low_ |= std::uint64_t{rand[7]} << 16; | ||
| 57 | low_ |= std::uint64_t{rand[8]} << 8; | ||
| 58 | low_ |= std::uint64_t{rand[9]}; | ||
| 59 | } | ||
| 60 | |||
| 61 | auto format(std::array<char, 37>& target) -> void | ||
| 62 | { | ||
| 63 | auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; | ||
| 64 | auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; | ||
| 65 | auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; | ||
| 66 | auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; | ||
| 67 | auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; | ||
| 68 | 15 | ||
| 69 | std::format_to( | 16 | auto format(std::array<char, 37>& target) -> void; |
| 70 | target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", high_high, | ||
| 71 | high_low_high, low_low_high, high_low, low_low); | ||
| 72 | target.back() = '\0'; | ||
| 73 | } | ||
| 74 | }; | 17 | }; |
| 75 | 18 | ||
| 76 | export class id | 19 | export class id |
| @@ -78,12 +21,9 @@ export class id | |||
| 78 | std::array<char, 37> chars_; | 21 | std::array<char, 37> chars_; |
| 79 | 22 | ||
| 80 | public: | 23 | public: |
| 81 | id() { uuid7{}.format(chars_); } | 24 | id(); |
| 82 | 25 | ||
| 83 | auto as_string() const -> util::zstring_view | 26 | auto as_string() const -> util::zstring_view; |
| 84 | { | ||
| 85 | return util::zstring_view{chars_.data(), chars_.size() - 1}; | ||
| 86 | } | ||
| 87 | }; | 27 | }; |
| 88 | 28 | ||
| 89 | } // namespace routemon::trace | 29 | } // 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 @@ | |||
| 1 | module routemon:util$impl; | ||
| 2 | |||
| 3 | import :util; | ||
| 4 | |||
| 5 | namespace routemon::util { | ||
| 6 | |||
| 7 | lazy_zstring_view::lazy_zstring_view(lazy_zstring_view const& sv) noexcept | ||
| 8 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | ||
| 9 | { | ||
| 10 | } | ||
| 11 | |||
| 12 | lazy_zstring_view::lazy_zstring_view(lazy_zstring_view&& sv) noexcept | ||
| 13 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | ||
| 14 | { | ||
| 15 | } | ||
| 16 | |||
| 17 | auto lazy_zstring_view::operator=(lazy_zstring_view const& rhs) noexcept | ||
| 18 | -> lazy_zstring_view& | ||
| 19 | { | ||
| 20 | if (this != &rhs) | ||
| 21 | { | ||
| 22 | s_ = rhs.s_; | ||
| 23 | length_.store( | ||
| 24 | rhs.length_.load(std::memory_order_acquire), std::memory_order_release); | ||
| 25 | } | ||
| 26 | return *this; | ||
| 27 | } | ||
| 28 | |||
| 29 | auto lazy_zstring_view::operator=(lazy_zstring_view&& rhs) noexcept | ||
| 30 | -> lazy_zstring_view& | ||
| 31 | { | ||
| 32 | return *this = rhs; // use the copy assignment operator | ||
| 33 | } | ||
| 34 | |||
| 35 | auto lazy_zstring_view::length() const noexcept -> std::size_t | ||
| 36 | { | ||
| 37 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 38 | return v; | ||
| 39 | auto l = std::char_traits<char>::length(s_); | ||
| 40 | length_.store(l, std::memory_order_release); | ||
| 41 | return l; | ||
| 42 | } | ||
| 43 | |||
| 44 | auto lazy_zstring_view::c_str() const noexcept -> char const* { return s_; } | ||
| 45 | |||
| 46 | lazy_zstring_view::operator std::string_view() const noexcept | ||
| 47 | { | ||
| 48 | return std::string_view{s_, length()}; | ||
| 49 | } | ||
| 50 | |||
| 51 | lazy_zstring_view::operator char const*() const noexcept { return s_; } | ||
| 52 | |||
| 53 | auto lazy_zstring_view::operator==(std::string_view sv) const noexcept -> bool | ||
| 54 | { | ||
| 55 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 56 | if (sv.length() != v) | ||
| 57 | return false; | ||
| 58 | auto res = std::char_traits<char>::compare(s_, sv.data(), sv.length()); | ||
| 59 | if (res != 0) | ||
| 60 | return false; | ||
| 61 | // Strings are equal for sv.length() characters. | ||
| 62 | if (s_[sv.length()] != '\0') | ||
| 63 | return false; | ||
| 64 | // Strings are actually equal, and we have just found out the | ||
| 65 | // length of this string, so we might as well set it. | ||
| 66 | length_.store(sv.length(), std::memory_order_release); | ||
| 67 | return true; | ||
| 68 | } | ||
| 69 | |||
| 70 | auto operator==(zstring_view lhs, zstring_view rhs) -> bool | ||
| 71 | { | ||
| 72 | return std::string_view{lhs} == std::string_view{rhs}; | ||
| 73 | } | ||
| 74 | |||
| 75 | } // 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... | |||
| 13 | using Ts::operator()...; | 13 | using Ts::operator()...; |
| 14 | }; | 14 | }; |
| 15 | 15 | ||
| 16 | export constexpr auto parse_double( | 16 | constexpr auto parse_double( |
| 17 | std::string_view s, | 17 | std::string_view s, |
| 18 | std::chars_format fmt = std::chars_format::general) noexcept | 18 | std::chars_format fmt = std::chars_format::general) noexcept |
| 19 | -> std::optional<double> | 19 | -> std::optional<double> |
| @@ -30,7 +30,7 @@ export constexpr auto parse_double( | |||
| 30 | } | 30 | } |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | export constexpr auto parse_float( | 33 | constexpr auto parse_float( |
| 34 | std::string_view s, | 34 | std::string_view s, |
| 35 | std::chars_format fmt = std::chars_format::general) noexcept | 35 | std::chars_format fmt = std::chars_format::general) noexcept |
| 36 | -> std::optional<float> | 36 | -> std::optional<float> |
| @@ -47,7 +47,7 @@ export constexpr auto parse_float( | |||
| 47 | } | 47 | } |
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | export template <class T> | 50 | template <class T> |
| 51 | class aolist : public std::enable_shared_from_this<aolist<T>> | 51 | class aolist : public std::enable_shared_from_this<aolist<T>> |
| 52 | { | 52 | { |
| 53 | T v_; | 53 | T v_; |
| @@ -72,7 +72,7 @@ public: | |||
| 72 | auto value() const noexcept -> T const& { return v_; } | 72 | auto value() const noexcept -> T const& { return v_; } |
| 73 | }; | 73 | }; |
| 74 | 74 | ||
| 75 | export constexpr auto size_from_int(int x) -> std::optional<std::size_t> | 75 | constexpr auto size_from_int(int x) -> std::optional<std::size_t> |
| 76 | { | 76 | { |
| 77 | static_assert( | 77 | static_assert( |
| 78 | sizeof(int) <= sizeof(std::size_t), | 78 | sizeof(int) <= sizeof(std::size_t), |
| @@ -82,7 +82,7 @@ export constexpr auto size_from_int(int x) -> std::optional<std::size_t> | |||
| 82 | return static_cast<std::size_t>(x); | 82 | return static_cast<std::size_t>(x); |
| 83 | } | 83 | } |
| 84 | 84 | ||
| 85 | export constexpr auto int_from_size(std::size_t x) -> std::optional<int> | 85 | constexpr auto int_from_size(std::size_t x) -> std::optional<int> |
| 86 | { | 86 | { |
| 87 | constexpr auto int_max = size_from_int(std::numeric_limits<int>::max()); | 87 | constexpr auto int_max = size_from_int(std::numeric_limits<int>::max()); |
| 88 | static_assert(int_max.has_value()); | 88 | static_assert(int_max.has_value()); |
| @@ -91,7 +91,7 @@ export constexpr auto int_from_size(std::size_t x) -> std::optional<int> | |||
| 91 | return static_cast<int>(x); | 91 | return static_cast<int>(x); |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | export class zstring_view | 94 | class zstring_view |
| 95 | { | 95 | { |
| 96 | char const* s_; | 96 | char const* s_; |
| 97 | std::size_t length_; | 97 | std::size_t length_; |
| @@ -107,13 +107,16 @@ public: | |||
| 107 | { | 107 | { |
| 108 | } | 108 | } |
| 109 | 109 | ||
| 110 | auto length() const -> std::size_t { return length_; } | 110 | constexpr auto length() const -> std::size_t { return length_; } |
| 111 | 111 | ||
| 112 | auto c_str() const -> char const* { return s_; } | 112 | constexpr auto c_str() const -> char const* { return s_; } |
| 113 | 113 | ||
| 114 | operator std::string_view() const { return std::string_view{s_, length_}; } | 114 | constexpr operator std::string_view() const |
| 115 | { | ||
| 116 | return std::string_view{s_, length_}; | ||
| 117 | } | ||
| 115 | 118 | ||
| 116 | operator char const*() const { return s_; } | 119 | constexpr operator char const*() const { return s_; } |
| 117 | }; | 120 | }; |
| 118 | 121 | ||
| 119 | // View for null-terminated strings for which we might not | 122 | // View for null-terminated strings for which we might not |
| @@ -123,7 +126,7 @@ public: | |||
| 123 | // | 126 | // |
| 124 | // It is undefined behavior to assign to a lazy_zstring_view when | 127 | // It is undefined behavior to assign to a lazy_zstring_view when |
| 125 | // it is in use by other threads. | 128 | // it is in use by other threads. |
| 126 | export class lazy_zstring_view | 129 | class lazy_zstring_view |
| 127 | { | 130 | { |
| 128 | static constexpr auto unset_length = std::numeric_limits<std::size_t>::max(); | 131 | static constexpr auto unset_length = std::numeric_limits<std::size_t>::max(); |
| 129 | 132 | ||
| @@ -136,70 +139,21 @@ public: | |||
| 136 | : s_{s}, length_{s ? unset_length : 0} | 139 | : s_{s}, length_{s ? unset_length : 0} |
| 137 | { | 140 | { |
| 138 | } | 141 | } |
| 142 | lazy_zstring_view(lazy_zstring_view const& sv) noexcept; | ||
| 143 | lazy_zstring_view(lazy_zstring_view&& sv) noexcept; | ||
| 139 | 144 | ||
| 140 | ~lazy_zstring_view() = default; | 145 | ~lazy_zstring_view() = default; |
| 141 | 146 | ||
| 142 | lazy_zstring_view(lazy_zstring_view const& sv) noexcept | 147 | auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view&; |
| 143 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | 148 | auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view&; |
| 144 | { | ||
| 145 | } | ||
| 146 | |||
| 147 | lazy_zstring_view(lazy_zstring_view&& sv) noexcept | ||
| 148 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | ||
| 149 | { | ||
| 150 | } | ||
| 151 | |||
| 152 | auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& | ||
| 153 | { | ||
| 154 | if (this != &rhs) | ||
| 155 | { | ||
| 156 | s_ = rhs.s_; | ||
| 157 | length_.store( | ||
| 158 | rhs.length_.load(std::memory_order_acquire), | ||
| 159 | std::memory_order_release); | ||
| 160 | } | ||
| 161 | return *this; | ||
| 162 | } | ||
| 163 | |||
| 164 | auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& | ||
| 165 | { | ||
| 166 | return *this = rhs; // use the copy assignment operator | ||
| 167 | } | ||
| 168 | |||
| 169 | auto length() const noexcept -> std::size_t | ||
| 170 | { | ||
| 171 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 172 | return v; | ||
| 173 | auto l = std::char_traits<char>::length(s_); | ||
| 174 | length_.store(l, std::memory_order_release); | ||
| 175 | return l; | ||
| 176 | } | ||
| 177 | |||
| 178 | auto c_str() const noexcept -> char const* { return s_; } | ||
| 179 | 149 | ||
| 180 | operator std::string_view() const noexcept | 150 | auto length() const noexcept -> std::size_t; |
| 181 | { | 151 | auto c_str() const noexcept -> char const*; |
| 182 | return std::string_view{s_, length()}; | ||
| 183 | } | ||
| 184 | 152 | ||
| 185 | operator char const*() const noexcept { return s_; } | 153 | operator std::string_view() const noexcept; |
| 154 | operator char const*() const noexcept; | ||
| 186 | 155 | ||
| 187 | auto operator==(std::string_view sv) const noexcept -> bool | 156 | auto operator==(std::string_view sv) const noexcept -> bool; |
| 188 | { | ||
| 189 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 190 | if (sv.length() != v) | ||
| 191 | return false; | ||
| 192 | auto res = std::char_traits<char>::compare(s_, sv.data(), sv.length()); | ||
| 193 | if (res != 0) | ||
| 194 | return false; | ||
| 195 | // Strings are equal for sv.length() characters. | ||
| 196 | if (s_[sv.length()] != '\0') | ||
| 197 | return false; | ||
| 198 | // Strings are actually equal, and we have just found out the | ||
| 199 | // length of this string, so we might as well set it. | ||
| 200 | length_.store(sv.length(), std::memory_order_release); | ||
| 201 | return true; | ||
| 202 | } | ||
| 203 | }; | 157 | }; |
| 204 | 158 | ||
| 205 | constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept | 159 | 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 | |||
| 208 | return zstring_view{s, length}; | 162 | return zstring_view{s, length}; |
| 209 | } | 163 | } |
| 210 | 164 | ||
| 211 | auto operator==(zstring_view lhs, zstring_view rhs) -> bool | 165 | auto operator==(zstring_view lhs, zstring_view rhs) -> bool; |
| 212 | { | ||
| 213 | return std::string_view{lhs} == std::string_view{rhs}; | ||
| 214 | } | ||
| 215 | 166 | ||
| 216 | export constexpr auto split_on(std::string_view s, char c) | 167 | constexpr auto split_on(std::string_view s, char c) |
| 217 | -> std::pair<std::string_view, std::optional<std::string_view>> | 168 | -> std::pair<std::string_view, std::optional<std::string_view>> |
| 218 | { | 169 | { |
| 219 | if (auto i = s.find(c); i != std::string_view::npos) | 170 | 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) | |||
| 221 | return std::make_pair(s, std::nullopt); | 172 | return std::make_pair(s, std::nullopt); |
| 222 | } | 173 | } |
| 223 | 174 | ||
| 224 | export template <class T> | 175 | template <class T> |
| 225 | class not_null; | 176 | class not_null; |
| 226 | 177 | ||
| 227 | export template <class T> | 178 | template <class T> |
| 228 | class not_null<T*> | 179 | class not_null<T*> |
| 229 | { | 180 | { |
| 230 | T* p_; | 181 | T* p_; |
| @@ -264,10 +215,10 @@ public: | |||
| 264 | 215 | ||
| 265 | auto operator->() const noexcept -> T* { return p_; } | 216 | auto operator->() const noexcept -> T* { return p_; } |
| 266 | }; | 217 | }; |
| 267 | export template <class T> | 218 | template <class T> |
| 268 | explicit not_null(T*) -> not_null<T*>; | 219 | explicit not_null(T*) -> not_null<T*>; |
| 269 | 220 | ||
| 270 | export template <> | 221 | template <> |
| 271 | class not_null<lazy_zstring_view> | 222 | class not_null<lazy_zstring_view> |
| 272 | { | 223 | { |
| 273 | lazy_zstring_view s_; | 224 | lazy_zstring_view s_; |
| @@ -285,6 +236,6 @@ public: | |||
| 285 | operator std::string_view() const noexcept { return s_; } | 236 | operator std::string_view() const noexcept { return s_; } |
| 286 | operator char const*() const noexcept { return s_; } | 237 | operator char const*() const noexcept { return s_; } |
| 287 | }; | 238 | }; |
| 288 | export explicit not_null(lazy_zstring_view s) -> not_null<lazy_zstring_view>; | 239 | explicit not_null(lazy_zstring_view s) -> not_null<lazy_zstring_view>; |
| 289 | 240 | ||
| 290 | } // namespace routemon::util | 241 | } // 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 | |||
| 201 | 201 | ||
| 202 | using event = std::variant< | 202 | using event = std::variant< |
| 203 | start_element_event, end_element_event, character_data_event, | 203 | start_element_event, end_element_event, character_data_event, |
| 204 | processing_instructions_event, xml_decl_event, eof_event>; | 204 | processing_instructions_event, xml_decl_event, eof_event |
| 205 | >; | ||
| 205 | 206 | ||
| 206 | template <class T> | 207 | template <class T> |
| 207 | concept event_type = requires(event ev) { std::get<T>(ev); }; | 208 | concept event_type = requires(event ev) { std::get<T>(ev); }; |
| @@ -522,6 +523,7 @@ public: | |||
| 522 | } | 523 | } |
| 523 | constexpr auto await_resume() const noexcept -> void { return; } | 524 | constexpr auto await_resume() const noexcept -> void { return; } |
| 524 | }; | 525 | }; |
| 526 | |||
| 525 | if (this->continuation()) | 527 | if (this->continuation()) |
| 526 | { | 528 | { |
| 527 | return awaiter{this->continuation()}; | 529 | return awaiter{this->continuation()}; |
| @@ -548,16 +550,19 @@ public: | |||
| 548 | { | 550 | { |
| 549 | return !executor_->advance_flag(); | 551 | return !executor_->advance_flag(); |
| 550 | } | 552 | } |
| 553 | |||
| 551 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> void | 554 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> void |
| 552 | { | 555 | { |
| 553 | executor_->set_continuation(h.promise().base_handle()); | 556 | executor_->set_continuation(h.promise().base_handle()); |
| 554 | } | 557 | } |
| 558 | |||
| 555 | [[nodiscard]] auto await_resume() const -> event | 559 | [[nodiscard]] auto await_resume() const -> event |
| 556 | { | 560 | { |
| 557 | assert(executor_->event()); | 561 | assert(executor_->event()); |
| 558 | return executor_->event().value(); | 562 | return executor_->event().value(); |
| 559 | } | 563 | } |
| 560 | }; | 564 | }; |
| 565 | |||
| 561 | return awaiter{req.executor}; | 566 | return awaiter{req.executor}; |
| 562 | } | 567 | } |
| 563 | 568 | ||
| @@ -572,6 +577,7 @@ public: | |||
| 572 | { | 577 | { |
| 573 | return false; | 578 | return false; |
| 574 | } | 579 | } |
| 580 | |||
| 575 | auto await_suspend(std::coroutine_handle<promise<T>> h) | 581 | auto await_suspend(std::coroutine_handle<promise<T>> h) |
| 576 | -> std::coroutine_handle<> | 582 | -> std::coroutine_handle<> |
| 577 | { | 583 | { |
| @@ -580,6 +586,7 @@ public: | |||
| 580 | next_->set_continuation(h.promise().base_handle()); | 586 | next_->set_continuation(h.promise().base_handle()); |
| 581 | return next_->handle(); | 587 | return next_->handle(); |
| 582 | } | 588 | } |
| 589 | |||
| 583 | auto await_resume() -> U | 590 | auto await_resume() -> U |
| 584 | { | 591 | { |
| 585 | // Promise is still valid since coroutine frame is still alive | 592 | // Promise is still valid since coroutine frame is still alive |
| @@ -599,6 +606,7 @@ public: | |||
| 599 | } | 606 | } |
| 600 | } | 607 | } |
| 601 | }; | 608 | }; |
| 609 | |||
| 602 | return awaiter{util::not_null{&coro.promise()}}; | 610 | return awaiter{util::not_null{&coro.promise()}}; |
| 603 | } | 611 | } |
| 604 | }; | 612 | }; |
| @@ -717,14 +725,16 @@ auto expect_element( | |||
| 717 | co_await expect_end_element(e, want); | 725 | co_await expect_end_element(e, want); |
| 718 | co_await ignore_whitespace(e); | 726 | co_await ignore_whitespace(e); |
| 719 | co_return std::forward< | 727 | co_return std::forward< |
| 720 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res); | 728 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view> |
| 729 | >(res); | ||
| 721 | } | 730 | } |
| 722 | 731 | ||
| 723 | auto allow_element( | 732 | auto allow_element( |
| 724 | executor_ref e, qname_view want, | 733 | executor_ref e, qname_view want, |
| 725 | parser_invocable<executor_ref, attribute_view> auto p) | 734 | parser_invocable<executor_ref, attribute_view> auto p) |
| 726 | -> parser<std::optional< | 735 | -> parser<std::optional< |
| 727 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>> | 736 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view> |
| 737 | >> | ||
| 728 | { | 738 | { |
| 729 | co_await ignore_whitespace(e); | 739 | co_await ignore_whitespace(e); |
| 730 | if (auto mattrs = co_await allow_start_element(e, want)) | 740 | if (auto mattrs = co_await allow_start_element(e, want)) |
| @@ -734,8 +744,8 @@ auto allow_element( | |||
| 734 | co_await ignore_whitespace(e); | 744 | co_await ignore_whitespace(e); |
| 735 | co_return std::make_optional( | 745 | co_return std::make_optional( |
| 736 | std::forward< | 746 | std::forward< |
| 737 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>( | 747 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view> |
| 738 | res)); | 748 | >(res)); |
| 739 | } | 749 | } |
| 740 | co_return std::nullopt; | 750 | co_return std::nullopt; |
| 741 | } | 751 | } |
| @@ -744,7 +754,8 @@ auto allow_element( | |||
| 744 | executor_ref e, qname_view want, | 754 | executor_ref e, qname_view want, |
| 745 | parser_invocable<executor_ref, attribute_view> auto p) -> parser<bool> | 755 | parser_invocable<executor_ref, attribute_view> auto p) -> parser<bool> |
| 746 | requires std::is_void_v< | 756 | requires std::is_void_v< |
| 747 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> | 757 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view> |
| 758 | > | ||
| 748 | { | 759 | { |
| 749 | co_await ignore_whitespace(e); | 760 | co_await ignore_whitespace(e); |
| 750 | if (auto mattrs = co_await allow_start_element(e, want)) | 761 | if (auto mattrs = co_await allow_start_element(e, want)) |