From a5d95afb96eb9a3b65d82f5f84bf8e4a4fb4c8ac Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 21:12:55 +0200 Subject: clang-format C++ sources --- server/src/api.cpp | 391 +++++++------ server/src/api.cppm | 140 +++-- server/src/config.cpp | 337 ++++++----- server/src/config.cppm | 66 ++- server/src/database.cppm | 62 ++- server/src/datex2.cppm | 570 +++++++++++-------- server/src/geo.cppm | 63 ++- server/src/gpx.cpp | 383 +++++++------ server/src/gpx.cppm | 69 +-- server/src/http_client.cppm | 82 +-- server/src/http_common.cppm | 244 ++++---- server/src/http_server.cppm | 1301 +++++++++++++++++++++++++------------------ server/src/locale.cppm | 434 ++++++++------- server/src/log.cppm | 253 +++++---- server/src/main.cpp | 101 ++-- server/src/problem.cppm | 87 +-- server/src/req_ctx.cppm | 71 ++- server/src/rwgps.cppm | 231 ++++---- server/src/sqlite3.cppm | 486 +++++++++------- server/src/srv.cppm | 384 +++++++------ server/src/time.cppm | 369 ++++++------ server/src/trace.cppm | 120 ++-- server/src/util.cppm | 450 ++++++++------- server/src/xml.cpp | 94 ++-- server/src/xml.cppm | 1206 ++++++++++++++++++++++----------------- 25 files changed, 4573 insertions(+), 3421 deletions(-) (limited to 'server/src') diff --git a/server/src/api.cpp b/server/src/api.cpp index b3c8e31..a6eef23 100644 --- a/server/src/api.cpp +++ b/server/src/api.cpp @@ -17,190 +17,259 @@ import :trace; namespace { - namespace chrono = std::chrono; - namespace json = boost::json; - namespace views = std::views; +namespace chrono = std::chrono; +namespace json = boost::json; +namespace views = std::views; -} // namespace +} // namespace namespace routemon::api { - auto json_value_from_point(geo::point const& p) -> json::value { - return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; - } - auto json_value_from_linestring(geo::linestring const& ls) -> json::value { - json::array a; - for (auto const& p : ls) - a.push_back(json_value_from_point(p)); - return a; - } - auto json_value_from_linestrings(std::vector const& lss) -> json::value { - json::array a; - for (auto const& ls : lss) - a.push_back(json_value_from_linestring(ls)); - return a; - } +auto json_value_from_point(geo::point const& p) -> json::value +{ + return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; +} + +auto json_value_from_linestring(geo::linestring const& ls) -> json::value +{ + json::array a; + for (auto const& p : ls) + a.push_back(json_value_from_point(p)); + return a; +} - auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void { - jv = json::object{ +auto json_value_from_linestrings(std::vector const& lss) + -> json::value +{ + json::array a; + for (auto const& ls : lss) + a.push_back(json_value_from_linestring(ls)); + return a; +} + +auto tag_invoke( + json::value_from_tag, json::value& jv, relevant_road_closure const& clo) + -> void +{ + jv = json::object{ {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, - }; - } - auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void { - jv = json::object{ - {"id", json::value_from(sit.id)}, - {"location", sit.location ? json_value_from_point(*sit.location) : nullptr}, - {"comments", json::value_from(sit.comments)}, + }; +} + +auto tag_invoke( + json::value_from_tag, json::value& jv, relevant_situation const& sit) + -> void +{ + jv = json::object{ + {"id", json::value_from(sit.id)}, + {"location", + sit.location ? json_value_from_point(*sit.location) : nullptr}, + {"comments", json::value_from(sit.comments)}, {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, - }; - } - auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void { - jv = json::object{ + }; +} + +auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) + -> void +{ + jv = json::object{ {"points", json_value_from_linestring(seg.points)}, - }; - } - auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void { - jv = json::object{ + }; +} + +auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) + -> void +{ + jv = json::object{ {"segments", json::value_from(track.segments)}, - }; - } - auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void { - jv = json::object{ - {"tracks", json::value_from(res.tracks)}, + }; +} + +auto tag_invoke( + json::value_from_tag, json::value& jv, process_gpx_result const& res) + -> void +{ + jv = json::object{ + {"tracks", json::value_from(res.tracks)}, {"relevant_situations", json::value_from(res.relevant_situations)}, - }; - } - auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void { - jv = json::object{ - {"using_publication_of", std::format("{:%FT%TZ}", info.using_publication_of)}, - }; - } + }; +} + +auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) + -> void +{ + jv = json::object{ + {"using_publication_of", + std::format("{:%FT%TZ}", info.using_publication_of)}, + }; +} - handler::handler(log::logger const& l, datex2::situation_publication pub) - : l_{l.sub("handler")}, pub_{std::move(pub)} +handler::handler(log::logger const& l, datex2::situation_publication pub) + : l_{l.sub("handler")}, pub_{std::move(pub)} +{ + l_.info("Building indices"); + auto const before_build = chrono::steady_clock::now(); + for (auto const& sit : pub_.situations) { - l_.info("Building indices"); - auto const before_build = chrono::steady_clock::now(); - for (auto const& sit : pub_.situations) { - for (auto const& rc : sit->road_closures) { - for (auto const& ls : rc->relevant_line_strings) { - auto box = geo::box{}; - bgeo::envelope(*ls, box); - lse_index_.insert(std::make_tuple(box, ls, rc)); - } - for (auto p : rc->relevant_points) { - p_index_.insert(std::make_pair(p, rc)); - } + for (auto const& rc : sit->road_closures) + { + for (auto const& ls : rc->relevant_line_strings) + { + auto box = geo::box{}; + bgeo::envelope(*ls, box); + lse_index_.insert(std::make_tuple(box, ls, rc)); + } + for (auto p : rc->relevant_points) + { + p_index_.insert(std::make_pair(p, rc)); } } - auto const after_build = chrono::steady_clock::now(); - auto const dur_build = chrono::duration_cast(after_build - before_build); - l_.info("Indices built in {}", dur_build); - l_.info("LSE index size: {}", lse_index_.size()); - l_.info("Point index size: {}", p_index_.size()); } + auto const after_build = chrono::steady_clock::now(); + auto const dur_build = + chrono::duration_cast(after_build - before_build); + l_.info("Indices built in {}", dur_build); + l_.info("LSE index size: {}", lse_index_.size()); + l_.info("Point index size: {}", p_index_.size()); +} - auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional { - auto const now = chrono::utc_clock::now(); - auto const relevant = std::initializer_list{time::period{now - chrono::days(7), now + chrono::days(7)}}; - auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; - - auto splits_with_overlap_segments = std::vector{}; - for (auto const& track : gpx_file.tracks) - for (auto const& seg : track.segments) - geo::split_linestring_with_overlap_segments(seg.waypoints, 5000 /* meters max total dist until a new split is forced */, - splits_with_overlap_segments); - auto const before_query = chrono::steady_clock::now(); - - l_.debug("Querying for relevant situations"); - auto relevant_road_closures = std::unordered_set>{}; - auto ls_checked = 0uz; - auto p_checked = 0uz; - auto i = 0; - for (geo::linestring const& part : splits_with_overlap_segments) { - l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); - - auto part_box = geo::box{}; - bgeo::envelope(part, part_box); - - for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) { - // Cannot use structured bindings here, as boost::geometry::get interferes with ADL. - // It is a candidate as the namespace boost::geometry is part of the associated namespace set, - // which happens because geo::linestring ≡ boost::geometry::model::linestring is part - // of the whole tuple type (lse_index_value) that is the value_type of the iterator. - std::shared_ptr const& ls = std::get<1>(*it); - std::shared_ptr const& rc = std::get<2>(*it); - if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) - continue; - if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) - relevant_road_closures.emplace(rc); - ls_checked++; - } - for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) { - // Cannot use structured bindings here for the same reason as above. - geo::point const& p = std::get<0>(*it); - std::shared_ptr const& rc = std::get<1>(*it); - if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) - continue; - if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) - relevant_road_closures.emplace(rc); - p_checked++; - } +auto handler::process_gpx(gpx::file&& gpx_file) + -> std::optional +{ + auto const now = chrono::utc_clock::now(); + auto const relevant = std::initializer_list{ + time::period{now - chrono::days(7), now + chrono::days(7)} + }; + auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; + + auto splits_with_overlap_segments = std::vector{}; + for (auto const& track : gpx_file.tracks) + for (auto const& seg : track.segments) + geo::split_linestring_with_overlap_segments( + seg.waypoints, + 5000 /* meters max total dist until a new split is forced */, + splits_with_overlap_segments); + auto const before_query = chrono::steady_clock::now(); + + l_.debug("Querying for relevant situations"); + auto relevant_road_closures = + std::unordered_set>{}; + auto ls_checked = 0uz; + auto p_checked = 0uz; + auto i = 0; + for (geo::linestring const& part : splits_with_overlap_segments) + { + l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); + + auto part_box = geo::box{}; + bgeo::envelope(part, part_box); + + for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); + it != lse_index_.qend(); it++) + { + // Cannot use structured bindings here, as boost::geometry::get + // interferes with ADL. It is a candidate as the namespace + // boost::geometry is part of the associated namespace set, which + // happens because geo::linestring ≡ + // boost::geometry::model::linestring is part of the + // whole tuple type (lse_index_value) that is the value_type of the + // iterator. + std::shared_ptr const& ls = std::get<1>(*it); + std::shared_ptr const& rc = std::get<2>(*it); + if (rc->validity + && rc->validity->intersect(check_periods).periods().empty()) + continue; + if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) + relevant_road_closures.emplace(rc); + ls_checked++; + } + for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); + it != p_index_.qend(); it++) + { + // Cannot use structured bindings here for the same reason as above. + geo::point const& p = std::get<0>(*it); + std::shared_ptr const& rc = std::get<1>(*it); + if (rc->validity + && rc->validity->intersect(check_periods).periods().empty()) + continue; + if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) + relevant_road_closures.emplace(rc); + p_checked++; } + } - auto const after_query = chrono::steady_clock::now(); - l_.debug("Done (checked {} line string(s) and {} point(s)) in {}", - ls_checked, p_checked, chrono::duration_cast(after_query - before_query)); + auto const after_query = chrono::steady_clock::now(); + l_.debug( + "Done (checked {} line string(s) and {} point(s)) in {}", ls_checked, + p_checked, + chrono::duration_cast(after_query - before_query)); - auto relevant_situations = std::unordered_set>{}; - for (auto const& rc : relevant_road_closures) - relevant_situations.emplace(rc->parent); + auto relevant_situations = + std::unordered_set>{}; + for (auto const& rc : relevant_road_closures) + relevant_situations.emplace(rc->parent); - l_.debug("Identified {} relevant road closure(s), part of {} unique situation(s)", - relevant_road_closures.size(), relevant_situations.size()); - for (auto const& sit : relevant_situations) - l_.debug("Relevant situation: {}", sit->id); + l_.debug( + "Identified {} relevant road closure(s), part of {} unique " + "situation(s)", + relevant_road_closures.size(), relevant_situations.size()); + for (auto const& sit : relevant_situations) + l_.debug("Relevant situation: {}", sit->id); - return process_gpx_result{ + return process_gpx_result{ .tracks = gpx_file.tracks - | views::transform([](auto const& trk) -> track { - return { - .segments = trk.segments - | views::transform([](auto const& seg) -> track_segment { - return {.points = seg.waypoints}; - }) - | std::ranges::to>(), - }; - }) - | std::ranges::to>(), - .relevant_situations = relevant_situations - | views::transform([&](std::shared_ptr sit) -> relevant_situation { - return { - .id = sit->id, - .location = sit->location, - .comments = sit->comments, - .relevant_road_closures = relevant_road_closures - | views::filter([&](std::shared_ptr const& rc) -> bool { - return std::shared_ptr{rc->parent} == sit; - }) - | views::transform([](std::shared_ptr const& rc) -> relevant_road_closure { - return { - .relevant_lss = rc->relevant_line_strings - | views::transform([](auto const& lsp) -> geo::linestring { - return *lsp; + | views::transform( + [](auto const& trk) -> track + { + return { + .segments = + trk.segments + | views::transform( + [](auto const& seg) -> track_segment + { return {.points = seg.waypoints}; }) + | std::ranges::to>(), + }; + }) + | std::ranges::to>(), + .relevant_situations = + relevant_situations + | views::transform( + [&](std::shared_ptr sit) -> relevant_situation + { + return { + .id = sit->id, + .location = sit->location, + .comments = sit->comments, + .relevant_road_closures = + relevant_road_closures + | views::filter( + [&](std::shared_ptr const& rc) + -> bool + { return std::shared_ptr{rc->parent} == sit; }) + | views::transform( + [](std::shared_ptr const& rc) + -> relevant_road_closure + { + return { + .relevant_lss = + rc->relevant_line_strings + | views::transform( + [](auto const& lsp) -> geo::linestring + { return *lsp; }) + | std::ranges::to< + std::vector>(), + }; + }) + | std::ranges::to>(), + }; }) - | std::ranges::to>(), - }; - }) - | std::ranges::to>(), - }; - }) - | std::ranges::to>(), - }; - } + | std::ranges::to>(), + }; +} - auto handler::sysinfo() -> struct sysinfo { - return {.using_publication_of = pub_.publication_time}; - } +auto handler::sysinfo() -> struct sysinfo +{ + return {.using_publication_of = pub_.publication_time}; +} } // namespace routemon::api diff --git a/server/src/api.cppm b/server/src/api.cppm index caeb44c..66e8f45 100644 --- a/server/src/api.cppm +++ b/server/src/api.cppm @@ -15,65 +15,85 @@ import :trace; namespace { - namespace chrono = std::chrono; - namespace json = boost::json; - namespace views = std::views; - -} // namespace - -export -namespace routemon::api { - - struct relevant_road_closure { - std::vector relevant_lss; - }; - auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void; - - struct relevant_situation { - std::string id; - std::optional location; - std::vector comments; - std::vector relevant_road_closures; - }; - auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void; - - struct track_segment { - geo::linestring points; - }; - auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void; - - struct track { - std::vector segments; - }; - auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void; - - struct process_gpx_result { - std::vector tracks; - std::vector relevant_situations; - }; - auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void; - - struct sysinfo { - time::timestamp using_publication_of; - }; - auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void; - - class handler { - using lse_index_value = std::tuple, std::shared_ptr>; - using p_index_value = std::pair>; - using lse_index = bgeo::index::rtree>; - using p_index = bgeo::index::rtree>; - - log::logger l_; - datex2::situation_publication pub_; - lse_index lse_index_; - p_index p_index_; - - public: - explicit handler(log::logger const& l, datex2::situation_publication pub); - - auto process_gpx(gpx::file&& gpx_file) -> std::optional; - auto sysinfo() -> sysinfo; - }; +namespace chrono = std::chrono; +namespace json = boost::json; +namespace views = std::views; + +} // namespace + +export namespace routemon::api { + +struct relevant_road_closure +{ + std::vector relevant_lss; +}; + +struct relevant_situation +{ + std::string id; + std::optional location; + std::vector comments; + std::vector relevant_road_closures; +}; + +struct track_segment +{ + geo::linestring points; +}; + +struct track +{ + std::vector segments; +}; + +struct process_gpx_result +{ + std::vector tracks; + std::vector relevant_situations; +}; + +struct sysinfo +{ + time::timestamp using_publication_of; +}; + +auto tag_invoke( + json::value_from_tag, json::value& jv, relevant_road_closure const& clo) + -> void; +auto tag_invoke( + json::value_from_tag, json::value& jv, relevant_situation const& sit) + -> void; +auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) + -> void; +auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) + -> void; +auto tag_invoke( + json::value_from_tag, json::value& jv, process_gpx_result const& res) + -> void; +auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) + -> void; + +class handler +{ + using lse_index_value = std::tuple< + geo::box, std::shared_ptr, + std::shared_ptr>; + using p_index_value = + std::pair>; + using lse_index = + bgeo::index::rtree>; + using p_index = bgeo::index::rtree>; + + log::logger l_; + datex2::situation_publication pub_; + lse_index lse_index_; + p_index p_index_; + +public: + explicit handler(log::logger const& l, datex2::situation_publication pub); + + auto process_gpx(gpx::file&& gpx_file) -> std::optional; + auto sysinfo() -> sysinfo; +}; } // namespace routemon::api diff --git a/server/src/config.cpp b/server/src/config.cpp index 3b17691..84c436a 100644 --- a/server/src/config.cpp +++ b/server/src/config.cpp @@ -14,154 +14,225 @@ namespace json = boost::json; namespace routemon::config { - class location { - std::optional>> next_; - - auto append_to(std::string& s) const -> void { - if (next_) { - s += next_->first; - s += "."; - next_->second->append_to(s); - } - } - - public: - location() = default; - - explicit location(location const& next, std::string_view entry) - : next_{std::make_pair(entry, util::not_null{&next})} - {} - - [[nodiscard]] auto to_string() const -> std::string { - if (!next_) - return ""; - auto s = std::string{next_->first}; +class location +{ + std::optional>> + next_; + + auto append_to(std::string& s) const -> void + { + if (next_) + { + s += next_->first; + s += "."; next_->second->append_to(s); - return s; - } - - auto sub(std::string_view entry) const& -> location { - return location{*this, entry}; - } - }; - - class object_reader { - location loc_; - json::object const& obj_; - std::unordered_set visited_; - - public: - object_reader(json::value const& jv, location loc) - try : loc_{std::move(loc)}, obj_{jv.as_object()} - {} catch (boost::system::system_error const&) { - throw std::runtime_error{std::format("expected an object at {}", loc.to_string())}; - } - - auto check_unused() const -> void { - for (auto const& kv : obj_) { - if (!visited_.contains(std::string{kv.key()})) { - throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())}; - } - } - } - - template - auto expect_at(std::string_view key) -> T { - visited_.emplace(key); - auto const loc = loc_.sub(key); - if (auto const jv = obj_.try_at(key)) { - try { - return json::value_to(*jv, loc); - } catch (boost::system::system_error const& e) { - throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())}; - } - } else { - throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())}; - } } - }; - - auto as_checked_object(json::value const& jv, location const& loc, std::invocable auto const& f) -> decltype(f(std::declval())) { - auto r = object_reader{jv, loc}; - auto&& v = f(r); - r.check_unused(); - return std::forward(v); } - auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> rwgps { - return as_checked_object(jv, loc, [](object_reader& r) -> rwgps { - return { - .api_key = r.expect_at("api_key"), - .auth_token = r.expect_at("auth_token"), - }; - }); - } +public: + location() = default; - auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> situations { - return as_checked_object(jv, loc, [](object_reader& r) -> situations { - return { - .datex2_filename = r.expect_at("datex2_filename"), - }; - }); + explicit location(location const& next, std::string_view entry) + : next_{std::make_pair(entry, util::not_null{&next})} + { } - auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> database { - return as_checked_object(jv, loc, [](object_reader& r) -> database { - return { - .sqlite3_filename = r.expect_at("sqlite3_filename"), - }; - }); + [[nodiscard]] auto to_string() const -> std::string + { + if (!next_) + return ""; + auto s = std::string{next_->first}; + next_->second->append_to(s); + return s; } - auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> http_server { - return as_checked_object(jv, loc, [](object_reader& r) -> http_server { - return { - .lax_cors = r.expect_at("lax_cors"), - }; - }); + auto sub(std::string_view entry) const& -> location + { + return location{*this, entry}; } - - auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> logger { - return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger { - auto const level_str = r.expect_at("level"); - auto level = log::level{}; - if (level_str == "debug") - level = log::level::debug; - else if (level_str == "info") - level = log::level::info; - else if (level_str == "warn") - level = log::level::warn; - else if (level_str == "error") - level = log::level::error; - else - throw std::runtime_error{std::format("unable to parse log level {:?} at {}: expected one of {{debug, info, warn, error}}", level_str, obj_loc.sub("level").to_string())}; - return logger{level}; - }); +}; + +class object_reader +{ + location loc_; + json::object const& obj_; + std::unordered_set visited_; + +public: + object_reader(json::value const& jv, location loc) + try : loc_{std::move(loc)}, obj_{jv.as_object()} {} + catch (boost::system::system_error const&) + { + throw std::runtime_error{std::format( + "expected an object at {}", loc.to_string())}; } - auto json_value_to_app(json::value const& jv) -> app { - return as_checked_object(jv, location{}, [](object_reader& r) -> app { - return { - .rwgps = r.expect_at("rwgps"), - .situations = r.expect_at("situations"), - .database = r.expect_at("database"), - .http_server = r.expect_at("http_server"), - .logger = r.expect_at("logger"), - }; - }); + auto check_unused() const -> void + { + for (auto const& kv : obj_) + { + if (!visited_.contains(std::string{kv.key()})) + { + throw std::runtime_error{std::format( + "unexpected key {}", loc_.sub(kv.key()).to_string())}; + } + } } - auto load_file(std::string const& filename) -> app { - auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode? - if (!f.is_open()) - throw std::runtime_error{std::format("failed to open {}", filename)}; - auto jv = json::value{}; - try { - jv = json::parse(f); - } catch (boost::system::system_error const& e) { - throw std::runtime_error{std::format("failed to parse: {}", e.code().message())}; + template + auto expect_at(std::string_view key) -> T + { + visited_.emplace(key); + auto const loc = loc_.sub(key); + if (auto const jv = obj_.try_at(key)) + { + try + { + return json::value_to(*jv, loc); + } + catch (boost::system::system_error const& e) + { + throw std::runtime_error{std::format( + "failed to read {}: {}", loc.to_string(), e.code().message())}; + } } - return json_value_to_app(jv); + else + { + throw std::runtime_error{std::format( + "did not find expected key {}", loc.to_string())}; + } + } +}; + +auto as_checked_object( + json::value const& jv, location const& loc, + std::invocable auto const& f) + -> decltype(f(std::declval())) +{ + auto r = object_reader{jv, loc}; + auto&& v = f(r); + r.check_unused(); + return std::forward(v); +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv, + location const& loc) -> rwgps +{ + return as_checked_object( + jv, loc, + [](object_reader& r) -> rwgps + { + return { + .api_key = r.expect_at("api_key"), + .auth_token = r.expect_at("auth_token"), + }; + }); +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv, + location const& loc) -> situations +{ + return as_checked_object( + jv, loc, + [](object_reader& r) -> situations + { + return { + .datex2_filename = r.expect_at("datex2_filename"), + }; + }); +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv, + location const& loc) -> database +{ + return as_checked_object( + jv, loc, + [](object_reader& r) -> database + { + return { + .sqlite3_filename = r.expect_at("sqlite3_filename"), + }; + }); +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv, + location const& loc) -> http_server +{ + return as_checked_object( + jv, loc, + [](object_reader& r) -> http_server + { + return { + .lax_cors = r.expect_at("lax_cors"), + }; + }); +} + +auto tag_invoke( + json::value_to_tag const&, json::value const& jv, + location const& loc) -> logger +{ + return as_checked_object( + jv, loc, + [obj_loc = loc](object_reader& r) -> logger + { + auto const level_str = r.expect_at("level"); + auto level = log::level{}; + if (level_str == "debug") + level = log::level::debug; + else if (level_str == "info") + level = log::level::info; + else if (level_str == "warn") + level = log::level::warn; + else if (level_str == "error") + level = log::level::error; + else + throw std::runtime_error{std::format( + "unable to parse log level {:?} at {}: expected one " + "of {{debug, info, warn, error}}", + level_str, obj_loc.sub("level").to_string())}; + return logger{level}; + }); +} + +auto json_value_to_app(json::value const& jv) -> app +{ + return as_checked_object( + jv, location{}, + [](object_reader& r) -> app + { + return { + .rwgps = r.expect_at("rwgps"), + .situations = r.expect_at("situations"), + .database = r.expect_at("database"), + .http_server = r.expect_at("http_server"), + .logger = r.expect_at("logger"), + }; + }); +} + +auto load_file(std::string const& filename) -> app +{ + auto f = std::ifstream{ + filename + }; // TODO: ensure that we are opening in binary mode? + if (!f.is_open()) + throw std::runtime_error{std::format("failed to open {}", filename)}; + auto jv = json::value{}; + try + { + jv = json::parse(f); + } + catch (boost::system::system_error const& e) + { + throw std::runtime_error{std::format( + "failed to parse: {}", e.code().message())}; } + return json_value_to_app(jv); +} } // namespace routemon::config diff --git a/server/src/config.cppm b/server/src/config.cppm index ef827a6..e5b48f4 100644 --- a/server/src/config.cppm +++ b/server/src/config.cppm @@ -5,35 +5,41 @@ import :log; namespace routemon::config { - export struct rwgps { - std::string api_key; - std::string auth_token; - }; - - export struct situations { - std::string datex2_filename; - }; - - export struct database { - std::string sqlite3_filename; - }; - - export struct http_server { - bool lax_cors; - }; - - export struct logger { - log::level level; - }; - - export struct app { - rwgps rwgps; - situations situations; - database database; - http_server http_server; - logger logger; - }; - - export auto load_file(std::string const& filename) -> app; +export struct rwgps +{ + std::string api_key; + std::string auth_token; +}; + +export struct situations +{ + std::string datex2_filename; +}; + +export struct database +{ + std::string sqlite3_filename; +}; + +export struct http_server +{ + bool lax_cors; +}; + +export struct logger +{ + log::level level; +}; + +export struct app +{ + rwgps rwgps; + situations situations; + database database; + http_server http_server; + logger logger; +}; + +export auto load_file(std::string const& filename) -> app; } // namespace routemon::config diff --git a/server/src/database.cppm b/server/src/database.cppm index 0312dda..86ed0d7 100644 --- a/server/src/database.cppm +++ b/server/src/database.cppm @@ -5,33 +5,43 @@ import :sqlite3; namespace routemon::database { - static constexpr std::int64_t expected_database_version = 1; - - export class connection { - sqlite3::connection dbc_; - - explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {} - - friend auto open(std::string const& filename) -> std::shared_ptr; - - public: - // Nothing here yet - }; - - export auto open(std::string const& filename) -> std::shared_ptr { - auto dbc = sqlite3::open(filename); - try { - auto version = std::optional{}; - dbc.query("SELECT version FROM migration;").scan_single(version); - if (!version) - throw std::runtime_error{"failed to fetch database migration version"}; - if (version != expected_database_version) { - throw std::runtime_error{std::format("database migration version ({}) does not match expected version ({}), consider running migrations", *version, expected_database_version)}; - } - } catch (std::exception const& e) { - throw std::runtime_error{std::format("failed to query database version: {}", e.what())}; +static constexpr std::int64_t expected_database_version = 1; + +export class connection +{ + sqlite3::connection dbc_; + + explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {} + + friend auto open(std::string const& filename) -> std::shared_ptr; + +public: + // Nothing here yet +}; + +export auto open(std::string const& filename) -> std::shared_ptr +{ + auto dbc = sqlite3::open(filename); + try + { + auto version = std::optional{}; + dbc.query("SELECT version FROM migration;").scan_single(version); + if (!version) + throw std::runtime_error{"failed to fetch database migration version"}; + if (version != expected_database_version) + { + throw std::runtime_error{std::format( + "database migration version ({}) does not match expected " + "version ({}), consider running migrations", + *version, expected_database_version)}; } - return std::shared_ptr{new connection{std::move(dbc)}}; } + catch (std::exception const& e) + { + throw std::runtime_error{std::format( + "failed to query database version: {}", e.what())}; + } + return std::shared_ptr{new connection{std::move(dbc)}}; +} } // namespace routemon::database diff --git a/server/src/datex2.cppm b/server/src/datex2.cppm index b507e6f..b4c795f 100644 --- a/server/src/datex2.cppm +++ b/server/src/datex2.cppm @@ -1,8 +1,8 @@ module; #include -#include #include +#include #include @@ -17,205 +17,293 @@ using namespace std::literals::string_view_literals; namespace routemon::datex2 { - export struct situation; - - export struct road_closure { - std::weak_ptr parent; - std::optional validity; - std::vector relevant_points = {}; - std::vector> relevant_line_strings = {}; - }; - - export struct situation { - std::string id; - std::optional location = std::nullopt; // as shown on the map, not used for querying - std::vector comments = {}; - std::vector> road_closures = {}; - }; - - export struct situation_publication { - time::timestamp publication_time; - std::vector> situations; - }; - - auto parse_timestamp(char const* in) -> std::optional { - auto res = time::timestamp{}; - auto is = std::istringstream{in}; - is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); - return is.fail() ? std::nullopt : std::make_optional(res); - } - - export class loader { - // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) - bgeo::srs::transformation, bgeo::srs::static_epsg<4326>> etrs89_to_wgs84_{}; - - std::multiset warnings_; - - auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) -> void { - auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; - if (loc_xml_type == "loc:ItineraryByIndexedLocations") { - for (auto const loc_cont_xml : loc_xml.children("loc:locationContainedInItinerary")) { - add_location_from_xml(rc, loc_cont_xml.child("loc:location")); - } - } else if (loc_xml_type == "loc:LinearLocation" || loc_xml_type == "loc:SingleRoadLinearLocation") { - auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); - if (!loc_gml_xml) - return; +export struct situation; + +export struct road_closure +{ + std::weak_ptr parent; + std::optional validity; + std::vector relevant_points = {}; + std::vector> relevant_line_strings = {}; +}; + +export struct situation +{ + std::string id; + std::optional location = + std::nullopt; // as shown on the map, not used for querying + std::vector comments = {}; + std::vector> road_closures = {}; +}; + +export struct situation_publication +{ + time::timestamp publication_time; + std::vector> situations; +}; + +auto parse_timestamp(char const* in) -> std::optional +{ + auto res = time::timestamp{}; + auto is = std::istringstream{in}; + is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); + return is.fail() ? std::nullopt : std::make_optional(res); +} + +export class loader +{ + // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) + bgeo::srs::transformation< + bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> + etrs89_to_wgs84_{}; + + std::multiset warnings_; + + auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) + -> void + { + auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; + if (loc_xml_type == "loc:ItineraryByIndexedLocations") + { + for (auto const loc_cont_xml : + loc_xml.children("loc:locationContainedInItinerary")) + { + add_location_from_xml(rc, loc_cont_xml.child("loc:location")); + } + } + else if (loc_xml_type == "loc:LinearLocation" + || loc_xml_type == "loc:SingleRoadLinearLocation") + { + auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); + if (!loc_gml_xml) + return; - auto const srs_name = std::string_view{loc_gml_xml.attribute("srsName").value()}; - if (srs_name != "WGS 84"sv) { - warnings_.insert(std::format("don't now how to handle the CRS {}", srs_name)); + auto const srs_name = + std::string_view{loc_gml_xml.attribute("srsName").value()}; + if (srs_name != "WGS 84"sv) + { + warnings_.insert( + std::format("don't now how to handle the CRS {}", srs_name)); + return; + } + auto const pos_list_str = + std::string_view{loc_gml_xml.child_value("loc:posList")}; + // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn + + auto ls = std::make_shared(); + + auto lat_set = false; + auto lat = 0.0; + for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) + { + auto mlat_or_long = + util::parse_double(std::string_view{lat_or_long_str}); + if (!mlat_or_long) + { + warnings_.insert( + std::format( + "failed to parse coordinate {:?}", + std::string_view{lat_or_long_str})); return; } - auto const pos_list_str = std::string_view{loc_gml_xml.child_value("loc:posList")}; - // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn - - auto ls = std::make_shared(); - - auto lat_set = false; - auto lat = 0.0; - for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) { - auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); - if (!mlat_or_long) { - warnings_.insert(std::format("failed to parse coordinate {:?}", std::string_view{lat_or_long_str})); - return; - } - - if (!lat_set) { - lat = *mlat_or_long; - lat_set = true; - } else { - bgeo::append(*ls, geo::point{*mlat_or_long, lat}); - lat = 0; - lat_set = false; - } - } - if (bgeo::is_empty(*ls)) { - warnings_.emplace("empty line string in data set"); - return; + if (!lat_set) + { + lat = *mlat_or_long; + lat_set = true; } - - rc.relevant_line_strings.push_back(ls); - } else if (loc_xml_type == "loc:PointLocation") { - auto const& coords_xml = loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); - if (!coords_xml) - return; - - auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); - auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); - if (!mlat || !mlon) { - warnings_.emplace("failed to parse PointLocation coordinates"); - return; + else + { + bgeo::append(*ls, geo::point{*mlat_or_long, lat}); + lat = 0; + lat_set = false; } + } - // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: - // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ - // maar heeft het UML-model van DATEX II v3 het over ETRS 89: - // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm - - auto const coords_etrs89 = geo::point{*mlon, *mlat}; - auto coords_wgs84 = geo::point{}; - etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); - - rc.relevant_points.push_back(coords_wgs84); - } else { - warnings_.insert(std::format("don't know how to hande location of type {}, ignoring", loc_xml.attribute("xsi:type").value())); + if (bgeo::is_empty(*ls)) + { + warnings_.emplace("empty line string in data set"); return; } - } - auto handle_road_or_carriageway_or_lane_management(pugi::xml_node const& record_xml, std::weak_ptr parent) -> std::optional> { - auto const type = std::string_view{record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value()}; - if (type != "carriagewayClosures" && type != "roadClosed") - // TODO: checken of er nog andere types fietsers de doorgang zouden kunnen blokkeren? - return std::nullopt; + rc.relevant_line_strings.push_back(ls); + } + else if (loc_xml_type == "loc:PointLocation") + { + auto const& coords_xml = + loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); + if (!coords_xml) + return; - auto const& restricted_vehicle_types_xml = record_xml.child("sit:forVehiclesWithCharacteristicsOf"); - bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); - for (auto const vehicle_type_xml : restricted_vehicle_types_xml.children("com:vehicleType")) { - auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; - if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" || - vehicle_type == "unknown" || vehicle_type == "other") { - likely_restriction_for_bikes = true; - } + auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); + auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); + if (!mlat || !mlon) + { + warnings_.emplace("failed to parse PointLocation coordinates"); + return; } - if (!likely_restriction_for_bikes) - return std::nullopt; - //---- Check if within the defined validity period + // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: + // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ + // maar heeft het UML-model van DATEX II v3 het over ETRS 89: + // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm - auto validity = std::optional{}; - auto const& validity_xml = record_xml.child("sit:validity"); - if (validity_xml && validity_xml.child_value("com:validityStatus") == "definedByValidityTimeSpec"sv) { - auto const& validity_spec_xml = validity_xml.child("com:validityTimeSpecification"); + auto const coords_etrs89 = geo::point{*mlon, *mlat}; + auto coords_wgs84 = geo::point{}; + etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); - auto valid_periods = std::vector{}; - auto exception_periods = std::vector{}; - - // TODO: com:overallEndTime may be missing (according to the DATEX II v3 data model) - auto const overall_start_time = parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); - auto const overall_end_time = parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); - if (overall_start_time && overall_end_time && *overall_start_time < *overall_end_time) { - valid_periods.emplace_back(*overall_start_time, *overall_end_time); + rc.relevant_points.push_back(coords_wgs84); + } + else + { + warnings_.insert( + std::format( + "don't know how to hande location of type {}, ignoring", + loc_xml.attribute("xsi:type").value())); + return; + } + } - for (auto const valid_period_xml : validity_xml.children("com:validPeriod")) { - auto const start_of_period = parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); - auto const end_of_period = parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); - if (start_of_period && end_of_period && *start_of_period < *end_of_period) { - valid_periods.emplace_back(*start_of_period, *end_of_period); - } + auto handle_road_or_carriageway_or_lane_management( + pugi::xml_node const& record_xml, std::weak_ptr parent) + -> std::optional> + { + auto const type = + std::string_view{record_xml + .child("sit:roadOrCarriagewayOrLaneManagementType") + .child_value()}; + if (type != "carriagewayClosures" && type != "roadClosed") + // TODO: checken of er nog andere types fietsers de doorgang zouden + // kunnen blokkeren? + return std::nullopt; + + auto const& restricted_vehicle_types_xml = + record_xml.child("sit:forVehiclesWithCharacteristicsOf"); + bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); + for (auto const vehicle_type_xml : + restricted_vehicle_types_xml.children("com:vehicleType")) + { + auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; + if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" + || vehicle_type == "unknown" || vehicle_type == "other") + { + likely_restriction_for_bikes = true; + } + } + if (!likely_restriction_for_bikes) + return std::nullopt; + + //---- Check if within the defined validity period + + auto validity = std::optional{}; + auto const& validity_xml = record_xml.child("sit:validity"); + if (validity_xml + && validity_xml.child_value("com:validityStatus") + == "definedByValidityTimeSpec"sv) + { + auto const& validity_spec_xml = + validity_xml.child("com:validityTimeSpecification"); + + auto valid_periods = std::vector{}; + auto exception_periods = std::vector{}; + + // TODO: com:overallEndTime may be missing (according to the DATEX + // II v3 data model) + auto const overall_start_time = parse_timestamp( + validity_spec_xml.child_value("com:overallStartTime")); + auto const overall_end_time = + parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); + if (overall_start_time && overall_end_time + && *overall_start_time < *overall_end_time) + { + valid_periods.emplace_back(*overall_start_time, *overall_end_time); + + for (auto const valid_period_xml : + validity_xml.children("com:validPeriod")) + { + auto const start_of_period = parse_timestamp( + valid_period_xml.child_value("com:startOfPeriod")); + auto const end_of_period = + parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); + if (start_of_period && end_of_period + && *start_of_period < *end_of_period) + { + valid_periods.emplace_back(*start_of_period, *end_of_period); } - for (auto const exception_period_xml : validity_xml.children("com:exceptionPeriod")) { - auto const start_of_period = parse_timestamp(exception_period_xml.child_value("com:startOfPeriod")); - auto const end_of_period = parse_timestamp(exception_period_xml.child_value("com:endOfPeriod")); - if (start_of_period && end_of_period && *start_of_period < *end_of_period) { - exception_periods.emplace_back(*start_of_period, *end_of_period); - } + } + for (auto const exception_period_xml : + validity_xml.children("com:exceptionPeriod")) + { + auto const start_of_period = parse_timestamp( + exception_period_xml.child_value("com:startOfPeriod")); + auto const end_of_period = parse_timestamp( + exception_period_xml.child_value("com:endOfPeriod")); + if (start_of_period && end_of_period + && *start_of_period < *end_of_period) + { + exception_periods.emplace_back(*start_of_period, *end_of_period); } - - validity = time::period_seq{valid_periods.begin(), valid_periods.end()} - .except(time::period_seq{exception_periods.begin(), exception_periods.end()}); - } else { - warnings_.insert(std::format("invalid overall start / end time (start time: {}, end time: {})", - validity_spec_xml.child_value("com:overallStartTime"), - validity_spec_xml.child_value("com:overallEndTime"))); - return std::nullopt; } + + validity = + time::period_seq{valid_periods.begin(), valid_periods.end()}.except( + time::period_seq{ + exception_periods.begin(), exception_periods.end() + }); } + else + { + warnings_.insert( + std::format( + "invalid overall start / end time (start time: {}, end " + "time: {})", + validity_spec_xml.child_value("com:overallStartTime"), + validity_spec_xml.child_value("com:overallEndTime"))); + return std::nullopt; + } + } - //---- Try to extract the location info + //---- Try to extract the location info - auto rc = std::make_shared(std::move(parent), validity); - add_location_from_xml(*rc, record_xml.child("sit:locationReference")); - return rc; - } + auto rc = std::make_shared(std::move(parent), validity); + add_location_from_xml(*rc, record_xml.child("sit:locationReference")); + return rc; + } - public: - [[nodiscard]] auto load_situation_publication(std::string const& filename) -> situation_publication { - auto doc = pugi::xml_document{}; - if (auto result = doc.load_file(filename.c_str()); !result) { - throw std::runtime_error{result.description()}; - } - auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); - auto mpublication_time = parse_timestamp(payload_xml.child_value("com:publicationTime")); - if (!mpublication_time) - throw std::runtime_error{"provided publication does not name publication time"}; +public: + [[nodiscard]] auto load_situation_publication(std::string const& filename) + -> situation_publication + { + auto doc = pugi::xml_document{}; + if (auto result = doc.load_file(filename.c_str()); !result) + { + throw std::runtime_error{result.description()}; + } + auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); + auto mpublication_time = + parse_timestamp(payload_xml.child_value("com:publicationTime")); + if (!mpublication_time) + throw std::runtime_error{ + "provided publication does not name publication time" + }; - auto situations = std::vector>{}; - for (auto const sit_xml : payload_xml.children("sit:situation")) { - auto id = std::string_view{sit_xml.attribute("id").value()}; + auto situations = std::vector>{}; + for (auto const sit_xml : payload_xml.children("sit:situation")) + { + auto id = std::string_view{sit_xml.attribute("id").value()}; - auto const sit = std::make_shared(std::string{id}); - situations.push_back(sit); + auto const sit = std::make_shared(std::string{id}); + situations.push_back(sit); - auto const& header_info_xml = sit_xml.child("sit:headerInformation"); - if (header_info_xml.child_value("com:informationStatus") != "real"sv) - continue; + auto const& header_info_xml = sit_xml.child("sit:headerInformation"); + if (header_info_xml.child_value("com:informationStatus") != "real"sv) + continue; - for (auto const record_xml : sit_xml.children("sit:situationRecord")) { - auto const record_type = std::string_view{record_xml.attribute("xsi:type").value()}; - auto const primary_record_types = std::unordered_set{ + for (auto const record_xml : sit_xml.children("sit:situationRecord")) + { + auto const record_type = + std::string_view{record_xml.attribute("xsi:type").value()}; + auto const primary_record_types = std::unordered_set{ "sit:Roadworks", /* { */ "sit:MaintenanceWorks", /* | */ "sit:ConstructionWorks", @@ -228,52 +316,84 @@ namespace routemon::datex2 { "sit:Activity", /* { */ "sit:PublicEvent", /* } */ - }; - - if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") { - if (auto rc = handle_road_or_carriageway_or_lane_management(record_xml, sit)) { - sit->road_closures.push_back(*rc); + }; + + if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") + { + if (auto rc = handle_road_or_carriageway_or_lane_management( + record_xml, sit)) + { + sit->road_closures.push_back(*rc); + } + } + else if (primary_record_types.contains(record_type)) + { + for (auto const comment_xml : + record_xml.children("sit:generalPublicComment")) + { + // if + // (comment_xml.child_value("sit:commentType") + // == "internalNote"sv) { + auto candidate = std::optional>{}; // (text, language) + for (auto const comment_value_xml : comment_xml.child("sit:comment") + .child("com:values") + .children("com:value")) + { + if (!candidate + || comment_value_xml.attribute("lang").value() == "nl"sv + || (candidate->second != "nl"sv + && comment_value_xml.attribute("lang").value() == "nl"sv)) + { + candidate = std::make_pair( + comment_value_xml.child_value(), + comment_value_xml.attribute("lang").value()); + } } - } else if (primary_record_types.contains(record_type)) { - for (auto const comment_xml : record_xml.children("sit:generalPublicComment")) { -// if (comment_xml.child_value("sit:commentType") == "internalNote"sv) { - auto candidate = std::optional>{}; // (text, language) - for (auto const comment_value_xml : comment_xml.child("sit:comment").child("com:values").children("com:value")) { - if (!candidate || - comment_value_xml.attribute("lang").value() == "nl"sv || - (candidate->second != "nl"sv && comment_value_xml.attribute("lang").value() == "nl"sv)) { - candidate = std::make_pair(comment_value_xml.child_value(), comment_value_xml.attribute("lang").value()); - } - } - if (candidate) { - auto already_present = false; - for (auto const& comment : sit->comments) - already_present = already_present || comment == candidate->first; - if (!already_present) { - sit->comments.emplace_back(candidate->first); - } - } -// } + if (candidate) + { + auto already_present = false; + for (auto const& comment : sit->comments) + already_present = + already_present || comment == candidate->first; + if (!already_present) + { + sit->comments.emplace_back(candidate->first); + } } + // } + } - if (auto const location_ref_xml = record_xml.child("sit:locationReference")) { - if (location_ref_xml.attribute("xsi:type").value() == "loc:PointLocation"sv) { - if (auto const coords_xml = location_ref_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates")) { - auto const mlat = util::parse_double(coords_xml.child_value("loc:latitude")); - auto const mlon = util::parse_double(coords_xml.child_value("loc:longitude")); - if (mlat && mlon) { - // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: - // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ - // maar heeft het UML-model van DATEX II v3 het over ETRS 89: - // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm - - auto const coords_etrs89 = geo::point{*mlon, *mlat}; - auto coords_wgs84 = geo::point{}; - etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); - - if (!sit->location) { - sit->location = coords_wgs84; - } + if (auto const location_ref_xml = + record_xml.child("sit:locationReference")) + { + if (location_ref_xml.attribute("xsi:type").value() + == "loc:PointLocation"sv) + { + if (auto const coords_xml = + location_ref_xml.child("loc:pointByCoordinates") + .child("loc:pointCoordinates")) + { + auto const mlat = + util::parse_double(coords_xml.child_value("loc:latitude")); + auto const mlon = + util::parse_double(coords_xml.child_value("loc:longitude")); + if (mlat && mlon) + { + // Vaag genoeg zegt NDW dat het hier om WGS + // 84 gaat: + // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ + // maar heeft het UML-model van DATEX II v3 + // het over ETRS 89: + // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm + + auto const coords_etrs89 = geo::point{*mlon, *mlat}; + auto coords_wgs84 = geo::point{}; + etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); + + if (!sit->location) + { + sit->location = coords_wgs84; } } } @@ -281,16 +401,18 @@ namespace routemon::datex2 { } } } + } - return { + return { .publication_time = *mpublication_time, - .situations = situations, - }; - } + .situations = situations, + }; + } - [[nodiscard]] auto warnings() const -> std::multiset const& { - return warnings_; - } - }; + [[nodiscard]] auto warnings() const -> std::multiset const& + { + return warnings_; + } +}; } // namespace routemon::datex2 diff --git a/server/src/geo.cppm b/server/src/geo.cppm index 5cdbdd9..b599ad4 100644 --- a/server/src/geo.cppm +++ b/server/src/geo.cppm @@ -8,33 +8,42 @@ export namespace bgeo = boost::geometry; export namespace routemon::geo { - using point = bgeo::model::point>; - using linestring = bgeo::model::linestring; - using box = bgeo::model::box; - using stype = bgeo::srs::spheroid; - using vincenty_strategy = bgeo::strategy::distance::vincenty; - - auto split_linestring_with_overlap_segments(linestring const& ls, double max_split_distance_m, std::vector& append_to) -> void { - if (bgeo::is_empty(ls)) - return; - - auto current_ls = linestring{}; - auto current_ls_length = 0.0; - auto previous = std::optional{}; - bgeo::for_each_point(ls, [&](point p) -> void { - bgeo::append(current_ls, p); - if (previous) { - auto d = bgeo::distance(*previous, p, vincenty_strategy()); - current_ls_length += d; - if (current_ls_length > max_split_distance_m) { - append_to.push_back(std::move(current_ls)); - current_ls = linestring{*previous, p}; - current_ls_length = d; +using point = + bgeo::model::point>; +using linestring = bgeo::model::linestring; +using box = bgeo::model::box; +using stype = bgeo::srs::spheroid; +using vincenty_strategy = bgeo::strategy::distance::vincenty; + +auto split_linestring_with_overlap_segments( + linestring const& ls, double max_split_distance_m, + std::vector& append_to) -> void +{ + if (bgeo::is_empty(ls)) + return; + + auto current_ls = linestring{}; + auto current_ls_length = 0.0; + auto previous = std::optional{}; + bgeo::for_each_point( + ls, + [&](point p) -> void + { + bgeo::append(current_ls, p); + if (previous) + { + auto d = bgeo::distance(*previous, p, vincenty_strategy()); + current_ls_length += d; + if (current_ls_length > max_split_distance_m) + { + append_to.push_back(std::move(current_ls)); + current_ls = linestring{*previous, p}; + current_ls_length = d; + } } - } - previous = p; - }); - append_to.emplace_back(std::move(current_ls)); - } + previous = p; + }); + append_to.emplace_back(std::move(current_ls)); +} } // namespace routemon::geo diff --git a/server/src/gpx.cpp b/server/src/gpx.cpp index 62dccff..44a6bb2 100644 --- a/server/src/gpx.cpp +++ b/server/src/gpx.cpp @@ -13,177 +13,226 @@ using namespace std::literals::string_view_literals; namespace routemon::gpx { - namespace v10 { - - constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; - auto qname(std::string_view local) -> xml::qname_view { - return {.ns_uri = xmlns, .local = local}; - } - - auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { - auto parse_xml_double = [](std::string_view sv) -> std::optional { - return util::parse_double(sv, std::chars_format::fixed); - }; - auto mlat = std::optional{}; - auto mlon = std::optional{}; - for (auto const& [name, value] : attrs) { - if (name == qname("lat")) { - mlat = parse_xml_double(value); - } else if (name == qname("lon")) { - mlon = parse_xml_double(value); - } - } - if (!mlat || !mlon) - throw std::runtime_error{"expected valid latitude and longitude for waypoint"}; - co_await xml::ignore_contents(e); - co_return geo::point{*mlon, *mlat}; - } - - auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser { - auto s = track_segment{}; - while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo())) - bgeo::append(s.waypoints, *mwpt); - co_return std::move(s); - } - - auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser { - auto t = track{}; - t.name = co_await allow_element(e, qname("name"), xml::hohalo()); - co_await allow_element(e, qname("cmt"), xml::hohalo()); - t.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); - co_await xml::ignore_contents(e, /* until */ qname("trkseg")); - while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo())) - t.segments.push_back(std::move(*mseg)); - co_return std::move(t); - } - - auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { - auto f = file{}; - if (attrs.lookup(qname("version")) != "1.0"sv) - throw std::runtime_error{"expected GPX version to be 1.0"}; - if (auto mcreator = attrs.lookup(qname("creator"))) - f.creator = *mcreator; - else - throw std::runtime_error{"expected GPX file to have creator"}; - - f.meta.name = co_await allow_element(e, qname("name"), xml::hohalo()); - f.meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); - - co_await xml::ignore_contents(e, /* until */ qname("trk")); - while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo())) - f.tracks.push_back(std::move(*mtrk)); - co_await xml::ignore_contents(e); - - co_return std::move(f); - } - - } // namespace v10 - - namespace v11 { - - constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv; - auto qname(std::string_view local) -> xml::qname_view { - return {.ns_uri = xmlns, .local = local}; +namespace v10 { + +constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; +auto qname(std::string_view local) -> xml::qname_view +{ + return {.ns_uri = xmlns, .local = local}; +} + +auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) + -> xml::parser +{ + auto parse_xml_double = [](std::string_view sv) -> std::optional + { return util::parse_double(sv, std::chars_format::fixed); }; + auto mlat = std::optional{}; + auto mlon = std::optional{}; + for (auto const& [name, value] : attrs) + { + if (name == qname("lat")) + { + mlat = parse_xml_double(value); } - - auto parse_metadata(xml::executor_ref e, xml::attribute_view) -> xml::parser { - auto meta = metadata{}; - meta.name = co_await allow_element(e, qname("name"), xml::hohalo()); - meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); - co_await xml::ignore_contents(e); - co_return meta; - } - - auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { - auto parse_xml_double = [](std::string_view sv) -> std::optional { - return util::parse_double(sv, std::chars_format::fixed); - }; - auto mlat = std::optional{}; - auto mlon = std::optional{}; - for (auto const& [name, value] : attrs) { - if (name == qname("lat")) { - mlat = parse_xml_double(value); - } else if (name == qname("lon")) { - mlon = parse_xml_double(value); - } - } - if (!mlat || !mlon) - throw std::runtime_error{"expected valid latitude and longitude for waypoint"}; - co_await xml::ignore_contents(e); - co_return geo::point{*mlon, *mlat}; - } - - auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser { - auto s = track_segment{}; - while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo())) - bgeo::append(s.waypoints, *mwpt); - co_await allow_element(e, qname("extensions"), xml::hohalo()); - co_return std::move(s); + else if (name == qname("lon")) + { + mlon = parse_xml_double(value); } - - auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser { - auto t = track{}; - t.name = co_await allow_element(e, qname("name"), xml::hohalo()); - co_await allow_element(e, qname("cmt"), xml::hohalo()); - t.desc = co_await allow_element(e, qname("desc"), xml::hohalo()); - co_await xml::ignore_contents(e, /* until */ qname("trkseg")); - while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo())) - t.segments.push_back(std::move(*mseg)); - co_return std::move(t); + } + if (!mlat || !mlon) + throw std::runtime_error{ + "expected valid latitude and longitude for waypoint" + }; + co_await xml::ignore_contents(e); + co_return geo::point{*mlon, *mlat}; +} + +auto parse_trkseg(xml::executor_ref e, xml::attribute_view) + -> xml::parser +{ + auto s = track_segment{}; + while (auto mwpt = co_await allow_element( + e, qname("trkpt"), xml::hohalo())) + bgeo::append(s.waypoints, *mwpt); + co_return std::move(s); +} + +auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser +{ + auto t = track{}; + t.name = co_await allow_element( + e, qname("name"), xml::hohalo()); + co_await allow_element( + e, qname("cmt"), xml::hohalo()); + t.desc = co_await allow_element( + e, qname("desc"), xml::hohalo()); + co_await xml::ignore_contents(e, /* until */ qname("trkseg")); + while (auto mseg = co_await allow_element( + e, qname("trkseg"), xml::hohalo())) + t.segments.push_back(std::move(*mseg)); + co_return std::move(t); +} + +auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) + -> xml::parser +{ + auto f = file{}; + if (attrs.lookup(qname("version")) != "1.0"sv) + throw std::runtime_error{"expected GPX version to be 1.0"}; + if (auto mcreator = attrs.lookup(qname("creator"))) + f.creator = *mcreator; + else + throw std::runtime_error{"expected GPX file to have creator"}; + + f.meta.name = co_await allow_element( + e, qname("name"), xml::hohalo()); + f.meta.desc = co_await allow_element( + e, qname("desc"), xml::hohalo()); + + co_await xml::ignore_contents(e, /* until */ qname("trk")); + while (auto mtrk = + co_await allow_element(e, qname("trk"), xml::hohalo())) + f.tracks.push_back(std::move(*mtrk)); + co_await xml::ignore_contents(e); + + co_return std::move(f); +} + +} // namespace v10 + +namespace v11 { + +constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv; +auto qname(std::string_view local) -> xml::qname_view +{ + return {.ns_uri = xmlns, .local = local}; +} + +auto parse_metadata(xml::executor_ref e, xml::attribute_view) + -> xml::parser +{ + auto meta = metadata{}; + meta.name = co_await allow_element( + e, qname("name"), xml::hohalo()); + meta.desc = co_await allow_element( + e, qname("desc"), xml::hohalo()); + co_await xml::ignore_contents(e); + co_return meta; +} + +auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) + -> xml::parser +{ + auto parse_xml_double = [](std::string_view sv) -> std::optional + { return util::parse_double(sv, std::chars_format::fixed); }; + auto mlat = std::optional{}; + auto mlon = std::optional{}; + for (auto const& [name, value] : attrs) + { + if (name == qname("lat")) + { + mlat = parse_xml_double(value); } - - auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser { - auto f = file{}; - if (attrs.lookup(qname("version")) != "1.1"sv) - throw std::runtime_error{"expected GPX version to be 1.1"}; - if (auto mcreator = attrs.lookup(qname("creator"))) - f.creator = *mcreator; - else - throw std::runtime_error{"expected GPX file to have creator"}; - - if (auto mmeta = co_await allow_element(e, qname("metadata"), xml::hohalo())) - f.meta = *mmeta; - co_await xml::ignore_contents(e, /* until */ qname("trk")); - while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo())) - f.tracks.push_back(std::move(*mtrk)); - co_await allow_element(e, qname("extensions"), xml::hohalo()); - - co_return std::move(f); + else if (name == qname("lon")) + { + mlon = parse_xml_double(value); } - - } // namespace v11 - - auto parse_file(xml::executor_ref e) -> xml::parser { - auto decl = co_await expect_event(e); - if (decl.version != "1.0"sv) - throw std::runtime_error{std::format("unsupported XML version, got {}", std::string_view{decl.version})}; - if (decl.encoding != "UTF-8"sv) - throw std::runtime_error{"unsupported encoding"}; - if (auto mf = co_await allow_element(e, v10::qname("gpx"), xml::hohalo())) - co_return std::move(*mf); - if (auto mf = co_await allow_element(e, v11::qname("gpx"), xml::hohalo())) - co_return std::move(*mf); - throw std::runtime_error{"no supported GPX document found"}; - } - - reader::reader() - : p_{parse_file(util::not_null{&e_})} - { e_.set_continuation(p_.promise().base_handle()); } - - auto reader::init() -> void { - e_.start(); - } - - auto reader::put(std::string_view buf) -> void { - e_.read(buf, false); - } - - auto reader::finish() -> gpx::file { - e_.read(std::string_view{}, true); - e_.end(); - // Promise is still alive since the last coroutine performs a - // symmetric transfer to std::noop_coroutine() in final_suspend(). - return std::move(p_.promise().returned_value()); } + if (!mlat || !mlon) + throw std::runtime_error{ + "expected valid latitude and longitude for waypoint" + }; + co_await xml::ignore_contents(e); + co_return geo::point{*mlon, *mlat}; +} + +auto parse_trkseg(xml::executor_ref e, xml::attribute_view) + -> xml::parser +{ + auto s = track_segment{}; + while (auto mwpt = co_await allow_element( + e, qname("trkpt"), xml::hohalo())) + bgeo::append(s.waypoints, *mwpt); + co_await allow_element( + e, qname("extensions"), xml::hohalo()); + co_return std::move(s); +} + +auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser +{ + auto t = track{}; + t.name = co_await allow_element( + e, qname("name"), xml::hohalo()); + co_await allow_element( + e, qname("cmt"), xml::hohalo()); + t.desc = co_await allow_element( + e, qname("desc"), xml::hohalo()); + co_await xml::ignore_contents(e, /* until */ qname("trkseg")); + while (auto mseg = co_await allow_element( + e, qname("trkseg"), xml::hohalo())) + t.segments.push_back(std::move(*mseg)); + co_return std::move(t); +} + +auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) + -> xml::parser +{ + auto f = file{}; + if (attrs.lookup(qname("version")) != "1.1"sv) + throw std::runtime_error{"expected GPX version to be 1.1"}; + if (auto mcreator = attrs.lookup(qname("creator"))) + f.creator = *mcreator; + else + throw std::runtime_error{"expected GPX file to have creator"}; + + if (auto mmeta = co_await allow_element( + e, qname("metadata"), xml::hohalo())) + f.meta = *mmeta; + co_await xml::ignore_contents(e, /* until */ qname("trk")); + while (auto mtrk = + co_await allow_element(e, qname("trk"), xml::hohalo())) + f.tracks.push_back(std::move(*mtrk)); + co_await allow_element( + e, qname("extensions"), xml::hohalo()); + + co_return std::move(f); +} + +} // namespace v11 + +auto parse_file(xml::executor_ref e) -> xml::parser +{ + auto decl = co_await expect_event(e); + if (decl.version != "1.0"sv) + throw std::runtime_error{std::format( + "unsupported XML version, got {}", std::string_view{decl.version})}; + if (decl.encoding != "UTF-8"sv) + throw std::runtime_error{"unsupported encoding"}; + if (auto mf = co_await allow_element( + e, v10::qname("gpx"), xml::hohalo())) + co_return std::move(*mf); + if (auto mf = co_await allow_element( + e, v11::qname("gpx"), xml::hohalo())) + co_return std::move(*mf); + throw std::runtime_error{"no supported GPX document found"}; +} + +reader::reader() : p_{parse_file(util::not_null{&e_})} +{ + e_.set_continuation(p_.promise().base_handle()); +} + +auto reader::init() -> void { e_.start(); } + +auto reader::put(std::string_view buf) -> void { e_.read(buf, false); } + +auto reader::finish() -> gpx::file +{ + e_.read(std::string_view{}, true); + e_.end(); + // Promise is still alive since the last coroutine performs a + // symmetric transfer to std::noop_coroutine() in final_suspend(). + return std::move(p_.promise().returned_value()); +} } // namespace routemon::gpx diff --git a/server/src/gpx.cppm b/server/src/gpx.cppm index 20e6242..5872805 100644 --- a/server/src/gpx.cppm +++ b/server/src/gpx.cppm @@ -7,37 +7,42 @@ import :xml; namespace routemon::gpx { - struct metadata { - std::optional name; - std::optional desc; - }; - - struct track_segment { - geo::linestring waypoints; - }; - - struct track { - std::optional name; - std::optional desc; - std::vector segments; - }; - - struct file { - std::string creator; - metadata meta; - std::vector tracks; - }; - - class reader { - xml::executor e_; - xml::parser p_; - - public: - explicit reader(); - - auto init() -> void; - auto put(std::string_view buf) -> void; - [[nodiscard]] auto finish() -> gpx::file; - }; +struct metadata +{ + std::optional name; + std::optional desc; +}; + +struct track_segment +{ + geo::linestring waypoints; +}; + +struct track +{ + std::optional name; + std::optional desc; + std::vector segments; +}; + +struct file +{ + std::string creator; + metadata meta; + std::vector tracks; +}; + +class reader +{ + xml::executor e_; + xml::parser p_; + +public: + explicit reader(); + + auto init() -> void; + auto put(std::string_view buf) -> void; + [[nodiscard]] auto finish() -> gpx::file; +}; } // namespace routemon::gpx diff --git a/server/src/http_client.cppm b/server/src/http_client.cppm index d5316d6..bea5384 100644 --- a/server/src/http_client.cppm +++ b/server/src/http_client.cppm @@ -16,47 +16,51 @@ using tcp = net::ip::tcp; namespace routemon::http { - export class client { - net::io_context& ioc_; - ssl::context sslc_{ssl::context::tlsv12_client}; - tcp::resolver resolver_; - - public: - explicit client(net::io_context& ioc) - : ioc_{ioc}, resolver_{ioc} +export class client +{ + net::io_context& ioc_; + ssl::context sslc_{ssl::context::tlsv12_client}; + tcp::resolver resolver_; + +public: + explicit client(net::io_context& ioc) : ioc_{ioc}, resolver_{ioc} + { + sslc_.set_default_verify_paths(); + sslc_.set_verify_mode( + net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); + } + + template + auto do_request(bhttp::request& req) + -> bhttp::response + { + auto stream = ssl::stream{ioc_, sslc_}; + + auto host = std::string{req.at(bhttp::field::host)}; + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { - sslc_.set_default_verify_paths(); - sslc_.set_verify_mode(net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); + throw beast::system_error( + static_cast(::ERR_get_error()), net::error::get_ssl_category()); } + stream.set_verify_callback(ssl::host_name_verification(host)); + auto const results = resolver_.resolve(host, "443"); + beast::get_lowest_layer(stream).connect(results); + stream.handshake(ssl::stream_base::client); - template - auto do_request(bhttp::request& req) -> bhttp::response { - auto stream = ssl::stream{ioc_, sslc_}; - - auto host = std::string{req.at(bhttp::field::host)}; - if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { - throw beast::system_error(static_cast(::ERR_get_error()), - net::error::get_ssl_category()); - } - stream.set_verify_callback(ssl::host_name_verification(host)); - auto const results = resolver_.resolve(host, "443"); - beast::get_lowest_layer(stream).connect(results); - stream.handshake(ssl::stream_base::client); - - req.set(bhttp::field::user_agent, "routemon/1.0"); - bhttp::write(stream, req); - - auto buffer = beast::flat_buffer{}; - auto res = bhttp::response{}; - bhttp::read(stream, buffer, res); - - auto ec = beast::error_code{}; - stream.shutdown(ec); - if (ec != net::ssl::error::stream_truncated) - throw beast::system_error{ec}; - - return res; - } - }; + req.set(bhttp::field::user_agent, "routemon/1.0"); + bhttp::write(stream, req); + + auto buffer = beast::flat_buffer{}; + auto res = bhttp::response{}; + bhttp::read(stream, buffer, res); + + auto ec = beast::error_code{}; + stream.shutdown(ec); + if (ec != net::ssl::error::stream_truncated) + throw beast::system_error{ec}; + + return res; + } +}; } // namespace routemon::http diff --git a/server/src/http_common.cppm b/server/src/http_common.cppm index 3c38da7..606a5d5 100644 --- a/server/src/http_common.cppm +++ b/server/src/http_common.cppm @@ -10,130 +10,160 @@ export namespace bhttp = beast::http; namespace boost::beast { - namespace concepts { +namespace concepts { - template - concept buffers_generator = beast::is_buffers_generator::value; +template +concept buffers_generator = beast::is_buffers_generator::value; - template - concept const_buffer_sequence = beast::is_const_buffer_sequence::value; +template +concept const_buffer_sequence = beast::is_const_buffer_sequence::value; - } // namespace concepts +} // namespace concepts - namespace http::concepts { +namespace http::concepts { - template - concept fields = is_fields::value; +template +concept fields = is_fields::value; - template - concept body = is_body::value; +template +concept body = is_body::value; - template - concept body_reader = is_body_reader::value; +template +concept body_reader = is_body_reader::value; - } // namespace http::concepts +} // namespace http::concepts } // namespace boost::beast namespace routemon::http { - struct supported_verb { - enum supported_verb_t : std::uint8_t { - options, - delete_, - get, - head, - post, - put, - }; - - supported_verb_t value; - - supported_verb(supported_verb_t value) : value{value} {} - - static auto from(bhttp::verb v) -> std::optional { - switch (v) { - case bhttp::verb::options: return supported_verb::options; - case bhttp::verb::delete_: return supported_verb::delete_; - case bhttp::verb::get: return supported_verb::get; - case bhttp::verb::head: return supported_verb::head; - case bhttp::verb::post: return supported_verb::post; - case bhttp::verb::put: return supported_verb::put; - default: return std::nullopt; - } - } - - operator bhttp::verb() const { - switch (value) { - case supported_verb::options: return bhttp::verb::options; - case supported_verb::delete_: return bhttp::verb::delete_; - case supported_verb::get: return bhttp::verb::get; - case supported_verb::head: return bhttp::verb::head; - case supported_verb::post: return bhttp::verb::post; - case supported_verb::put: return bhttp::verb::put; - } - } +struct supported_verb +{ + enum supported_verb_t : std::uint8_t + { + options, + delete_, + get, + head, + post, + put, }; - export struct verb_set { - bool delete_ : 1 = false; - bool get : 1 = false; - bool head : 1 = false; - bool post : 1 = false; - bool put : 1 = false; - bool options : 1 = false; - - auto enable(supported_verb v) -> void { - switch (v.value) { - case supported_verb::delete_: - delete_ = true; - break; - case supported_verb::get: - get = true; - break; - case supported_verb::head: - head = true; - break; - case supported_verb::post: - post = true; - break; - case supported_verb::put: - put = true; - break; - case supported_verb::options: - options = true; - break; - default:; - } + supported_verb_t value; + + supported_verb(supported_verb_t value) : value{value} {} + + static auto from(bhttp::verb v) -> std::optional + { + switch (v) + { + case bhttp::verb::options: + return supported_verb::options; + case bhttp::verb::delete_: + return supported_verb::delete_; + case bhttp::verb::get: + return supported_verb::get; + case bhttp::verb::head: + return supported_verb::head; + case bhttp::verb::post: + return supported_verb::post; + case bhttp::verb::put: + return supported_verb::put; + default: + return std::nullopt; } - - auto operator==(verb_set const& rhs) const noexcept -> bool = default; - - auto empty() const -> bool { - return *this == verb_set{}; + } + + operator bhttp::verb() const + { + switch (value) + { + case supported_verb::options: + return bhttp::verb::options; + case supported_verb::delete_: + return bhttp::verb::delete_; + case supported_verb::get: + return bhttp::verb::get; + case supported_verb::head: + return bhttp::verb::head; + case supported_verb::post: + return bhttp::verb::post; + case supported_verb::put: + return bhttp::verb::put; } - - verb_set(std::initializer_list vs) { - for (auto const v : vs) enable(v); + } +}; + +export struct verb_set +{ + bool delete_ : 1 = false; + bool get : 1 = false; + bool head : 1 = false; + bool post : 1 = false; + bool put : 1 = false; + bool options : 1 = false; + + auto enable(supported_verb v) -> void + { + switch (v.value) + { + case supported_verb::delete_: + delete_ = true; + break; + case supported_verb::get: + get = true; + break; + case supported_verb::head: + head = true; + break; + case supported_verb::post: + post = true; + break; + case supported_verb::put: + put = true; + break; + case supported_verb::options: + options = true; + break; + default:; } - - auto to_string() const -> std::string { - std::ostringstream ss; - bool wrote = false; - auto write = [&](bhttp::verb v) { - if (wrote) - ss << ", "; - ss << v; - wrote = true; - }; - if (delete_) write(bhttp::verb::delete_); - if (get) write(bhttp::verb::get); - if (head) write(bhttp::verb::head); - if (post) write(bhttp::verb::post); - if (put) write(bhttp::verb::put); - if (options) write(bhttp::verb::options); - return ss.str(); - } - }; + } + + auto operator==(verb_set const& rhs) const noexcept -> bool = default; + + auto empty() const -> bool { return *this == verb_set{}; } + + verb_set(std::initializer_list vs) + { + for (auto const v : vs) + enable(v); + } + + auto to_string() const -> std::string + { + std::ostringstream ss; + bool wrote = false; + auto write = [&](bhttp::verb v) + { + if (wrote) + ss << ", "; + ss << v; + wrote = true; + }; + if (delete_) + write(bhttp::verb::delete_); + if (get) + write(bhttp::verb::get); + if (head) + write(bhttp::verb::head); + if (post) + write(bhttp::verb::post); + if (put) + write(bhttp::verb::put); + if (options) + write(bhttp::verb::options); + return ss.str(); + } +}; } // namespace routemon::http diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index 0770d97..0f7fda7 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -1,12 +1,12 @@ module; -#include #include #include #include #include #include #include +#include #include #include @@ -23,639 +23,836 @@ using tcp = net::ip::tcp; namespace routemon::http { - struct readable_request { - util::not_null*> p; - util::not_null strm; - util::not_null buf; +struct readable_request +{ + util::not_null*> p; + util::not_null strm; + util::not_null buf; +}; + +class presponse +{ +public: + using const_buffers_type = beast::span; + +private: + struct impl_base + { + virtual ~impl_base() = default; + virtual auto header() -> bhttp::response_header& = 0; + virtual auto header() const + -> bhttp::response_header const& = 0; + virtual auto is_done() const -> bool = 0; + virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; + virtual auto consume(std::size_t n) -> void = 0; + virtual auto keep_alive() const -> bool = 0; }; - - class presponse { - public: - using const_buffers_type = beast::span; - - private: - struct impl_base { - virtual ~impl_base() = default; - virtual auto header() -> bhttp::response_header& = 0; - virtual auto header() const -> bhttp::response_header const& = 0; - virtual auto is_done() const -> bool = 0; - virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; - virtual auto consume(std::size_t n) -> void = 0; - virtual auto keep_alive() const -> bool = 0; - }; - std::unique_ptr impl_; - - template - class impl : public impl_base { - // Initializes in the response state. - // At the first call to prepare, we switch to the message generator state. - // After that point, header may not be called anymore (it will throw). - std::variant, bhttp::message_generator> state_; - - auto ensure_message_generator() -> bhttp::message_generator& { - if (auto prsp = std::get_if>(&state_)) { - auto rsp = bhttp::response{std::move(*prsp)}; - state_.template emplace(std::move(rsp)); - } - return std::get(state_); + std::unique_ptr impl_; + + template + class impl : public impl_base + { + // Initializes in the response state. + // At the first call to prepare, we switch to the message generator + // state. After that point, header may not be called anymore (it will + // throw). + std::variant, bhttp::message_generator> state_; + + auto ensure_message_generator() -> bhttp::message_generator& + { + if (auto prsp = std::get_if>(&state_)) + { + auto rsp = bhttp::response{std::move(*prsp)}; + state_.template emplace(std::move(rsp)); } + return std::get(state_); + } - public: - explicit impl(bhttp::response&& rsp) : state_{std::move(rsp)} {} + public: + explicit impl(bhttp::response&& rsp) : state_{std::move(rsp)} {} - auto header() -> bhttp::response_header& override { - if (auto prsp = std::get_if>(&state_)) { - return prsp->base(); - } else { - // TODO: define custom exception type presponse::bad_header_access - throw std::logic_error{"header() may not be called after prepare()"}; - } + auto header() -> bhttp::response_header& override + { + if (auto prsp = std::get_if>(&state_)) + { + return prsp->base(); } - auto header() const -> bhttp::response_header const& override { - if (auto prsp = std::get_if>(&state_)) { - return prsp->base(); - } else { - throw std::logic_error{"header() may not be called after prepare()"}; - } + else + { + // TODO: define custom exception type + // presponse::bad_header_access + throw std::logic_error{"header() may not be called after prepare()"}; } - - auto is_done() const -> bool override { - if (auto pgen = std::get_if(&state_)) { - return pgen->is_done(); - } else /* still in the response state */ { - return false; - } + } + auto header() const -> bhttp::response_header const& override + { + if (auto prsp = std::get_if>(&state_)) + { + return prsp->base(); } - - auto prepare(beast::error_code& ec) -> const_buffers_type override { - return ensure_message_generator().prepare(ec); + else + { + throw std::logic_error{"header() may not be called after prepare()"}; } + } - auto consume(std::size_t n) -> void override { - ensure_message_generator().consume(n); + auto is_done() const -> bool override + { + if (auto pgen = std::get_if(&state_)) + { + return pgen->is_done(); } - - auto keep_alive() const noexcept -> bool override { - return state_.visit(util::overloaded{ - [](bhttp::response const& rsp) -> bool { - return rsp.keep_alive(); - }, - [](bhttp::message_generator const& gen) -> bool { - return gen.keep_alive(); - }, - }); + else /* still in the response state */ + { + return false; } - }; - - public: - template - explicit presponse(bhttp::response&& rsp) - : impl_{new impl{std::move(rsp)}} - {} - - auto header() -> bhttp::response_header& { - return impl_->header(); - } - auto header() const -> bhttp::response_header const& { - return impl_->header(); } - auto is_done() const -> bool { - return impl_->is_done(); - } - - auto prepare(beast::error_code& ec) -> const_buffers_type { - return impl_->prepare(ec); + auto prepare(beast::error_code& ec) -> const_buffers_type override + { + return ensure_message_generator().prepare(ec); } - auto consume(std::size_t n) -> void { - return impl_->consume(n); + auto consume(std::size_t n) -> void override + { + ensure_message_generator().consume(n); } - auto keep_alive() const noexcept -> bool { - return impl_->keep_alive(); + auto keep_alive() const noexcept -> bool override + { + return state_.visit( + util::overloaded{ + [](bhttp::response const& rsp) -> bool + { return rsp.keep_alive(); }, + [](bhttp::message_generator const& gen) -> bool + { return gen.keep_alive(); }, + }); } }; - static_assert(beast::concepts::buffers_generator); - - template - using next_handler_t = std::function net::awaitable>; - - template - using middleware_t = std::function&, next_handler_t) -> net::awaitable>; - - template - auto lax_cors_middleware(Ctx ctx, bhttp::request_header& req_hdr, next_handler_t next) -> net::awaitable { - std::ignore = req_hdr; - auto prersp = co_await next(ctx); - prersp.header().set(bhttp::field::access_control_allow_origin, "*"); - co_return std::move(prersp); - } - - template - struct trace_id_ctx : InnerCtx { - trace::id trace_id = {}; - }; - template - auto trace_id_middleware(OuterCtx ctx0, bhttp::request_header& req_hdr, next_handler_t> next) -> net::awaitable { - std::ignore = req_hdr; - auto ctx = trace_id_ctx{std::move(ctx0)}; - auto prersp = co_await next(std::move(ctx)); - prersp.header().set("X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); - prersp.header().insert(bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); - co_return std::move(prersp); +public: + template + explicit presponse(bhttp::response&& rsp) + : impl_{new impl{std::move(rsp)}} + { } - struct base_ctx { - std::locale locale; - }; - - template - using basic_route_handler_fn_t = std::function const& matches) -> net::awaitable>; - - struct keep_alive { - bool value; - - explicit keep_alive(bool value) : value{value} {} - }; - - template - auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response { - auto rsp = bhttp::response{}; // HTTP version gets set later - rsp.result(status); - rsp.keep_alive(ka.value); - return rsp; + auto header() -> bhttp::response_header& + { + return impl_->header(); } - - auto problem_rsp(base_ctx const& ctx, problem::details const& problem, keep_alive ka) -> presponse { - auto rsp = make_rsp(problem.status, ka); - rsp.set(bhttp::field::content_type, "application/problem+json"); - rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); - rsp.prepare_payload(); - return presponse{std::move(rsp)}; + auto header() const -> bhttp::response_header const& + { + return impl_->header(); } - struct preflight_response { - verb_set allow_methods; - std::vector allow_headers; - }; - auto make_preflight_rsp(preflight_response res, keep_alive ka) -> bhttp::response { - auto rsp = make_rsp(bhttp::status::no_content, ka); - auto allow_headers_str = res.allow_headers - | std::views::transform([](auto const& field) -> std::string_view { return bhttp::to_string(field); }) - | std::views::join_with(std::string_view{", "}) - | std::ranges::to(); - rsp.set(bhttp::field::access_control_allow_methods, res.allow_methods.to_string()); - rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); - rsp.prepare_payload(); - return rsp; - } + auto is_done() const -> bool { return impl_->is_done(); } - // Using base_ctx instead of a template here since that saves you - // typing on invocation (and we do not care about the context type - // anyway, but all context types should derive from base_ctx). - template - auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable, presponse>> { - std::ignore = ctx; - auto p = bhttp::request_parser{std::move(*r.p)}; - co_await bhttp::async_read(*r.strm, *r.buf, p); - co_return std::move(p.release()); + auto prepare(beast::error_code& ec) -> const_buffers_type + { + return impl_->prepare(ec); } - template<> - auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable, presponse>> { - auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); - if (ec == bhttp::error::unexpected_body) { - auto tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("No body expected for this request"), + auto consume(std::size_t n) -> void { return impl_->consume(n); } + + auto keep_alive() const noexcept -> bool { return impl_->keep_alive(); } +}; +static_assert(beast::concepts::buffers_generator); + +template +using next_handler_t = std::functionnet::awaitable>; + +template +using middleware_t = + std::function&, + next_handler_t) + ->net::awaitable>; + +template +auto lax_cors_middleware( + Ctx ctx, bhttp::request_header& req_hdr, + next_handler_t next) -> net::awaitable +{ + std::ignore = req_hdr; + auto prersp = co_await next(ctx); + prersp.header().set(bhttp::field::access_control_allow_origin, "*"); + co_return std::move(prersp); +} + +template +struct trace_id_ctx : InnerCtx +{ + trace::id trace_id = {}; +}; + +template +auto trace_id_middleware( + OuterCtx ctx0, bhttp::request_header& req_hdr, + next_handler_t> next) -> net::awaitable +{ + std::ignore = req_hdr; + auto ctx = trace_id_ctx{std::move(ctx0)}; + auto prersp = co_await next(std::move(ctx)); + prersp.header().set( + "X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); + prersp.header().insert( + bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); + co_return std::move(prersp); +} + +struct base_ctx +{ + std::locale locale; +}; + +template +using basic_route_handler_fn_t = std::function< + auto(Ctx, readable_request, std::vector const& matches) + ->net::awaitable>; + +struct keep_alive +{ + bool value; + + explicit keep_alive(bool value) : value{value} {} +}; + +template +auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response +{ + auto rsp = bhttp::response{}; // HTTP version gets set later + rsp.result(status); + rsp.keep_alive(ka.value); + return rsp; +} + +auto problem_rsp( + base_ctx const& ctx, problem::details const& problem, keep_alive ka) + -> presponse +{ + auto rsp = make_rsp(problem.status, ka); + rsp.set(bhttp::field::content_type, "application/problem+json"); + rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); + rsp.prepare_payload(); + return presponse{std::move(rsp)}; +} + +struct preflight_response +{ + verb_set allow_methods; + std::vector allow_headers; +}; +auto make_preflight_rsp(preflight_response res, keep_alive ka) + -> bhttp::response +{ + auto rsp = make_rsp(bhttp::status::no_content, ka); + auto allow_headers_str = res.allow_headers + | std::views::transform( + [](auto const& field) -> std::string_view + { return bhttp::to_string(field); }) + | std::views::join_with(std::string_view{", "}) + | std::ranges::to(); + rsp.set( + bhttp::field::access_control_allow_methods, + res.allow_methods.to_string()); + rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); + rsp.prepare_payload(); + return rsp; +} + +// Using base_ctx instead of a template here since that saves you +// typing on invocation (and we do not care about the context type +// anyway, but all context types should derive from base_ctx). +template +auto read_request(base_ctx const& ctx, readable_request&& r) + -> net::awaitable, presponse>> +{ + std::ignore = ctx; + auto p = bhttp::request_parser{std::move(*r.p)}; + co_await bhttp::async_read(*r.strm, *r.buf, p); + co_return std::move(p.release()); +} + +template <> +auto read_request(base_ctx const& ctx, readable_request&& r) + -> net::awaitable< + std::expected, presponse>> +{ + auto [ec, _] = + co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); + if (ec == bhttp::error::unexpected_body) + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("No body expected for this request"), .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body", - }; - co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})}; - } else if (ec) { - throw boost::system::system_error{ec}; - } - co_return r.p->release(); + }; + co_return std::unexpected{problem_rsp( + ctx, tpl.instantiate(), keep_alive{false})}; } + else if (ec) + { + throw boost::system::system_error{ec}; + } + co_return r.p->release(); +} - template - struct routed_ctx : InnerCtx { - verb_set route_methods; - }; +template +struct routed_ctx : InnerCtx +{ + verb_set route_methods; +}; - template +template requires requires(Ctx ctx) { // Ctx must be derived from an instantiation of routed_ctx [](routed_ctx const&) {}(ctx); } - auto default_options_handler(Ctx const& ctx, readable_request r, std::vector const&) -> net::awaitable { - auto mreq = co_await read_request(ctx, std::move(r)); - if (!mreq) - co_return std::move(mreq.error()); - - if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) { - // CORS preflight request - co_return make_preflight_rsp(preflight_response{ - // TODO: should access-control-allow-methods contain OPTIONS? - .allow_methods = ctx.route_methods, - .allow_headers = {bhttp::field::content_type}, - }, keep_alive{mreq->keep_alive()}); - } else { - // Normal OPTIONS request - auto rsp = make_rsp(bhttp::status::no_content, keep_alive{mreq->keep_alive()}); - rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); - rsp.prepare_payload(); - co_return std::move(rsp); - } +auto default_options_handler( + Ctx const& ctx, readable_request r, std::vector const&) + -> net::awaitable +{ + auto mreq = co_await read_request(ctx, std::move(r)); + if (!mreq) + co_return std::move(mreq.error()); + + if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) + { + // CORS preflight request + co_return make_preflight_rsp( + preflight_response{ + // TODO: should access-control-allow-methods contain OPTIONS? + .allow_methods = ctx.route_methods, + .allow_headers = {bhttp::field::content_type}, + }, + keep_alive{mreq->keep_alive()}); } - - auto global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable { - // TODO: switch to "small (4KB) discarded" body type, similar to what Go does? - // Same goes for default_options_handler? Not sure. - if (auto res = co_await read_request(ctx, std::move(r)); !res) - co_return std::move(res.error()); - auto req = r.p->release(); - auto rsp = make_rsp(bhttp::status::no_content, keep_alive{req.keep_alive()}); + else + { + // Normal OPTIONS request + auto rsp = make_rsp( + bhttp::status::no_content, keep_alive{mreq->keep_alive()}); + rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); rsp.prepare_payload(); co_return std::move(rsp); } +} + +auto global_options_handler(base_ctx const& ctx, readable_request r) + -> net::awaitable +{ + // TODO: switch to "small (4KB) discarded" body type, similar to what Go + // does? Same goes for default_options_handler? Not sure. + if (auto res = co_await read_request(ctx, std::move(r)); + !res) + co_return std::move(res.error()); + auto req = r.p->release(); + auto rsp = make_rsp( + bhttp::status::no_content, keep_alive{req.keep_alive()}); + rsp.prepare_payload(); + co_return std::move(rsp); +} + +template +auto id_middleware( + Ctx ctx, bhttp::request_header&, next_handler_t next) + -> net::awaitable +{ + co_return co_await next(std::move(ctx)); +} + +template +auto middleware_compose(middleware_t ab, middleware_t bc) + -> middleware_t +{ + return [ab = std::move(ab), bc = std::move(bc)]( + A a, bhttp::request_header& header, + next_handler_t next) -> net::awaitable + { + co_return co_await ab( + std::move(a), header, [&](B b) -> net::awaitable + { co_return co_await bc(std::move(b), header, next); }); + }; +} + +template +auto middleware_wrap_fn(middleware_t ab, basic_route_handler_fn_t fn) + -> basic_route_handler_fn_t +{ + return + [ab = std::move(ab), fn = std::move(fn)]( + A a_ctx, readable_request r, + std::vector const& matches) -> net::awaitable + { + co_return co_await ab( + std::move(a_ctx), r.p->get().base(), + [&](B b_ctx) -> net::awaitable + { co_return co_await fn(std::move(b_ctx), r, matches); }); + }; +} - template - auto id_middleware(Ctx ctx, bhttp::request_header&, next_handler_t next) -> net::awaitable { - co_return co_await next(std::move(ctx)); - } - - template - auto middleware_compose(middleware_t ab, middleware_t bc) -> middleware_t { - return [ab = std::move(ab), bc = std::move(bc)](A a, bhttp::request_header& header, next_handler_t next) -> net::awaitable { - co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable { - co_return co_await bc(std::move(b), header, next); - }); - }; - } - - template - auto middleware_wrap_fn(middleware_t ab, basic_route_handler_fn_t fn) -> basic_route_handler_fn_t { - return [ab = std::move(ab), fn = std::move(fn)](A a_ctx, readable_request r, std::vector const& matches) -> net::awaitable { - co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable { - co_return co_await fn(std::move(b_ctx), r, matches); - }); - }; - } - - template +template requires requires(V v) { { static_cast(v) }; } - struct handler_map { - V options = {}; - V delete_ = {}; - V get = {}; - V head = {}; - V post = {}; - V put = {}; - - template - auto lookup(this Self&& self, supported_verb v) -> auto&& { - switch (v.value) { - case supported_verb::options: return std::forward(self).options; - case supported_verb::delete_: return std::forward(self).delete_; - case supported_verb::get: return std::forward(self).get; - case supported_verb::head: return std::forward(self).head; - case supported_verb::post: return std::forward(self).post; - case supported_verb::put: return std::forward(self).put; - } +struct handler_map +{ + V options = {}; + V delete_ = {}; + V get = {}; + V head = {}; + V post = {}; + V put = {}; + + template + auto lookup(this Self&& self, supported_verb v) -> auto&& + { + switch (v.value) + { + case supported_verb::options: + return std::forward(self).options; + case supported_verb::delete_: + return std::forward(self).delete_; + case supported_verb::get: + return std::forward(self).get; + case supported_verb::head: + return std::forward(self).head; + case supported_verb::post: + return std::forward(self).post; + case supported_verb::put: + return std::forward(self).put; } + } - auto verbs() const -> verb_set { - auto set = verb_set{}; - if (static_cast(options)) - set.enable(supported_verb::options); - if (static_cast(delete_)) - set.enable(supported_verb::delete_); - if (static_cast(get)) - set.enable(supported_verb::get); - if (static_cast(head)) - set.enable(supported_verb::head); - if (static_cast(post)) - set.enable(supported_verb::post); - if (static_cast(put)) - set.enable(supported_verb::put); - return set; - } + auto verbs() const -> verb_set + { + auto set = verb_set{}; + if (static_cast(options)) + set.enable(supported_verb::options); + if (static_cast(delete_)) + set.enable(supported_verb::delete_); + if (static_cast(get)) + set.enable(supported_verb::get); + if (static_cast(head)) + set.enable(supported_verb::head); + if (static_cast(post)) + set.enable(supported_verb::post); + if (static_cast(put)) + set.enable(supported_verb::put); + return set; + } - auto empty() const -> bool { - return verbs().empty(); - } + auto empty() const -> bool { return verbs().empty(); } - template - auto map(std::invocable auto f) const -> handler_map - requires std::assignable_from> - { - return { + template + auto map(std::invocable auto f) const -> handler_map + requires std::assignable_from< + U&, std::invoke_result_t> + { + return { .options = static_cast(options) ? f(options) : U{}, .delete_ = static_cast(delete_) ? f(delete_) : U{}, - .get = static_cast(get) ? f(get) : U{}, - .head = static_cast(head) ? f(head) : U{}, - .post = static_cast(post) ? f(post) : U{}, - .put = static_cast(put) ? f(put) : U{}, - }; - } - }; - - template - struct route_tree { - using leaves = handler_map>; - using named_subtrees = std::unordered_map; - using wildcard_subtree = std::indirect; - - leaves here; - // TODO: consider making the first alternative a radix tree - // Note: the map is the first variant here; the variant will be - // default-constructed with the default-constructed first - // alternative. The empty map denotes a lack of subtrees. - std::variant sub; - }; - - template - auto middleware_wrap_tree(middleware_t mw, route_tree const& tree) -> route_tree { - auto new_leaves = tree.here.template map>(std::bind_front(middleware_wrap_fn, mw)); - auto new_sub = tree.sub.visit(util::overloaded{ - [&mw](route_tree::named_subtrees const& subtrees) -> decltype(route_tree::sub) { - auto new_subtrees = typename route_tree::named_subtrees{}; - for (auto [seg, subtree] : subtrees) - new_subtrees[seg] = middleware_wrap_tree(mw, subtree); - return new_subtrees; - }, - [&mw](route_tree::wildcard_subtree const& subtree) -> decltype(route_tree::sub) { - return typename route_tree::wildcard_subtree{middleware_wrap_tree(mw, *subtree)}; - }, + .get = static_cast(get) ? f(get) : U{}, + .head = static_cast(head) ? f(head) : U{}, + .post = static_cast(post) ? f(post) : U{}, + .put = static_cast(put) ? f(put) : U{}, + }; + } +}; + +template +struct route_tree +{ + using leaves = handler_map>; + using named_subtrees = std::unordered_map; + using wildcard_subtree = std::indirect; + + leaves here; + // TODO: consider making the first alternative a radix tree + // Note: the map is the first variant here; the variant will be + // default-constructed with the default-constructed first + // alternative. The empty map denotes a lack of subtrees. + std::variant sub; +}; + +template +auto middleware_wrap_tree( + middleware_t mw, route_tree const& tree) + -> route_tree +{ + auto new_leaves = tree.here.template map>( + std::bind_front(middleware_wrap_fn, mw)); + auto new_sub = tree.sub.visit( + util::overloaded{ + [&mw](route_tree::named_subtrees const& subtrees) + -> decltype(route_tree::sub) + { + auto new_subtrees = typename route_tree::named_subtrees{}; + for (auto [seg, subtree] : subtrees) + new_subtrees[seg] = middleware_wrap_tree(mw, subtree); + return new_subtrees; + }, + [&mw](route_tree::wildcard_subtree const& subtree) + -> decltype(route_tree::sub) + { + return typename route_tree::wildcard_subtree{ + middleware_wrap_tree(mw, *subtree) + }; + }, }); - return {.here = new_leaves, .sub = new_sub}; + return {.here = new_leaves, .sub = new_sub}; +} + +template +concept match_arg = std::constructible_from; + +template +using route_handler_fn_t = std::function< + auto(Ctx, readable_request, MatchArgs...)->net::awaitable>; + +template +auto degen_route_handler(route_handler_fn_t fn) + -> basic_route_handler_fn_t +{ + return + [fn = std::move(fn)]( + Ctx ctx, readable_request r, + std::vector const& matches) -> net::awaitable + { + if (sizeof...(MatchArgs) != matches.size()) + throw std::runtime_error{"got unexpected amount of matches"}; + auto it = matches.begin(); + co_return co_await fn( + std::move(ctx), r, + MatchArgs{static_cast(*it++)}...); + }; +} + +template +struct ctree : route_tree +{ + template + auto wrap(middleware_t mw) const + -> ctree + { + return {middleware_wrap_tree(std::move(mw), *this)}; + } +}; + +template +struct dtree : handler_map> +{ + [[nodiscard]] auto to_leaves() const -> typename route_tree::leaves + { + auto here = this->template map>( + degen_route_handler); + if (!here.verbs().empty() && !static_cast(this->options)) + here.options = default_options_handler; + return here; } - template - concept match_arg = std::constructible_from; - - template - using route_handler_fn_t = std::function net::awaitable>; - - template - auto degen_route_handler(route_handler_fn_t fn) -> basic_route_handler_fn_t { - return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector const& matches) -> net::awaitable { - if (sizeof...(MatchArgs) != matches.size()) - throw std::runtime_error{"got unexpected amount of matches"}; - auto it = matches.begin(); - co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast(*it++)}...); + [[nodiscard]] auto named_subtrees( + std::initializer_list>> + subtrees) const -> ctree + { + auto sub = typename route_tree::named_subtrees{ + std::from_range, subtrees + | std::views::transform( + [](auto const& p) + { + return std::make_pair( + p.first, + static_cast>(p.second)); + }) }; + return {route_tree{.here = to_leaves(), .sub = sub}}; } - template - struct ctree : route_tree { - template - auto wrap(middleware_t mw) const -> ctree { - return {middleware_wrap_tree(std::move(mw), *this)}; - } - }; - - template - struct dtree : handler_map> { - [[nodiscard]] auto to_leaves() const -> typename route_tree::leaves { - auto here = this->template map>(degen_route_handler); - if (!here.verbs().empty() && !static_cast(this->options)) - here.options = default_options_handler; - return here; - } + template + [[nodiscard]] auto + wildcard_subtree(ctree subtree) + -> ctree + { + return {route_tree{ + .here = to_leaves(), + .sub = typename route_tree::wildcard_subtree{ + static_cast>(subtree) + } + }}; + } - [[nodiscard]] auto named_subtrees(std::initializer_list>> subtrees) const -> ctree { - auto sub = typename route_tree::named_subtrees{ - std::from_range, - subtrees | std::views::transform([](auto const& p) { - return std::make_pair(p.first, static_cast>(p.second)); - }) - }; - return {route_tree{.here = to_leaves(), .sub = sub}}; - } + [[nodiscard]] auto no_subtrees() const -> ctree + { + return {route_tree{.here = to_leaves(), .sub = {}}}; + } +}; + +template PreRouteCtx> +class server +{ + log::logger l_; + locale::selector lsel_; + middleware_t global_middleware_; + route_tree> routes_; + +public: + explicit server( + log::logger const& l, locale::selector&& lsel, + middleware_t global_middleware, + route_tree> routes) + : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, + global_middleware_{std::move(global_middleware)}, + routes_{std::move(routes)} + { + } - template - [[nodiscard]] auto wildcard_subtree(ctree subtree) -> ctree { - return {route_tree{.here = to_leaves(), .sub = typename route_tree::wildcard_subtree{static_cast>(subtree)}}}; - } + struct match_result + { + util::not_null< + handler_map>> const*> + route_handlers; + std::vector wildcard_matches; - [[nodiscard]] auto no_subtrees() const -> ctree { - return {route_tree{.here = to_leaves(), .sub = {}}}; - } + auto allowed_methods() const -> verb_set { return route_handlers->verbs(); } }; - template PreRouteCtx> - class server { - log::logger l_; - locale::selector lsel_; - middleware_t global_middleware_; - route_tree> routes_; - - public: - explicit server(log::logger const& l, locale::selector&& lsel, middleware_t global_middleware, route_tree> routes) - : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)} - {} - - struct match_result { - util::not_null>> const*> route_handlers; - std::vector wildcard_matches; - - auto allowed_methods() const -> verb_set { - return route_handlers->verbs(); - } - }; - - auto match(boost::urls::segments_view segments) const -> std::optional { - auto const* tree = &routes_; - auto wildcard_matches = std::vector{}; - for (auto const& seg : segments) { - tree->sub.visit(util::overloaded{ - [&](route_tree>::named_subtrees const& subtrees) { - auto it = subtrees.find(seg); - tree = it == subtrees.end() ? nullptr : &it->second; - }, - [&](route_tree>::wildcard_subtree const& wildcard_subtree) { - wildcard_matches.push_back(seg); - tree = &*wildcard_subtree; - }, + auto match(boost::urls::segments_view segments) const + -> std::optional + { + auto const* tree = &routes_; + auto wildcard_matches = std::vector{}; + for (auto const& seg : segments) + { + tree->sub.visit( + util::overloaded{ + [&](route_tree>::named_subtrees const& + subtrees) + { + auto it = subtrees.find(seg); + tree = it == subtrees.end() ? nullptr : &it->second; + }, + [&](route_tree>::wildcard_subtree const& + wildcard_subtree) + { + wildcard_matches.push_back(seg); + tree = &*wildcard_subtree; + }, }); - if (!tree) return std::nullopt; - } - if (tree->here.empty()) return std::nullopt; - return match_result{ - .route_handlers = util::not_null{&tree->here}, - .wildcard_matches = wildcard_matches, - }; + if (!tree) + return std::nullopt; } + if (tree->here.empty()) + return std::nullopt; + return match_result{ + .route_handlers = util::not_null{&tree->here}, + .wildcard_matches = wildcard_matches, + }; + } - auto route_request(PreRouteCtx ctx, readable_request r) const -> net::awaitable { - auto req_base = r.p->get().base(); + auto route_request(PreRouteCtx ctx, readable_request r) const + -> net::awaitable + { + auto req_base = r.p->get().base(); - auto const bad_request_tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("Bad request"), + auto const bad_request_tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Bad request"), .type_uri = "https://routemon.fautchen.eu/problems/bad-request", - }; - - if (req_base.target() == "*") { - // request-target is in asterisk-form (RFC 9112, § 3.2.4), - // so the request must be a server-wide OPTIONS request. - - if (req_base.method() != bhttp::verb::options) { - auto tpl = problem::tpl{ - .status = bhttp::status::method_not_allowed, - .title = translate("Method not allowed"), - .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + }; - co_return co_await global_options_handler(ctx, r); - } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) { - // request-target is in origin-form (RFC 9112, § 3.2.1), - // so it must be a normal request (not a CONNECT or - // server-wide OPTIONS request). - - auto req_url = boost::urls::url{*mreq_url0}; - req_url.normalize(); - if (!req_url.is_path_absolute()) { - auto problem = bad_request_tpl.instantiate(). - set_detail(translate("Path of normalized (RFC 3986, § 6) " - "origin-form request-target (RFC " - "9112, § 3.2.1) should be " - "absolute")); - co_return problem_rsp(ctx, problem, keep_alive{false}); - } + if (req_base.target() == "*") + { + // request-target is in asterisk-form (RFC 9112, § 3.2.4), + // so the request must be a server-wide OPTIONS request. + + if (req_base.method() != bhttp::verb::options) + { + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-allowed", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } - auto mres = match(req_url.segments()); - if (!mres) { - auto tpl = problem::tpl{ - .status = bhttp::status::not_found, - .title = translate("Not found"), - .type_uri = "https://routemon.fautchen.eu/problems/not-found", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + co_return co_await global_options_handler(ctx, r); + } + else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) + { + // request-target is in origin-form (RFC 9112, § 3.2.1), + // so it must be a normal request (not a CONNECT or + // server-wide OPTIONS request). + + auto req_url = boost::urls::url{*mreq_url0}; + req_url.normalize(); + if (!req_url.is_path_absolute()) + { + auto problem = bad_request_tpl.instantiate().set_detail(translate( + "Path of normalized (RFC 3986, § 6) " + "origin-form request-target (RFC " + "9112, § 3.2.1) should be " + "absolute")); + co_return problem_rsp(ctx, problem, keep_alive{false}); + } - auto mverb = supported_verb::from(req_base.method()); - if (!mverb) { - // Method not implemented. - auto tpl = problem::tpl{ - .status = bhttp::status::not_implemented, - .title = translate("Method not implemented"), - .type_uri = "https://routemon.fautchen.eu/problems/method-not-implemented", - }; - co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - } + auto mres = match(req_url.segments()); + if (!mres) + { + auto tpl = problem::tpl{ + .status = bhttp::status::not_found, + .title = translate("Not found"), + .type_uri = "https://routemon.fautchen.eu/problems/not-found", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } - if (auto mhdl = mres->route_handlers->lookup(*mverb)) { - auto new_ctx = routed_ctx{std::move(ctx), mres->allowed_methods()}; - co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); - } else { - // Path recognized, but method not allowed. - auto tpl = problem::tpl{ - .status = bhttp::status::method_not_allowed, - .title = translate("Method not allowed"), - .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", - }; - auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); - rsp.header().set(bhttp::field::allow, mres->allowed_methods().to_string()); - co_return std::move(rsp); - } - } else { - // We do not accept any other request-target forms. + auto mverb = supported_verb::from(req_base.method()); + if (!mverb) + { + // Method not implemented. + auto tpl = problem::tpl{ + .status = bhttp::status::not_implemented, + .title = translate("Method not implemented"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-implemented", + }; + co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + } - auto problem = bad_request_tpl.instantiate(). - set_detail(translate("Invalid request-target, expected " - "asterisk-form or origin-form " - "(see RFC 9112, § 3.2)")); - co_return problem_rsp(ctx, problem, keep_alive{false}); + if (auto mhdl = mres->route_handlers->lookup(*mverb)) + { + auto new_ctx = + routed_ctx{std::move(ctx), mres->allowed_methods()}; + co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); + } + else + { + // Path recognized, but method not allowed. + auto tpl = problem::tpl{ + .status = bhttp::status::method_not_allowed, + .title = translate("Method not allowed"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "method-not-allowed", + }; + auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); + rsp.header().set( + bhttp::field::allow, mres->allowed_methods().to_string()); + co_return std::move(rsp); } } + else + { + // We do not accept any other request-target forms. - auto handle_request(readable_request r) const -> net::awaitable { - auto header = r.p->get().base(); - auto locale = lsel_.select(header[bhttp::field::accept_language]); - auto ctx0 = base_ctx{.locale = locale}; - - co_return co_await global_middleware_(std::move(ctx0), header, [&](PreRouteCtx ctx) -> net::awaitable { - co_return co_await route_request(std::move(ctx), std::move(r)); - }); + auto problem = bad_request_tpl.instantiate().set_detail(translate( + "Invalid request-target, expected " + "asterisk-form or origin-form " + "(see RFC 9112, § 3.2)")); + co_return problem_rsp(ctx, problem, keep_alive{false}); } + } - auto do_session(beast::tcp_stream strm) -> net::awaitable { - auto buf = beast::flat_buffer{}; - - while (true) { - auto p0 = bhttp::request_parser{}; - p0.body_limit(boost::none); - auto [ec, _] = co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); - if (ec == bhttp::error::end_of_stream) { - break; - } else if (ec) { - throw boost::system::system_error{ec}; - } + auto handle_request(readable_request r) const -> net::awaitable + { + auto header = r.p->get().base(); + auto locale = lsel_.select(header[bhttp::field::accept_language]); + auto ctx0 = base_ctx{.locale = locale}; - auto http_version = p0.get().version(); - auto&& rsp = co_await handle_request(readable_request{ - .p = util::not_null{&p0}, - .strm = util::not_null{&strm}, - .buf = util::not_null{&buf}, - }); - rsp.header().version(http_version); - bool keep_alive = rsp.keep_alive(); - co_await beast::async_write(strm, std::move(rsp)); - if (!keep_alive) { - break; - } - } + co_return co_await global_middleware_( + std::move(ctx0), header, + [&](PreRouteCtx ctx) -> net::awaitable + { co_return co_await route_request(std::move(ctx), std::move(r)); }); + } - strm.socket().shutdown(tcp::socket::shutdown_send); - } + auto do_session(beast::tcp_stream strm) -> net::awaitable + { + auto buf = beast::flat_buffer{}; - auto do_listen(tcp::endpoint endpoint) -> net::awaitable { - auto executor = co_await net::this_coro::executor; - auto acceptor = tcp::acceptor{executor, endpoint}; - - l_.with("endpoint", endpoint.address().to_string()). - with("port", std::to_string(endpoint.port())). - info("Serving"); - while (true) { - net::co_spawn(executor, - do_session(beast::tcp_stream{co_await acceptor.async_accept()}), - [this](std::exception_ptr e) { - if (e) { - try { - std::rethrow_exception(e); - } catch (std::exception const& e) { - l_.error("Error in session: {}", e.what()); - } - } - }); + while (true) + { + auto p0 = bhttp::request_parser{}; + p0.body_limit(boost::none); + auto [ec, _] = + co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); + if (ec == bhttp::error::end_of_stream) + break; + else if (ec) + throw boost::system::system_error{ec}; + + auto http_version = p0.get().version(); + auto&& rsp = co_await handle_request( + readable_request{ + .p = util::not_null{&p0}, + .strm = util::not_null{&strm}, + .buf = util::not_null{&buf}, + }); + rsp.header().version(http_version); + bool keep_alive = rsp.keep_alive(); + co_await beast::async_write(strm, std::move(rsp)); + if (!keep_alive) + { + break; } } - auto spawn(net::io_context& ioc) -> void { - auto const addr = net::ip::make_address("0.0.0.0"); - auto const endpoint = tcp::endpoint{addr, 8284}; - - // TODO: make exception handling as nice as in srv.cpp - net::co_spawn(ioc, - do_listen(endpoint), - [this](std::exception_ptr e) { - if (e) { - try { - std::rethrow_exception(e); - } catch (std::exception const& e) { - l_.error("Error: {}", e.what()); - } - } - }); + strm.socket().shutdown(tcp::socket::shutdown_send); + } + + auto do_listen(tcp::endpoint endpoint) -> net::awaitable + { + auto executor = co_await net::this_coro::executor; + auto acceptor = tcp::acceptor{executor, endpoint}; + + l_.with("endpoint", endpoint.address().to_string()) + .with("port", std::to_string(endpoint.port())) + .info("Serving"); + while (true) + { + net::co_spawn( + executor, + do_session(beast::tcp_stream{co_await acceptor.async_accept()}), + [this](std::exception_ptr e) + { + if (e) + { + try + { + std::rethrow_exception(e); + } + catch (std::exception const& e) + { + l_.error("Error in session: {}", e.what()); + } + } + }); } - }; + } + + auto spawn(net::io_context& ioc) -> void + { + auto const addr = net::ip::make_address("0.0.0.0"); + auto const endpoint = tcp::endpoint{addr, 8284}; + + // TODO: make exception handling as nice as in srv.cpp + net::co_spawn( + ioc, do_listen(endpoint), + [this](std::exception_ptr e) + { + if (e) + { + try + { + std::rethrow_exception(e); + } + catch (std::exception const& e) + { + l_.error("Error: {}", e.what()); + } + } + }); + } +}; } // namespace routemon::http diff --git a/server/src/locale.cppm b/server/src/locale.cppm index 65ae2ea..8fd867b 100644 --- a/server/src/locale.cppm +++ b/server/src/locale.cppm @@ -11,235 +11,285 @@ import :util; export namespace blocale = boost::locale; export namespace routemon { - using lformat = blocale::format; - using blocale::translate; - using blocale::gettext; +using lformat = blocale::format; +using blocale::gettext; +using blocale::translate; } // namespace routemon namespace routemon::locale { - struct locale_priority { - float weight; - std::size_t original_index; - }; +struct locale_priority +{ + float weight; + std::size_t original_index; +}; - auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool { - if (lhs.weight != rhs.weight) - return lhs.weight > rhs.weight; - return lhs.original_index < rhs.original_index; - } +auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool +{ + if (lhs.weight != rhs.weight) + return lhs.weight > rhs.weight; + return lhs.original_index < rhs.original_index; +} - struct icu_locale_hash { - std::size_t operator()(icu::Locale const& l) const noexcept { - static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); - std::int32_t hash = l.hashCode(); - if (hash < 0) { - return static_cast(std::numeric_limits::max()) + static_cast(-hash) + 1; - } else { - return static_cast(hash); - } +struct icu_locale_hash +{ + std::size_t operator()(icu::Locale const& l) const noexcept + { + static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); + std::int32_t hash = l.hashCode(); + if (hash < 0) + { + return static_cast(std::numeric_limits::max()) + + static_cast(-hash) + 1; + } + else + { + return static_cast(hash); } - }; + } +}; + +using icu_locale_priority_map = + std::unordered_map; +using icu_priority_locale = std::pair; + +auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) + -> bool +{ + return lhs.second < rhs.second; +} - using icu_locale_priority_map = std::unordered_map; - using icu_priority_locale = std::pair; +class icu_priority_locale_vec_iterator : public icu::Locale::Iterator +{ + std::size_t i_ = 0uz; + std::vector ls_; - auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) -> bool { - return lhs.second < rhs.second; +public: + explicit icu_priority_locale_vec_iterator( + std::vector&& ls) + : ls_(std::move(ls)) + { } - class icu_priority_locale_vec_iterator : public icu::Locale::Iterator { - std::size_t i_ = 0uz; - std::vector ls_; + auto hasNext() const -> UBool override { return i_ < ls_.size(); } - public: - explicit icu_priority_locale_vec_iterator(std::vector&& ls) - : ls_(std::move(ls)) - {} + auto next() -> icu::Locale const& override { return ls_[i_++].first; } - auto hasNext() const -> UBool override { - return i_ < ls_.size(); - } + ~icu_priority_locale_vec_iterator() override = default; +}; - auto next() -> icu::Locale const& override { - return ls_[i_++].first; - } +export template +concept locale_input_range = + std::ranges::input_range + && std::same_as< + std::locale const&, std::ranges::range_const_reference_t>; - ~icu_priority_locale_vec_iterator() override = default; - }; - - export template - concept locale_input_range = - std::ranges::input_range && - std::same_as>; - - // Helps select a locale based on the Accept-Language header in an - // HTTP request. - export class selector { - std::locale default_; - icu::LocaleMatcher matcher_; - std::shared_ptr lgen_; - - auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) { - auto builder = icu::LocaleMatcher::Builder{}; - for (auto const& supported_locale : supported_locales) { - auto const& supported_locale_info = std::use_facet(supported_locale); - auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()}; - if (supported_icu_locale.isBogus()) - throw std::runtime_error{"supported locale gives rise to bogus ICU locale"}; - builder.addSupportedLocale(supported_icu_locale); - } - auto const& default_locale_info = std::use_facet(default_locale); - auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; - if (default_icu_locale.isBogus()) - throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; - builder.setDefaultLocale(&default_icu_locale); - auto ec = UErrorCode::U_ZERO_ERROR; - auto matcher = builder.build(ec); - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to build icu::LocaleMatcher"}; - return matcher; +// Helps select a locale based on the Accept-Language header in an +// HTTP request. +export class selector +{ + std::locale default_; + icu::LocaleMatcher matcher_; + std::shared_ptr lgen_; + + auto make_matcher( + locale_input_range auto supported_locales, std::locale default_locale) + { + auto builder = icu::LocaleMatcher::Builder{}; + for (auto const& supported_locale : supported_locales) + { + auto const& supported_locale_info = + std::use_facet(supported_locale); + auto supported_icu_locale = + icu::Locale{supported_locale_info.name().c_str()}; + if (supported_icu_locale.isBogus()) + throw std::runtime_error{ + "supported locale gives rise to bogus ICU locale" + }; + builder.addSupportedLocale(supported_icu_locale); } + auto const& default_locale_info = + std::use_facet(default_locale); + auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; + if (default_icu_locale.isBogus()) + throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; + builder.setDefaultLocale(&default_icu_locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto matcher = builder.build(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to build icu::LocaleMatcher"}; + return matcher; + } - // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. - static auto ltrim_ows(std::string_view s) -> std::string_view { - if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) - s.remove_prefix(i); - return s; + // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. + static auto ltrim_ows(std::string_view s) -> std::string_view + { + if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) + s.remove_prefix(i); + return s; + } + static auto rtrim_ows(std::string_view s) -> std::string_view + { + if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) + return s.substr(0, i + 1); + return s; + } + static auto trim_ows(std::string_view s) -> std::string_view + { + return rtrim_ows(ltrim_ows(s)); + } + + auto from_icu_locale(icu::Locale const& l) const -> std::locale + { + auto posix_name = std::string{l.getLanguage()}; + if (l.getScript() && std::strlen(l.getScript()) > 0) + { + posix_name += "_"; + posix_name += l.getScript(); } - static auto rtrim_ows(std::string_view s) -> std::string_view { - if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) - return s.substr(0, i + 1); - return s; + if (l.getCountry() && std::strlen(l.getCountry()) > 0) + { + posix_name += "_"; + posix_name += l.getCountry(); } - static auto trim_ows(std::string_view s) -> std::string_view { - return rtrim_ows(ltrim_ows(s)); + posix_name += ".UTF-8"; + auto added_at = false; + if (l.getVariant() && std::strlen(l.getVariant()) > 0) + { + added_at = true; + posix_name += "@"; + posix_name += l.getVariant(); } - - auto from_icu_locale(icu::Locale const& l) const -> std::locale { - auto posix_name = std::string{l.getLanguage()}; - if (l.getScript() && std::strlen(l.getScript()) > 0) { - posix_name += "_"; - posix_name += l.getScript(); - } - if (l.getCountry() && std::strlen(l.getCountry()) > 0) { - posix_name += "_"; - posix_name += l.getCountry(); - } - posix_name += ".UTF-8"; - auto added_at = false; - if (l.getVariant() && std::strlen(l.getVariant()) > 0) { - added_at = true; - posix_name += "@"; - posix_name += l.getVariant(); - } - auto ec = UErrorCode::U_ZERO_ERROR; - auto* keywords = l.createKeywords(ec); - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to create keywords"}; - if (keywords) { - std::int32_t kw_len = 0; - char const* kw = nullptr; - while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) { - auto value = l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); - if (!added_at) { - posix_name += "@"; - added_at = true; - } else { - posix_name += ";"; - } - posix_name += kw; - posix_name += "="; - posix_name += value; + auto ec = UErrorCode::U_ZERO_ERROR; + auto* keywords = l.createKeywords(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to create keywords"}; + if (keywords) + { + std::int32_t kw_len = 0; + char const* kw = nullptr; + while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) + { + auto value = + l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); + if (!added_at) + { + posix_name += "@"; + added_at = true; + } + else + { + posix_name += ";"; } - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to iterate over keywords"}; - delete keywords; + posix_name += kw; + posix_name += "="; + posix_name += value; } - return lgen_->generate(posix_name); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to iterate over keywords"}; + delete keywords; } + return lgen_->generate(posix_name); + } + +public: + // Note: lgen must live at least as long as the selector constructed here! + // It is unfortunately not possible to copy/move a blocale::generator. + explicit selector( + locale_input_range auto locales, std::locale default_, + std::shared_ptr lgen) + : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} + { + } + + auto select(std::string_view accept_language) const -> std::locale + { + using namespace std::literals::string_view_literals; + // NOTE: can also contain a *;q=0.1 + // q should have at most 3 digits after period + auto dlpm = icu_locale_priority_map{}; + for (auto const [i, lang_prio] : + accept_language | std::views::split(","sv) | std::views::enumerate) + { + auto [lang_range_ut, mweight_ut] = + util::split_on(std::string_view{lang_prio}, ';'); + auto lang_range_str = trim_ows(lang_range_ut); + auto mweight_str = mweight_ut.transform(trim_ows); + if (lang_range_str == "*") + break; + + auto ec = UErrorCode::U_ZERO_ERROR; + auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); + if (U_FAILURE(ec) || icu_locale.isBogus()) + continue; // ignore this locale - public: - // Note: lgen must live at least as long as the selector constructed here! - // It is unfortunately not possible to copy/move a blocale::generator. - explicit selector(locale_input_range auto locales, std::locale default_, std::shared_ptr lgen) - : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} - {} - - auto select(std::string_view accept_language) const -> std::locale { - using namespace std::literals::string_view_literals; - // NOTE: can also contain a *;q=0.1 - // q should have at most 3 digits after period - auto dlpm = icu_locale_priority_map{}; - for (auto const [i, lang_prio] : accept_language | std::views::split(","sv) | std::views::enumerate) { - auto [lang_range_ut, mweight_ut] = util::split_on(std::string_view{lang_prio}, ';'); - auto lang_range_str = trim_ows(lang_range_ut); - auto mweight_str = mweight_ut.transform(trim_ows); - if (lang_range_str == "*") - break; - - auto ec = UErrorCode::U_ZERO_ERROR; - auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); - if (U_FAILURE(ec) || icu_locale.isBogus()) - continue; // ignore this locale - - auto weight = 1.0f; - if (mweight_str && mweight_str->starts_with("q=")) { - auto weight_str = mweight_str->substr(2, 4); - if (auto mweight = util::parse_float(weight_str, std::chars_format::fixed); - mweight && 0.0f < *mweight && *mweight < 1.0f) { - weight = *mweight; - } + auto weight = 1.0f; + if (mweight_str && mweight_str->starts_with("q=")) + { + auto weight_str = mweight_str->substr(2, 4); + if (auto mweight = + util::parse_float(weight_str, std::chars_format::fixed); + mweight && 0.0f < *mweight && *mweight < 1.0f) + { + weight = *mweight; } + } - if (weight > 0.0f) { - dlpm[icu_locale] = { + if (weight > 0.0f) + { + dlpm[icu_locale] = { .weight = weight, .original_index = static_cast(i), - }; - } else { - dlpm.erase(icu_locale); - } + }; + } + else + { + dlpm.erase(icu_locale); } - - auto desired_locales = std::vector{dlpm.begin(), dlpm.end()}; - std::sort(desired_locales.begin(), desired_locales.end()); - auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; - auto ec = UErrorCode::U_ZERO_ERROR; - auto res = matcher_.getBestMatchResult(it, ec); - if (U_FAILURE(ec)) - return default_; - auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? - if (U_FAILURE(ec)) - return from_icu_locale(*res.getSupportedLocale()); - return from_icu_locale(resolved); } - }; - export auto to_bcp47_lang_tag(std::locale locale) -> std::optional { - auto const& locale_info = std::use_facet(locale); + auto desired_locales = + std::vector{dlpm.begin(), dlpm.end()}; + std::sort(desired_locales.begin(), desired_locales.end()); + auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; auto ec = UErrorCode::U_ZERO_ERROR; - auto bcp47_lang_tag = icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); + auto res = matcher_.getBestMatchResult(it, ec); + if (U_FAILURE(ec)) + return default_; + auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? if (U_FAILURE(ec)) - return std::nullopt; - return bcp47_lang_tag; + return from_icu_locale(*res.getSupportedLocale()); + return from_icu_locale(resolved); } +}; + +export auto to_bcp47_lang_tag(std::locale locale) -> std::optional +{ + auto const& locale_info = std::use_facet(locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto bcp47_lang_tag = + icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); + if (U_FAILURE(ec)) + return std::nullopt; + return bcp47_lang_tag; +} #ifdef LOCALEDIR -# define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) -# define LOCALEDIR_AUX_STR(s) #s - constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; -# undef LOCALEDIR_AUX_STR -# undef LOCALEDIR_AUX_XSTR -#else // ifdef LOCALEDIR - constexpr auto messages_path = std::string_view{"locale/dev"}; +#define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) +#define LOCALEDIR_AUX_STR(s) #s +constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; +#undef LOCALEDIR_AUX_STR +#undef LOCALEDIR_AUX_XSTR +#else // ifdef LOCALEDIR +constexpr auto messages_path = std::string_view{"locale/dev"}; #endif // ifdef LOCALEDIR - export auto make_generator() -> std::shared_ptr { - auto lgen = std::make_shared(); - lgen->add_messages_path(std::string{messages_path}); - lgen->add_messages_domain("routemon"); - return std::static_pointer_cast(lgen); - } +export auto make_generator() -> std::shared_ptr +{ + auto lgen = std::make_shared(); + lgen->add_messages_path(std::string{messages_path}); + lgen->add_messages_domain("routemon"); + return std::static_pointer_cast(lgen); +} } // namespace routemon::locale diff --git a/server/src/log.cppm b/server/src/log.cppm index d7e2bff..0bfdf7d 100644 --- a/server/src/log.cppm +++ b/server/src/log.cppm @@ -12,137 +12,160 @@ import std; namespace routemon::log { - export enum class level : std::uint8_t { - debug, - info, - warn, - error, +export enum class level : std::uint8_t { + debug, + info, + warn, + error, +}; + +namespace { + +auto operator<<(std::ostream& os, level lvl) -> std::ostream& +{ + switch (lvl) + { + case level::debug: + os << "dbg"; + break; + case level::info: + os << "inf"; + break; + case level::warn: + os << "wrn"; + break; + case level::error: + os << "err"; + break; + } + return os; +} + +} // namespace + +export class sink +{ + std::atomic lvl_; + std::ostream& os_ = std::cout; + + struct tmp_message + { + level lvl; + std::string_view component; + std::string_view txt; + std::map const& attrs; }; - namespace { - - auto operator<<(std::ostream& os, level lvl) -> std::ostream& { - switch (lvl) { - case level::debug: os << "dbg"; break; - case level::info: os << "inf"; break; - case level::warn: os << "wrn"; break; - case level::error: os << "err"; break; - } - return os; - } - - } // namespace (unique) - - export class sink { - std::atomic lvl_; - std::ostream& os_ = std::cout; - - struct tmp_message { - level lvl; - std::string_view component; - std::string_view txt; - std::map const& attrs; - }; - - auto write(tmp_message msg) -> void { - auto sos = std::osyncstream{os_}; - sos << "[" << msg.lvl; - if (!msg.component.empty()) - sos << " " << msg.component; - sos << "] " << msg.txt; - for (auto const& [k, v] : msg.attrs) { - sos << " " << k << "=" << std::quoted(v); - } - sos << '\n'; - } - - explicit sink(level lvl) - : lvl_{lvl} - {} - - friend auto make_sink(level lvl) -> std::shared_ptr; - friend class logger; - - public: - [[nodiscard]] auto level() const -> enum level { - return lvl_; - } - - auto set_level(enum level lvl) -> void { - lvl_ = lvl; + auto write(tmp_message msg) -> void + { + auto sos = std::osyncstream{os_}; + sos << "[" << msg.lvl; + if (!msg.component.empty()) + sos << " " << msg.component; + sos << "] " << msg.txt; + for (auto const& [k, v] : msg.attrs) + { + sos << " " << k << "=" << std::quoted(v); } - }; - - export auto make_sink(level lvl) -> std::shared_ptr { - return std::shared_ptr{new sink{lvl}}; + sos << '\n'; } - export class logger { - std::shared_ptr sink_; - std::string component_; - std::map attrs_; - - template - auto log_at(std::string_view fmt, std::format_args args) -> logger& { - if (sink_->level() <= lvl) - sink_->write(sink::tmp_message{ - .lvl = lvl, - .component = component_, - .txt = std::vformat(fmt, args), - .attrs = attrs_, + explicit sink(level lvl) : lvl_{lvl} {} + + friend auto make_sink(level lvl) -> std::shared_ptr; + friend class logger; + +public: + [[nodiscard]] auto level() const -> enum level { return lvl_; } + + auto set_level(enum level lvl) -> void { lvl_ = lvl; } +}; + +export auto make_sink(level lvl) -> std::shared_ptr +{ + return std::shared_ptr{new sink{lvl}}; +} + +export class logger +{ + std::shared_ptr sink_; + std::string component_; + std::map attrs_; + + template + auto log_at(std::string_view fmt, std::format_args args) -> logger& + { + if (sink_->level() <= lvl) + sink_->write( + sink::tmp_message{ + .lvl = lvl, + .component = component_, + .txt = std::vformat(fmt, args), + .attrs = attrs_, }); - return *this; - } + return *this; + } - public: - explicit logger(std::shared_ptr const& sink) - : sink_{sink} +public: + explicit logger(std::shared_ptr const& sink) : sink_{sink} + { + if (!sink) { - if (!sink) { - throw std::invalid_argument{"logger sink may not be null"}; - } + throw std::invalid_argument{"logger sink may not be null"}; } + } - [[nodiscard]] auto sub(std::string_view component) const -> logger { - auto l = *this; - if (l.component_.empty()) { - l.component_ = component; - } else { - l.component_ += "."; - l.component_ += component; - } - return l; + [[nodiscard]] auto sub(std::string_view component) const -> logger + { + auto l = *this; + if (l.component_.empty()) + { + l.component_ = component; } - - [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger { - auto l = *this; - l.attrs_[k] = std::move(v); - return l; + else + { + l.component_ += "."; + l.component_ += component; } + return l; + } - [[nodiscard]] auto with(std::string const& k, std::string_view v) const -> logger { - return with(k, std::string{v}); - } + [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger + { + auto l = *this; + l.attrs_[k] = std::move(v); + return l; + } - template - auto debug(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); - } + [[nodiscard]] auto with(std::string const& k, std::string_view v) const + -> logger + { + return with(k, std::string{v}); + } - template - auto info(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); - } + template + auto debug(std::format_string fmt, Args&&... args) -> logger& + { + return log_at(fmt.get(), std::make_format_args(args...)); + } - template - auto warn(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); - } + template + auto info(std::format_string fmt, Args&&... args) -> logger& + { + return log_at(fmt.get(), std::make_format_args(args...)); + } - template - auto error(std::format_string fmt, Args&&... args) -> logger& { - return log_at(fmt.get(), std::make_format_args(args...)); - } - }; + template + auto warn(std::format_string fmt, Args&&... args) -> logger& + { + return log_at(fmt.get(), std::make_format_args(args...)); + } + + template + auto error(std::format_string fmt, Args&&... args) -> logger& + { + return log_at(fmt.get(), std::make_format_args(args...)); + } +}; } // namespace routemon::log diff --git a/server/src/main.cpp b/server/src/main.cpp index a40b0b0..0a7a784 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -1,5 +1,5 @@ -#include // for malloc_trim(3) #include +#include // for malloc_trim(3) import std; import routemon; @@ -7,17 +7,23 @@ import routemon; namespace chrono = std::chrono; namespace net = boost::asio; -enum class exit_status { +enum class exit_status +{ failure, bad_usage, }; -auto real_main(std::span args) -> exit_status { +auto real_main(std::span args) -> exit_status +{ auto sink = routemon::log::make_sink(routemon::log::level::info); auto l = routemon::log::logger{sink}; - if (args.size() != 2) { - l.error("Fatal: expected exactly one argument (the configuration file location), got {}", args.size() - 1); + if (args.size() != 2) + { + l.error( + "Fatal: expected exactly one argument (the configuration file " + "location), got {}", + args.size() - 1); return exit_status::bad_usage; } auto const* config_filename = args[1]; @@ -25,54 +31,73 @@ auto real_main(std::span args) -> exit_status { auto lgen = routemon::locale::make_generator(); auto default_locale = lgen->generate("en_US.UTF-8"); auto locales = { - default_locale, - lgen->generate("nl_NL.UTF-8"), - lgen->generate("de_DE.UTF-8"), - lgen->generate("en_GB.UTF-8"), + default_locale, + lgen->generate("nl_NL.UTF-8"), + lgen->generate("de_DE.UTF-8"), + lgen->generate("en_GB.UTF-8"), }; auto lsel = routemon::locale::selector{locales, default_locale, lgen}; auto ioc = net::io_context{1 /* concurrency hint */}; auto config = routemon::config::app{}; - try { + try + { config = routemon::config::load_file(config_filename); - } catch (std::exception const& e) { - l.with("filename", std::string_view{config_filename}). - error("Failed to load configuration: {}", e.what()); + } + catch (std::exception const& e) + { + l.with("filename", std::string_view{config_filename}) + .error("Failed to load configuration: {}", e.what()); return exit_status::failure; } sink->set_level(config.logger.level); - // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, config.rwgps.auth_token}; - // for (auto route : rwgps_client.get_all_routes()) { - // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, route.name, route.url); + // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, + // config.rwgps.auth_token}; for (auto route : + // rwgps_client.get_all_routes()) + // { + // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, + // route.name, route.url); // } auto dbc = std::shared_ptr{}; - try { + try + { dbc = routemon::database::open(config.database.sqlite3_filename); - } catch (std::exception const& e) { - l.with("filename", config.database.sqlite3_filename). - error("Failed to open database: {}", e.what()); + } + catch (std::exception const& e) + { + l.with("filename", config.database.sqlite3_filename) + .error("Failed to open database: {}", e.what()); return exit_status::failure; } - l.with("filename", config.situations.datex2_filename). - info("Loading situations"); + l.with("filename", config.situations.datex2_filename) + .info("Loading situations"); auto const before_load = chrono::steady_clock::now(); auto d2loader = routemon::datex2::loader{}; auto pub = routemon::datex2::situation_publication{}; - try { - pub = d2loader.load_situation_publication(config.situations.datex2_filename); - } catch (std::exception const& e) { + try + { + pub = + d2loader.load_situation_publication(config.situations.datex2_filename); + } + catch (std::exception const& e) + { l.error("Failed to load DATEX II situations publication: {}", e.what()); return exit_status::failure; } - if (!d2loader.warnings().empty()) { + if (!d2loader.warnings().empty()) + { auto const& warns = d2loader.warnings(); - l.warn("Encountered {} unique warnings while loading DATEX II situations publication", warns.size()); + l.warn( + "Encountered {} unique warnings while loading DATEX II situations " + "publication", + warns.size()); auto i = 0uz; - for (auto it = warns.begin(); it != warns.end(); it = warns.upper_bound(*it)) { + for (auto it = warns.begin(); it != warns.end(); + it = warns.upper_bound(*it)) + { l.warn("Warning {} (appeared {}×): {}", ++i, warns.count(*it), *it); } } @@ -82,11 +107,13 @@ auto real_main(std::span args) -> exit_status { // to return as much memory as possible to the OS. malloc_trim(0); auto const after_load = chrono::steady_clock::now(); - auto const dur_load = chrono::duration_cast(after_load - before_load); + auto const dur_load = + chrono::duration_cast(after_load - before_load); l.info("Loading situations finished in {}", dur_load); auto handler = routemon::api::handler{l, std::move(pub)}; - auto http_server = routemon::srv::server{l, std::move(lsel), std::move(handler)}; + auto http_server = + routemon::srv::server{l, std::move(lsel), std::move(handler)}; http_server.spawn(ioc); ioc.run(); @@ -94,13 +121,19 @@ auto real_main(std::span args) -> exit_status { return exit_status::failure; } -auto main(int argc, char* argv[]) -> int { - if (argc < 0) { +auto main(int argc, char* argv[]) -> int +{ + if (argc < 0) + { std::cout << "Fatal: argument count below zero" << std::endl; return EXIT_FAILURE; } - auto res = real_main(std::span{const_cast(argv), static_cast(argc)}); - switch (res) { + auto res = real_main( + std::span{ + const_cast(argv), static_cast(argc) + }); + switch (res) + { case exit_status::failure: return EXIT_FAILURE; case exit_status::bad_usage: diff --git a/server/src/problem.cppm b/server/src/problem.cppm index 8764962..a124312 100644 --- a/server/src/problem.cppm +++ b/server/src/problem.cppm @@ -14,47 +14,58 @@ namespace json = boost::json; namespace routemon::problem { - export struct details { - http::status status; - blocale::message title; - std::string_view type_uri; - std::optional detail = std::nullopt; - std::optional instance = std::nullopt; - - auto set_detail(blocale::message detail) -> details& { - this->detail = detail; - return *this; - } - - auto set_instance(std::string&& instance) -> details& { - this->instance = instance; - return *this; - } - auto set_instance(std::string_view instance) -> details& { - this->instance = std::string{instance}; - return *this; - } - }; +export struct details +{ + http::status status; + blocale::message title; + std::string_view type_uri; + std::optional detail = std::nullopt; + std::optional instance = std::nullopt; - export auto tag_invoke(json::value_from_tag, json::value& jv, details const& details, std::locale locale) -> void { - auto obj = json::object{ - {"type", details.type_uri}, - {"title", details.title.str(locale)}, - {"status", static_cast(details.status)}, - }; - if (details.detail) obj["detail"] = details.detail->str(locale); - if (details.instance) obj["instance"] = *details.instance; - jv = obj; + auto set_detail(blocale::message detail) -> details& + { + this->detail = detail; + return *this; } - export struct tpl { - http::status status; - blocale::message title; - std::string_view type_uri; + auto set_instance(std::string&& instance) -> details& + { + this->instance = instance; + return *this; + } + auto set_instance(std::string_view instance) -> details& + { + this->instance = std::string{instance}; + return *this; + } +}; - auto instantiate() const -> details { - return details{status, title, type_uri}; - } +export auto tag_invoke( + json::value_from_tag, json::value& jv, details const& details, + std::locale locale) -> void +{ + auto obj = json::object{ + {"type", details.type_uri}, + {"title", details.title.str(locale)}, + {"status", static_cast(details.status)}, }; - + if (details.detail) + obj["detail"] = details.detail->str(locale); + if (details.instance) + obj["instance"] = *details.instance; + jv = obj; } + +export struct tpl +{ + http::status status; + blocale::message title; + std::string_view type_uri; + + auto instantiate() const -> details + { + return details{status, title, type_uri}; + } +}; + +} // namespace routemon::problem diff --git a/server/src/req_ctx.cppm b/server/src/req_ctx.cppm index 797c51d..4a64d7d 100644 --- a/server/src/req_ctx.cppm +++ b/server/src/req_ctx.cppm @@ -10,42 +10,39 @@ import :trace; namespace routemon { - export class req_ctx { - trace::id tid_; - std::locale locale_; - http::verb_set route_verbs_; - bool keep_alive_; - bhttp::request_header const& req_header_; - - public: - explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bool keep_alive, bhttp::request_header const& req_header) - : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, keep_alive_{keep_alive}, req_header_{req_header} - {} - - template - explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bhttp::request const& req) - : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} - {} - - auto trace_id() const -> trace::id { - return tid_; - } - - auto locale() const -> std::locale { - return locale_; - } - - auto route_verbs() const -> http::verb_set { - return route_verbs_; - } - - auto keep_alive() const -> bool { - return keep_alive_; - } - - auto req_header() const -> bhttp::request_header const& { - return req_header_; - } - }; +export class req_ctx +{ + trace::id tid_; + std::locale locale_; + http::verb_set route_verbs_; + bool keep_alive_; + bhttp::request_header const& req_header_; + +public: + explicit req_ctx( + trace::id tid, std::locale locale, http::verb_set route_verbs, + bool keep_alive, bhttp::request_header const& req_header) + : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, + keep_alive_{keep_alive}, req_header_{req_header} + { + } + + template + explicit req_ctx( + trace::id tid, std::locale locale, http::verb_set route_verbs, + bhttp::request const& req) + : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} + { + } + + auto trace_id() const -> trace::id { return tid_; } + auto locale() const -> std::locale { return locale_; } + auto route_verbs() const -> http::verb_set { return route_verbs_; } + auto keep_alive() const -> bool { return keep_alive_; } + auto req_header() const -> bhttp::request_header const& + { + return req_header_; + } +}; } // namespace routemon diff --git a/server/src/rwgps.cppm b/server/src/rwgps.cppm index 7ad6605..8269ee8 100644 --- a/server/src/rwgps.cppm +++ b/server/src/rwgps.cppm @@ -17,121 +17,148 @@ namespace json = boost::json; namespace routemon::rwgps { - export struct route_summary { - std::int64_t id; - std::int64_t user_id; - std::string url; - std::string name; - std::string description; - }; - - struct pagination { - std::size_t record_count; - std::size_t page_count; - std::size_t page_size; - std::optional next_page_url; - }; - - struct get_routes_meta { - pagination pagination; +export struct route_summary +{ + std::int64_t id; + std::int64_t user_id; + std::string url; + std::string name; + std::string description; +}; + +struct pagination +{ + std::size_t record_count; + std::size_t page_count; + std::size_t page_size; + std::optional next_page_url; +}; + +struct get_routes_meta +{ + pagination pagination; +}; + +struct get_routes_response +{ + std::vector routes; + get_routes_meta meta; +}; + +auto tag_invoke(json::value_to_tag const&, json::value const& jv) + -> route_summary +{ + return { + .id = json::value_to(jv.at("id")), + .user_id = json::value_to(jv.at("user_id")), + .url = json::value_to(jv.at("url")), + .name = json::value_to(jv.at("name")), + .description = json::value_to(jv.at("description")), }; - - struct get_routes_response { - std::vector routes; - get_routes_meta meta; +} + +auto tag_invoke(json::value_to_tag const&, json::value const& jv) + -> pagination +{ + return { + .record_count = json::value_to(jv.at("record_count")), + .page_count = json::value_to(jv.at("page_count")), + .page_size = json::value_to(jv.at("page_size")), + .next_page_url = + json::value_to>(jv.at("next_page_url")), }; +} - auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> route_summary { - return { - .id = json::value_to(jv.at("id")), - .user_id = json::value_to(jv.at("user_id")), - .url = json::value_to(jv.at("url")), - .name = json::value_to(jv.at("name")), - .description = json::value_to(jv.at("description")), - }; - } - - auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> pagination { - return { - .record_count = json::value_to(jv.at("record_count")), - .page_count = json::value_to(jv.at("page_count")), - .page_size = json::value_to(jv.at("page_size")), - .next_page_url = json::value_to>(jv.at("next_page_url")), - }; - } - - auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> get_routes_meta { - return { +auto tag_invoke( + json::value_to_tag const&, json::value const& jv) + -> get_routes_meta +{ + return { .pagination = json::value_to(jv.at("pagination")), - }; - } + }; +} - auto tag_invoke(json::value_to_tag const&, json::value const& jv) -> get_routes_response { - return { +auto tag_invoke( + json::value_to_tag const&, json::value const& jv) + -> get_routes_response +{ + return { .routes = json::value_to>(jv.at("routes")), - .meta = json::value_to(jv.at("meta")), + .meta = json::value_to(jv.at("meta")), + }; +} + +auto json_value_to_get_routes_response(json::value const& jv) + -> get_routes_response +{ + return json::value_to(jv); +} + +export class client +{ + log::logger l_; + http::client hc_; + std::string api_key_; + std::string auth_token_; + + static constexpr std::string host = "ridewithgps.com"; + + // TODO: handle failure appropriately + auto get_routes_page(std::size_t page) -> get_routes_response + { + auto req = bhttp::request{ + bhttp::verb::get, + std::format("/api/v1/routes.json?page_size=200?page={}", page), + 11, // HTTP 1.1 }; + req.set(bhttp::field::host, host); + req.set("x-rwgps-api-key", api_key_); + req.set("x-rwgps-auth-token", auth_token_); + + auto rsp = hc_.do_request(req); + auto p = json::stream_parser{}; + for (auto const frag : rsp.body().cdata()) + { + p.write(static_cast(frag.data()), frag.size()); + } + assert(p.done()); + return json_value_to_get_routes_response(p.release()); } - auto json_value_to_get_routes_response(json::value const& jv) -> get_routes_response { - return json::value_to(jv); +public: + explicit client( + net::io_context& ioc, log::logger const& l, std::string api_key, + std::string auth_token) + : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, + auth_token_{std::move(auth_token)} + { } - export class client { - log::logger l_; - http::client hc_; - std::string api_key_; - std::string auth_token_; - - static constexpr std::string host = "ridewithgps.com"; - - // TODO: handle failure appropriately - auto get_routes_page(std::size_t page) -> get_routes_response { - auto req = bhttp::request{ - bhttp::verb::get, - std::format("/api/v1/routes.json?page_size=200?page={}", page), - 11, // HTTP 1.1 - }; - req.set(bhttp::field::host, host); - req.set("x-rwgps-api-key", api_key_); - req.set("x-rwgps-auth-token", auth_token_); - - auto rsp = hc_.do_request(req); - auto p = json::stream_parser{}; - for (auto const frag : rsp.body().cdata()) { - p.write(static_cast(frag.data()), frag.size()); + auto get_all_routes() -> std::vector + { + // TODO: make sure that there are no duplicates here. + // What does RWGPS sort on, by default? + // Consider using an associative container instead of a vector. + auto record_count = 0uz; + auto current_page = 0uz; + auto routes = std::vector{}; + + while (true) + { + auto rsp = get_routes_page(current_page); + if (rsp.meta.pagination.next_page_url) + l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); + routes.append_range(rsp.routes); + if (rsp.meta.pagination.record_count > 0) + record_count = rsp.meta.pagination.record_count; + if (rsp.routes.empty() || routes.size() >= record_count) + { + break; } - assert(p.done()); - return json_value_to_get_routes_response(p.release()); } - public: - explicit client(net::io_context& ioc, log::logger const& l, std::string api_key, std::string auth_token) - : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, auth_token_{std::move(auth_token)} - {} - - auto get_all_routes() -> std::vector { - // TODO: make sure that there are no duplicates here. - // What does RWGPS sort on, by default? - // Consider using an associative container instead of a vector. - auto record_count = 0uz; - auto current_page = 0uz; - auto routes = std::vector{}; - - while (true) { - auto rsp = get_routes_page(current_page); - if (rsp.meta.pagination.next_page_url) - l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); - routes.append_range(rsp.routes); - if (rsp.meta.pagination.record_count > 0) - record_count = rsp.meta.pagination.record_count; - if (rsp.routes.empty() || routes.size() >= record_count) { - break; - } - } - - return routes; - } - }; + return routes; + } +}; } // namespace routemon::rwgps diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm index f226c6b..70f58c0 100644 --- a/server/src/sqlite3.cppm +++ b/server/src/sqlite3.cppm @@ -9,249 +9,303 @@ import :util; namespace routemon::sqlite3 { - class mutex_guard { - explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} { - ::sqlite3_mutex_enter(mut_); - } - - friend auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) -> decltype(f(std::declval())); - - public: - mutex_guard(mutex_guard const&) = delete; - ~mutex_guard() { - ::sqlite3_mutex_leave(mut_); - } - - private: - ::sqlite3_mutex* mut_; - }; - - auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) -> decltype(f(std::declval())) { - return f(mutex_guard{mut}); +class mutex_guard +{ + explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} + { + ::sqlite3_mutex_enter(mut_); } - auto do_guarded(::sqlite3* dbc, std::invocable auto f) -> decltype(f(std::declval())) { - return do_guarded(::sqlite3_db_mutex(dbc), f); + friend auto + do_guarded(::sqlite3_mutex* mut, std::invocable auto f) + -> decltype(f(std::declval())); + +public: + mutex_guard(mutex_guard const&) = delete; + ~mutex_guard() { ::sqlite3_mutex_leave(mut_); } + +private: + ::sqlite3_mutex* mut_; +}; + +auto do_guarded(::sqlite3_mutex* mut, std::invocable auto f) + -> decltype(f(std::declval())) +{ + return f(mutex_guard{mut}); +} + +auto do_guarded(::sqlite3* dbc, std::invocable auto f) + -> decltype(f(std::declval())) +{ + return do_guarded(::sqlite3_db_mutex(dbc), f); +} + +class error : public std::exception +{ + int code_; + std::string message_; + +public: + explicit error(mutex_guard const&, int code, ::sqlite3* dbc) + : code_{code}, message_{::sqlite3_errmsg(dbc)} + { } - class error : public std::exception { - int code_; - std::string message_; - - public: - explicit error(mutex_guard const&, int code, ::sqlite3* dbc) - : code_{code}, message_{::sqlite3_errmsg(dbc)} - {} - - explicit error(int code) - : code_{code}, message_{::sqlite3_errstr(code)} - {} - - [[nodiscard]] auto what() const noexcept -> char const* override { - return message_.c_str(); - } - - [[nodiscard]] auto code() const noexcept -> int { - return code_; - } - }; - - template concept C> - concept optional_of = requires { - typename T::value_type; - requires std::same_as>; - requires C; - }; - - template - concept scannable_prim = - std::same_as || - std::same_as || - std::same_as; - - template - concept scannable = scannable_prim || optional_of; - - class statement { - ::sqlite3_stmt* stmt_; - - public: - explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} - statement(statement const&) = delete; - statement(statement&& s) noexcept { - stmt_ = s.stmt_; - s.stmt_ = nullptr; - } - ~statement() { - ::sqlite3_finalize(stmt_); - } - auto get() -> ::sqlite3_stmt* { - return stmt_; - } - }; - - class row_reader { - statement stmt_; + explicit error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} - explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} + [[nodiscard]] auto what() const noexcept -> char const* override + { + return message_.c_str(); + } - friend class connection; + [[nodiscard]] auto code() const noexcept -> int { return code_; } +}; + +template concept C> +concept optional_of = requires { + typename T::value_type; + requires std::same_as>; + requires C; +}; + +template +concept scannable_prim = std::same_as || std::same_as + || std::same_as; + +template +concept scannable = scannable_prim || optional_of; + +class statement +{ + ::sqlite3_stmt* stmt_; + +public: + explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} + statement(statement const&) = delete; + statement(statement&& s) noexcept + { + stmt_ = s.stmt_; + s.stmt_ = nullptr; + } + ~statement() { ::sqlite3_finalize(stmt_); } + auto get() -> ::sqlite3_stmt* { return stmt_; } +}; + +class row_reader +{ + statement stmt_; + + explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} + + friend class connection; + + void scan(int col, std::string& s) + { + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) + throw std::invalid_argument{"invalid type for scan"}; + unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); + auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); + if (!size.has_value()) + throw std::logic_error{"unexpected negative amount of bytes in column"}; + s = std::string{reinterpret_cast(chs), *size}; + } - void scan(int col, std::string& s) { - if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) - throw std::invalid_argument{"invalid type for scan"}; - unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); - auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); - if (!size.has_value()) - throw std::logic_error{"unexpected negative amount of bytes in column"}; - s = std::string{reinterpret_cast(chs), *size}; - } + void scan(int col, double& v) + { + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) + throw std::invalid_argument{"invalid type for scan"}; + v = ::sqlite3_column_double(stmt_.get(), col); + } - void scan(int col, double& v) { - if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) - throw std::invalid_argument{"invalid type for scan"}; - v = ::sqlite3_column_double(stmt_.get(), col); - } + void scan(int col, std::int64_t& v) + { + if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) + throw std::invalid_argument{"invalid type for scan"}; + v = ::sqlite3_column_int64(stmt_.get(), col); + } - void scan(int col, std::int64_t& v) { - if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) - throw std::invalid_argument{"invalid type for scan"}; - v = ::sqlite3_column_int64(stmt_.get(), col); + void scan(int col, optional_of auto& v) + { + if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) + { + v.reset(); } - - void scan(int col, optional_of auto& v) { - if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) { - v.reset(); - } else { - typename std::remove_cvref_t::value_type tmp; - scan(col, tmp); - v = std::move(tmp); - } + else + { + typename std::remove_cvref_t::value_type tmp; + scan(col, tmp); + v = std::move(tmp); } + } - public: - auto next() -> bool { - ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); - return do_guarded(dbc, [&](auto const& guard) -> bool { - auto const s = ::sqlite3_step(stmt_.get()); - if (s == SQLITE_ROW) - return true; - if (s == SQLITE_DONE) - return false; - throw error{guard, s, dbc}; - }); - } +public: + auto next() -> bool + { + ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); + return do_guarded( + dbc, + [&](auto const& guard) -> bool + { + auto const s = ::sqlite3_step(stmt_.get()); + if (s == SQLITE_ROW) + return true; + if (s == SQLITE_DONE) + return false; + throw error{guard, s, dbc}; + }); + } - auto scan(scannable auto&... args) -> void { - auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); - if (!ncols.has_value()) - throw std::logic_error{"got unexpected negative amount of columns"}; - if (sizeof...(args) > *ncols) - throw std::invalid_argument{"more scanning arguments provided than columns in result set"}; - auto col = 0; (..., scan(col++, args)); - } + auto scan(scannable auto&... args) -> void + { + auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); + if (!ncols.has_value()) + throw std::logic_error{"got unexpected negative amount of columns"}; + if (sizeof...(args) > *ncols) + throw std::invalid_argument{ + "more scanning arguments provided than columns in result set" + }; + auto col = 0; + (..., scan(col++, args)); + } - auto scan_single(scannable auto&... args) -> void { - if (!next()) - throw std::logic_error{"no row in result set"}; - scan(args...); - if (next()) { - throw std::logic_error{"more than one row in result set"}; - } + auto scan_single(scannable auto&... args) -> void + { + if (!next()) + throw std::logic_error{"no row in result set"}; + scan(args...); + if (next()) + { + throw std::logic_error{"more than one row in result set"}; } - }; - - class binder { - statement& stmt_; - - explicit binder(statement& stmt) : stmt_{stmt} {} - - friend class connection; - - public: - auto text(std::string const& param_name, std::string_view str) -> void { - int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); - if (i == 0) - throw std::invalid_argument{std::format("bind: no parameter with name {} found", param_name)}; - auto str_size = util::int_from_size(str.size()); - if (!str_size.has_value()) - throw std::invalid_argument{"bind: provided text is too long"}; - if (auto s = ::sqlite3_bind_text(stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) { - throw error{s}; - } + } +}; + +class binder +{ + statement& stmt_; + + explicit binder(statement& stmt) : stmt_{stmt} {} + + friend class connection; + +public: + auto text(std::string const& param_name, std::string_view str) -> void + { + int const i = + ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); + if (i == 0) + throw std::invalid_argument{std::format( + "bind: no parameter with name {} found", param_name)}; + auto str_size = util::int_from_size(str.size()); + if (!str_size.has_value()) + throw std::invalid_argument{"bind: provided text is too long"}; + if (auto s = ::sqlite3_bind_text( + stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); + s != SQLITE_OK) + { + throw error{s}; } + } - static auto noop(binder&) -> void {} - }; + static auto noop(binder&) -> void {} +}; - export class connection { - ::sqlite3* dbc_; - ::sqlite3_mutex* mut_; +export class connection +{ + ::sqlite3* dbc_; + ::sqlite3_mutex* mut_; - explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} {} + explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} + { + } - friend auto open(std::string const& filename) -> connection; + friend auto open(std::string const& filename) -> connection; - public: - connection(connection const&) = delete; - connection(connection&& c) noexcept { - dbc_ = c.dbc_; - mut_ = c.mut_; - c.dbc_ = nullptr; - c.mut_ = nullptr; - } +public: + connection(connection const&) = delete; + connection(connection&& c) noexcept + { + dbc_ = c.dbc_; + mut_ = c.mut_; + c.dbc_ = nullptr; + c.mut_ = nullptr; + } - [[nodiscard]] auto query(std::string const& sql, std::function const& bf = binder::noop) -> row_reader { - ::sqlite3_stmt* pstmt = nullptr; - char const* sql_tail = nullptr; - auto sql_size = util::int_from_size(sql.size()); - if (!sql_size.has_value() || *sql_size >= std::numeric_limits::max() - 1) - throw std::invalid_argument{"provided input text too large"}; - do_guarded(mut_, [&](auto const& guard) -> void { - if (auto s = ::sqlite3_prepare_v2(dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) { - if (pstmt != nullptr) { - // Use contract_assert when having a compiler with contracts available - ::sqlite3_finalize(pstmt); - throw std::logic_error{"expected stmt to be null after failed preparation"}; + [[nodiscard]] auto query( + std::string const& sql, + std::function const& bf = binder::noop) -> row_reader + { + ::sqlite3_stmt* pstmt = nullptr; + char const* sql_tail = nullptr; + auto sql_size = util::int_from_size(sql.size()); + if (!sql_size.has_value() + || *sql_size >= std::numeric_limits::max() - 1) + throw std::invalid_argument{"provided input text too large"}; + do_guarded( + mut_, + [&](auto const& guard) -> void + { + if (auto s = ::sqlite3_prepare_v2( + dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); + s != SQLITE_OK) + { + if (pstmt != nullptr) + { + // Use contract_assert when having a compiler with + // contracts available + ::sqlite3_finalize(pstmt); + throw std::logic_error{ + "expected stmt to be null after failed preparation" + }; + } + throw error{guard, s, dbc_}; } - throw error{guard, s, dbc_}; - } - }); - if (!pstmt) - throw std::invalid_argument{"provided input text contains no SQL"}; - auto stmt = statement{pstmt}; - if (sql_tail && std::strlen(sql_tail) > 0) - throw std::invalid_argument{"provided input text contains more than one SQL statement"}; - auto b = binder{stmt}; bf(b); - return row_reader{std::move(stmt)}; - } + }); + if (!pstmt) + throw std::invalid_argument{"provided input text contains no SQL"}; + auto stmt = statement{pstmt}; + if (sql_tail && std::strlen(sql_tail) > 0) + throw std::invalid_argument{ + "provided input text contains more than one SQL statement" + }; + auto b = binder{stmt}; + bf(b); + return row_reader{std::move(stmt)}; + } - auto exec(std::string const& sql, std::function const& bf = binder::noop) -> void { - auto reader = query(sql, bf); - while (reader.next()); - } + auto exec( + std::string const& sql, + std::function const& bf = binder::noop) -> void + { + auto reader = query(sql, bf); + while (reader.next()) + ; + } - ~connection() { - std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); + ~connection() { std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); } +}; + +export auto open(std::string const& filename) -> connection +{ + ::sqlite3* dbc = nullptr; + auto s = ::sqlite3_open_v2( + filename.c_str(), &dbc, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX + | SQLITE_OPEN_EXRESCODE, + nullptr); + if (s != SQLITE_OK) + { + if (dbc) + { + do_guarded( + dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); } - }; - - export auto open(std::string const& filename) -> connection { - ::sqlite3* dbc = nullptr; - auto s = ::sqlite3_open_v2(filename.c_str(), &dbc, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | - SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE, - nullptr); - if (s != SQLITE_OK) { - if (dbc) { - do_guarded(dbc, [&](auto const& guard) -> void { - throw error{guard, s, dbc}; - }); - } else { - throw error{s}; - } + else + { + throw error{s}; } - return connection{dbc}; } + return connection{dbc}; +} } // namespace routemon::sqlite3 diff --git a/server/src/srv.cppm b/server/src/srv.cppm index 2ca48c6..d9ff880 100644 --- a/server/src/srv.cppm +++ b/server/src/srv.cppm @@ -1,12 +1,12 @@ module; -#include -#include #include #include #include +#include #include #include +#include #include #include @@ -32,190 +32,248 @@ using tcp = boost::asio::ip::tcp; namespace routemon::srv { - class gpx_parse_error_category_impl : public std::error_category { - public: - char const* name() const noexcept override { return "gpx_parse"; } - auto message(int condition) const noexcept -> std::string override { - std::ignore = condition; - return "failed to parse GPX file"; - } - }; - auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& { - static auto const inst = gpx_parse_error_category_impl{}; - return inst; +class gpx_parse_error_category_impl : public std::error_category +{ +public: + char const* name() const noexcept override { return "gpx_parse"; } + auto message(int condition) const noexcept -> std::string override + { + std::ignore = condition; + return "failed to parse GPX file"; } - auto gpx_parse_error() noexcept -> std::error_code { - return std::error_code{1, gpx_parse_error_category()}; +}; +auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& +{ + static auto const inst = gpx_parse_error_category_impl{}; + return inst; +} +auto gpx_parse_error() noexcept -> std::error_code +{ + return std::error_code{1, gpx_parse_error_category()}; +} + +class gpx_parse_result +{ + std::variant res_; + +public: + auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } + auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } + + auto unwrap() -> gpx::file&& + { + return std::visit( + util::overloaded{ + [](std::exception_ptr ex) -> gpx::file&& + { + if (ex) + std::rethrow_exception(ex); + else + throw std::runtime_error{"no GPX file parse result available"}; + }, + [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, + }, + std::move(res_)); } +}; - class gpx_parse_result { - std::variant res_; +struct readable_gpx_body +{ + using value_type = gpx_parse_result; - public: - auto set_exception(std::exception_ptr ex) noexcept { - res_ = ex; - } - auto set_gpx_file(gpx::file&& f) noexcept { - res_ = std::move(f); - } + class reader + { + gpx::reader r_; + util::not_null res_; - auto unwrap() -> gpx::file&& { - return std::visit(util::overloaded{ - [](std::exception_ptr ex) -> gpx::file&& { - if (ex) std::rethrow_exception(ex); - else throw std::runtime_error{"no GPX file parse result available"}; - }, - [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, - }, std::move(res_)); + public: + template + explicit reader(bhttp::header&, value_type& v) : res_{&v} + { } - }; - struct readable_gpx_body { - using value_type = gpx_parse_result; - - class reader { - gpx::reader r_; - util::not_null res_; - - public: - template - explicit reader(bhttp::header&, value_type& v) - : res_{&v} - {} - - // The following methods (which are called by Beast) are marked - // noexcept, since Beast does not ensure that exceptions thrown - // here are appropriately directed to the caller of - // (async_)read(_some), so throwing here might cause the program - // to crash. - - auto init(boost::optional /* n */, beast::error_code& ec) noexcept -> void { - try { - r_.init(); - ec = {}; - } catch (std::exception& ex) { - res_->set_exception(std::current_exception()); - ec = gpx_parse_error(); - } + // The following methods (which are called by Beast) are marked + // noexcept, since Beast does not ensure that exceptions thrown + // here are appropriately directed to the caller of + // (async_)read(_some), so throwing here might cause the program + // to crash. + + auto + init(boost::optional /* n */, beast::error_code& ec) noexcept + -> void + { + try + { + r_.init(); + ec = {}; } - - auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { - auto total = 0uz; - try { - for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { - r_.put(std::string_view{static_cast(it->data()), it->size()}); - total += it->size(); - } - ec = {}; - } catch (std::exception& ex) { - res_->set_exception(std::current_exception()); - ec = gpx_parse_error(); - } - return total; + catch (std::exception& ex) + { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); } + } - auto finish(beast::error_code& ec) noexcept { - try { - res_->set_gpx_file(r_.finish()); - ec = {}; - } catch (std::exception& ex) { - res_->set_exception(std::current_exception()); - ec = gpx_parse_error(); + auto + put(beast::concepts::const_buffer_sequence auto b, + beast::error_code& ec) noexcept -> std::size_t + { + auto total = 0uz; + try + { + for (auto it = net::buffer_sequence_begin(b); + it != net::buffer_sequence_end(b); it++) + { + r_.put( + std::string_view{ + static_cast(it->data()), it->size() + }); + total += it->size(); } + ec = {}; } - }; - }; - static_assert(bhttp::concepts::body); - static_assert(bhttp::concepts::body_reader); - - class handler { - api::handler inner_; - - public: - using outer_ctx = http::trace_id_ctx; - using l0_ctx = http::routed_ctx; - - private: - auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable { - auto gpx_file = gpx::file{}; - try { - auto req = co_await http::read_request(ctx, std::move(r)); - gpx_file = std::move(req->body().unwrap()); - } catch (std::exception& ex) { - // TODO: more detailed problem reporting - auto tpl = problem::tpl{ - .status = bhttp::status::bad_request, - .title = translate("Failed to parse GPX file"), - .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", - }; - co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false}); + catch (std::exception& ex) + { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); } + return total; + } - // TODO: catch handler exceptions and return 500 when raised? - // (keep-alive depends on whether whole request was read) - auto mres = inner_.process_gpx(std::move(gpx_file)); - if (!mres) { - auto tpl = problem::tpl{ - .status = bhttp::status::internal_server_error, - .title = translate("Internal server error"), - .type_uri = "https://routemon.fautchen.eu/problems/internal-server-error", - }; - co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true}); + auto finish(beast::error_code& ec) noexcept + { + try + { + res_->set_gpx_file(r_.finish()); + ec = {}; + } + catch (std::exception& ex) + { + res_->set_exception(std::current_exception()); + ec = gpx_parse_error(); } - - auto rsp = http::make_rsp(bhttp::status::ok, http::keep_alive{true}); - rsp.set(bhttp::field::content_type, "application/json"); - rsp.body() = json::serialize(json::value_from(*mres)); - rsp.prepare_payload(); - co_return rsp; } - - auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable { - auto req = co_await http::read_request(ctx, std::move(r)); - auto info = inner_.sysinfo(); - - auto rsp = http::make_rsp(bhttp::status::ok, http::keep_alive{true}); - rsp.set(bhttp::field::content_type, "application/json"); - rsp.body() = json::serialize(json::value_from(info)); - rsp.prepare_payload(); - co_return rsp; + }; +}; +static_assert(bhttp::concepts::body); +static_assert(bhttp::concepts::body_reader); + +class handler +{ + api::handler inner_; + +public: + using outer_ctx = http::trace_id_ctx; + using l0_ctx = http::routed_ctx; + +private: + auto handle_process_gpx(l0_ctx ctx, http::readable_request r) + -> net::awaitable + { + auto gpx_file = gpx::file{}; + try + { + auto req = + co_await http::read_request(ctx, std::move(r)); + gpx_file = std::move(req->body().unwrap()); } - - public: - handler(api::handler&& inner) : inner_{std::move(inner)} {} - - auto make_routes() -> http::route_tree> { - auto handler = [this](MemFn member) { - return std::bind_front(member, this); + catch (std::exception& ex) + { + // TODO: more detailed problem reporting + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Failed to parse GPX file"), + .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } - return http::dtree>{}.named_subtrees({ - {"gpx", http::dtree{{ - .post = handler(&handler::handle_process_gpx), - }}.no_subtrees()}, - {"sysinfo", http::dtree{{ - .get = handler(&handler::handle_sysinfo), - }}.no_subtrees()}, - }); + // TODO: catch handler exceptions and return 500 when raised? + // (keep-alive depends on whether whole request was read) + auto mres = inner_.process_gpx(std::move(gpx_file)); + if (!mres) + { + auto tpl = problem::tpl{ + .status = bhttp::status::internal_server_error, + .title = translate("Internal server error"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "internal-server-error", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{true}); } - }; - export class server { - handler handler_; - http::server srv_; + auto rsp = http::make_rsp( + bhttp::status::ok, http::keep_alive{true}); + rsp.set(bhttp::field::content_type, "application/json"); + rsp.body() = json::serialize(json::value_from(*mres)); + rsp.prepare_payload(); + co_return rsp; + } - static auto make_global_middleware() -> http::middleware_t { - return http::middleware_compose, http::trace_id_ctx>(http::trace_id_middleware, http::lax_cors_middleware>); - } + auto handle_sysinfo(l0_ctx ctx, http::readable_request r) + -> net::awaitable + { + auto req = + co_await http::read_request(ctx, std::move(r)); + auto info = inner_.sysinfo(); + + auto rsp = http::make_rsp( + bhttp::status::ok, http::keep_alive{true}); + rsp.set(bhttp::field::content_type, "application/json"); + rsp.body() = json::serialize(json::value_from(info)); + rsp.prepare_payload(); + co_return rsp; + } - public: - server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) - : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} - {} +public: + handler(api::handler&& inner) : inner_{std::move(inner)} {} + + auto make_routes() -> http::route_tree> + { + auto handler = [this](MemFn member) + { return std::bind_front(member, this); }; + + return http::dtree>{}.named_subtrees({ + {"gpx", + http::dtree{ + { + .post = handler(&handler::handle_process_gpx), + } + }.no_subtrees()}, + {"sysinfo", http::dtree{ + { + .get = handler(&handler::handle_sysinfo), + } + }.no_subtrees()}, + }); + } +}; + +export class server +{ + handler handler_; + http::server srv_; + + static auto make_global_middleware() + -> http::middleware_t + { + return http::middleware_compose< + http::base_ctx, http::trace_id_ctx, + http::trace_id_ctx>( + http::trace_id_middleware, + http::lax_cors_middleware>); + } - auto spawn(net::io_context& ioc) -> void { - srv_.spawn(ioc); - } - }; +public: + server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) + : handler_{std::move(inner)}, + srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} + { + } + + auto spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } +}; } // namespace routemon::srv diff --git a/server/src/time.cppm b/server/src/time.cppm index 767883c..907cec4 100644 --- a/server/src/time.cppm +++ b/server/src/time.cppm @@ -4,202 +4,253 @@ import std; export namespace routemon::time { - using timestamp = std::chrono::time_point; - - class period { - // Assuming [start, end). Unfortunately the DATEX II model is not - // clear about this. - timestamp start_; - timestamp end_; - - public: - explicit period(timestamp start, timestamp end) - : start_{start}, end_{end} +using timestamp = std::chrono::time_point; + +class period +{ + // Assuming [start, end). Unfortunately the DATEX II model is not + // clear about this. + timestamp start_; + timestamp end_; + +public: + explicit period(timestamp start, timestamp end) : start_{start}, end_{end} + { + if (start >= end) { - if (start >= end) { - throw std::invalid_argument("period: start should be before end"); - } + throw std::invalid_argument("period: start should be before end"); } + } - [[nodiscard]] auto intersect(period other) const -> std::optional { - auto const new_start = start_ < other.start() ? other.start() : start_; - auto const new_end = other.end() < end_ ? other.end() : end_; - return new_start < new_end ? std::make_optional(period{new_start, new_end}) : std::nullopt; - } + [[nodiscard]] auto intersect(period other) const -> std::optional + { + auto const new_start = start_ < other.start() ? other.start() : start_; + auto const new_end = other.end() < end_ ? other.end() : end_; + return new_start < new_end ? std::make_optional(period{new_start, new_end}) + : std::nullopt; + } - [[nodiscard]] auto except(period other) const -> std::pair, std::optional> { - auto const before_start = start_; - auto const before_end = other.start(); - auto const after_start = end_; - auto const after_end = other.end(); - std::optional before, after; - if (before_start < before_end) - before = period{before_start, before_end}; - if (after_start < after_end) - after = period{after_end, after_start}; - return std::make_pair(before, after); - } + [[nodiscard]] auto except(period other) const + -> std::pair, std::optional> + { + auto const before_start = start_; + auto const before_end = other.start(); + auto const after_start = end_; + auto const after_end = other.end(); + std::optional before, after; + if (before_start < before_end) + before = period{before_start, before_end}; + if (after_start < after_end) + after = period{after_end, after_start}; + return std::make_pair(before, after); + } - [[nodiscard]] auto start() const -> timestamp { return start_; } - [[nodiscard]] auto end() const -> timestamp { return end_; } - }; + [[nodiscard]] auto start() const -> timestamp { return start_; } + [[nodiscard]] auto end() const -> timestamp { return end_; } +}; - class period_seq { - std::vector periods_; +class period_seq +{ + std::vector periods_; - // The way lt and ge are ordered makes a difference for how the sorting - // (insertion based on lower_bound) works. Do not carelessly reorder this. - enum lt_ge : std::uint8_t { - ge, // >= - lt, // < - }; + // The way lt and ge are ordered makes a difference for how the sorting + // (insertion based on lower_bound) works. Do not carelessly reorder this. + enum lt_ge : std::uint8_t + { + ge, // >= + lt, // < + }; - // O(n log n) - template S> + // O(n log n) + template S> requires std::same_as, period> - static auto consolidate(I begin, S end) -> std::vector { - auto periods = std::vector{}; - auto preds = std::vector>{}; + static auto consolidate(I begin, S end) -> std::vector + { + auto periods = std::vector{}; + auto preds = std::vector>{}; - for (auto it = begin; it != end; it++) { - auto const& period = *it; - - auto const a = std::make_pair(period.start(), ge); - auto const b = std::make_pair(period.end(), lt); - preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); - preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); - } + for (auto it = begin; it != end; it++) + { + auto const& period = *it; - if (preds.empty()) - return periods; - - if (preds.size() < 2) - throw std::logic_error{"period_seq::consolidate: amount of predicates should be >= 2"}; - if (preds.front().second != ge) - throw std::logic_error{"period_seq::consolidate: first element of preds should be a ge-element"}; - if (preds.back().second != lt) - throw std::logic_error{"period_seq::consolidate: last element of preds should be an lt-element"}; - - auto period_start = preds[0].first; - for (std::size_t i = 1; i < preds.size(); i++) { - if (preds[i].second == lt && (i + 1 == preds.size() || preds[i + 1].second == ge)) { - auto const period_end = preds[i].first; - if (!periods.empty() && periods.back().start() == period_start) - periods.back() = period{periods.back().end(), period_end}; - else - periods.emplace_back(period_start, period_end); - if (i + 1 != preds.size()) { - period_start = preds[i + 1].first; - i++; - } - } - } + auto const a = std::make_pair(period.start(), ge); + auto const b = std::make_pair(period.end(), lt); + preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); + preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); + } + if (preds.empty()) return periods; - } - explicit period_seq(std::vector periods) - : periods_{std::move(periods)} + if (preds.size() < 2) + throw std::logic_error{ + "period_seq::consolidate: amount of predicates should be >= 2" + }; + if (preds.front().second != ge) + throw std::logic_error{"period_seq::consolidate: first element of preds " + "should be a ge-element"}; + if (preds.back().second != lt) + throw std::logic_error{"period_seq::consolidate: last element of preds " + "should be an lt-element"}; + + auto period_start = preds[0].first; + for (std::size_t i = 1; i < preds.size(); i++) { - for (auto i = 0uz; i < periods_.size(); i++) { - if (i + 1 < periods_.size()) { - if (periods_[i].end() >= periods_[i + 1].start()) { - throw std::logic_error{"period_seq: vector provided to private constructor not ordered properly"}; - } + if (preds[i].second == lt + && (i + 1 == preds.size() || preds[i + 1].second == ge)) + { + auto const period_end = preds[i].first; + if (!periods.empty() && periods.back().start() == period_start) + periods.back() = period{periods.back().end(), period_end}; + else + periods.emplace_back(period_start, period_end); + if (i + 1 != preds.size()) + { + period_start = preds[i + 1].first; + i++; } } } - public: - template S> - requires std::same_as, period> - explicit period_seq(I begin, S end) - : periods_{consolidate(begin, end)} - {} - - explicit period_seq(period singleton) - : periods_{singleton} - {} - - [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq { - auto it1 = periods_.begin(); auto end1 = periods_.end(); - auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); - - auto res = std::vector{}; - while (it1 != end1 && it2 != end2) { - auto overlap = it1->intersect(*it2); - if (overlap) { - res.push_back(*overlap); - if (it1->end() < it2->end()) { - it1++; - } else { - it2++; - } - } else { - if (it1->end() < it2->start()) { - it1++; - } else { - it2++; - } + return periods; + } + + explicit period_seq(std::vector periods) + : periods_{std::move(periods)} + { + for (auto i = 0uz; i < periods_.size(); i++) + { + if (i + 1 < periods_.size()) + { + if (periods_[i].end() >= periods_[i + 1].start()) + { + throw std::logic_error{"period_seq: vector provided to private " + "constructor not ordered properly"}; } } - - return period_seq{res}; } + } - [[nodiscard]] auto except(period_seq const& other) const -> period_seq { - // This code was pretty tricky to write, I wouldn't be surprised if it has some bugs in it. +public: + template S> + requires std::same_as, period> + explicit period_seq(I begin, S end) : periods_{consolidate(begin, end)} + { + } - auto it1 = periods_.begin(); auto end1 = periods_.end(); - auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); + explicit period_seq(period singleton) : periods_{singleton} {} - auto res = std::vector{}; - if (it1 == end1) - return period_seq{res}; - if (it2 == end2) - return period_seq{periods_}; - auto period1 = period{*it1++}; + [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq + { + auto it1 = periods_.begin(); + auto end1 = periods_.end(); + auto it2 = other.periods_.begin(); + auto end2 = other.periods_.end(); - while (it1 != end1 && it2 != end2) { - if (period1.end() <= it2->start()) { - res.push_back(period1); - period1 = *it1++; - } else if (it2->end() <= period1.start()) { + auto res = std::vector{}; + while (it1 != end1 && it2 != end2) + { + auto overlap = it1->intersect(*it2); + if (overlap) + { + res.push_back(*overlap); + if (it1->end() < it2->end()) + { + it1++; + } + else + { it2++; - } else /* period1.begin() < it2->end() && it2->begin() < period1.end() */ { - auto const [mbefore, mafter] = period1.except(*it2); - if (mbefore) - res.push_back(*mbefore); - if (mafter) { - period1 = *mafter; - } else { - period1 = *it1++; - } } } + else + { + if (it1->end() < it2->start()) + { + it1++; + } + else + { + it2++; + } + } + } + + return period_seq{res}; + } + + [[nodiscard]] auto except(period_seq const& other) const -> period_seq + { + // This code was pretty tricky to write, I wouldn't be surprised if it + // has some bugs in it. + + auto it1 = periods_.begin(); + auto end1 = periods_.end(); + auto it2 = other.periods_.begin(); + auto end2 = other.periods_.end(); + auto res = std::vector{}; + if (it1 == end1) return period_seq{res}; - } + if (it2 == end2) + return period_seq{periods_}; + auto period1 = period{*it1++}; - [[nodiscard]] auto periods() const -> std::vector const& { - return periods_; + while (it1 != end1 && it2 != end2) + { + if (period1.end() <= it2->start()) + { + res.push_back(period1); + period1 = *it1++; + } + else if (it2->end() <= period1.start()) + { + it2++; + } + else /* period1.begin() < it2->end() && it2->begin() < + period1.end() */ + { + auto const [mbefore, mafter] = period1.except(*it2); + if (mbefore) + res.push_back(*mbefore); + if (mafter) + { + period1 = *mafter; + } + else + { + period1 = *it1++; + } + } } - }; - auto operator<<(std::ostream& os, period const& p) -> std::ostream& { - return os << "[" << p.start() << ", " << p.end() << ")"; + return period_seq{res}; } - auto operator<<(std::ostream &os, period_seq const& ps) -> std::ostream& { - os << "{"; - auto it = ps.periods().begin(); - while (it != ps.periods().end()) { - os << " " << *it; - if (++it != ps.periods().end()) { - os << ","; - } + [[nodiscard]] auto periods() const -> std::vector const& + { + return periods_; + } +}; + +auto operator<<(std::ostream& os, period const& p) -> std::ostream& +{ + return os << "[" << p.start() << ", " << p.end() << ")"; +} + +auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream& +{ + os << "{"; + auto it = ps.periods().begin(); + while (it != ps.periods().end()) + { + os << " " << *it; + if (++it != ps.periods().end()) + { + os << ","; } - return os << " }"; } + return os << " }"; +} } // namespace routemon::time diff --git a/server/src/trace.cppm b/server/src/trace.cppm index 00ecd05..35d32f3 100644 --- a/server/src/trace.cppm +++ b/server/src/trace.cppm @@ -11,69 +11,79 @@ import :util; namespace routemon::trace { - class uuid7 { - std::uint64_t high_ = 0; - std::uint64_t low_ = 0; +class uuid7 +{ + std::uint64_t high_ = 0; + std::uint64_t low_ = 0; - public: - uuid7() { - namespace chrono = std::chrono; - auto const unix_time_ms_signed = static_cast(chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count()); - if (unix_time_ms_signed < 0) - throw std::runtime_error{"system time before UNIX epoch"}; - auto const unix_time_ms = static_cast(unix_time_ms_signed); - if (std::countl_zero(unix_time_ms) < 16) - throw std::runtime_error{"system time too great"}; +public: + uuid7() + { + namespace chrono = std::chrono; + auto const unix_time_ms_signed = static_cast( + chrono::duration_cast( + chrono::system_clock::now().time_since_epoch()) + .count()); + if (unix_time_ms_signed < 0) + throw std::runtime_error{"system time before UNIX epoch"}; + auto const unix_time_ms = static_cast(unix_time_ms_signed); + if (std::countl_zero(unix_time_ms) < 16) + throw std::runtime_error{"system time too great"}; - auto rand = std::array{}; - int s = RAND_bytes(rand.data(), static_cast(rand.size())); - if (s != 1) { - unsigned long e = ERR_get_error(); - throw std::runtime_error{std::format("failed to generate UUID(v7): {} ({}, code {})", ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; - } + auto rand = std::array{}; + int s = RAND_bytes(rand.data(), static_cast(rand.size())); + if (s != 1) + { + unsigned long e = ERR_get_error(); + throw std::runtime_error{std::format( + "failed to generate UUID(v7): {} ({}, code {})", + ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; + } - auto version = std::uint64_t{0b0111}; - auto variant = std::uint64_t{0b10}; + auto version = std::uint64_t{0b0111}; + auto variant = std::uint64_t{0b10}; - high_ |= unix_time_ms << 16; - high_ |= version << 12; - high_ |= std::uint64_t{rand[0]} << 4; - high_ |= std::uint64_t{rand[1]}; - low_ |= variant << 62; - low_ |= std::uint64_t{rand[2]} << 54; - low_ |= std::uint64_t{rand[3]} << 48; - low_ |= std::uint64_t{rand[4]} << 40; - low_ |= std::uint64_t{rand[5]} << 32; - low_ |= std::uint64_t{rand[6]} << 24; - low_ |= std::uint64_t{rand[7]} << 16; - low_ |= std::uint64_t{rand[8]} << 8; - low_ |= std::uint64_t{rand[9]}; - } + high_ |= unix_time_ms << 16; + high_ |= version << 12; + high_ |= std::uint64_t{rand[0]} << 4; + high_ |= std::uint64_t{rand[1]}; + low_ |= variant << 62; + low_ |= std::uint64_t{rand[2]} << 54; + low_ |= std::uint64_t{rand[3]} << 48; + low_ |= std::uint64_t{rand[4]} << 40; + low_ |= std::uint64_t{rand[5]} << 32; + low_ |= std::uint64_t{rand[6]} << 24; + low_ |= std::uint64_t{rand[7]} << 16; + low_ |= std::uint64_t{rand[8]} << 8; + low_ |= std::uint64_t{rand[9]}; + } - auto format(std::array& target) -> void { - auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; - auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; - auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; - auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; - auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; + auto format(std::array& target) -> void + { + auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; + auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; + auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; + auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; + auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; - std::format_to(target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", - high_high, high_low_high, low_low_high, high_low, low_low); - target.back() = '\0'; - } - }; + std::format_to( + target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", high_high, + high_low_high, low_low_high, high_low, low_low); + target.back() = '\0'; + } +}; - export class id { - std::array chars_; +export class id +{ + std::array chars_; - public: - id() { - uuid7{}.format(chars_); - } +public: + id() { uuid7{}.format(chars_); } - auto as_string() const -> util::zstring_view { - return util::zstring_view{chars_.data(), chars_.size() - 1}; - } - }; + auto as_string() const -> util::zstring_view + { + return util::zstring_view{chars_.data(), chars_.size() - 1}; + } +}; } // namespace routemon::trace diff --git a/server/src/util.cppm b/server/src/util.cppm index 65f4d67..bdccf10 100644 --- a/server/src/util.cppm +++ b/server/src/util.cppm @@ -6,249 +6,285 @@ import std; namespace routemon::util { - // For use with e.g. std::visit (on std::variant). - template - struct overloaded : Ts... { - using Ts::operator()...; - }; - - export constexpr auto parse_double(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional { - auto x = 0.0; - auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); - if (ec == std::errc{}) { - return x; - } else { - return std::nullopt; - } +// For use with e.g. std::visit (on std::variant). +template +struct overloaded : Ts... +{ + using Ts::operator()...; +}; + +export constexpr auto parse_double( + std::string_view s, + std::chars_format fmt = std::chars_format::general) noexcept + -> std::optional +{ + auto x = 0.0; + auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); + if (ec == std::errc{}) + { + return x; } - - export constexpr auto parse_float(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional { - auto x = 0.0; - auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); - if (ec == std::errc{}) { - return x; - } else { - return std::nullopt; - } + else + { + return std::nullopt; } +} + +export constexpr auto parse_float( + std::string_view s, + std::chars_format fmt = std::chars_format::general) noexcept + -> std::optional +{ + auto x = 0.0; + auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); + if (ec == std::errc{}) + { + return x; + } + else + { + return std::nullopt; + } +} - export template - class aolist : public std::enable_shared_from_this> { - T v_; - std::shared_ptr const> next_; - - explicit aolist(T v, std::shared_ptr const> next) - : v_{v}, next_{next} - {} - - public: - static auto nil() -> std::shared_ptr> { - return nullptr; - } - - static auto cons(T v, std::shared_ptr const> l) -> std::shared_ptr> { - return std::shared_ptr>{new aolist{v, l}}; - } +export template +class aolist : public std::enable_shared_from_this> +{ + T v_; + std::shared_ptr const> next_; - auto next() const -> std::shared_ptr const> { - return next_; - } + explicit aolist(T v, std::shared_ptr const> next) + : v_{v}, next_{next} + { + } - auto value() const noexcept -> T const& { - return v_; - } - }; +public: + static auto nil() -> std::shared_ptr> { return nullptr; } - export constexpr auto size_from_int(int x) -> std::optional { - static_assert(sizeof(int) <= sizeof(std::size_t), "cannot cast int to smaller size_t type"); - if (x < 0) - return std::nullopt; - return static_cast(x); + static auto cons(T v, std::shared_ptr const> l) + -> std::shared_ptr> + { + return std::shared_ptr>{new aolist{v, l}}; } - export constexpr auto int_from_size(std::size_t x) -> std::optional { - constexpr auto int_max = size_from_int(std::numeric_limits::max()); - static_assert(int_max.has_value()); - if (x > *int_max) - return std::nullopt; - return static_cast(x); + auto next() const -> std::shared_ptr const> { return next_; } + + auto value() const noexcept -> T const& { return v_; } +}; + +export constexpr auto size_from_int(int x) -> std::optional +{ + static_assert( + sizeof(int) <= sizeof(std::size_t), + "cannot cast int to smaller size_t type"); + if (x < 0) + return std::nullopt; + return static_cast(x); +} + +export constexpr auto int_from_size(std::size_t x) -> std::optional +{ + constexpr auto int_max = size_from_int(std::numeric_limits::max()); + static_assert(int_max.has_value()); + if (x > *int_max) + return std::nullopt; + return static_cast(x); +} + +export class zstring_view +{ + char const* s_; + std::size_t length_; + +public: + constexpr explicit zstring_view(char const* s, std::size_t length) + : s_{s}, length_{length} + { } - export class zstring_view { - char const* s_; - std::size_t length_; + constexpr zstring_view(char const* s) + : zstring_view{s, std::char_traits::length(s)} + { + } - public: - constexpr explicit zstring_view(char const* s, std::size_t length) : - s_{s}, length_{length} - {} + auto length() const -> std::size_t { return length_; } - constexpr zstring_view(char const* s) : - zstring_view{s, std::char_traits::length(s)} - {} + auto c_str() const -> char const* { return s_; } - auto length() const -> std::size_t { - return length_; - } + operator std::string_view() const { return std::string_view{s_, length_}; } - auto c_str() const -> char const* { - return s_; - } + operator char const*() const { return s_; } +}; - operator std::string_view() const { - return std::string_view{s_, length_}; - } +// View for null-terminated strings for which we might not +// necessarily be interested in the length. String length is only +// calculated on demand, at most once on each thread (on more than +// one thread when racing). May be null. +// +// It is undefined behavior to assign to a lazy_zstring_view when +// it is in use by other threads. +export class lazy_zstring_view +{ + static constexpr auto unset_length = std::numeric_limits::max(); - operator char const*() const { - return s_; - } - }; + char const* s_; // nullable + mutable std::atomic length_; + static_assert(decltype(length_)::is_always_lock_free); - // View for null-terminated strings for which we might not - // necessarily be interested in the length. String length is only - // calculated on demand, at most once on each thread (on more than - // one thread when racing). May be null. - // - // It is undefined behavior to assign to a lazy_zstring_view when - // it is in use by other threads. - export class lazy_zstring_view { - static constexpr auto unset_length = std::numeric_limits::max(); - - char const* s_; // nullable - mutable std::atomic length_; - static_assert(decltype(length_)::is_always_lock_free); - - public: - constexpr explicit lazy_zstring_view(char const* s) : - s_{s}, length_{s ? unset_length : 0} - {} - ~lazy_zstring_view() = default; - - lazy_zstring_view(lazy_zstring_view const& sv) noexcept - : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} - {} - lazy_zstring_view(lazy_zstring_view&& sv) noexcept - : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} - {} - auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& { - if (this != &rhs) { - s_ = rhs.s_; - length_.store(rhs.length_.load(std::memory_order_acquire), std::memory_order_release); - } - return *this; - } - auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& { - return *this = rhs; // use the copy assignment operator - } - - auto length() const noexcept -> std::size_t { - if (auto v = length_.load(std::memory_order_acquire); v != unset_length) - return v; - auto l = std::char_traits::length(s_); - length_.store(l, std::memory_order_release); - return l; - } +public: + constexpr explicit lazy_zstring_view(char const* s) + : s_{s}, length_{s ? unset_length : 0} + { + } - auto c_str() const noexcept -> char const* { - return s_; - } + ~lazy_zstring_view() = default; - operator std::string_view() const noexcept { - return std::string_view{s_, length()}; - } + lazy_zstring_view(lazy_zstring_view const& sv) noexcept + : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} + { + } - operator char const*() const noexcept { - return s_; - } + lazy_zstring_view(lazy_zstring_view&& sv) noexcept + : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} + { + } - auto operator==(std::string_view sv) const noexcept -> bool { - if (auto v = length_.load(std::memory_order_acquire); v != unset_length) - if (sv.length() != v) - return false; - auto res = std::char_traits::compare(s_, sv.data(), sv.length()); - if (res != 0) - return false; - // Strings are equal for sv.length() characters. - if (s_[sv.length()] != '\0') - return false; - // Strings are actually equal, and we have just found out the - // length of this string, so we might as well set it. - length_.store(sv.length(), std::memory_order_release); - return true; + auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& + { + if (this != &rhs) + { + s_ = rhs.s_; + length_.store( + rhs.length_.load(std::memory_order_acquire), + std::memory_order_release); } - }; - - constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept -> zstring_view { - return zstring_view{s, length}; + return *this; } - auto operator==(zstring_view lhs, zstring_view rhs) -> bool { - return std::string_view{lhs} == std::string_view{rhs}; + auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& + { + return *this = rhs; // use the copy assignment operator } - export constexpr auto split_on(std::string_view s, char c) -> std::pair> { - if (auto i = s.find(c); i != std::string_view::npos) - return std::make_pair(s.substr(0, i), s.substr(i + 1)); - return std::make_pair(s, std::nullopt); + auto length() const noexcept -> std::size_t + { + if (auto v = length_.load(std::memory_order_acquire); v != unset_length) + return v; + auto l = std::char_traits::length(s_); + length_.store(l, std::memory_order_release); + return l; } - export template - class not_null; - - export template - class not_null { - T* p_; - - struct guaranteed_not_null_t {}; - explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} + auto c_str() const noexcept -> char const* { return s_; } - public: - explicit not_null(T* p) - : p_{p} - { if (!p_) throw std::runtime_error{"not_null constructed with null pointer"}; } - ~not_null() = default; - - not_null(not_null const& other) = default; - not_null(not_null&& other) noexcept = default; - auto operator=(not_null const& rhs) noexcept -> not_null& = default; - auto operator=(not_null&& rhs) noexcept -> not_null& = default; + operator std::string_view() const noexcept + { + return std::string_view{s_, length()}; + } - friend auto make_not_null(T* p) noexcept -> std::optional { - if (p) return not_null(p, guaranteed_not_null_t{}); - else return std::nullopt; - } + operator char const*() const noexcept { return s_; } - [[nodiscard]] auto get() const noexcept -> T* { - return p_; - } + auto operator==(std::string_view sv) const noexcept -> bool + { + if (auto v = length_.load(std::memory_order_acquire); v != unset_length) + if (sv.length() != v) + return false; + auto res = std::char_traits::compare(s_, sv.data(), sv.length()); + if (res != 0) + return false; + // Strings are equal for sv.length() characters. + if (s_[sv.length()] != '\0') + return false; + // Strings are actually equal, and we have just found out the + // length of this string, so we might as well set it. + length_.store(sv.length(), std::memory_order_release); + return true; + } +}; + +constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept + -> zstring_view +{ + return zstring_view{s, length}; +} + +auto operator==(zstring_view lhs, zstring_view rhs) -> bool +{ + return std::string_view{lhs} == std::string_view{rhs}; +} + +export constexpr auto split_on(std::string_view s, char c) + -> std::pair> +{ + if (auto i = s.find(c); i != std::string_view::npos) + return std::make_pair(s.substr(0, i), s.substr(i + 1)); + return std::make_pair(s, std::nullopt); +} + +export template +class not_null; + +export template +class not_null +{ + T* p_; + + struct guaranteed_not_null_t + { + }; + explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} - auto operator*() const noexcept -> std::add_lvalue_reference_t { - return *p_; - } +public: + explicit not_null(T* p) : p_{p} + { + if (!p_) + throw std::runtime_error{"not_null constructed with null pointer"}; + } + ~not_null() = default; + + not_null(not_null const& other) = default; + not_null(not_null&& other) noexcept = default; + auto operator=(not_null const& rhs) noexcept -> not_null& = default; + auto operator=(not_null&& rhs) noexcept -> not_null& = default; + + friend auto make_not_null(T* p) noexcept -> std::optional + { + if (p) + return not_null(p, guaranteed_not_null_t{}); + else + return std::nullopt; + } - auto operator->() const noexcept -> T* { - return p_; - } - }; - export template explicit not_null(T*) -> not_null; + [[nodiscard]] auto get() const noexcept -> T* { return p_; } - export template<> - class not_null { - lazy_zstring_view s_; + auto operator*() const noexcept -> std::add_lvalue_reference_t + { + return *p_; + } - public: - explicit not_null(lazy_zstring_view s) - : s_{std::move(s)} - { if (!s_) throw std::runtime_error{"not_null constructed with null pointer"}; } + auto operator->() const noexcept -> T* { return p_; } +}; +export template +explicit not_null(T*) -> not_null; + +export template <> +class not_null +{ + lazy_zstring_view s_; + +public: + explicit not_null(lazy_zstring_view s) : s_{std::move(s)} + { + if (!s_) + throw std::runtime_error{"not_null constructed with null pointer"}; + } - [[nodiscard]] auto get() const noexcept -> lazy_zstring_view { - return s_; - } + [[nodiscard]] auto get() const noexcept -> lazy_zstring_view { return s_; } - operator lazy_zstring_view() const noexcept { return s_; } - operator std::string_view() const noexcept { return s_; } - operator char const*() const noexcept { return s_; } - }; - export explicit not_null(lazy_zstring_view s) -> not_null; + operator lazy_zstring_view() const noexcept { return s_; } + operator std::string_view() const noexcept { return s_; } + operator char const*() const noexcept { return s_; } +}; +export explicit not_null(lazy_zstring_view s) -> not_null; } // namespace routemon::util diff --git a/server/src/xml.cpp b/server/src/xml.cpp index cad42d1..c1b4358 100644 --- a/server/src/xml.cpp +++ b/server/src/xml.cpp @@ -9,53 +9,65 @@ import :xml; namespace routemon::xml { - auto qname_view::operator==(qname_view const& rhs) const -> bool { - return ns_uri == rhs.ns_uri && local == rhs.local; - } +auto qname_view::operator==(qname_view const& rhs) const -> bool +{ + return ns_uri == rhs.ns_uri && local == rhs.local; +} - executor::executor() : - p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)} - { - XML_SetUserData(p_, this); - XML_SetElementHandler(p_, handle_start_element, handle_end_element); - XML_SetCharacterDataHandler(p_, handle_character_data); - XML_SetProcessingInstructionHandler(p_, handle_processing_instructions); - XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref); - XML_SetNamespaceDeclHandler(p_, handle_start_namespace_decl, handle_end_namespace_decl); - XML_SetXmlDeclHandler(p_, handle_xml_decl); - } +executor::executor() : p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)} +{ + XML_SetUserData(p_, this); + XML_SetElementHandler(p_, handle_start_element, handle_end_element); + XML_SetCharacterDataHandler(p_, handle_character_data); + XML_SetProcessingInstructionHandler(p_, handle_processing_instructions); + XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref); + XML_SetNamespaceDeclHandler( + p_, handle_start_namespace_decl, handle_end_namespace_decl); + XML_SetXmlDeclHandler(p_, handle_xml_decl); +} - executor::~executor() { - XML_ParserFree(p_); - } +executor::~executor() { XML_ParserFree(p_); } - auto executor::start() -> void { - continuation_.resume(); - if (ex_) std::rethrow_exception(ex_); - } +auto executor::start() -> void +{ + continuation_.resume(); + if (ex_) + std::rethrow_exception(ex_); +} - auto executor::read(std::string_view xml, bool is_final) -> void { - if (ex_) - throw std::runtime_error{"refusing to restart parser that was thrown in"}; - // TODO: narrow_cast - if (auto s = XML_Parse(p_, xml.data(), static_cast(xml.size()), is_final); s != XML_STATUS_OK) { - auto errc = XML_GetErrorCode(p_); - if (errc == XML_ERROR_ABORTED) { - assert(ex_); - std::rethrow_exception(ex_); - } else { - throw std::runtime_error{std::format("failed to parse XML: {}", XML_ErrorString(errc))}; - } +auto executor::read(std::string_view xml, bool is_final) -> void +{ + if (ex_) + throw std::runtime_error{"refusing to restart parser that was thrown in"}; + // TODO: narrow_cast + if (auto s = + XML_Parse(p_, xml.data(), static_cast(xml.size()), is_final); + s != XML_STATUS_OK) + { + auto errc = XML_GetErrorCode(p_); + if (errc == XML_ERROR_ABORTED) + { + assert(ex_); + std::rethrow_exception(ex_); + } + else + { + throw std::runtime_error{std::format( + "failed to parse XML: {}", XML_ErrorString(errc))}; } } +} - auto executor::end() -> void { - if (ex_) - throw std::runtime_error{"refusing to restart parser that was thrown in"}; - ev_ = eof_event{}; - advance_ = false; - while (continuation_) continuation_.resume(); - if (ex_) std::rethrow_exception(ex_); - } +auto executor::end() -> void +{ + if (ex_) + throw std::runtime_error{"refusing to restart parser that was thrown in"}; + ev_ = eof_event{}; + advance_ = false; + while (continuation_) + continuation_.resume(); + if (ex_) + std::rethrow_exception(ex_); +} } // namespace routemon::xml diff --git a/server/src/xml.cppm b/server/src/xml.cppm index 957f149..ff27c5c 100644 --- a/server/src/xml.cppm +++ b/server/src/xml.cppm @@ -23,610 +23,808 @@ static_assert(std::is_same_v); namespace routemon::xml { - struct qname_view { - std::string_view ns_uri; - std::string_view local; +struct qname_view +{ + std::string_view ns_uri; + std::string_view local; - auto operator==(qname_view const& rhs) const -> bool; - }; + auto operator==(qname_view const& rhs) const -> bool; +}; - namespace detail { +namespace detail { - constexpr auto qname_sep = '\xFF'; - auto split_name(char const* name) noexcept -> qname_view { - auto [l, r] = util::split_on(name, qname_sep); - if (r) return { .ns_uri = l, .local = *r }; - else return { .ns_uri = std::string_view{}, .local = l }; - } +constexpr auto qname_sep = '\xFF'; +auto split_name(char const* name) noexcept -> qname_view +{ + auto [l, r] = util::split_on(name, qname_sep); + if (r) + return {.ns_uri = l, .local = *r}; + else + return {.ns_uri = std::string_view{}, .local = l}; +} - } // namespace detail +} // namespace detail - class attribute_view_iterator { - std::string_view default_ns_uri_; - char const* const* attrs_; +class attribute_view_iterator +{ + std::string_view default_ns_uri_; + char const* const* attrs_; - auto advance() -> void { - attrs_ += 2; - } + auto advance() -> void { attrs_ += 2; } - public: - using difference_type = std::ptrdiff_t; - using value_type = std::pair; +public: + using difference_type = std::ptrdiff_t; + using value_type = std::pair; - struct sentinel { - friend constexpr auto operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool { - return !*it.attrs_; - } - }; - - inline explicit attribute_view_iterator(std::string_view default_ns_uri, char const* const* attrs) - : default_ns_uri_{default_ns_uri}, attrs_{attrs} - {} - - inline auto operator*() const -> std::pair { - if (!*attrs_) - throw std::runtime_error{"end of attribute list"}; - auto qname = detail::split_name(attrs_[0]); - if (qname.ns_uri.empty()) - qname.ns_uri = default_ns_uri_; - return std::make_pair(qname, attrs_[1]); + struct sentinel + { + friend constexpr auto + operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool + { + return !*it.attrs_; } + }; - // Pre-increment - inline auto operator++() -> attribute_view_iterator& { - advance(); - return *this; - } + inline explicit attribute_view_iterator( + std::string_view default_ns_uri, char const* const* attrs) + : default_ns_uri_{default_ns_uri}, attrs_{attrs} + { + } - // Post-increment - inline auto operator++(int) -> attribute_view_iterator { - auto pre = *this; - advance(); - return pre; - } - }; - static_assert(std::input_iterator); + inline auto operator*() const -> std::pair + { + if (!*attrs_) + throw std::runtime_error{"end of attribute list"}; + auto qname = detail::split_name(attrs_[0]); + if (qname.ns_uri.empty()) + qname.ns_uri = default_ns_uri_; + return std::make_pair(qname, attrs_[1]); + } - class attribute_view : std::ranges::view_base { - std::string_view default_ns_uri_; - char const* const* attrs_; + // Pre-increment + inline auto operator++() -> attribute_view_iterator& + { + advance(); + return *this; + } - public: - inline explicit attribute_view(std::string_view default_ns_uri, char const** attrs) - : default_ns_uri_{default_ns_uri}, attrs_{const_cast(attrs)} - {} + // Post-increment + inline auto operator++(int) -> attribute_view_iterator + { + auto pre = *this; + advance(); + return pre; + } +}; +static_assert(std::input_iterator); + +class attribute_view : std::ranges::view_base +{ + std::string_view default_ns_uri_; + char const* const* attrs_; + +public: + inline explicit attribute_view( + std::string_view default_ns_uri, char const** attrs) + : default_ns_uri_{default_ns_uri}, + attrs_{const_cast(attrs)} + { + } - [[nodiscard]] inline auto begin() const -> attribute_view_iterator { - return attribute_view_iterator{default_ns_uri_, attrs_}; - } + [[nodiscard]] inline auto begin() const -> attribute_view_iterator + { + return attribute_view_iterator{default_ns_uri_, attrs_}; + } - [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel { - return {}; - } + [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel + { + return {}; + } - inline auto lookup(qname_view want) -> std::optional> { - for (auto const& [name, v] : *this) { - if (name == want) { - return util::not_null{util::lazy_zstring_view{v}}; - } + inline auto lookup(qname_view want) + -> std::optional> + { + for (auto const& [name, v] : *this) + { + if (name == want) + { + return util::not_null{util::lazy_zstring_view{v}}; } - return std::nullopt; } - }; - static_assert(std::ranges::input_range); + return std::nullopt; + } +}; +static_assert(std::ranges::input_range); - template class promise; +template +class promise; - template - struct [[clang::coro_await_elidable, clang::coro_return_type]] parser { - using promise_type = promise; - using result_type = promise_type::result_type; - using handle_type = std::coroutine_handle; +template +struct [[clang::coro_await_elidable, clang::coro_return_type]] parser +{ + using promise_type = promise; + using result_type = promise_type::result_type; + using handle_type = std::coroutine_handle; - private: - handle_type h_; +private: + handle_type h_; - public: - explicit parser(handle_type h) - : h_{h} - { assert(h); } +public: + explicit parser(handle_type h) : h_{h} { assert(h); } - parser(const parser&) = delete; - parser(parser&& c) noexcept - : h_{std::exchange(c.h_, nullptr)} - {} - auto operator=(const parser&) -> parser& = delete; - auto operator=(parser&&) -> parser& = delete; + parser(const parser&) = delete; + parser(parser&& c) noexcept : h_{std::exchange(c.h_, nullptr)} {} + auto operator=(const parser&) -> parser& = delete; + auto operator=(parser&&) -> parser& = delete; - [[nodiscard]] auto promise() const -> promise_type& { - return h_.promise(); - } - - ~parser() { - if (h_) h_.destroy(); - } - }; + [[nodiscard]] auto promise() const -> promise_type& { return h_.promise(); } - struct start_element_event { - qname_view name; - attribute_view attrs; - }; - struct end_element_event { - qname_view name; - }; - struct character_data_event { - std::string_view data; - }; - struct processing_instructions_event { - util::lazy_zstring_view target; - util::lazy_zstring_view data; - }; - struct xml_decl_event { - util::lazy_zstring_view version; - util::lazy_zstring_view encoding; - std::optional standalone; - }; - struct eof_event {}; - using event = std::variant; - template - concept event_type = requires(event ev) { std::get(ev); }; - - class executor; - using executor_ref = util::not_null; - - struct current_event_t { - executor_ref executor; - }; - auto current_event(executor_ref executor) -> current_event_t { - return current_event_t{executor}; + ~parser() + { + if (h_) + h_.destroy(); + } +}; + +struct start_element_event +{ + qname_view name; + attribute_view attrs; +}; + +struct end_element_event +{ + qname_view name; +}; + +struct character_data_event +{ + std::string_view data; +}; + +struct processing_instructions_event +{ + util::lazy_zstring_view target; + util::lazy_zstring_view data; +}; + +struct xml_decl_event +{ + util::lazy_zstring_view version; + util::lazy_zstring_view encoding; + std::optional standalone; +}; + +struct eof_event +{ +}; + +using event = std::variant< + start_element_event, end_element_event, character_data_event, + processing_instructions_event, xml_decl_event, eof_event>; + +template +concept event_type = requires(event ev) { std::get(ev); }; + +class executor; +using executor_ref = util::not_null; + +struct current_event_t +{ + executor_ref executor; +}; + +auto current_event(executor_ref executor) -> current_event_t +{ + return current_event_t{executor}; +} + +class promise_base +{ + executor_ref executor_; + std::coroutine_handle continuation_ = nullptr; + +public: + // Not having this constructor marked inline messes with coroutine + // HALO. (Hours 'wasted': many) + inline explicit promise_base(executor_ref executor) : executor_{executor} {} + + [[nodiscard]] inline auto executor() const -> executor& { return *executor_; } + + inline auto base_handle() -> std::coroutine_handle + { + return std::coroutine_handle::from_promise(*this); } - class promise_base { - executor_ref executor_; - std::coroutine_handle continuation_ = nullptr; - - public: - // Not having this constructor marked inline messes with coroutine - // HALO. (Hours 'wasted': many) - inline explicit promise_base(executor_ref executor) - : executor_{executor} - {} - - [[nodiscard]] inline auto executor() const -> executor& { - return *executor_; - } - - inline auto base_handle() -> std::coroutine_handle { - return std::coroutine_handle::from_promise(*this); - } - - [[nodiscard]] inline auto continuation() const -> std::coroutine_handle { - return continuation_; - } - inline auto set_continuation(std::coroutine_handle c) -> void { - continuation_ = c; - } - }; - - struct position { - std::size_t line; - std::size_t col; - }; - - class executor { - XML_Parser p_; - std::exception_ptr ex_ = nullptr; - std::coroutine_handle continuation_ = nullptr; - std::vector> default_namespace_; - std::unordered_map> namespaces_; - std::optional ev_; - bool advance_ = true; - - inline auto try_handle_event(event ev) noexcept -> void { - assert(!ex_); - - try { - ev_ = std::move(ev); - } catch (...) { - ex_ = std::current_exception(); - return; - } - advance_ = false; - if (!continuation_) { - // Parser returned (all subparsers are done) and has set the continuation to nullptr. - if (auto s = XML_StopParser(p_, /* resumable */ false); s != XML_STATUS_OK) { - ex_ = std::make_exception_ptr(std::runtime_error{"unexpected error when stopping XML parser"}); - return; - } - ex_ = std::make_exception_ptr(std::runtime_error{"parser did not consume entire XML document"}); + [[nodiscard]] inline auto continuation() const + -> std::coroutine_handle + { + return continuation_; + } + inline auto set_continuation(std::coroutine_handle c) -> void + { + continuation_ = c; + } +}; + +struct position +{ + std::size_t line; + std::size_t col; +}; + +class executor +{ + XML_Parser p_; + std::exception_ptr ex_ = nullptr; + std::coroutine_handle continuation_ = nullptr; + std::vector> default_namespace_; + std::unordered_map> namespaces_; + std::optional ev_; + bool advance_ = true; + + inline auto try_handle_event(event ev) noexcept -> void + { + assert(!ex_); + + try + { + ev_ = std::move(ev); + } + catch (...) + { + ex_ = std::current_exception(); + return; + } + advance_ = false; + if (!continuation_) + { + // Parser returned (all subparsers are done) and has set the + // continuation to nullptr. + if (auto s = XML_StopParser(p_, /* resumable */ false); + s != XML_STATUS_OK) + { + ex_ = std::make_exception_ptr( + std::runtime_error{"unexpected error when stopping XML parser"}); return; } - continuation_.resume(); - if (ex_) { - // Not sure if it's useful to report this error. - std::ignore = XML_StopParser(p_, /* resumable */ false); - } + ex_ = std::make_exception_ptr( + std::runtime_error{"parser did not consume entire XML document"}); + return; + } + continuation_.resume(); + if (ex_) + { + // Not sure if it's useful to report this error. + std::ignore = XML_StopParser(p_, /* resumable */ false); } + } - static auto handle_start_element(void* ctx, char const* name, char const** attrs) noexcept -> void { - auto qname = detail::split_name(name); - static_cast(ctx)->try_handle_event(start_element_event{ - .name = qname, - .attrs = attribute_view{qname.ns_uri, attrs}, + static auto + handle_start_element(void* ctx, char const* name, char const** attrs) noexcept + -> void + { + auto qname = detail::split_name(name); + static_cast(ctx)->try_handle_event( + start_element_event{ + .name = qname, + .attrs = attribute_view{qname.ns_uri, attrs}, }); - } - static auto handle_end_element(void* ctx, char const* name) noexcept -> void { - static_cast(ctx)->try_handle_event(end_element_event{ - .name = detail::split_name(name), + } + + static auto handle_end_element(void* ctx, char const* name) noexcept -> void + { + static_cast(ctx)->try_handle_event( + end_element_event{ + .name = detail::split_name(name), }); - } - static auto handle_character_data(void* ctx, char const* s, int len) noexcept -> void { - static_cast(ctx)->try_handle_event(character_data_event{ - .data = std::string_view{s, static_cast(len)}, + } + + static auto handle_character_data(void* ctx, char const* s, int len) noexcept + -> void + { + static_cast(ctx)->try_handle_event( + character_data_event{ + .data = std::string_view{s, static_cast(len)}, }); - } - static auto handle_processing_instructions(void* ctx, char const* target, char const* data) noexcept -> void { - static_cast(ctx)->try_handle_event(processing_instructions_event{ - .target = util::lazy_zstring_view{target}, - .data = util::lazy_zstring_view{data}, + } + + static auto handle_processing_instructions( + void* ctx, char const* target, char const* data) noexcept -> void + { + static_cast(ctx)->try_handle_event( + processing_instructions_event{ + .target = util::lazy_zstring_view{target}, + .data = util::lazy_zstring_view{data}, }); + } + + static auto handle_external_entity_ref( + XML_Parser, char const* /* context */, char const* /* base */, + char const* /* system_id */, char const* /* public_id */) noexcept -> int + { + return XML_STATUS_ERROR; + } + + static auto handle_start_namespace_decl( + void* ctx, char const* prefix, char const* uri) noexcept -> void + { + if (prefix) + { + static_cast(ctx) + ->namespaces_[std::string_view{prefix}] + .emplace_back(uri); } - static auto handle_external_entity_ref(XML_Parser, char const* /* context */, char const* /* base */, char const* /* system_id */, char const* /* public_id */) noexcept -> int { - return XML_STATUS_ERROR; + else + { + static_cast(ctx)->default_namespace_.push_back( + uri ? std::make_optional(uri) : std::nullopt); } - static auto handle_start_namespace_decl(void* ctx, char const* prefix, char const* uri) noexcept -> void { - if (prefix) { - static_cast(ctx)->namespaces_[std::string_view{prefix}].emplace_back(uri); - } else { - static_cast(ctx)->default_namespace_.push_back(uri ? std::make_optional(uri) : std::nullopt); - } + } + + static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept + -> void + { + if (prefix) + { + static_cast(ctx) + ->namespaces_[std::string_view{prefix}] + .pop_back(); } - static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept -> void { - if (prefix) { - static_cast(ctx)->namespaces_[std::string_view{prefix}].pop_back(); - } else { - static_cast(ctx)->default_namespace_.pop_back(); - } + else + { + static_cast(ctx)->default_namespace_.pop_back(); } - static auto handle_xml_decl(void * ctx, char const* version, char const* encoding, int standalone) noexcept -> void { - static_cast(ctx)->try_handle_event(xml_decl_event{ - .version = util::lazy_zstring_view{version}, - .encoding = util::lazy_zstring_view{encoding}, - .standalone = standalone < 0 ? std::nullopt : std::make_optional(standalone > 0), + } + + static auto handle_xml_decl( + void* ctx, char const* version, char const* encoding, + int standalone) noexcept -> void + { + static_cast(ctx)->try_handle_event( + xml_decl_event{ + .version = util::lazy_zstring_view{version}, + .encoding = util::lazy_zstring_view{encoding}, + .standalone = standalone < 0 ? std::nullopt + : std::make_optional(standalone > 0), }); - } + } - inline auto advance_flag() -> bool { - return advance_; - } - inline auto set_exception(std::exception_ptr ex) -> void { - ex_ = std::move(ex); - } - [[nodiscard]] inline auto take_exception() -> std::exception_ptr { - return std::exchange(ex_, nullptr); - } + inline auto advance_flag() -> bool { return advance_; } - template friend class promise; + inline auto set_exception(std::exception_ptr ex) -> void + { + ex_ = std::move(ex); + } - public: - executor(); + [[nodiscard]] inline auto take_exception() -> std::exception_ptr + { + return std::exchange(ex_, nullptr); + } - executor(executor const&) = delete; - executor(executor&&) = delete; - auto operator=(executor const&) -> executor& = delete; - auto operator=(executor&&) -> executor& = delete; + template + friend class promise; - ~executor(); +public: + executor(); - inline auto set_continuation(std::coroutine_handle c) -> void { - continuation_ = c; - } - inline auto set_advance_flag() -> void { - if (!ev_ || !std::holds_alternative(ev_.value())) { - advance_ = true; - } - } - inline auto event() const -> std::optional const& { - return ev_; - } - inline auto resolve_namespace(std::string_view prefix) -> std::optional { - if (auto it = namespaces_.find(prefix); it != namespaces_.end() && !it->second.empty()) - return it->second.back(); - return std::nullopt; + executor(executor const&) = delete; + executor(executor&&) = delete; + auto operator=(executor const&) -> executor& = delete; + auto operator=(executor&&) -> executor& = delete; + + ~executor(); + + inline auto set_continuation(std::coroutine_handle c) -> void + { + continuation_ = c; + } + + inline auto set_advance_flag() -> void + { + if (!ev_ || !std::holds_alternative(ev_.value())) + { + advance_ = true; } - [[nodiscard]] inline auto position() -> position { - return { + } + + inline auto event() const -> std::optional const& { return ev_; } + + inline auto resolve_namespace(std::string_view prefix) + -> std::optional + { + if (auto it = namespaces_.find(prefix); + it != namespaces_.end() && !it->second.empty()) + return it->second.back(); + return std::nullopt; + } + + [[nodiscard]] inline auto position() -> position + { + return { .line = XML_GetCurrentLineNumber(p_), .col = XML_GetCurrentColumnNumber(p_), - }; - } + }; + } - auto start() -> void; - auto read(std::string_view xml, bool is_final) -> void; - auto end() -> void; - }; + auto start() -> void; + auto read(std::string_view xml, bool is_final) -> void; + auto end() -> void; +}; - template - class promise_returnable : public promise_base { - std::optional returned_value_; +template +class promise_returnable : public promise_base +{ + std::optional returned_value_; - public: - using result_type = T; - using promise_base::promise_base; +public: + using result_type = T; + using promise_base::promise_base; - template - auto return_value(U&& v) -> void { - returned_value_.emplace(std::forward(v)); - } - auto returned_value() -> T&& { - if (!returned_value_) - throw std::runtime_error{"XML coroutine did not return"}; - return std::forward(returned_value_.value()); - } - }; + template + auto return_value(U&& v) -> void + { + returned_value_.emplace(std::forward(v)); + } - template<> - class promise_returnable : public promise_base { - public: - using result_type = void; - using promise_base::promise_base; + auto returned_value() -> T&& + { + if (!returned_value_) + throw std::runtime_error{"XML coroutine did not return"}; + return std::forward(returned_value_.value()); + } +}; + +template <> +class promise_returnable : public promise_base +{ +public: + using result_type = void; + using promise_base::promise_base; + + auto return_void() -> void {} +}; + +template +class promise : public promise_returnable +{ +public: + // Called with all the coroutine's arguments. + // Ignoring all but the first argument, which should be the executor. + template + explicit promise(executor_ref executor, Args&&...) + : promise_returnable{executor} + { + } - auto return_void() -> void {} - }; + auto handle() -> std::coroutine_handle> + { + return {parser::handle_type::from_promise(*this)}; + } - template - class promise : public promise_returnable { - public: - // Called with all the coroutine's arguments. - // Ignoring all but the first argument, which should be the executor. - template - explicit promise(executor_ref executor, Args&&...) - : promise_returnable{executor} - {} - - auto handle() -> std::coroutine_handle> { - return {parser::handle_type::from_promise(*this)}; - } + auto get_return_object() -> parser { return parser{handle()}; } - auto get_return_object() -> parser { - return parser{handle()}; - } + auto initial_suspend() { return std::suspend_always{}; } - auto initial_suspend() { - return std::suspend_always{}; - } - auto final_suspend() noexcept { - struct awaiter { - std::coroutine_handle<> h_; - - [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } - auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> { return h_; } - constexpr auto await_resume() const noexcept -> void { return; } - }; - if (this->continuation()) { - return awaiter{this->continuation()}; - } else { - this->executor().set_continuation(nullptr); - return awaiter{std::noop_coroutine()}; + auto final_suspend() noexcept + { + struct awaiter + { + std::coroutine_handle<> h_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool + { + return false; } + auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> + { + return h_; + } + constexpr auto await_resume() const noexcept -> void { return; } + }; + if (this->continuation()) + { + return awaiter{this->continuation()}; } - - auto unhandled_exception() -> void { - this->executor().set_exception(std::current_exception()); + else + { + this->executor().set_continuation(nullptr); + return awaiter{std::noop_coroutine()}; } + } - auto await_transform(current_event_t const& req) { - struct awaiter { - executor_ref executor_; + auto unhandled_exception() -> void + { + this->executor().set_exception(std::current_exception()); + } - [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { - return !executor_->advance_flag(); - } - auto await_suspend(std::coroutine_handle> h) -> void { - executor_->set_continuation(h.promise().base_handle()); - } - [[nodiscard]] auto await_resume() const -> event { - assert(executor_->event()); - return executor_->event().value(); - } - }; - return awaiter{req.executor}; - } + auto await_transform(current_event_t const& req) + { + struct awaiter + { + executor_ref executor_; - template - auto await_transform(parser const& coro) { - struct [[clang::coro_await_elidable]] awaiter { - util::not_null*> next_; + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool + { + return !executor_->advance_flag(); + } + auto await_suspend(std::coroutine_handle> h) -> void + { + executor_->set_continuation(h.promise().base_handle()); + } + [[nodiscard]] auto await_resume() const -> event + { + assert(executor_->event()); + return executor_->event().value(); + } + }; + return awaiter{req.executor}; + } - [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } - auto await_suspend(std::coroutine_handle> h) -> std::coroutine_handle<> { - // Passed coroutine handle will be the same as parser::handle_type::from_promise(*this) - next_->set_continuation(h.promise().base_handle()); - return next_->handle(); + template + auto await_transform(parser const& coro) + { + struct [[clang::coro_await_elidable]] awaiter + { + util::not_null*> next_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool + { + return false; + } + auto await_suspend(std::coroutine_handle> h) + -> std::coroutine_handle<> + { + // Passed coroutine handle will be the same as + // parser::handle_type::from_promise(*this) + next_->set_continuation(h.promise().base_handle()); + return next_->handle(); + } + auto await_resume() -> U + { + // Promise is still valid since coroutine frame is still alive + // (and suspended): control was transferred back to this + // coroutine via symmetric transfer in final_suspend(). Assuming + // that the destructor for coro still needs to run. + if (auto ex = next_->executor().take_exception()) + { + std::rethrow_exception(ex); } - auto await_resume() -> U { - // Promise is still valid since coroutine frame is still alive (and suspended): - // control was transferred back to this coroutine via symmetric transfer in - // final_suspend(). Assuming that the destructor for coro still needs to run. - if (auto ex = next_->executor().take_exception()) { - std::rethrow_exception(ex); - } else { - if constexpr (!std::is_void_v) { - return std::move(next_->returned_value()); - } + else + { + if constexpr (!std::is_void_v) + { + return std::move(next_->returned_value()); } } - }; - return awaiter{util::not_null{&coro.promise()}}; - } - }; + } + }; + return awaiter{util::not_null{&coro.promise()}}; + } +}; - // Helpers for handling XML documents. Non-polymorphic functions - // should be marked inline to allow HALO across TU boundaries. +// Helpers for handling XML documents. Non-polymorphic functions +// should be marked inline to allow HALO across TU boundaries. - template - concept unconstrained = true; +template +concept unconstrained = true; - template concept C> - concept parser_of = requires { - typename T::result_type; - requires std::same_as, T>; - requires C; - }; +template concept C> +concept parser_of = requires { + typename T::result_type; + requires std::same_as, T>; + requires C; +}; - template T> - using parser_result_t = T::result_type; +template T> +using parser_result_t = T::result_type; - template - concept parser_invocable = std::invocable && parser_of, unconstrained>; +template +concept parser_invocable = + std::invocable + && parser_of, unconstrained>; - template +template requires parser_invocable - using parser_invoke_result_t = parser_result_t>; - - template auto expect_event(executor_ref e) -> parser { - auto ev = co_await current_event(e); - if (!std::holds_alternative(ev)) { - auto pos = e->position(); - throw std::runtime_error{std::format("at {}:{}: unexpected event type, have {}", pos.line, pos.col, ev.index())}; - } - e->set_advance_flag(); - co_return std::get(ev); +using parser_invoke_result_t = + parser_result_t>; + +template +auto expect_event(executor_ref e) -> parser +{ + auto ev = co_await current_event(e); + if (!std::holds_alternative(ev)) + { + auto pos = e->position(); + throw std::runtime_error{std::format( + "at {}:{}: unexpected event type, have {}", pos.line, pos.col, + ev.index())}; } - - inline auto expect_start_element(executor_ref e, qname_view want) -> parser { - auto ev = co_await expect_event(e); - if (ev.name != want) { - auto pos = e->position(); - throw std::runtime_error{std::format("at {}:{}: unexpected element started", pos.line, pos.col)}; - } - co_return ev.attrs; + e->set_advance_flag(); + co_return std::get(ev); +} + +inline auto expect_start_element(executor_ref e, qname_view want) + -> parser +{ + auto ev = co_await expect_event(e); + if (ev.name != want) + { + auto pos = e->position(); + throw std::runtime_error{std::format( + "at {}:{}: unexpected element started", pos.line, pos.col)}; } - - inline auto allow_start_element(executor_ref e, qname_view want) -> parser> { - auto ev = co_await current_event(e); - if (auto const* pev = std::get_if(&ev)) { - if (pev->name == want) { - e->set_advance_flag(); - co_return pev->attrs; - } + co_return ev.attrs; +} + +inline auto allow_start_element(executor_ref e, qname_view want) + -> parser> +{ + auto ev = co_await current_event(e); + if (auto const* pev = std::get_if(&ev)) + { + if (pev->name == want) + { + e->set_advance_flag(); + co_return pev->attrs; } - co_return std::nullopt; } + co_return std::nullopt; +} - inline auto expect_end_element(executor_ref e, qname_view want) -> parser { - auto ev = co_await expect_event(e); - if (ev.name != want) { - throw std::runtime_error{"unexpected element ended"}; - } +inline auto expect_end_element(executor_ref e, qname_view want) -> parser +{ + auto ev = co_await expect_event(e); + if (ev.name != want) + { + throw std::runtime_error{"unexpected element ended"}; } +} - inline auto ignore_whitespace(executor_ref e) -> parser { - auto all_whitespace = [](std::string_view s) -> bool { - for (auto c : s) - if (c != ' ' && c != '\r' && c != '\n' && c != '\t') - return false; - return true; - }; +inline auto ignore_whitespace(executor_ref e) -> parser +{ + auto all_whitespace = [](std::string_view s) -> bool + { + for (auto c : s) + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') + return false; + return true; + }; - while (true) { - auto ev = co_await current_event(e); - if (auto const* pev = std::get_if(&ev); pev && all_whitespace(pev->data)) { - e->set_advance_flag(); - } else { - co_return; - } + while (true) + { + auto ev = co_await current_event(e); + if (auto const* pev = std::get_if(&ev); + pev && all_whitespace(pev->data)) + { + e->set_advance_flag(); + } + else + { + co_return; } } +} - auto expect_element(executor_ref e, qname_view want, parser_invocable auto p) +auto expect_element( + executor_ref e, qname_view want, + parser_invocable auto p) -> parser> +{ + co_await ignore_whitespace(e); + auto attrs = co_await expect_start_element(e, want); + auto&& res = co_await p(e, attrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return std::forward< + parser_invoke_result_t>(res); +} + +auto allow_element( + executor_ref e, qname_view want, + parser_invocable auto p) + -> parser>> +{ + co_await ignore_whitespace(e); + if (auto mattrs = co_await allow_start_element(e, want)) { - co_await ignore_whitespace(e); - auto attrs = co_await expect_start_element(e, want); - auto&& res = co_await p(e, attrs); + auto&& res = co_await p(e, *mattrs); co_await expect_end_element(e, want); co_await ignore_whitespace(e); - co_return std::forward>(res); + co_return std::make_optional( + std::forward< + parser_invoke_result_t>( + res)); } - - auto allow_element(executor_ref e, qname_view want, parser_invocable auto p) - -> parser>> + co_return std::nullopt; +} + +auto allow_element( + executor_ref e, qname_view want, + parser_invocable auto p) -> parser + requires std::is_void_v< + parser_invoke_result_t> +{ + co_await ignore_whitespace(e); + if (auto mattrs = co_await allow_start_element(e, want)) { + co_await p(e, *mattrs); + co_await expect_end_element(e, want); co_await ignore_whitespace(e); - if (auto mattrs = co_await allow_start_element(e, want)) { - auto&& res = co_await p(e, *mattrs); - co_await expect_end_element(e, want); - co_await ignore_whitespace(e); - co_return std::make_optional(std::forward>(res)); - } - co_return std::nullopt; + co_return true; } - - auto allow_element(executor_ref e, qname_view want, parser_invocable auto p) - -> parser - requires std::is_void_v> + co_return false; +} + +inline auto +ignore_contents(executor_ref e, std::optional muntil = std::nullopt) + -> parser +{ + std::size_t depth = 0; + while (true) { - co_await ignore_whitespace(e); - if (auto mattrs = co_await allow_start_element(e, want)) { - co_await p(e, *mattrs); - co_await expect_end_element(e, want); - co_await ignore_whitespace(e); - co_return true; - } - co_return false; - } - - inline auto ignore_contents(executor_ref e, std::optional muntil = std::nullopt) -> parser { - std::size_t depth = 0; - while (true) { - auto ev = co_await current_event(e); - if (auto* pev = std::get_if(&ev)) { - if (depth == 0 && muntil && pev->name == *muntil) { - co_return; - } else { - depth++; - } - } else if (std::holds_alternative(ev)) { - if (depth == 0) { - co_return; - } else { - depth--; - } + auto ev = co_await current_event(e); + if (auto* pev = std::get_if(&ev)) + { + if (depth == 0 && muntil && pev->name == *muntil) + { + co_return; + } + else + { + depth++; } - e->set_advance_flag(); } - } - inline auto ignore_element_contents(executor_ref e, attribute_view) -> parser { - co_await ignore_contents(e); - } - - inline auto read_string(executor_ref e) -> parser { - std::string s; - while (true) { - auto ev = co_await current_event(e); - if (auto* pev = std::get_if(&ev)) { - e->set_advance_flag(); - s += pev->data; - } else { - co_return s; + else if (std::holds_alternative(ev)) + { + if (depth == 0) + { + co_return; + } + else + { + depth--; } } + e->set_advance_flag(); } - inline auto read_string_contents(executor_ref e, attribute_view) -> parser { - co_return co_await read_string(e); - } - - template - auto hohalo() { - return [](Args&&... args) -> std::invoke_result_t { - co_return co_await f(std::forward(args)...); - }; +} +inline auto ignore_element_contents(executor_ref e, attribute_view) + -> parser +{ + co_await ignore_contents(e); +} + +inline auto read_string(executor_ref e) -> parser +{ + std::string s; + while (true) + { + auto ev = co_await current_event(e); + if (auto* pev = std::get_if(&ev)) + { + e->set_advance_flag(); + s += pev->data; + } + else + { + co_return s; + } } +} +inline auto read_string_contents(executor_ref e, attribute_view) + -> parser +{ + co_return co_await read_string(e); +} + +template +auto hohalo() +{ + return []( + Args&&... args) -> std::invoke_result_t + { co_return co_await f(std::forward(args)...); }; +} } // namespace routemon::xml -- cgit v1.3