diff options
Diffstat (limited to 'server/src')
| -rw-r--r-- | server/src/api.cpp | 206 | ||||
| -rw-r--r-- | server/src/api.cppm | 79 | ||||
| -rw-r--r-- | server/src/config.cpp | 167 | ||||
| -rw-r--r-- | server/src/config.cppm | 39 | ||||
| -rw-r--r-- | server/src/database.cppm | 37 | ||||
| -rw-r--r-- | server/src/datex2.cppm | 296 | ||||
| -rw-r--r-- | server/src/geo.cppm | 40 | ||||
| -rw-r--r-- | server/src/gpx.cpp | 189 | ||||
| -rw-r--r-- | server/src/gpx.cppm | 43 | ||||
| -rw-r--r-- | server/src/http_client.cppm | 62 | ||||
| -rw-r--r-- | server/src/http_common.cppm | 139 | ||||
| -rw-r--r-- | server/src/http_server.cppm | 661 | ||||
| -rw-r--r-- | server/src/locale.cppm | 245 | ||||
| -rw-r--r-- | server/src/log.cppm | 148 | ||||
| -rw-r--r-- | server/src/main.cpp | 109 | ||||
| -rw-r--r-- | server/src/problem.cppm | 60 | ||||
| -rw-r--r-- | server/src/req_ctx.cppm | 51 | ||||
| -rw-r--r-- | server/src/routemon.cppm | 11 | ||||
| -rw-r--r-- | server/src/rwgps.cppm | 137 | ||||
| -rw-r--r-- | server/src/sqlite3.cppm | 257 | ||||
| -rw-r--r-- | server/src/srv.cppm | 221 | ||||
| -rw-r--r-- | server/src/time.cppm | 205 | ||||
| -rw-r--r-- | server/src/trace.cppm | 79 | ||||
| -rw-r--r-- | server/src/util.cppm | 254 | ||||
| -rw-r--r-- | server/src/xml.cpp | 61 | ||||
| -rw-r--r-- | server/src/xml.cppm | 632 |
26 files changed, 4428 insertions, 0 deletions
diff --git a/server/src/api.cpp b/server/src/api.cpp new file mode 100644 index 0000000..b3c8e31 --- /dev/null +++ b/server/src/api.cpp | |||
| @@ -0,0 +1,206 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry.hpp> | ||
| 4 | #include <boost/json.hpp> | ||
| 5 | |||
| 6 | module routemon:api$impl; | ||
| 7 | |||
| 8 | import std; | ||
| 9 | import :api; | ||
| 10 | import :datex2; | ||
| 11 | import :geo; | ||
| 12 | import :gpx; | ||
| 13 | import :log; | ||
| 14 | import :req_ctx; | ||
| 15 | import :time; | ||
| 16 | import :trace; | ||
| 17 | |||
| 18 | namespace { | ||
| 19 | |||
| 20 | namespace chrono = std::chrono; | ||
| 21 | namespace json = boost::json; | ||
| 22 | namespace views = std::views; | ||
| 23 | |||
| 24 | } // namespace <anonymous> | ||
| 25 | |||
| 26 | namespace routemon::api { | ||
| 27 | |||
| 28 | auto json_value_from_point(geo::point const& p) -> json::value { | ||
| 29 | return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; | ||
| 30 | } | ||
| 31 | auto json_value_from_linestring(geo::linestring const& ls) -> json::value { | ||
| 32 | json::array a; | ||
| 33 | for (auto const& p : ls) | ||
| 34 | a.push_back(json_value_from_point(p)); | ||
| 35 | return a; | ||
| 36 | } | ||
| 37 | auto json_value_from_linestrings(std::vector<geo::linestring> const& lss) -> json::value { | ||
| 38 | json::array a; | ||
| 39 | for (auto const& ls : lss) | ||
| 40 | a.push_back(json_value_from_linestring(ls)); | ||
| 41 | return a; | ||
| 42 | } | ||
| 43 | |||
| 44 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void { | ||
| 45 | jv = json::object{ | ||
| 46 | {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, | ||
| 47 | }; | ||
| 48 | } | ||
| 49 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void { | ||
| 50 | jv = json::object{ | ||
| 51 | {"id", json::value_from(sit.id)}, | ||
| 52 | {"location", sit.location ? json_value_from_point(*sit.location) : nullptr}, | ||
| 53 | {"comments", json::value_from(sit.comments)}, | ||
| 54 | {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, | ||
| 55 | }; | ||
| 56 | } | ||
| 57 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void { | ||
| 58 | jv = json::object{ | ||
| 59 | {"points", json_value_from_linestring(seg.points)}, | ||
| 60 | }; | ||
| 61 | } | ||
| 62 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void { | ||
| 63 | jv = json::object{ | ||
| 64 | {"segments", json::value_from(track.segments)}, | ||
| 65 | }; | ||
| 66 | } | ||
| 67 | auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void { | ||
| 68 | jv = json::object{ | ||
| 69 | {"tracks", json::value_from(res.tracks)}, | ||
| 70 | {"relevant_situations", json::value_from(res.relevant_situations)}, | ||
| 71 | }; | ||
| 72 | } | ||
| 73 | auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void { | ||
| 74 | jv = json::object{ | ||
| 75 | {"using_publication_of", std::format("{:%FT%TZ}", info.using_publication_of)}, | ||
| 76 | }; | ||
| 77 | } | ||
| 78 | |||
| 79 | handler::handler(log::logger const& l, datex2::situation_publication pub) | ||
| 80 | : l_{l.sub("handler")}, pub_{std::move(pub)} | ||
| 81 | { | ||
| 82 | l_.info("Building indices"); | ||
| 83 | auto const before_build = chrono::steady_clock::now(); | ||
| 84 | for (auto const& sit : pub_.situations) { | ||
| 85 | for (auto const& rc : sit->road_closures) { | ||
| 86 | for (auto const& ls : rc->relevant_line_strings) { | ||
| 87 | auto box = geo::box{}; | ||
| 88 | bgeo::envelope(*ls, box); | ||
| 89 | lse_index_.insert(std::make_tuple(box, ls, rc)); | ||
| 90 | } | ||
| 91 | for (auto p : rc->relevant_points) { | ||
| 92 | p_index_.insert(std::make_pair(p, rc)); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | auto const after_build = chrono::steady_clock::now(); | ||
| 97 | auto const dur_build = chrono::duration_cast<chrono::milliseconds>(after_build - before_build); | ||
| 98 | l_.info("Indices built in {}", dur_build); | ||
| 99 | l_.info("LSE index size: {}", lse_index_.size()); | ||
| 100 | l_.info("Point index size: {}", p_index_.size()); | ||
| 101 | } | ||
| 102 | |||
| 103 | auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result> { | ||
| 104 | auto const now = chrono::utc_clock::now(); | ||
| 105 | auto const relevant = std::initializer_list<time::period>{time::period{now - chrono::days(7), now + chrono::days(7)}}; | ||
| 106 | auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; | ||
| 107 | |||
| 108 | auto splits_with_overlap_segments = std::vector<geo::linestring>{}; | ||
| 109 | for (auto const& track : gpx_file.tracks) | ||
| 110 | for (auto const& seg : track.segments) | ||
| 111 | geo::split_linestring_with_overlap_segments(seg.waypoints, 5000 /* meters max total dist until a new split is forced */, | ||
| 112 | splits_with_overlap_segments); | ||
| 113 | auto const before_query = chrono::steady_clock::now(); | ||
| 114 | |||
| 115 | l_.debug("Querying for relevant situations"); | ||
| 116 | auto relevant_road_closures = std::unordered_set<std::shared_ptr<datex2::road_closure>>{}; | ||
| 117 | auto ls_checked = 0uz; | ||
| 118 | auto p_checked = 0uz; | ||
| 119 | auto i = 0; | ||
| 120 | for (geo::linestring const& part : splits_with_overlap_segments) { | ||
| 121 | l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); | ||
| 122 | |||
| 123 | auto part_box = geo::box{}; | ||
| 124 | bgeo::envelope(part, part_box); | ||
| 125 | |||
| 126 | for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) { | ||
| 127 | // Cannot use structured bindings here, as boost::geometry::get interferes with ADL. | ||
| 128 | // It is a candidate as the namespace boost::geometry is part of the associated namespace set, | ||
| 129 | // which happens because geo::linestring ≡ boost::geometry::model::linestring<geo::point> is part | ||
| 130 | // of the whole tuple type (lse_index_value) that is the value_type of the iterator. | ||
| 131 | std::shared_ptr<geo::linestring> const& ls = std::get<1>(*it); | ||
| 132 | std::shared_ptr<datex2::road_closure> const& rc = std::get<2>(*it); | ||
| 133 | if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) | ||
| 134 | continue; | ||
| 135 | if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) | ||
| 136 | relevant_road_closures.emplace(rc); | ||
| 137 | ls_checked++; | ||
| 138 | } | ||
| 139 | for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) { | ||
| 140 | // Cannot use structured bindings here for the same reason as above. | ||
| 141 | geo::point const& p = std::get<0>(*it); | ||
| 142 | std::shared_ptr<datex2::road_closure> const& rc = std::get<1>(*it); | ||
| 143 | if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) | ||
| 144 | continue; | ||
| 145 | if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) | ||
| 146 | relevant_road_closures.emplace(rc); | ||
| 147 | p_checked++; | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | auto const after_query = chrono::steady_clock::now(); | ||
| 152 | l_.debug("Done (checked {} line string(s) and {} point(s)) in {}", | ||
| 153 | ls_checked, p_checked, chrono::duration_cast<chrono::milliseconds>(after_query - before_query)); | ||
| 154 | |||
| 155 | auto relevant_situations = std::unordered_set<std::shared_ptr<datex2::situation>>{}; | ||
| 156 | for (auto const& rc : relevant_road_closures) | ||
| 157 | relevant_situations.emplace(rc->parent); | ||
| 158 | |||
| 159 | l_.debug("Identified {} relevant road closure(s), part of {} unique situation(s)", | ||
| 160 | relevant_road_closures.size(), relevant_situations.size()); | ||
| 161 | for (auto const& sit : relevant_situations) | ||
| 162 | l_.debug("Relevant situation: {}", sit->id); | ||
| 163 | |||
| 164 | return process_gpx_result{ | ||
| 165 | .tracks = gpx_file.tracks | ||
| 166 | | views::transform([](auto const& trk) -> track { | ||
| 167 | return { | ||
| 168 | .segments = trk.segments | ||
| 169 | | views::transform([](auto const& seg) -> track_segment { | ||
| 170 | return {.points = seg.waypoints}; | ||
| 171 | }) | ||
| 172 | | std::ranges::to<std::vector<track_segment>>(), | ||
| 173 | }; | ||
| 174 | }) | ||
| 175 | | std::ranges::to<std::vector<track>>(), | ||
| 176 | .relevant_situations = relevant_situations | ||
| 177 | | views::transform([&](std::shared_ptr<datex2::situation> sit) -> relevant_situation { | ||
| 178 | return { | ||
| 179 | .id = sit->id, | ||
| 180 | .location = sit->location, | ||
| 181 | .comments = sit->comments, | ||
| 182 | .relevant_road_closures = relevant_road_closures | ||
| 183 | | views::filter([&](std::shared_ptr<datex2::road_closure> const& rc) -> bool { | ||
| 184 | return std::shared_ptr{rc->parent} == sit; | ||
| 185 | }) | ||
| 186 | | views::transform([](std::shared_ptr<datex2::road_closure> const& rc) -> relevant_road_closure { | ||
| 187 | return { | ||
| 188 | .relevant_lss = rc->relevant_line_strings | ||
| 189 | | views::transform([](auto const& lsp) -> geo::linestring { | ||
| 190 | return *lsp; | ||
| 191 | }) | ||
| 192 | | std::ranges::to<std::vector<geo::linestring>>(), | ||
| 193 | }; | ||
| 194 | }) | ||
| 195 | | std::ranges::to<std::vector<relevant_road_closure>>(), | ||
| 196 | }; | ||
| 197 | }) | ||
| 198 | | std::ranges::to<std::vector<relevant_situation>>(), | ||
| 199 | }; | ||
| 200 | } | ||
| 201 | |||
| 202 | auto handler::sysinfo() -> struct sysinfo { | ||
| 203 | return {.using_publication_of = pub_.publication_time}; | ||
| 204 | } | ||
| 205 | |||
| 206 | } // namespace routemon::api | ||
diff --git a/server/src/api.cppm b/server/src/api.cppm new file mode 100644 index 0000000..caeb44c --- /dev/null +++ b/server/src/api.cppm | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry.hpp> | ||
| 4 | #include <boost/json.hpp> | ||
| 5 | |||
| 6 | export module routemon:api; | ||
| 7 | |||
| 8 | import std; | ||
| 9 | import :datex2; | ||
| 10 | import :geo; | ||
| 11 | import :gpx; | ||
| 12 | import :log; | ||
| 13 | import :time; | ||
| 14 | import :trace; | ||
| 15 | |||
| 16 | namespace { | ||
| 17 | |||
| 18 | namespace chrono = std::chrono; | ||
| 19 | namespace json = boost::json; | ||
| 20 | namespace views = std::views; | ||
| 21 | |||
| 22 | } // namespace <anonymous> | ||
| 23 | |||
| 24 | export | ||
| 25 | namespace routemon::api { | ||
| 26 | |||
| 27 | struct relevant_road_closure { | ||
| 28 | std::vector<geo::linestring> relevant_lss; | ||
| 29 | }; | ||
| 30 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void; | ||
| 31 | |||
| 32 | struct relevant_situation { | ||
| 33 | std::string id; | ||
| 34 | std::optional<geo::point> location; | ||
| 35 | std::vector<std::string> comments; | ||
| 36 | std::vector<relevant_road_closure> relevant_road_closures; | ||
| 37 | }; | ||
| 38 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void; | ||
| 39 | |||
| 40 | struct track_segment { | ||
| 41 | geo::linestring points; | ||
| 42 | }; | ||
| 43 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void; | ||
| 44 | |||
| 45 | struct track { | ||
| 46 | std::vector<track_segment> segments; | ||
| 47 | }; | ||
| 48 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void; | ||
| 49 | |||
| 50 | struct process_gpx_result { | ||
| 51 | std::vector<track> tracks; | ||
| 52 | std::vector<relevant_situation> relevant_situations; | ||
| 53 | }; | ||
| 54 | auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void; | ||
| 55 | |||
| 56 | struct sysinfo { | ||
| 57 | time::timestamp using_publication_of; | ||
| 58 | }; | ||
| 59 | auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void; | ||
| 60 | |||
| 61 | class handler { | ||
| 62 | using lse_index_value = std::tuple<geo::box, std::shared_ptr<geo::linestring>, std::shared_ptr<datex2::road_closure>>; | ||
| 63 | using p_index_value = std::pair<geo::point, std::shared_ptr<datex2::road_closure>>; | ||
| 64 | using lse_index = bgeo::index::rtree<lse_index_value, bgeo::index::quadratic<16>>; | ||
| 65 | using p_index = bgeo::index::rtree<p_index_value, bgeo::index::quadratic<16>>; | ||
| 66 | |||
| 67 | log::logger l_; | ||
| 68 | datex2::situation_publication pub_; | ||
| 69 | lse_index lse_index_; | ||
| 70 | p_index p_index_; | ||
| 71 | |||
| 72 | public: | ||
| 73 | explicit handler(log::logger const& l, datex2::situation_publication pub); | ||
| 74 | |||
| 75 | auto process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result>; | ||
| 76 | auto sysinfo() -> sysinfo; | ||
| 77 | }; | ||
| 78 | |||
| 79 | } // namespace routemon::api | ||
diff --git a/server/src/config.cpp b/server/src/config.cpp new file mode 100644 index 0000000..3b17691 --- /dev/null +++ b/server/src/config.cpp | |||
| @@ -0,0 +1,167 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/json.hpp> | ||
| 4 | #include <boost/system/system_error.hpp> | ||
| 5 | |||
| 6 | module routemon:config$impl; | ||
| 7 | |||
| 8 | import std; | ||
| 9 | import :config; | ||
| 10 | import :log; | ||
| 11 | import :util; | ||
| 12 | |||
| 13 | namespace json = boost::json; | ||
| 14 | |||
| 15 | namespace routemon::config { | ||
| 16 | |||
| 17 | class location { | ||
| 18 | std::optional<std::pair<std::string_view, util::not_null<location const*>>> next_; | ||
| 19 | |||
| 20 | auto append_to(std::string& s) const -> void { | ||
| 21 | if (next_) { | ||
| 22 | s += next_->first; | ||
| 23 | s += "."; | ||
| 24 | next_->second->append_to(s); | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | public: | ||
| 29 | location() = default; | ||
| 30 | |||
| 31 | explicit location(location const& next, std::string_view entry) | ||
| 32 | : next_{std::make_pair(entry, util::not_null{&next})} | ||
| 33 | {} | ||
| 34 | |||
| 35 | [[nodiscard]] auto to_string() const -> std::string { | ||
| 36 | if (!next_) | ||
| 37 | return ""; | ||
| 38 | auto s = std::string{next_->first}; | ||
| 39 | next_->second->append_to(s); | ||
| 40 | return s; | ||
| 41 | } | ||
| 42 | |||
| 43 | auto sub(std::string_view entry) const& -> location { | ||
| 44 | return location{*this, entry}; | ||
| 45 | } | ||
| 46 | }; | ||
| 47 | |||
| 48 | class object_reader { | ||
| 49 | location loc_; | ||
| 50 | json::object const& obj_; | ||
| 51 | std::unordered_set<std::string> visited_; | ||
| 52 | |||
| 53 | public: | ||
| 54 | object_reader(json::value const& jv, location loc) | ||
| 55 | try : loc_{std::move(loc)}, obj_{jv.as_object()} | ||
| 56 | {} catch (boost::system::system_error const&) { | ||
| 57 | throw std::runtime_error{std::format("expected an object at {}", loc.to_string())}; | ||
| 58 | } | ||
| 59 | |||
| 60 | auto check_unused() const -> void { | ||
| 61 | for (auto const& kv : obj_) { | ||
| 62 | if (!visited_.contains(std::string{kv.key()})) { | ||
| 63 | throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())}; | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | template<class T> | ||
| 69 | auto expect_at(std::string_view key) -> T { | ||
| 70 | visited_.emplace(key); | ||
| 71 | auto const loc = loc_.sub(key); | ||
| 72 | if (auto const jv = obj_.try_at(key)) { | ||
| 73 | try { | ||
| 74 | return json::value_to<T>(*jv, loc); | ||
| 75 | } catch (boost::system::system_error const& e) { | ||
| 76 | throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())}; | ||
| 77 | } | ||
| 78 | } else { | ||
| 79 | throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())}; | ||
| 80 | } | ||
| 81 | } | ||
| 82 | }; | ||
| 83 | |||
| 84 | auto as_checked_object(json::value const& jv, location const& loc, std::invocable<object_reader&> auto const& f) -> decltype(f(std::declval<object_reader&>())) { | ||
| 85 | auto r = object_reader{jv, loc}; | ||
| 86 | auto&& v = f(r); | ||
| 87 | r.check_unused(); | ||
| 88 | return std::forward<decltype(f(r))>(v); | ||
| 89 | } | ||
| 90 | |||
| 91 | auto tag_invoke(json::value_to_tag<rwgps> const&, json::value const& jv, location const& loc) -> rwgps { | ||
| 92 | return as_checked_object(jv, loc, [](object_reader& r) -> rwgps { | ||
| 93 | return { | ||
| 94 | .api_key = r.expect_at<std::string>("api_key"), | ||
| 95 | .auth_token = r.expect_at<std::string>("auth_token"), | ||
| 96 | }; | ||
| 97 | }); | ||
| 98 | } | ||
| 99 | |||
| 100 | auto tag_invoke(json::value_to_tag<situations> const&, json::value const& jv, location const& loc) -> situations { | ||
| 101 | return as_checked_object(jv, loc, [](object_reader& r) -> situations { | ||
| 102 | return { | ||
| 103 | .datex2_filename = r.expect_at<std::string>("datex2_filename"), | ||
| 104 | }; | ||
| 105 | }); | ||
| 106 | } | ||
| 107 | |||
| 108 | auto tag_invoke(json::value_to_tag<database> const&, json::value const& jv, location const& loc) -> database { | ||
| 109 | return as_checked_object(jv, loc, [](object_reader& r) -> database { | ||
| 110 | return { | ||
| 111 | .sqlite3_filename = r.expect_at<std::string>("sqlite3_filename"), | ||
| 112 | }; | ||
| 113 | }); | ||
| 114 | } | ||
| 115 | |||
| 116 | auto tag_invoke(json::value_to_tag<http_server> const&, json::value const& jv, location const& loc) -> http_server { | ||
| 117 | return as_checked_object(jv, loc, [](object_reader& r) -> http_server { | ||
| 118 | return { | ||
| 119 | .lax_cors = r.expect_at<bool>("lax_cors"), | ||
| 120 | }; | ||
| 121 | }); | ||
| 122 | } | ||
| 123 | |||
| 124 | auto tag_invoke(json::value_to_tag<logger> const&, json::value const& jv, location const& loc) -> logger { | ||
| 125 | return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger { | ||
| 126 | auto const level_str = r.expect_at<std::string>("level"); | ||
| 127 | auto level = log::level{}; | ||
| 128 | if (level_str == "debug") | ||
| 129 | level = log::level::debug; | ||
| 130 | else if (level_str == "info") | ||
| 131 | level = log::level::info; | ||
| 132 | else if (level_str == "warn") | ||
| 133 | level = log::level::warn; | ||
| 134 | else if (level_str == "error") | ||
| 135 | level = log::level::error; | ||
| 136 | else | ||
| 137 | 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())}; | ||
| 138 | return logger{level}; | ||
| 139 | }); | ||
| 140 | } | ||
| 141 | |||
| 142 | auto json_value_to_app(json::value const& jv) -> app { | ||
| 143 | return as_checked_object(jv, location{}, [](object_reader& r) -> app { | ||
| 144 | return { | ||
| 145 | .rwgps = r.expect_at<rwgps>("rwgps"), | ||
| 146 | .situations = r.expect_at<situations>("situations"), | ||
| 147 | .database = r.expect_at<database>("database"), | ||
| 148 | .http_server = r.expect_at<http_server>("http_server"), | ||
| 149 | .logger = r.expect_at<logger>("logger"), | ||
| 150 | }; | ||
| 151 | }); | ||
| 152 | } | ||
| 153 | |||
| 154 | auto load_file(std::string const& filename) -> app { | ||
| 155 | auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode? | ||
| 156 | if (!f.is_open()) | ||
| 157 | throw std::runtime_error{std::format("failed to open {}", filename)}; | ||
| 158 | auto jv = json::value{}; | ||
| 159 | try { | ||
| 160 | jv = json::parse(f); | ||
| 161 | } catch (boost::system::system_error const& e) { | ||
| 162 | throw std::runtime_error{std::format("failed to parse: {}", e.code().message())}; | ||
| 163 | } | ||
| 164 | return json_value_to_app(jv); | ||
| 165 | } | ||
| 166 | |||
| 167 | } // namespace routemon::config | ||
diff --git a/server/src/config.cppm b/server/src/config.cppm new file mode 100644 index 0000000..ef827a6 --- /dev/null +++ b/server/src/config.cppm | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | export module routemon:config; | ||
| 2 | |||
| 3 | import std; | ||
| 4 | import :log; | ||
| 5 | |||
| 6 | namespace routemon::config { | ||
| 7 | |||
| 8 | export struct rwgps { | ||
| 9 | std::string api_key; | ||
| 10 | std::string auth_token; | ||
| 11 | }; | ||
| 12 | |||
| 13 | export struct situations { | ||
| 14 | std::string datex2_filename; | ||
| 15 | }; | ||
| 16 | |||
| 17 | export struct database { | ||
| 18 | std::string sqlite3_filename; | ||
| 19 | }; | ||
| 20 | |||
| 21 | export struct http_server { | ||
| 22 | bool lax_cors; | ||
| 23 | }; | ||
| 24 | |||
| 25 | export struct logger { | ||
| 26 | log::level level; | ||
| 27 | }; | ||
| 28 | |||
| 29 | export struct app { | ||
| 30 | rwgps rwgps; | ||
| 31 | situations situations; | ||
| 32 | database database; | ||
| 33 | http_server http_server; | ||
| 34 | logger logger; | ||
| 35 | }; | ||
| 36 | |||
| 37 | export auto load_file(std::string const& filename) -> app; | ||
| 38 | |||
| 39 | } // namespace routemon::config | ||
diff --git a/server/src/database.cppm b/server/src/database.cppm new file mode 100644 index 0000000..0312dda --- /dev/null +++ b/server/src/database.cppm | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | export module routemon:database; | ||
| 2 | |||
| 3 | import std; | ||
| 4 | import :sqlite3; | ||
| 5 | |||
| 6 | namespace routemon::database { | ||
| 7 | |||
| 8 | static constexpr std::int64_t expected_database_version = 1; | ||
| 9 | |||
| 10 | export class connection { | ||
| 11 | sqlite3::connection dbc_; | ||
| 12 | |||
| 13 | explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {} | ||
| 14 | |||
| 15 | friend auto open(std::string const& filename) -> std::shared_ptr<connection>; | ||
| 16 | |||
| 17 | public: | ||
| 18 | // Nothing here yet | ||
| 19 | }; | ||
| 20 | |||
| 21 | export auto open(std::string const& filename) -> std::shared_ptr<connection> { | ||
| 22 | auto dbc = sqlite3::open(filename); | ||
| 23 | try { | ||
| 24 | auto version = std::optional<std::int64_t>{}; | ||
| 25 | dbc.query("SELECT version FROM migration;").scan_single(version); | ||
| 26 | if (!version) | ||
| 27 | throw std::runtime_error{"failed to fetch database migration version"}; | ||
| 28 | if (version != expected_database_version) { | ||
| 29 | throw std::runtime_error{std::format("database migration version ({}) does not match expected version ({}), consider running migrations", *version, expected_database_version)}; | ||
| 30 | } | ||
| 31 | } catch (std::exception const& e) { | ||
| 32 | throw std::runtime_error{std::format("failed to query database version: {}", e.what())}; | ||
| 33 | } | ||
| 34 | return std::shared_ptr<connection>{new connection{std::move(dbc)}}; | ||
| 35 | } | ||
| 36 | |||
| 37 | } // namespace routemon::database | ||
diff --git a/server/src/datex2.cppm b/server/src/datex2.cppm new file mode 100644 index 0000000..b507e6f --- /dev/null +++ b/server/src/datex2.cppm | |||
| @@ -0,0 +1,296 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry/algorithms/is_empty.hpp> | ||
| 4 | #include <boost/geometry/srs/transformation.hpp> | ||
| 5 | #include <boost/geometry/srs/epsg.hpp> | ||
| 6 | |||
| 7 | #include <pugixml.hpp> | ||
| 8 | |||
| 9 | export module routemon:datex2; | ||
| 10 | |||
| 11 | import std; | ||
| 12 | import :geo; | ||
| 13 | import :time; | ||
| 14 | import :util; | ||
| 15 | |||
| 16 | using namespace std::literals::string_view_literals; | ||
| 17 | |||
| 18 | namespace routemon::datex2 { | ||
| 19 | |||
| 20 | export struct situation; | ||
| 21 | |||
| 22 | export struct road_closure { | ||
| 23 | std::weak_ptr<situation> parent; | ||
| 24 | std::optional<time::period_seq> validity; | ||
| 25 | std::vector<geo::point> relevant_points = {}; | ||
| 26 | std::vector<std::shared_ptr<geo::linestring>> relevant_line_strings = {}; | ||
| 27 | }; | ||
| 28 | |||
| 29 | export struct situation { | ||
| 30 | std::string id; | ||
| 31 | std::optional<geo::point> location = std::nullopt; // as shown on the map, not used for querying | ||
| 32 | std::vector<std::string> comments = {}; | ||
| 33 | std::vector<std::shared_ptr<road_closure>> road_closures = {}; | ||
| 34 | }; | ||
| 35 | |||
| 36 | export struct situation_publication { | ||
| 37 | time::timestamp publication_time; | ||
| 38 | std::vector<std::shared_ptr<situation>> situations; | ||
| 39 | }; | ||
| 40 | |||
| 41 | auto parse_timestamp(char const* in) -> std::optional<time::timestamp> { | ||
| 42 | auto res = time::timestamp{}; | ||
| 43 | auto is = std::istringstream{in}; | ||
| 44 | is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); | ||
| 45 | return is.fail() ? std::nullopt : std::make_optional(res); | ||
| 46 | } | ||
| 47 | |||
| 48 | export class loader { | ||
| 49 | // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) | ||
| 50 | bgeo::srs::transformation<bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> etrs89_to_wgs84_{}; | ||
| 51 | |||
| 52 | std::multiset<std::string> warnings_; | ||
| 53 | |||
| 54 | auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) -> void { | ||
| 55 | auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; | ||
| 56 | if (loc_xml_type == "loc:ItineraryByIndexedLocations") { | ||
| 57 | for (auto const loc_cont_xml : loc_xml.children("loc:locationContainedInItinerary")) { | ||
| 58 | add_location_from_xml(rc, loc_cont_xml.child("loc:location")); | ||
| 59 | } | ||
| 60 | } else if (loc_xml_type == "loc:LinearLocation" || loc_xml_type == "loc:SingleRoadLinearLocation") { | ||
| 61 | auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); | ||
| 62 | if (!loc_gml_xml) | ||
| 63 | return; | ||
| 64 | |||
| 65 | auto const srs_name = std::string_view{loc_gml_xml.attribute("srsName").value()}; | ||
| 66 | if (srs_name != "WGS 84"sv) { | ||
| 67 | warnings_.insert(std::format("don't now how to handle the CRS {}", srs_name)); | ||
| 68 | return; | ||
| 69 | } | ||
| 70 | auto const pos_list_str = std::string_view{loc_gml_xml.child_value("loc:posList")}; | ||
| 71 | // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn | ||
| 72 | |||
| 73 | auto ls = std::make_shared<geo::linestring>(); | ||
| 74 | |||
| 75 | auto lat_set = false; | ||
| 76 | auto lat = 0.0; | ||
| 77 | for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) { | ||
| 78 | auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); | ||
| 79 | if (!mlat_or_long) { | ||
| 80 | warnings_.insert(std::format("failed to parse coordinate {:?}", std::string_view{lat_or_long_str})); | ||
| 81 | return; | ||
| 82 | } | ||
| 83 | |||
| 84 | if (!lat_set) { | ||
| 85 | lat = *mlat_or_long; | ||
| 86 | lat_set = true; | ||
| 87 | } else { | ||
| 88 | bgeo::append(*ls, geo::point{*mlat_or_long, lat}); | ||
| 89 | lat = 0; | ||
| 90 | lat_set = false; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | if (bgeo::is_empty(*ls)) { | ||
| 95 | warnings_.emplace("empty line string in data set"); | ||
| 96 | return; | ||
| 97 | } | ||
| 98 | |||
| 99 | rc.relevant_line_strings.push_back(ls); | ||
| 100 | } else if (loc_xml_type == "loc:PointLocation") { | ||
| 101 | auto const& coords_xml = loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); | ||
| 102 | if (!coords_xml) | ||
| 103 | return; | ||
| 104 | |||
| 105 | auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 106 | auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 107 | if (!mlat || !mlon) { | ||
| 108 | warnings_.emplace("failed to parse PointLocation coordinates"); | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | |||
| 112 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | ||
| 113 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 114 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | ||
| 115 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 116 | |||
| 117 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 118 | auto coords_wgs84 = geo::point{}; | ||
| 119 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 120 | |||
| 121 | rc.relevant_points.push_back(coords_wgs84); | ||
| 122 | } else { | ||
| 123 | warnings_.insert(std::format("don't know how to hande location of type {}, ignoring", loc_xml.attribute("xsi:type").value())); | ||
| 124 | return; | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | auto handle_road_or_carriageway_or_lane_management(pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) -> std::optional<std::shared_ptr<road_closure>> { | ||
| 129 | auto const type = std::string_view{record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value()}; | ||
| 130 | if (type != "carriagewayClosures" && type != "roadClosed") | ||
| 131 | // TODO: checken of er nog andere types fietsers de doorgang zouden kunnen blokkeren? | ||
| 132 | return std::nullopt; | ||
| 133 | |||
| 134 | auto const& restricted_vehicle_types_xml = record_xml.child("sit:forVehiclesWithCharacteristicsOf"); | ||
| 135 | bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); | ||
| 136 | for (auto const vehicle_type_xml : restricted_vehicle_types_xml.children("com:vehicleType")) { | ||
| 137 | auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; | ||
| 138 | if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" || | ||
| 139 | vehicle_type == "unknown" || vehicle_type == "other") { | ||
| 140 | likely_restriction_for_bikes = true; | ||
| 141 | } | ||
| 142 | } | ||
| 143 | if (!likely_restriction_for_bikes) | ||
| 144 | return std::nullopt; | ||
| 145 | |||
| 146 | //---- Check if within the defined validity period | ||
| 147 | |||
| 148 | auto validity = std::optional<time::period_seq>{}; | ||
| 149 | auto const& validity_xml = record_xml.child("sit:validity"); | ||
| 150 | if (validity_xml && validity_xml.child_value("com:validityStatus") == "definedByValidityTimeSpec"sv) { | ||
| 151 | auto const& validity_spec_xml = validity_xml.child("com:validityTimeSpecification"); | ||
| 152 | |||
| 153 | auto valid_periods = std::vector<time::period>{}; | ||
| 154 | auto exception_periods = std::vector<time::period>{}; | ||
| 155 | |||
| 156 | // TODO: com:overallEndTime may be missing (according to the DATEX II v3 data model) | ||
| 157 | auto const overall_start_time = parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); | ||
| 158 | auto const overall_end_time = parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); | ||
| 159 | if (overall_start_time && overall_end_time && *overall_start_time < *overall_end_time) { | ||
| 160 | valid_periods.emplace_back(*overall_start_time, *overall_end_time); | ||
| 161 | |||
| 162 | for (auto const valid_period_xml : validity_xml.children("com:validPeriod")) { | ||
| 163 | auto const start_of_period = parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); | ||
| 164 | auto const end_of_period = parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); | ||
| 165 | if (start_of_period && end_of_period && *start_of_period < *end_of_period) { | ||
| 166 | valid_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 167 | } | ||
| 168 | } | ||
| 169 | for (auto const exception_period_xml : validity_xml.children("com:exceptionPeriod")) { | ||
| 170 | auto const start_of_period = parse_timestamp(exception_period_xml.child_value("com:startOfPeriod")); | ||
| 171 | auto const end_of_period = parse_timestamp(exception_period_xml.child_value("com:endOfPeriod")); | ||
| 172 | if (start_of_period && end_of_period && *start_of_period < *end_of_period) { | ||
| 173 | exception_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | validity = time::period_seq{valid_periods.begin(), valid_periods.end()} | ||
| 178 | .except(time::period_seq{exception_periods.begin(), exception_periods.end()}); | ||
| 179 | } else { | ||
| 180 | warnings_.insert(std::format("invalid overall start / end time (start time: {}, end time: {})", | ||
| 181 | validity_spec_xml.child_value("com:overallStartTime"), | ||
| 182 | validity_spec_xml.child_value("com:overallEndTime"))); | ||
| 183 | return std::nullopt; | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | //---- Try to extract the location info | ||
| 188 | |||
| 189 | auto rc = std::make_shared<road_closure>(std::move(parent), validity); | ||
| 190 | add_location_from_xml(*rc, record_xml.child("sit:locationReference")); | ||
| 191 | return rc; | ||
| 192 | } | ||
| 193 | |||
| 194 | public: | ||
| 195 | [[nodiscard]] auto load_situation_publication(std::string const& filename) -> situation_publication { | ||
| 196 | auto doc = pugi::xml_document{}; | ||
| 197 | if (auto result = doc.load_file(filename.c_str()); !result) { | ||
| 198 | throw std::runtime_error{result.description()}; | ||
| 199 | } | ||
| 200 | auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); | ||
| 201 | auto mpublication_time = parse_timestamp(payload_xml.child_value("com:publicationTime")); | ||
| 202 | if (!mpublication_time) | ||
| 203 | throw std::runtime_error{"provided publication does not name publication time"}; | ||
| 204 | |||
| 205 | auto situations = std::vector<std::shared_ptr<situation>>{}; | ||
| 206 | for (auto const sit_xml : payload_xml.children("sit:situation")) { | ||
| 207 | auto id = std::string_view{sit_xml.attribute("id").value()}; | ||
| 208 | |||
| 209 | auto const sit = std::make_shared<situation>(std::string{id}); | ||
| 210 | situations.push_back(sit); | ||
| 211 | |||
| 212 | auto const& header_info_xml = sit_xml.child("sit:headerInformation"); | ||
| 213 | if (header_info_xml.child_value("com:informationStatus") != "real"sv) | ||
| 214 | continue; | ||
| 215 | |||
| 216 | for (auto const record_xml : sit_xml.children("sit:situationRecord")) { | ||
| 217 | auto const record_type = std::string_view{record_xml.attribute("xsi:type").value()}; | ||
| 218 | auto const primary_record_types = std::unordered_set<std::string_view>{ | ||
| 219 | "sit:Roadworks", | ||
| 220 | /* { */ "sit:MaintenanceWorks", | ||
| 221 | /* | */ "sit:ConstructionWorks", | ||
| 222 | /* } */ | ||
| 223 | "sit:Obstruction", | ||
| 224 | /* { */ "sit:EnvironmentalObstruction", | ||
| 225 | /* | */ "sit:GeneralObstruction", | ||
| 226 | /* | */ "sit:InfrastructureDamageObstruction", | ||
| 227 | /* } */ | ||
| 228 | "sit:Activity", | ||
| 229 | /* { */ "sit:PublicEvent", | ||
| 230 | /* } */ | ||
| 231 | }; | ||
| 232 | |||
| 233 | if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") { | ||
| 234 | if (auto rc = handle_road_or_carriageway_or_lane_management(record_xml, sit)) { | ||
| 235 | sit->road_closures.push_back(*rc); | ||
| 236 | } | ||
| 237 | } else if (primary_record_types.contains(record_type)) { | ||
| 238 | for (auto const comment_xml : record_xml.children("sit:generalPublicComment")) { | ||
| 239 | // if (comment_xml.child_value("sit:commentType") == "internalNote"sv) { | ||
| 240 | auto candidate = std::optional<std::pair<std::string_view, std::string_view>>{}; // (text, language) | ||
| 241 | for (auto const comment_value_xml : comment_xml.child("sit:comment").child("com:values").children("com:value")) { | ||
| 242 | if (!candidate || | ||
| 243 | comment_value_xml.attribute("lang").value() == "nl"sv || | ||
| 244 | (candidate->second != "nl"sv && comment_value_xml.attribute("lang").value() == "nl"sv)) { | ||
| 245 | candidate = std::make_pair(comment_value_xml.child_value(), comment_value_xml.attribute("lang").value()); | ||
| 246 | } | ||
| 247 | } | ||
| 248 | if (candidate) { | ||
| 249 | auto already_present = false; | ||
| 250 | for (auto const& comment : sit->comments) | ||
| 251 | already_present = already_present || comment == candidate->first; | ||
| 252 | if (!already_present) { | ||
| 253 | sit->comments.emplace_back(candidate->first); | ||
| 254 | } | ||
| 255 | } | ||
| 256 | // } | ||
| 257 | } | ||
| 258 | |||
| 259 | if (auto const location_ref_xml = record_xml.child("sit:locationReference")) { | ||
| 260 | if (location_ref_xml.attribute("xsi:type").value() == "loc:PointLocation"sv) { | ||
| 261 | if (auto const coords_xml = location_ref_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates")) { | ||
| 262 | auto const mlat = util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 263 | auto const mlon = util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 264 | if (mlat && mlon) { | ||
| 265 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | ||
| 266 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 267 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | ||
| 268 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 269 | |||
| 270 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 271 | auto coords_wgs84 = geo::point{}; | ||
| 272 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 273 | |||
| 274 | if (!sit->location) { | ||
| 275 | sit->location = coords_wgs84; | ||
| 276 | } | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | } | ||
| 281 | } | ||
| 282 | } | ||
| 283 | } | ||
| 284 | |||
| 285 | return { | ||
| 286 | .publication_time = *mpublication_time, | ||
| 287 | .situations = situations, | ||
| 288 | }; | ||
| 289 | } | ||
| 290 | |||
| 291 | [[nodiscard]] auto warnings() const -> std::multiset<std::string> const& { | ||
| 292 | return warnings_; | ||
| 293 | } | ||
| 294 | }; | ||
| 295 | |||
| 296 | } // namespace routemon::datex2 | ||
diff --git a/server/src/geo.cppm b/server/src/geo.cppm new file mode 100644 index 0000000..5cdbdd9 --- /dev/null +++ b/server/src/geo.cppm | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry.hpp> | ||
| 4 | |||
| 5 | export module routemon:geo; | ||
| 6 | |||
| 7 | export namespace bgeo = boost::geometry; | ||
| 8 | |||
| 9 | export namespace routemon::geo { | ||
| 10 | |||
| 11 | using point = bgeo::model::point<double, 2, bgeo::cs::spherical_equatorial<bgeo::degree>>; | ||
| 12 | using linestring = bgeo::model::linestring<point>; | ||
| 13 | using box = bgeo::model::box<point>; | ||
| 14 | using stype = bgeo::srs::spheroid<double>; | ||
| 15 | using vincenty_strategy = bgeo::strategy::distance::vincenty<stype>; | ||
| 16 | |||
| 17 | auto split_linestring_with_overlap_segments(linestring const& ls, double max_split_distance_m, std::vector<linestring>& append_to) -> void { | ||
| 18 | if (bgeo::is_empty(ls)) | ||
| 19 | return; | ||
| 20 | |||
| 21 | auto current_ls = linestring{}; | ||
| 22 | auto current_ls_length = 0.0; | ||
| 23 | auto previous = std::optional<point>{}; | ||
| 24 | bgeo::for_each_point(ls, [&](point p) -> void { | ||
| 25 | bgeo::append(current_ls, p); | ||
| 26 | if (previous) { | ||
| 27 | auto d = bgeo::distance(*previous, p, vincenty_strategy()); | ||
| 28 | current_ls_length += d; | ||
| 29 | if (current_ls_length > max_split_distance_m) { | ||
| 30 | append_to.push_back(std::move(current_ls)); | ||
| 31 | current_ls = linestring{*previous, p}; | ||
| 32 | current_ls_length = d; | ||
| 33 | } | ||
| 34 | } | ||
| 35 | previous = p; | ||
| 36 | }); | ||
| 37 | append_to.emplace_back(std::move(current_ls)); | ||
| 38 | } | ||
| 39 | |||
| 40 | } // namespace routemon::geo | ||
diff --git a/server/src/gpx.cpp b/server/src/gpx.cpp new file mode 100644 index 0000000..62dccff --- /dev/null +++ b/server/src/gpx.cpp | |||
| @@ -0,0 +1,189 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry/algorithms/append.hpp> | ||
| 4 | |||
| 5 | module routemon:gpx$impl; | ||
| 6 | |||
| 7 | import std; | ||
| 8 | import :gpx; | ||
| 9 | import :util; | ||
| 10 | import :xml; | ||
| 11 | |||
| 12 | using namespace std::literals::string_view_literals; | ||
| 13 | |||
| 14 | namespace routemon::gpx { | ||
| 15 | |||
| 16 | namespace v10 { | ||
| 17 | |||
| 18 | constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; | ||
| 19 | auto qname(std::string_view local) -> xml::qname_view { | ||
| 20 | return {.ns_uri = xmlns, .local = local}; | ||
| 21 | } | ||
| 22 | |||
| 23 | auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<geo::point> { | ||
| 24 | auto parse_xml_double = [](std::string_view sv) -> std::optional<double> { | ||
| 25 | return util::parse_double(sv, std::chars_format::fixed); | ||
| 26 | }; | ||
| 27 | auto mlat = std::optional<double>{}; | ||
| 28 | auto mlon = std::optional<double>{}; | ||
| 29 | for (auto const& [name, value] : attrs) { | ||
| 30 | if (name == qname("lat")) { | ||
| 31 | mlat = parse_xml_double(value); | ||
| 32 | } else if (name == qname("lon")) { | ||
| 33 | mlon = parse_xml_double(value); | ||
| 34 | } | ||
| 35 | } | ||
| 36 | if (!mlat || !mlon) | ||
| 37 | throw std::runtime_error{"expected valid latitude and longitude for waypoint"}; | ||
| 38 | co_await xml::ignore_contents(e); | ||
| 39 | co_return geo::point{*mlon, *mlat}; | ||
| 40 | } | ||
| 41 | |||
| 42 | auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser<track_segment> { | ||
| 43 | auto s = track_segment{}; | ||
| 44 | while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo<parse_wpt>())) | ||
| 45 | bgeo::append(s.waypoints, *mwpt); | ||
| 46 | co_return std::move(s); | ||
| 47 | } | ||
| 48 | |||
| 49 | auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> { | ||
| 50 | auto t = track{}; | ||
| 51 | t.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | ||
| 52 | co_await allow_element(e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 53 | t.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | ||
| 54 | co_await xml::ignore_contents(e, /* until */ qname("trkseg")); | ||
| 55 | while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo<parse_trkseg>())) | ||
| 56 | t.segments.push_back(std::move(*mseg)); | ||
| 57 | co_return std::move(t); | ||
| 58 | } | ||
| 59 | |||
| 60 | auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<file> { | ||
| 61 | auto f = file{}; | ||
| 62 | if (attrs.lookup(qname("version")) != "1.0"sv) | ||
| 63 | throw std::runtime_error{"expected GPX version to be 1.0"}; | ||
| 64 | if (auto mcreator = attrs.lookup(qname("creator"))) | ||
| 65 | f.creator = *mcreator; | ||
| 66 | else | ||
| 67 | throw std::runtime_error{"expected GPX file to have creator"}; | ||
| 68 | |||
| 69 | f.meta.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | ||
| 70 | f.meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | ||
| 71 | |||
| 72 | co_await xml::ignore_contents(e, /* until */ qname("trk")); | ||
| 73 | while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>())) | ||
| 74 | f.tracks.push_back(std::move(*mtrk)); | ||
| 75 | co_await xml::ignore_contents(e); | ||
| 76 | |||
| 77 | co_return std::move(f); | ||
| 78 | } | ||
| 79 | |||
| 80 | } // namespace v10 | ||
| 81 | |||
| 82 | namespace v11 { | ||
| 83 | |||
| 84 | constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv; | ||
| 85 | auto qname(std::string_view local) -> xml::qname_view { | ||
| 86 | return {.ns_uri = xmlns, .local = local}; | ||
| 87 | } | ||
| 88 | |||
| 89 | auto parse_metadata(xml::executor_ref e, xml::attribute_view) -> xml::parser<metadata> { | ||
| 90 | auto meta = metadata{}; | ||
| 91 | meta.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | ||
| 92 | meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | ||
| 93 | co_await xml::ignore_contents(e); | ||
| 94 | co_return meta; | ||
| 95 | } | ||
| 96 | |||
| 97 | auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<geo::point> { | ||
| 98 | auto parse_xml_double = [](std::string_view sv) -> std::optional<double> { | ||
| 99 | return util::parse_double(sv, std::chars_format::fixed); | ||
| 100 | }; | ||
| 101 | auto mlat = std::optional<double>{}; | ||
| 102 | auto mlon = std::optional<double>{}; | ||
| 103 | for (auto const& [name, value] : attrs) { | ||
| 104 | if (name == qname("lat")) { | ||
| 105 | mlat = parse_xml_double(value); | ||
| 106 | } else if (name == qname("lon")) { | ||
| 107 | mlon = parse_xml_double(value); | ||
| 108 | } | ||
| 109 | } | ||
| 110 | if (!mlat || !mlon) | ||
| 111 | throw std::runtime_error{"expected valid latitude and longitude for waypoint"}; | ||
| 112 | co_await xml::ignore_contents(e); | ||
| 113 | co_return geo::point{*mlon, *mlat}; | ||
| 114 | } | ||
| 115 | |||
| 116 | auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser<track_segment> { | ||
| 117 | auto s = track_segment{}; | ||
| 118 | while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo<parse_wpt>())) | ||
| 119 | bgeo::append(s.waypoints, *mwpt); | ||
| 120 | co_await allow_element(e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 121 | co_return std::move(s); | ||
| 122 | } | ||
| 123 | |||
| 124 | auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> { | ||
| 125 | auto t = track{}; | ||
| 126 | t.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | ||
| 127 | co_await allow_element(e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 128 | t.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | ||
| 129 | co_await xml::ignore_contents(e, /* until */ qname("trkseg")); | ||
| 130 | while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo<parse_trkseg>())) | ||
| 131 | t.segments.push_back(std::move(*mseg)); | ||
| 132 | co_return std::move(t); | ||
| 133 | } | ||
| 134 | |||
| 135 | auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<file> { | ||
| 136 | auto f = file{}; | ||
| 137 | if (attrs.lookup(qname("version")) != "1.1"sv) | ||
| 138 | throw std::runtime_error{"expected GPX version to be 1.1"}; | ||
| 139 | if (auto mcreator = attrs.lookup(qname("creator"))) | ||
| 140 | f.creator = *mcreator; | ||
| 141 | else | ||
| 142 | throw std::runtime_error{"expected GPX file to have creator"}; | ||
| 143 | |||
| 144 | if (auto mmeta = co_await allow_element(e, qname("metadata"), xml::hohalo<parse_metadata>())) | ||
| 145 | f.meta = *mmeta; | ||
| 146 | co_await xml::ignore_contents(e, /* until */ qname("trk")); | ||
| 147 | while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>())) | ||
| 148 | f.tracks.push_back(std::move(*mtrk)); | ||
| 149 | co_await allow_element(e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 150 | |||
| 151 | co_return std::move(f); | ||
| 152 | } | ||
| 153 | |||
| 154 | } // namespace v11 | ||
| 155 | |||
| 156 | auto parse_file(xml::executor_ref e) -> xml::parser<file> { | ||
| 157 | auto decl = co_await expect_event<xml::xml_decl_event>(e); | ||
| 158 | if (decl.version != "1.0"sv) | ||
| 159 | throw std::runtime_error{std::format("unsupported XML version, got {}", std::string_view{decl.version})}; | ||
| 160 | if (decl.encoding != "UTF-8"sv) | ||
| 161 | throw std::runtime_error{"unsupported encoding"}; | ||
| 162 | if (auto mf = co_await allow_element(e, v10::qname("gpx"), xml::hohalo<v10::parse_gpx>())) | ||
| 163 | co_return std::move(*mf); | ||
| 164 | if (auto mf = co_await allow_element(e, v11::qname("gpx"), xml::hohalo<v11::parse_gpx>())) | ||
| 165 | co_return std::move(*mf); | ||
| 166 | throw std::runtime_error{"no supported GPX document found"}; | ||
| 167 | } | ||
| 168 | |||
| 169 | reader::reader() | ||
| 170 | : p_{parse_file(util::not_null{&e_})} | ||
| 171 | { e_.set_continuation(p_.promise().base_handle()); } | ||
| 172 | |||
| 173 | auto reader::init() -> void { | ||
| 174 | e_.start(); | ||
| 175 | } | ||
| 176 | |||
| 177 | auto reader::put(std::string_view buf) -> void { | ||
| 178 | e_.read(buf, false); | ||
| 179 | } | ||
| 180 | |||
| 181 | auto reader::finish() -> gpx::file { | ||
| 182 | e_.read(std::string_view{}, true); | ||
| 183 | e_.end(); | ||
| 184 | // Promise is still alive since the last coroutine performs a | ||
| 185 | // symmetric transfer to std::noop_coroutine() in final_suspend(). | ||
| 186 | return std::move(p_.promise().returned_value()); | ||
| 187 | } | ||
| 188 | |||
| 189 | } // namespace routemon::gpx | ||
diff --git a/server/src/gpx.cppm b/server/src/gpx.cppm new file mode 100644 index 0000000..20e6242 --- /dev/null +++ b/server/src/gpx.cppm | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | export module routemon:gpx; | ||
| 2 | |||
| 3 | import std; | ||
| 4 | import :geo; | ||
| 5 | import :util; | ||
| 6 | import :xml; | ||
| 7 | |||
| 8 | namespace routemon::gpx { | ||
| 9 | |||
| 10 | struct metadata { | ||
| 11 | std::optional<std::string> name; | ||
| 12 | std::optional<std::string> desc; | ||
| 13 | }; | ||
| 14 | |||
| 15 | struct track_segment { | ||
| 16 | geo::linestring waypoints; | ||
| 17 | }; | ||
| 18 | |||
| 19 | struct track { | ||
| 20 | std::optional<std::string> name; | ||
| 21 | std::optional<std::string> desc; | ||
| 22 | std::vector<track_segment> segments; | ||
| 23 | }; | ||
| 24 | |||
| 25 | struct file { | ||
| 26 | std::string creator; | ||
| 27 | metadata meta; | ||
| 28 | std::vector<track> tracks; | ||
| 29 | }; | ||
| 30 | |||
| 31 | class reader { | ||
| 32 | xml::executor e_; | ||
| 33 | xml::parser<file> p_; | ||
| 34 | |||
| 35 | public: | ||
| 36 | explicit reader(); | ||
| 37 | |||
| 38 | auto init() -> void; | ||
| 39 | auto put(std::string_view buf) -> void; | ||
| 40 | [[nodiscard]] auto finish() -> gpx::file; | ||
| 41 | }; | ||
| 42 | |||
| 43 | } // namespace routemon::gpx | ||
diff --git a/server/src/http_client.cppm b/server/src/http_client.cppm new file mode 100644 index 0000000..d5316d6 --- /dev/null +++ b/server/src/http_client.cppm | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/asio/connect.hpp> | ||
| 4 | #include <boost/asio/ip/tcp.hpp> | ||
| 5 | #include <boost/asio/ssl.hpp> | ||
| 6 | #include <boost/beast/core.hpp> | ||
| 7 | #include <boost/beast/http.hpp> | ||
| 8 | |||
| 9 | export module routemon:http.client; | ||
| 10 | |||
| 11 | export import :http.common; | ||
| 12 | |||
| 13 | namespace net = boost::asio; | ||
| 14 | namespace ssl = net::ssl; | ||
| 15 | using tcp = net::ip::tcp; | ||
| 16 | |||
| 17 | namespace routemon::http { | ||
| 18 | |||
| 19 | export class client { | ||
| 20 | net::io_context& ioc_; | ||
| 21 | ssl::context sslc_{ssl::context::tlsv12_client}; | ||
| 22 | tcp::resolver resolver_; | ||
| 23 | |||
| 24 | public: | ||
| 25 | explicit client(net::io_context& ioc) | ||
| 26 | : ioc_{ioc}, resolver_{ioc} | ||
| 27 | { | ||
| 28 | sslc_.set_default_verify_paths(); | ||
| 29 | sslc_.set_verify_mode(net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); | ||
| 30 | } | ||
| 31 | |||
| 32 | template<class ReqBody> | ||
| 33 | auto do_request(bhttp::request<ReqBody>& req) -> bhttp::response<bhttp::dynamic_body> { | ||
| 34 | auto stream = ssl::stream<beast::tcp_stream>{ioc_, sslc_}; | ||
| 35 | |||
| 36 | auto host = std::string{req.at(bhttp::field::host)}; | ||
| 37 | if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { | ||
| 38 | throw beast::system_error(static_cast<int>(::ERR_get_error()), | ||
| 39 | net::error::get_ssl_category()); | ||
| 40 | } | ||
| 41 | stream.set_verify_callback(ssl::host_name_verification(host)); | ||
| 42 | auto const results = resolver_.resolve(host, "443"); | ||
| 43 | beast::get_lowest_layer(stream).connect(results); | ||
| 44 | stream.handshake(ssl::stream_base::client); | ||
| 45 | |||
| 46 | req.set(bhttp::field::user_agent, "routemon/1.0"); | ||
| 47 | bhttp::write(stream, req); | ||
| 48 | |||
| 49 | auto buffer = beast::flat_buffer{}; | ||
| 50 | auto res = bhttp::response<bhttp::dynamic_body>{}; | ||
| 51 | bhttp::read(stream, buffer, res); | ||
| 52 | |||
| 53 | auto ec = beast::error_code{}; | ||
| 54 | stream.shutdown(ec); | ||
| 55 | if (ec != net::ssl::error::stream_truncated) | ||
| 56 | throw beast::system_error{ec}; | ||
| 57 | |||
| 58 | return res; | ||
| 59 | } | ||
| 60 | }; | ||
| 61 | |||
| 62 | } // namespace routemon::http | ||
diff --git a/server/src/http_common.cppm b/server/src/http_common.cppm new file mode 100644 index 0000000..3c38da7 --- /dev/null +++ b/server/src/http_common.cppm | |||
| @@ -0,0 +1,139 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/core.hpp> | ||
| 4 | #include <boost/beast/http.hpp> | ||
| 5 | |||
| 6 | export module routemon:http.common; | ||
| 7 | |||
| 8 | namespace beast = boost::beast; | ||
| 9 | export namespace bhttp = beast::http; | ||
| 10 | |||
| 11 | namespace boost::beast { | ||
| 12 | |||
| 13 | namespace concepts { | ||
| 14 | |||
| 15 | template<class T> | ||
| 16 | concept buffers_generator = beast::is_buffers_generator<T>::value; | ||
| 17 | |||
| 18 | template<class T> | ||
| 19 | concept const_buffer_sequence = beast::is_const_buffer_sequence<T>::value; | ||
| 20 | |||
| 21 | } // namespace concepts | ||
| 22 | |||
| 23 | namespace http::concepts { | ||
| 24 | |||
| 25 | template<class T> | ||
| 26 | concept fields = is_fields<T>::value; | ||
| 27 | |||
| 28 | template<class T> | ||
| 29 | concept body = is_body<T>::value; | ||
| 30 | |||
| 31 | template<class T> | ||
| 32 | concept body_reader = is_body_reader<T>::value; | ||
| 33 | |||
| 34 | } // namespace http::concepts | ||
| 35 | |||
| 36 | } // namespace boost::beast | ||
| 37 | |||
| 38 | namespace routemon::http { | ||
| 39 | |||
| 40 | struct supported_verb { | ||
| 41 | enum supported_verb_t : std::uint8_t { | ||
| 42 | options, | ||
| 43 | delete_, | ||
| 44 | get, | ||
| 45 | head, | ||
| 46 | post, | ||
| 47 | put, | ||
| 48 | }; | ||
| 49 | |||
| 50 | supported_verb_t value; | ||
| 51 | |||
| 52 | supported_verb(supported_verb_t value) : value{value} {} | ||
| 53 | |||
| 54 | static auto from(bhttp::verb v) -> std::optional<supported_verb> { | ||
| 55 | switch (v) { | ||
| 56 | case bhttp::verb::options: return supported_verb::options; | ||
| 57 | case bhttp::verb::delete_: return supported_verb::delete_; | ||
| 58 | case bhttp::verb::get: return supported_verb::get; | ||
| 59 | case bhttp::verb::head: return supported_verb::head; | ||
| 60 | case bhttp::verb::post: return supported_verb::post; | ||
| 61 | case bhttp::verb::put: return supported_verb::put; | ||
| 62 | default: return std::nullopt; | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | operator bhttp::verb() const { | ||
| 67 | switch (value) { | ||
| 68 | case supported_verb::options: return bhttp::verb::options; | ||
| 69 | case supported_verb::delete_: return bhttp::verb::delete_; | ||
| 70 | case supported_verb::get: return bhttp::verb::get; | ||
| 71 | case supported_verb::head: return bhttp::verb::head; | ||
| 72 | case supported_verb::post: return bhttp::verb::post; | ||
| 73 | case supported_verb::put: return bhttp::verb::put; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | }; | ||
| 77 | |||
| 78 | export struct verb_set { | ||
| 79 | bool delete_ : 1 = false; | ||
| 80 | bool get : 1 = false; | ||
| 81 | bool head : 1 = false; | ||
| 82 | bool post : 1 = false; | ||
| 83 | bool put : 1 = false; | ||
| 84 | bool options : 1 = false; | ||
| 85 | |||
| 86 | auto enable(supported_verb v) -> void { | ||
| 87 | switch (v.value) { | ||
| 88 | case supported_verb::delete_: | ||
| 89 | delete_ = true; | ||
| 90 | break; | ||
| 91 | case supported_verb::get: | ||
| 92 | get = true; | ||
| 93 | break; | ||
| 94 | case supported_verb::head: | ||
| 95 | head = true; | ||
| 96 | break; | ||
| 97 | case supported_verb::post: | ||
| 98 | post = true; | ||
| 99 | break; | ||
| 100 | case supported_verb::put: | ||
| 101 | put = true; | ||
| 102 | break; | ||
| 103 | case supported_verb::options: | ||
| 104 | options = true; | ||
| 105 | break; | ||
| 106 | default:; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | auto operator==(verb_set const& rhs) const noexcept -> bool = default; | ||
| 111 | |||
| 112 | auto empty() const -> bool { | ||
| 113 | return *this == verb_set{}; | ||
| 114 | } | ||
| 115 | |||
| 116 | verb_set(std::initializer_list<supported_verb> vs) { | ||
| 117 | for (auto const v : vs) enable(v); | ||
| 118 | } | ||
| 119 | |||
| 120 | auto to_string() const -> std::string { | ||
| 121 | std::ostringstream ss; | ||
| 122 | bool wrote = false; | ||
| 123 | auto write = [&](bhttp::verb v) { | ||
| 124 | if (wrote) | ||
| 125 | ss << ", "; | ||
| 126 | ss << v; | ||
| 127 | wrote = true; | ||
| 128 | }; | ||
| 129 | if (delete_) write(bhttp::verb::delete_); | ||
| 130 | if (get) write(bhttp::verb::get); | ||
| 131 | if (head) write(bhttp::verb::head); | ||
| 132 | if (post) write(bhttp::verb::post); | ||
| 133 | if (put) write(bhttp::verb::put); | ||
| 134 | if (options) write(bhttp::verb::options); | ||
| 135 | return ss.str(); | ||
| 136 | } | ||
| 137 | }; | ||
| 138 | |||
| 139 | } // namespace routemon::http | ||
diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm new file mode 100644 index 0000000..0770d97 --- /dev/null +++ b/server/src/http_server.cppm | |||
| @@ -0,0 +1,661 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/config.hpp> | ||
| 4 | #include <boost/asio/as_tuple.hpp> | ||
| 5 | #include <boost/asio/awaitable.hpp> | ||
| 6 | #include <boost/asio/co_spawn.hpp> | ||
| 7 | #include <boost/asio/ip/tcp.hpp> | ||
| 8 | #include <boost/beast/core.hpp> | ||
| 9 | #include <boost/beast/http.hpp> | ||
| 10 | #include <boost/json/serialize.hpp> | ||
| 11 | #include <boost/url.hpp> | ||
| 12 | |||
| 13 | export module routemon:http.server; | ||
| 14 | |||
| 15 | import std; | ||
| 16 | import :config; | ||
| 17 | import :trace; | ||
| 18 | export import :http.common; | ||
| 19 | import :problem; | ||
| 20 | |||
| 21 | namespace net = boost::asio; | ||
| 22 | using tcp = net::ip::tcp; | ||
| 23 | |||
| 24 | namespace routemon::http { | ||
| 25 | |||
| 26 | struct readable_request { | ||
| 27 | util::not_null<bhttp::request_parser<bhttp::empty_body>*> p; | ||
| 28 | util::not_null<beast::tcp_stream*> strm; | ||
| 29 | util::not_null<beast::flat_buffer*> buf; | ||
| 30 | }; | ||
| 31 | |||
| 32 | class presponse { | ||
| 33 | public: | ||
| 34 | using const_buffers_type = beast::span<net::const_buffer>; | ||
| 35 | |||
| 36 | private: | ||
| 37 | struct impl_base { | ||
| 38 | virtual ~impl_base() = default; | ||
| 39 | virtual auto header() -> bhttp::response_header<bhttp::fields>& = 0; | ||
| 40 | virtual auto header() const -> bhttp::response_header<bhttp::fields> const& = 0; | ||
| 41 | virtual auto is_done() const -> bool = 0; | ||
| 42 | virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; | ||
| 43 | virtual auto consume(std::size_t n) -> void = 0; | ||
| 44 | virtual auto keep_alive() const -> bool = 0; | ||
| 45 | }; | ||
| 46 | std::unique_ptr<impl_base> impl_; | ||
| 47 | |||
| 48 | template<bhttp::concepts::body Body> | ||
| 49 | class impl : public impl_base { | ||
| 50 | // Initializes in the response state. | ||
| 51 | // At the first call to prepare, we switch to the message generator state. | ||
| 52 | // After that point, header may not be called anymore (it will throw). | ||
| 53 | std::variant<bhttp::response<Body>, bhttp::message_generator> state_; | ||
| 54 | |||
| 55 | auto ensure_message_generator() -> bhttp::message_generator& { | ||
| 56 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) { | ||
| 57 | auto rsp = bhttp::response<Body>{std::move(*prsp)}; | ||
| 58 | state_.template emplace<bhttp::message_generator>(std::move(rsp)); | ||
| 59 | } | ||
| 60 | return std::get<bhttp::message_generator>(state_); | ||
| 61 | } | ||
| 62 | |||
| 63 | public: | ||
| 64 | explicit impl(bhttp::response<Body>&& rsp) : state_{std::move(rsp)} {} | ||
| 65 | |||
| 66 | auto header() -> bhttp::response_header<bhttp::fields>& override { | ||
| 67 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) { | ||
| 68 | return prsp->base(); | ||
| 69 | } else { | ||
| 70 | // TODO: define custom exception type presponse::bad_header_access | ||
| 71 | throw std::logic_error{"header() may not be called after prepare()"}; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | auto header() const -> bhttp::response_header<bhttp::fields> const& override { | ||
| 75 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) { | ||
| 76 | return prsp->base(); | ||
| 77 | } else { | ||
| 78 | throw std::logic_error{"header() may not be called after prepare()"}; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | auto is_done() const -> bool override { | ||
| 83 | if (auto pgen = std::get_if<bhttp::message_generator>(&state_)) { | ||
| 84 | return pgen->is_done(); | ||
| 85 | } else /* still in the response state */ { | ||
| 86 | return false; | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | auto prepare(beast::error_code& ec) -> const_buffers_type override { | ||
| 91 | return ensure_message_generator().prepare(ec); | ||
| 92 | } | ||
| 93 | |||
| 94 | auto consume(std::size_t n) -> void override { | ||
| 95 | ensure_message_generator().consume(n); | ||
| 96 | } | ||
| 97 | |||
| 98 | auto keep_alive() const noexcept -> bool override { | ||
| 99 | return state_.visit(util::overloaded{ | ||
| 100 | [](bhttp::response<Body> const& rsp) -> bool { | ||
| 101 | return rsp.keep_alive(); | ||
| 102 | }, | ||
| 103 | [](bhttp::message_generator const& gen) -> bool { | ||
| 104 | return gen.keep_alive(); | ||
| 105 | }, | ||
| 106 | }); | ||
| 107 | } | ||
| 108 | }; | ||
| 109 | |||
| 110 | public: | ||
| 111 | template<bhttp::concepts::body Body> | ||
| 112 | explicit presponse(bhttp::response<Body>&& rsp) | ||
| 113 | : impl_{new impl{std::move(rsp)}} | ||
| 114 | {} | ||
| 115 | |||
| 116 | auto header() -> bhttp::response_header<bhttp::fields>& { | ||
| 117 | return impl_->header(); | ||
| 118 | } | ||
| 119 | auto header() const -> bhttp::response_header<bhttp::fields> const& { | ||
| 120 | return impl_->header(); | ||
| 121 | } | ||
| 122 | |||
| 123 | auto is_done() const -> bool { | ||
| 124 | return impl_->is_done(); | ||
| 125 | } | ||
| 126 | |||
| 127 | auto prepare(beast::error_code& ec) -> const_buffers_type { | ||
| 128 | return impl_->prepare(ec); | ||
| 129 | } | ||
| 130 | |||
| 131 | auto consume(std::size_t n) -> void { | ||
| 132 | return impl_->consume(n); | ||
| 133 | } | ||
| 134 | |||
| 135 | auto keep_alive() const noexcept -> bool { | ||
| 136 | return impl_->keep_alive(); | ||
| 137 | } | ||
| 138 | }; | ||
| 139 | static_assert(beast::concepts::buffers_generator<presponse>); | ||
| 140 | |||
| 141 | template<class Ctx> | ||
| 142 | using next_handler_t = std::function<auto(Ctx) -> net::awaitable<presponse>>; | ||
| 143 | |||
| 144 | template<class OuterCtx, class InnerCtx> | ||
| 145 | using middleware_t = std::function<auto(OuterCtx, bhttp::request_header<bhttp::fields>&, next_handler_t<InnerCtx>) -> net::awaitable<presponse>>; | ||
| 146 | |||
| 147 | template<class Ctx> | ||
| 148 | auto lax_cors_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<Ctx> next) -> net::awaitable<presponse> { | ||
| 149 | std::ignore = req_hdr; | ||
| 150 | auto prersp = co_await next(ctx); | ||
| 151 | prersp.header().set(bhttp::field::access_control_allow_origin, "*"); | ||
| 152 | co_return std::move(prersp); | ||
| 153 | } | ||
| 154 | |||
| 155 | template<class InnerCtx> | ||
| 156 | struct trace_id_ctx : InnerCtx { | ||
| 157 | trace::id trace_id = {}; | ||
| 158 | }; | ||
| 159 | |||
| 160 | template<class OuterCtx> | ||
| 161 | auto trace_id_middleware(OuterCtx ctx0, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<trace_id_ctx<OuterCtx>> next) -> net::awaitable<presponse> { | ||
| 162 | std::ignore = req_hdr; | ||
| 163 | auto ctx = trace_id_ctx{std::move(ctx0)}; | ||
| 164 | auto prersp = co_await next(std::move(ctx)); | ||
| 165 | prersp.header().set("X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); | ||
| 166 | prersp.header().insert(bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); | ||
| 167 | co_return std::move(prersp); | ||
| 168 | } | ||
| 169 | |||
| 170 | struct base_ctx { | ||
| 171 | std::locale locale; | ||
| 172 | }; | ||
| 173 | |||
| 174 | template<class Ctx> | ||
| 175 | using basic_route_handler_fn_t = std::function<auto(Ctx, readable_request, std::vector<std::string> const& matches) -> net::awaitable<presponse>>; | ||
| 176 | |||
| 177 | struct keep_alive { | ||
| 178 | bool value; | ||
| 179 | |||
| 180 | explicit keep_alive(bool value) : value{value} {} | ||
| 181 | }; | ||
| 182 | |||
| 183 | template<bhttp::concepts::body Body> | ||
| 184 | auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> { | ||
| 185 | auto rsp = bhttp::response<Body>{}; // HTTP version gets set later | ||
| 186 | rsp.result(status); | ||
| 187 | rsp.keep_alive(ka.value); | ||
| 188 | return rsp; | ||
| 189 | } | ||
| 190 | |||
| 191 | auto problem_rsp(base_ctx const& ctx, problem::details const& problem, keep_alive ka) -> presponse { | ||
| 192 | auto rsp = make_rsp<bhttp::string_body>(problem.status, ka); | ||
| 193 | rsp.set(bhttp::field::content_type, "application/problem+json"); | ||
| 194 | rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); | ||
| 195 | rsp.prepare_payload(); | ||
| 196 | return presponse{std::move(rsp)}; | ||
| 197 | } | ||
| 198 | |||
| 199 | struct preflight_response { | ||
| 200 | verb_set allow_methods; | ||
| 201 | std::vector<bhttp::field> allow_headers; | ||
| 202 | }; | ||
| 203 | auto make_preflight_rsp(preflight_response res, keep_alive ka) -> bhttp::response<bhttp::empty_body> { | ||
| 204 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka); | ||
| 205 | auto allow_headers_str = res.allow_headers | ||
| 206 | | std::views::transform([](auto const& field) -> std::string_view { return bhttp::to_string(field); }) | ||
| 207 | | std::views::join_with(std::string_view{", "}) | ||
| 208 | | std::ranges::to<std::string>(); | ||
| 209 | rsp.set(bhttp::field::access_control_allow_methods, res.allow_methods.to_string()); | ||
| 210 | rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); | ||
| 211 | rsp.prepare_payload(); | ||
| 212 | return rsp; | ||
| 213 | } | ||
| 214 | |||
| 215 | // Using base_ctx instead of a template here since that saves you | ||
| 216 | // typing on invocation (and we do not care about the context type | ||
| 217 | // anyway, but all context types should derive from base_ctx). | ||
| 218 | template<bhttp::concepts::body_reader Body> | ||
| 219 | auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<Body>, presponse>> { | ||
| 220 | std::ignore = ctx; | ||
| 221 | auto p = bhttp::request_parser<Body>{std::move(*r.p)}; | ||
| 222 | co_await bhttp::async_read(*r.strm, *r.buf, p); | ||
| 223 | co_return std::move(p.release()); | ||
| 224 | } | ||
| 225 | |||
| 226 | template<> | ||
| 227 | auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>> { | ||
| 228 | auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); | ||
| 229 | if (ec == bhttp::error::unexpected_body) { | ||
| 230 | auto tpl = problem::tpl{ | ||
| 231 | .status = bhttp::status::bad_request, | ||
| 232 | .title = translate("No body expected for this request"), | ||
| 233 | .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body", | ||
| 234 | }; | ||
| 235 | co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})}; | ||
| 236 | } else if (ec) { | ||
| 237 | throw boost::system::system_error{ec}; | ||
| 238 | } | ||
| 239 | co_return r.p->release(); | ||
| 240 | } | ||
| 241 | |||
| 242 | template<class InnerCtx> | ||
| 243 | struct routed_ctx : InnerCtx { | ||
| 244 | verb_set route_methods; | ||
| 245 | }; | ||
| 246 | |||
| 247 | template<class Ctx> | ||
| 248 | requires requires(Ctx ctx) { | ||
| 249 | // Ctx must be derived from an instantiation of routed_ctx | ||
| 250 | []<class InnerCtx>(routed_ctx<InnerCtx> const&) {}(ctx); | ||
| 251 | } | ||
| 252 | auto default_options_handler(Ctx const& ctx, readable_request r, std::vector<std::string> const&) -> net::awaitable<presponse> { | ||
| 253 | auto mreq = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 254 | if (!mreq) | ||
| 255 | co_return std::move(mreq.error()); | ||
| 256 | |||
| 257 | if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) { | ||
| 258 | // CORS preflight request | ||
| 259 | co_return make_preflight_rsp(preflight_response{ | ||
| 260 | // TODO: should access-control-allow-methods contain OPTIONS? | ||
| 261 | .allow_methods = ctx.route_methods, | ||
| 262 | .allow_headers = {bhttp::field::content_type}, | ||
| 263 | }, keep_alive{mreq->keep_alive()}); | ||
| 264 | } else { | ||
| 265 | // Normal OPTIONS request | ||
| 266 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{mreq->keep_alive()}); | ||
| 267 | rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); | ||
| 268 | rsp.prepare_payload(); | ||
| 269 | co_return std::move(rsp); | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | auto global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable<presponse> { | ||
| 274 | // TODO: switch to "small (4KB) discarded" body type, similar to what Go does? | ||
| 275 | // Same goes for default_options_handler? Not sure. | ||
| 276 | if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); !res) | ||
| 277 | co_return std::move(res.error()); | ||
| 278 | auto req = r.p->release(); | ||
| 279 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{req.keep_alive()}); | ||
| 280 | rsp.prepare_payload(); | ||
| 281 | co_return std::move(rsp); | ||
| 282 | } | ||
| 283 | |||
| 284 | template<class Ctx> | ||
| 285 | auto id_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>&, next_handler_t<Ctx> next) -> net::awaitable<presponse> { | ||
| 286 | co_return co_await next(std::move(ctx)); | ||
| 287 | } | ||
| 288 | |||
| 289 | template<class A, class B, class C> | ||
| 290 | auto middleware_compose(middleware_t<A, B> ab, middleware_t<B, C> bc) -> middleware_t<A, C> { | ||
| 291 | return [ab = std::move(ab), bc = std::move(bc)](A a, bhttp::request_header<bhttp::fields>& header, next_handler_t<C> next) -> net::awaitable<presponse> { | ||
| 292 | co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable<presponse> { | ||
| 293 | co_return co_await bc(std::move(b), header, next); | ||
| 294 | }); | ||
| 295 | }; | ||
| 296 | } | ||
| 297 | |||
| 298 | template<class A, class B> | ||
| 299 | auto middleware_wrap_fn(middleware_t<A, B> ab, basic_route_handler_fn_t<B> fn) -> basic_route_handler_fn_t<A> { | ||
| 300 | return [ab = std::move(ab), fn = std::move(fn)](A a_ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> { | ||
| 301 | co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable<presponse> { | ||
| 302 | co_return co_await fn(std::move(b_ctx), r, matches); | ||
| 303 | }); | ||
| 304 | }; | ||
| 305 | } | ||
| 306 | |||
| 307 | template<std::default_initializable V> | ||
| 308 | requires requires(V v) { | ||
| 309 | { static_cast<bool>(v) }; | ||
| 310 | } | ||
| 311 | struct handler_map { | ||
| 312 | V options = {}; | ||
| 313 | V delete_ = {}; | ||
| 314 | V get = {}; | ||
| 315 | V head = {}; | ||
| 316 | V post = {}; | ||
| 317 | V put = {}; | ||
| 318 | |||
| 319 | template<class Self> | ||
| 320 | auto lookup(this Self&& self, supported_verb v) -> auto&& { | ||
| 321 | switch (v.value) { | ||
| 322 | case supported_verb::options: return std::forward<Self>(self).options; | ||
| 323 | case supported_verb::delete_: return std::forward<Self>(self).delete_; | ||
| 324 | case supported_verb::get: return std::forward<Self>(self).get; | ||
| 325 | case supported_verb::head: return std::forward<Self>(self).head; | ||
| 326 | case supported_verb::post: return std::forward<Self>(self).post; | ||
| 327 | case supported_verb::put: return std::forward<Self>(self).put; | ||
| 328 | } | ||
| 329 | } | ||
| 330 | |||
| 331 | auto verbs() const -> verb_set { | ||
| 332 | auto set = verb_set{}; | ||
| 333 | if (static_cast<bool>(options)) | ||
| 334 | set.enable(supported_verb::options); | ||
| 335 | if (static_cast<bool>(delete_)) | ||
| 336 | set.enable(supported_verb::delete_); | ||
| 337 | if (static_cast<bool>(get)) | ||
| 338 | set.enable(supported_verb::get); | ||
| 339 | if (static_cast<bool>(head)) | ||
| 340 | set.enable(supported_verb::head); | ||
| 341 | if (static_cast<bool>(post)) | ||
| 342 | set.enable(supported_verb::post); | ||
| 343 | if (static_cast<bool>(put)) | ||
| 344 | set.enable(supported_verb::put); | ||
| 345 | return set; | ||
| 346 | } | ||
| 347 | |||
| 348 | auto empty() const -> bool { | ||
| 349 | return verbs().empty(); | ||
| 350 | } | ||
| 351 | |||
| 352 | template<std::default_initializable U> | ||
| 353 | auto map(std::invocable<V const&> auto f) const -> handler_map<U> | ||
| 354 | requires std::assignable_from<U&, std::invoke_result_t<decltype(f), V const&>> | ||
| 355 | { | ||
| 356 | return { | ||
| 357 | .options = static_cast<bool>(options) ? f(options) : U{}, | ||
| 358 | .delete_ = static_cast<bool>(delete_) ? f(delete_) : U{}, | ||
| 359 | .get = static_cast<bool>(get) ? f(get) : U{}, | ||
| 360 | .head = static_cast<bool>(head) ? f(head) : U{}, | ||
| 361 | .post = static_cast<bool>(post) ? f(post) : U{}, | ||
| 362 | .put = static_cast<bool>(put) ? f(put) : U{}, | ||
| 363 | }; | ||
| 364 | } | ||
| 365 | }; | ||
| 366 | |||
| 367 | template<class Ctx> | ||
| 368 | struct route_tree { | ||
| 369 | using leaves = handler_map<basic_route_handler_fn_t<Ctx>>; | ||
| 370 | using named_subtrees = std::unordered_map<std::string, route_tree>; | ||
| 371 | using wildcard_subtree = std::indirect<route_tree>; | ||
| 372 | |||
| 373 | leaves here; | ||
| 374 | // TODO: consider making the first alternative a radix tree | ||
| 375 | // Note: the map is the first variant here; the variant will be | ||
| 376 | // default-constructed with the default-constructed first | ||
| 377 | // alternative. The empty map denotes a lack of subtrees. | ||
| 378 | std::variant<named_subtrees, wildcard_subtree> sub; | ||
| 379 | }; | ||
| 380 | |||
| 381 | template<class OuterCtx, class InnerCtx> | ||
| 382 | auto middleware_wrap_tree(middleware_t<OuterCtx, InnerCtx> mw, route_tree<InnerCtx> const& tree) -> route_tree<OuterCtx> { | ||
| 383 | auto new_leaves = tree.here.template map<basic_route_handler_fn_t<OuterCtx>>(std::bind_front(middleware_wrap_fn<OuterCtx, InnerCtx>, mw)); | ||
| 384 | auto new_sub = tree.sub.visit(util::overloaded{ | ||
| 385 | [&mw](route_tree<InnerCtx>::named_subtrees const& subtrees) -> decltype(route_tree<OuterCtx>::sub) { | ||
| 386 | auto new_subtrees = typename route_tree<OuterCtx>::named_subtrees{}; | ||
| 387 | for (auto [seg, subtree] : subtrees) | ||
| 388 | new_subtrees[seg] = middleware_wrap_tree(mw, subtree); | ||
| 389 | return new_subtrees; | ||
| 390 | }, | ||
| 391 | [&mw](route_tree<InnerCtx>::wildcard_subtree const& subtree) -> decltype(route_tree<OuterCtx>::sub) { | ||
| 392 | return typename route_tree<OuterCtx>::wildcard_subtree{middleware_wrap_tree(mw, *subtree)}; | ||
| 393 | }, | ||
| 394 | }); | ||
| 395 | return {.here = new_leaves, .sub = new_sub}; | ||
| 396 | } | ||
| 397 | |||
| 398 | template<class T> | ||
| 399 | concept match_arg = std::constructible_from<T, std::string const&>; | ||
| 400 | |||
| 401 | template<class Ctx, match_arg... MatchArgs> | ||
| 402 | using route_handler_fn_t = std::function<auto(Ctx, readable_request, MatchArgs...) -> net::awaitable<presponse>>; | ||
| 403 | |||
| 404 | template<class Ctx, match_arg... MatchArgs> | ||
| 405 | auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) -> basic_route_handler_fn_t<Ctx> { | ||
| 406 | return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> { | ||
| 407 | if (sizeof...(MatchArgs) != matches.size()) | ||
| 408 | throw std::runtime_error{"got unexpected amount of matches"}; | ||
| 409 | auto it = matches.begin(); | ||
| 410 | co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast<std::string const&>(*it++)}...); | ||
| 411 | }; | ||
| 412 | } | ||
| 413 | |||
| 414 | template<class Ctx, match_arg... MatchArgs> | ||
| 415 | struct ctree : route_tree<Ctx> { | ||
| 416 | template<class OuterCtx> | ||
| 417 | auto wrap(middleware_t<OuterCtx, Ctx> mw) const -> ctree<OuterCtx, MatchArgs...> { | ||
| 418 | return {middleware_wrap_tree(std::move(mw), *this)}; | ||
| 419 | } | ||
| 420 | }; | ||
| 421 | |||
| 422 | template<class Ctx, match_arg... MatchArgs> | ||
| 423 | struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> { | ||
| 424 | [[nodiscard]] auto to_leaves() const -> typename route_tree<Ctx>::leaves { | ||
| 425 | auto here = this->template map<basic_route_handler_fn_t<Ctx>>(degen_route_handler<Ctx, MatchArgs...>); | ||
| 426 | if (!here.verbs().empty() && !static_cast<bool>(this->options)) | ||
| 427 | here.options = default_options_handler<Ctx>; | ||
| 428 | return here; | ||
| 429 | } | ||
| 430 | |||
| 431 | [[nodiscard]] auto named_subtrees(std::initializer_list<std::pair<std::string, ctree<Ctx, MatchArgs...>>> subtrees) const -> ctree<Ctx, MatchArgs...> { | ||
| 432 | auto sub = typename route_tree<Ctx>::named_subtrees{ | ||
| 433 | std::from_range, | ||
| 434 | subtrees | std::views::transform([](auto const& p) { | ||
| 435 | return std::make_pair(p.first, static_cast<route_tree<Ctx>>(p.second)); | ||
| 436 | }) | ||
| 437 | }; | ||
| 438 | return {route_tree<Ctx>{.here = to_leaves(), .sub = sub}}; | ||
| 439 | } | ||
| 440 | |||
| 441 | template<match_arg MatchArg> | ||
| 442 | [[nodiscard]] auto wildcard_subtree(ctree<Ctx, MatchArgs..., MatchArg> subtree) -> ctree<Ctx, MatchArgs...> { | ||
| 443 | return {route_tree<Ctx>{.here = to_leaves(), .sub = typename route_tree<Ctx>::wildcard_subtree{static_cast<route_tree<Ctx>>(subtree)}}}; | ||
| 444 | } | ||
| 445 | |||
| 446 | [[nodiscard]] auto no_subtrees() const -> ctree<Ctx, MatchArgs...> { | ||
| 447 | return {route_tree<Ctx>{.here = to_leaves(), .sub = {}}}; | ||
| 448 | } | ||
| 449 | }; | ||
| 450 | |||
| 451 | template<std::derived_from<base_ctx> PreRouteCtx> | ||
| 452 | class server { | ||
| 453 | log::logger l_; | ||
| 454 | locale::selector lsel_; | ||
| 455 | middleware_t<base_ctx, PreRouteCtx> global_middleware_; | ||
| 456 | route_tree<routed_ctx<PreRouteCtx>> routes_; | ||
| 457 | |||
| 458 | public: | ||
| 459 | explicit server(log::logger const& l, locale::selector&& lsel, middleware_t<base_ctx, PreRouteCtx> global_middleware, route_tree<routed_ctx<PreRouteCtx>> routes) | ||
| 460 | : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)} | ||
| 461 | {} | ||
| 462 | |||
| 463 | struct match_result { | ||
| 464 | util::not_null<handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const*> route_handlers; | ||
| 465 | std::vector<std::string> wildcard_matches; | ||
| 466 | |||
| 467 | auto allowed_methods() const -> verb_set { | ||
| 468 | return route_handlers->verbs(); | ||
| 469 | } | ||
| 470 | }; | ||
| 471 | |||
| 472 | auto match(boost::urls::segments_view segments) const -> std::optional<match_result> { | ||
| 473 | auto const* tree = &routes_; | ||
| 474 | auto wildcard_matches = std::vector<std::string>{}; | ||
| 475 | for (auto const& seg : segments) { | ||
| 476 | tree->sub.visit(util::overloaded{ | ||
| 477 | [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& subtrees) { | ||
| 478 | auto it = subtrees.find(seg); | ||
| 479 | tree = it == subtrees.end() ? nullptr : &it->second; | ||
| 480 | }, | ||
| 481 | [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& wildcard_subtree) { | ||
| 482 | wildcard_matches.push_back(seg); | ||
| 483 | tree = &*wildcard_subtree; | ||
| 484 | }, | ||
| 485 | }); | ||
| 486 | if (!tree) return std::nullopt; | ||
| 487 | } | ||
| 488 | if (tree->here.empty()) return std::nullopt; | ||
| 489 | return match_result{ | ||
| 490 | .route_handlers = util::not_null{&tree->here}, | ||
| 491 | .wildcard_matches = wildcard_matches, | ||
| 492 | }; | ||
| 493 | } | ||
| 494 | |||
| 495 | auto route_request(PreRouteCtx ctx, readable_request r) const -> net::awaitable<presponse> { | ||
| 496 | auto req_base = r.p->get().base(); | ||
| 497 | |||
| 498 | auto const bad_request_tpl = problem::tpl{ | ||
| 499 | .status = bhttp::status::bad_request, | ||
| 500 | .title = translate("Bad request"), | ||
| 501 | .type_uri = "https://routemon.fautchen.eu/problems/bad-request", | ||
| 502 | }; | ||
| 503 | |||
| 504 | if (req_base.target() == "*") { | ||
| 505 | // request-target is in asterisk-form (RFC 9112, § 3.2.4), | ||
| 506 | // so the request must be a server-wide OPTIONS request. | ||
| 507 | |||
| 508 | if (req_base.method() != bhttp::verb::options) { | ||
| 509 | auto tpl = problem::tpl{ | ||
| 510 | .status = bhttp::status::method_not_allowed, | ||
| 511 | .title = translate("Method not allowed"), | ||
| 512 | .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", | ||
| 513 | }; | ||
| 514 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 515 | } | ||
| 516 | |||
| 517 | co_return co_await global_options_handler(ctx, r); | ||
| 518 | } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) { | ||
| 519 | // request-target is in origin-form (RFC 9112, § 3.2.1), | ||
| 520 | // so it must be a normal request (not a CONNECT or | ||
| 521 | // server-wide OPTIONS request). | ||
| 522 | |||
| 523 | auto req_url = boost::urls::url{*mreq_url0}; | ||
| 524 | req_url.normalize(); | ||
| 525 | if (!req_url.is_path_absolute()) { | ||
| 526 | auto problem = bad_request_tpl.instantiate(). | ||
| 527 | set_detail(translate("Path of normalized (RFC 3986, § 6) " | ||
| 528 | "origin-form request-target (RFC " | ||
| 529 | "9112, § 3.2.1) should be " | ||
| 530 | "absolute")); | ||
| 531 | co_return problem_rsp(ctx, problem, keep_alive{false}); | ||
| 532 | } | ||
| 533 | |||
| 534 | auto mres = match(req_url.segments()); | ||
| 535 | if (!mres) { | ||
| 536 | auto tpl = problem::tpl{ | ||
| 537 | .status = bhttp::status::not_found, | ||
| 538 | .title = translate("Not found"), | ||
| 539 | .type_uri = "https://routemon.fautchen.eu/problems/not-found", | ||
| 540 | }; | ||
| 541 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 542 | } | ||
| 543 | |||
| 544 | auto mverb = supported_verb::from(req_base.method()); | ||
| 545 | if (!mverb) { | ||
| 546 | // Method not implemented. | ||
| 547 | auto tpl = problem::tpl{ | ||
| 548 | .status = bhttp::status::not_implemented, | ||
| 549 | .title = translate("Method not implemented"), | ||
| 550 | .type_uri = "https://routemon.fautchen.eu/problems/method-not-implemented", | ||
| 551 | }; | ||
| 552 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 553 | } | ||
| 554 | |||
| 555 | if (auto mhdl = mres->route_handlers->lookup(*mverb)) { | ||
| 556 | auto new_ctx = routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()}; | ||
| 557 | co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); | ||
| 558 | } else { | ||
| 559 | // Path recognized, but method not allowed. | ||
| 560 | auto tpl = problem::tpl{ | ||
| 561 | .status = bhttp::status::method_not_allowed, | ||
| 562 | .title = translate("Method not allowed"), | ||
| 563 | .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", | ||
| 564 | }; | ||
| 565 | auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 566 | rsp.header().set(bhttp::field::allow, mres->allowed_methods().to_string()); | ||
| 567 | co_return std::move(rsp); | ||
| 568 | } | ||
| 569 | } else { | ||
| 570 | // We do not accept any other request-target forms. | ||
| 571 | |||
| 572 | auto problem = bad_request_tpl.instantiate(). | ||
| 573 | set_detail(translate("Invalid request-target, expected " | ||
| 574 | "asterisk-form or origin-form " | ||
| 575 | "(see RFC 9112, § 3.2)")); | ||
| 576 | co_return problem_rsp(ctx, problem, keep_alive{false}); | ||
| 577 | } | ||
| 578 | } | ||
| 579 | |||
| 580 | auto handle_request(readable_request r) const -> net::awaitable<presponse> { | ||
| 581 | auto header = r.p->get().base(); | ||
| 582 | auto locale = lsel_.select(header[bhttp::field::accept_language]); | ||
| 583 | auto ctx0 = base_ctx{.locale = locale}; | ||
| 584 | |||
| 585 | co_return co_await global_middleware_(std::move(ctx0), header, [&](PreRouteCtx ctx) -> net::awaitable<presponse> { | ||
| 586 | co_return co_await route_request(std::move(ctx), std::move(r)); | ||
| 587 | }); | ||
| 588 | } | ||
| 589 | |||
| 590 | auto do_session(beast::tcp_stream strm) -> net::awaitable<void> { | ||
| 591 | auto buf = beast::flat_buffer{}; | ||
| 592 | |||
| 593 | while (true) { | ||
| 594 | auto p0 = bhttp::request_parser<bhttp::empty_body>{}; | ||
| 595 | p0.body_limit(boost::none); | ||
| 596 | auto [ec, _] = co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); | ||
| 597 | if (ec == bhttp::error::end_of_stream) { | ||
| 598 | break; | ||
| 599 | } else if (ec) { | ||
| 600 | throw boost::system::system_error{ec}; | ||
| 601 | } | ||
| 602 | |||
| 603 | auto http_version = p0.get().version(); | ||
| 604 | auto&& rsp = co_await handle_request(readable_request{ | ||
| 605 | .p = util::not_null{&p0}, | ||
| 606 | .strm = util::not_null{&strm}, | ||
| 607 | .buf = util::not_null{&buf}, | ||
| 608 | }); | ||
| 609 | rsp.header().version(http_version); | ||
| 610 | bool keep_alive = rsp.keep_alive(); | ||
| 611 | co_await beast::async_write(strm, std::move(rsp)); | ||
| 612 | if (!keep_alive) { | ||
| 613 | break; | ||
| 614 | } | ||
| 615 | } | ||
| 616 | |||
| 617 | strm.socket().shutdown(tcp::socket::shutdown_send); | ||
| 618 | } | ||
| 619 | |||
| 620 | auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void> { | ||
| 621 | auto executor = co_await net::this_coro::executor; | ||
| 622 | auto acceptor = tcp::acceptor{executor, endpoint}; | ||
| 623 | |||
| 624 | l_.with("endpoint", endpoint.address().to_string()). | ||
| 625 | with("port", std::to_string(endpoint.port())). | ||
| 626 | info("Serving"); | ||
| 627 | while (true) { | ||
| 628 | net::co_spawn(executor, | ||
| 629 | do_session(beast::tcp_stream{co_await acceptor.async_accept()}), | ||
| 630 | [this](std::exception_ptr e) { | ||
| 631 | if (e) { | ||
| 632 | try { | ||
| 633 | std::rethrow_exception(e); | ||
| 634 | } catch (std::exception const& e) { | ||
| 635 | l_.error("Error in session: {}", e.what()); | ||
| 636 | } | ||
| 637 | } | ||
| 638 | }); | ||
| 639 | } | ||
| 640 | } | ||
| 641 | |||
| 642 | auto spawn(net::io_context& ioc) -> void { | ||
| 643 | auto const addr = net::ip::make_address("0.0.0.0"); | ||
| 644 | auto const endpoint = tcp::endpoint{addr, 8284}; | ||
| 645 | |||
| 646 | // TODO: make exception handling as nice as in srv.cpp | ||
| 647 | net::co_spawn(ioc, | ||
| 648 | do_listen(endpoint), | ||
| 649 | [this](std::exception_ptr e) { | ||
| 650 | if (e) { | ||
| 651 | try { | ||
| 652 | std::rethrow_exception(e); | ||
| 653 | } catch (std::exception const& e) { | ||
| 654 | l_.error("Error: {}", e.what()); | ||
| 655 | } | ||
| 656 | } | ||
| 657 | }); | ||
| 658 | } | ||
| 659 | }; | ||
| 660 | |||
| 661 | } // namespace routemon::http | ||
diff --git a/server/src/locale.cppm b/server/src/locale.cppm new file mode 100644 index 0000000..65ae2ea --- /dev/null +++ b/server/src/locale.cppm | |||
| @@ -0,0 +1,245 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/locale.hpp> | ||
| 4 | #include <unicode/localematcher.h> | ||
| 5 | |||
| 6 | export module routemon:locale; | ||
| 7 | |||
| 8 | import std; | ||
| 9 | import :util; | ||
| 10 | |||
| 11 | export namespace blocale = boost::locale; | ||
| 12 | |||
| 13 | export namespace routemon { | ||
| 14 | using lformat = blocale::format; | ||
| 15 | using blocale::translate; | ||
| 16 | using blocale::gettext; | ||
| 17 | } // namespace routemon | ||
| 18 | |||
| 19 | namespace routemon::locale { | ||
| 20 | |||
| 21 | struct locale_priority { | ||
| 22 | float weight; | ||
| 23 | std::size_t original_index; | ||
| 24 | }; | ||
| 25 | |||
| 26 | auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool { | ||
| 27 | if (lhs.weight != rhs.weight) | ||
| 28 | return lhs.weight > rhs.weight; | ||
| 29 | return lhs.original_index < rhs.original_index; | ||
| 30 | } | ||
| 31 | |||
| 32 | struct icu_locale_hash { | ||
| 33 | std::size_t operator()(icu::Locale const& l) const noexcept { | ||
| 34 | static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); | ||
| 35 | std::int32_t hash = l.hashCode(); | ||
| 36 | if (hash < 0) { | ||
| 37 | return static_cast<std::size_t>(std::numeric_limits<int>::max()) + static_cast<std::size_t>(-hash) + 1; | ||
| 38 | } else { | ||
| 39 | return static_cast<std::size_t>(hash); | ||
| 40 | } | ||
| 41 | } | ||
| 42 | }; | ||
| 43 | |||
| 44 | using icu_locale_priority_map = std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>; | ||
| 45 | using icu_priority_locale = std::pair<icu::Locale, locale_priority>; | ||
| 46 | |||
| 47 | auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) -> bool { | ||
| 48 | return lhs.second < rhs.second; | ||
| 49 | } | ||
| 50 | |||
| 51 | class icu_priority_locale_vec_iterator : public icu::Locale::Iterator { | ||
| 52 | std::size_t i_ = 0uz; | ||
| 53 | std::vector<icu_priority_locale> ls_; | ||
| 54 | |||
| 55 | public: | ||
| 56 | explicit icu_priority_locale_vec_iterator(std::vector<icu_priority_locale>&& ls) | ||
| 57 | : ls_(std::move(ls)) | ||
| 58 | {} | ||
| 59 | |||
| 60 | auto hasNext() const -> UBool override { | ||
| 61 | return i_ < ls_.size(); | ||
| 62 | } | ||
| 63 | |||
| 64 | auto next() -> icu::Locale const& override { | ||
| 65 | return ls_[i_++].first; | ||
| 66 | } | ||
| 67 | |||
| 68 | ~icu_priority_locale_vec_iterator() override = default; | ||
| 69 | }; | ||
| 70 | |||
| 71 | export template<class T> | ||
| 72 | concept locale_input_range = | ||
| 73 | std::ranges::input_range<T> && | ||
| 74 | std::same_as<std::locale const&, std::ranges::range_const_reference_t<T>>; | ||
| 75 | |||
| 76 | // Helps select a locale based on the Accept-Language header in an | ||
| 77 | // HTTP request. | ||
| 78 | export class selector { | ||
| 79 | std::locale default_; | ||
| 80 | icu::LocaleMatcher matcher_; | ||
| 81 | std::shared_ptr<blocale::generator const> lgen_; | ||
| 82 | |||
| 83 | auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) { | ||
| 84 | auto builder = icu::LocaleMatcher::Builder{}; | ||
| 85 | for (auto const& supported_locale : supported_locales) { | ||
| 86 | auto const& supported_locale_info = std::use_facet<blocale::info>(supported_locale); | ||
| 87 | auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()}; | ||
| 88 | if (supported_icu_locale.isBogus()) | ||
| 89 | throw std::runtime_error{"supported locale gives rise to bogus ICU locale"}; | ||
| 90 | builder.addSupportedLocale(supported_icu_locale); | ||
| 91 | } | ||
| 92 | auto const& default_locale_info = std::use_facet<blocale::info>(default_locale); | ||
| 93 | auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; | ||
| 94 | if (default_icu_locale.isBogus()) | ||
| 95 | throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; | ||
| 96 | builder.setDefaultLocale(&default_icu_locale); | ||
| 97 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 98 | auto matcher = builder.build(ec); | ||
| 99 | if (U_FAILURE(ec)) | ||
| 100 | throw std::runtime_error{"failed to build icu::LocaleMatcher"}; | ||
| 101 | return matcher; | ||
| 102 | } | ||
| 103 | |||
| 104 | // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. | ||
| 105 | static auto ltrim_ows(std::string_view s) -> std::string_view { | ||
| 106 | if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) | ||
| 107 | s.remove_prefix(i); | ||
| 108 | return s; | ||
| 109 | } | ||
| 110 | static auto rtrim_ows(std::string_view s) -> std::string_view { | ||
| 111 | if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) | ||
| 112 | return s.substr(0, i + 1); | ||
| 113 | return s; | ||
| 114 | } | ||
| 115 | static auto trim_ows(std::string_view s) -> std::string_view { | ||
| 116 | return rtrim_ows(ltrim_ows(s)); | ||
| 117 | } | ||
| 118 | |||
| 119 | auto from_icu_locale(icu::Locale const& l) const -> std::locale { | ||
| 120 | auto posix_name = std::string{l.getLanguage()}; | ||
| 121 | if (l.getScript() && std::strlen(l.getScript()) > 0) { | ||
| 122 | posix_name += "_"; | ||
| 123 | posix_name += l.getScript(); | ||
| 124 | } | ||
| 125 | if (l.getCountry() && std::strlen(l.getCountry()) > 0) { | ||
| 126 | posix_name += "_"; | ||
| 127 | posix_name += l.getCountry(); | ||
| 128 | } | ||
| 129 | posix_name += ".UTF-8"; | ||
| 130 | auto added_at = false; | ||
| 131 | if (l.getVariant() && std::strlen(l.getVariant()) > 0) { | ||
| 132 | added_at = true; | ||
| 133 | posix_name += "@"; | ||
| 134 | posix_name += l.getVariant(); | ||
| 135 | } | ||
| 136 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 137 | auto* keywords = l.createKeywords(ec); | ||
| 138 | if (U_FAILURE(ec)) | ||
| 139 | throw std::runtime_error{"failed to create keywords"}; | ||
| 140 | if (keywords) { | ||
| 141 | std::int32_t kw_len = 0; | ||
| 142 | char const* kw = nullptr; | ||
| 143 | while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) { | ||
| 144 | auto value = l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec); | ||
| 145 | if (!added_at) { | ||
| 146 | posix_name += "@"; | ||
| 147 | added_at = true; | ||
| 148 | } else { | ||
| 149 | posix_name += ";"; | ||
| 150 | } | ||
| 151 | posix_name += kw; | ||
| 152 | posix_name += "="; | ||
| 153 | posix_name += value; | ||
| 154 | } | ||
| 155 | if (U_FAILURE(ec)) | ||
| 156 | throw std::runtime_error{"failed to iterate over keywords"}; | ||
| 157 | delete keywords; | ||
| 158 | } | ||
| 159 | return lgen_->generate(posix_name); | ||
| 160 | } | ||
| 161 | |||
| 162 | public: | ||
| 163 | // Note: lgen must live at least as long as the selector constructed here! | ||
| 164 | // It is unfortunately not possible to copy/move a blocale::generator. | ||
| 165 | explicit selector(locale_input_range auto locales, std::locale default_, std::shared_ptr<blocale::generator const> lgen) | ||
| 166 | : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} | ||
| 167 | {} | ||
| 168 | |||
| 169 | auto select(std::string_view accept_language) const -> std::locale { | ||
| 170 | using namespace std::literals::string_view_literals; | ||
| 171 | // NOTE: can also contain a *;q=0.1 | ||
| 172 | // q should have at most 3 digits after period | ||
| 173 | auto dlpm = icu_locale_priority_map{}; | ||
| 174 | for (auto const [i, lang_prio] : accept_language | std::views::split(","sv) | std::views::enumerate) { | ||
| 175 | auto [lang_range_ut, mweight_ut] = util::split_on(std::string_view{lang_prio}, ';'); | ||
| 176 | auto lang_range_str = trim_ows(lang_range_ut); | ||
| 177 | auto mweight_str = mweight_ut.transform(trim_ows); | ||
| 178 | if (lang_range_str == "*") | ||
| 179 | break; | ||
| 180 | |||
| 181 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 182 | auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); | ||
| 183 | if (U_FAILURE(ec) || icu_locale.isBogus()) | ||
| 184 | continue; // ignore this locale | ||
| 185 | |||
| 186 | auto weight = 1.0f; | ||
| 187 | if (mweight_str && mweight_str->starts_with("q=")) { | ||
| 188 | auto weight_str = mweight_str->substr(2, 4); | ||
| 189 | if (auto mweight = util::parse_float(weight_str, std::chars_format::fixed); | ||
| 190 | mweight && 0.0f < *mweight && *mweight < 1.0f) { | ||
| 191 | weight = *mweight; | ||
| 192 | } | ||
| 193 | } | ||
| 194 | |||
| 195 | if (weight > 0.0f) { | ||
| 196 | dlpm[icu_locale] = { | ||
| 197 | .weight = weight, | ||
| 198 | .original_index = static_cast<std::size_t>(i), | ||
| 199 | }; | ||
| 200 | } else { | ||
| 201 | dlpm.erase(icu_locale); | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | auto desired_locales = std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()}; | ||
| 206 | std::sort(desired_locales.begin(), desired_locales.end()); | ||
| 207 | auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; | ||
| 208 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 209 | auto res = matcher_.getBestMatchResult(it, ec); | ||
| 210 | if (U_FAILURE(ec)) | ||
| 211 | return default_; | ||
| 212 | auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? | ||
| 213 | if (U_FAILURE(ec)) | ||
| 214 | return from_icu_locale(*res.getSupportedLocale()); | ||
| 215 | return from_icu_locale(resolved); | ||
| 216 | } | ||
| 217 | }; | ||
| 218 | |||
| 219 | export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> { | ||
| 220 | auto const& locale_info = std::use_facet<blocale::info>(locale); | ||
| 221 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 222 | auto bcp47_lang_tag = icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec); | ||
| 223 | if (U_FAILURE(ec)) | ||
| 224 | return std::nullopt; | ||
| 225 | return bcp47_lang_tag; | ||
| 226 | } | ||
| 227 | |||
| 228 | #ifdef LOCALEDIR | ||
| 229 | # define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) | ||
| 230 | # define LOCALEDIR_AUX_STR(s) #s | ||
| 231 | constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; | ||
| 232 | # undef LOCALEDIR_AUX_STR | ||
| 233 | # undef LOCALEDIR_AUX_XSTR | ||
| 234 | #else // ifdef LOCALEDIR | ||
| 235 | constexpr auto messages_path = std::string_view{"locale/dev"}; | ||
| 236 | #endif // ifdef LOCALEDIR | ||
| 237 | |||
| 238 | export auto make_generator() -> std::shared_ptr<blocale::generator const> { | ||
| 239 | auto lgen = std::make_shared<blocale::generator>(); | ||
| 240 | lgen->add_messages_path(std::string{messages_path}); | ||
| 241 | lgen->add_messages_domain("routemon"); | ||
| 242 | return std::static_pointer_cast<blocale::generator const>(lgen); | ||
| 243 | } | ||
| 244 | |||
| 245 | } // namespace routemon::locale | ||
diff --git a/server/src/log.cppm b/server/src/log.cppm new file mode 100644 index 0000000..d7e2bff --- /dev/null +++ b/server/src/log.cppm | |||
| @@ -0,0 +1,148 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | // Seems like ADL for std::quoted is broken with | ||
| 4 | // import std; | ||
| 5 | // Might be because the _Quoted_string object is defined in | ||
| 6 | // std::__detail, which is not exported by the module. | ||
| 7 | #include <iomanip> | ||
| 8 | |||
| 9 | export module routemon:log; | ||
| 10 | |||
| 11 | import std; | ||
| 12 | |||
| 13 | namespace routemon::log { | ||
| 14 | |||
| 15 | export enum class level : std::uint8_t { | ||
| 16 | debug, | ||
| 17 | info, | ||
| 18 | warn, | ||
| 19 | error, | ||
| 20 | }; | ||
| 21 | |||
| 22 | namespace { | ||
| 23 | |||
| 24 | auto operator<<(std::ostream& os, level lvl) -> std::ostream& { | ||
| 25 | switch (lvl) { | ||
| 26 | case level::debug: os << "dbg"; break; | ||
| 27 | case level::info: os << "inf"; break; | ||
| 28 | case level::warn: os << "wrn"; break; | ||
| 29 | case level::error: os << "err"; break; | ||
| 30 | } | ||
| 31 | return os; | ||
| 32 | } | ||
| 33 | |||
| 34 | } // namespace (unique) | ||
| 35 | |||
| 36 | export class sink { | ||
| 37 | std::atomic<enum level> lvl_; | ||
| 38 | std::ostream& os_ = std::cout; | ||
| 39 | |||
| 40 | struct tmp_message { | ||
| 41 | level lvl; | ||
| 42 | std::string_view component; | ||
| 43 | std::string_view txt; | ||
| 44 | std::map<std::string, std::string> const& attrs; | ||
| 45 | }; | ||
| 46 | |||
| 47 | auto write(tmp_message msg) -> void { | ||
| 48 | auto sos = std::osyncstream{os_}; | ||
| 49 | sos << "[" << msg.lvl; | ||
| 50 | if (!msg.component.empty()) | ||
| 51 | sos << " " << msg.component; | ||
| 52 | sos << "] " << msg.txt; | ||
| 53 | for (auto const& [k, v] : msg.attrs) { | ||
| 54 | sos << " " << k << "=" << std::quoted(v); | ||
| 55 | } | ||
| 56 | sos << '\n'; | ||
| 57 | } | ||
| 58 | |||
| 59 | explicit sink(level lvl) | ||
| 60 | : lvl_{lvl} | ||
| 61 | {} | ||
| 62 | |||
| 63 | friend auto make_sink(level lvl) -> std::shared_ptr<sink>; | ||
| 64 | friend class logger; | ||
| 65 | |||
| 66 | public: | ||
| 67 | [[nodiscard]] auto level() const -> enum level { | ||
| 68 | return lvl_; | ||
| 69 | } | ||
| 70 | |||
| 71 | auto set_level(enum level lvl) -> void { | ||
| 72 | lvl_ = lvl; | ||
| 73 | } | ||
| 74 | }; | ||
| 75 | |||
| 76 | export auto make_sink(level lvl) -> std::shared_ptr<sink> { | ||
| 77 | return std::shared_ptr<sink>{new sink{lvl}}; | ||
| 78 | } | ||
| 79 | |||
| 80 | export class logger { | ||
| 81 | std::shared_ptr<sink> sink_; | ||
| 82 | std::string component_; | ||
| 83 | std::map<std::string, std::string> attrs_; | ||
| 84 | |||
| 85 | template<log::level lvl> | ||
| 86 | auto log_at(std::string_view fmt, std::format_args args) -> logger& { | ||
| 87 | if (sink_->level() <= lvl) | ||
| 88 | sink_->write(sink::tmp_message{ | ||
| 89 | .lvl = lvl, | ||
| 90 | .component = component_, | ||
| 91 | .txt = std::vformat(fmt, args), | ||
| 92 | .attrs = attrs_, | ||
| 93 | }); | ||
| 94 | return *this; | ||
| 95 | } | ||
| 96 | |||
| 97 | public: | ||
| 98 | explicit logger(std::shared_ptr<sink> const& sink) | ||
| 99 | : sink_{sink} | ||
| 100 | { | ||
| 101 | if (!sink) { | ||
| 102 | throw std::invalid_argument{"logger sink may not be null"}; | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | [[nodiscard]] auto sub(std::string_view component) const -> logger { | ||
| 107 | auto l = *this; | ||
| 108 | if (l.component_.empty()) { | ||
| 109 | l.component_ = component; | ||
| 110 | } else { | ||
| 111 | l.component_ += "."; | ||
| 112 | l.component_ += component; | ||
| 113 | } | ||
| 114 | return l; | ||
| 115 | } | ||
| 116 | |||
| 117 | [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger { | ||
| 118 | auto l = *this; | ||
| 119 | l.attrs_[k] = std::move(v); | ||
| 120 | return l; | ||
| 121 | } | ||
| 122 | |||
| 123 | [[nodiscard]] auto with(std::string const& k, std::string_view v) const -> logger { | ||
| 124 | return with(k, std::string{v}); | ||
| 125 | } | ||
| 126 | |||
| 127 | template<class... Args> | ||
| 128 | auto debug(std::format_string<Args...> fmt, Args&&... args) -> logger& { | ||
| 129 | return log_at<level::debug>(fmt.get(), std::make_format_args(args...)); | ||
| 130 | } | ||
| 131 | |||
| 132 | template<class... Args> | ||
| 133 | auto info(std::format_string<Args...> fmt, Args&&... args) -> logger& { | ||
| 134 | return log_at<level::info>(fmt.get(), std::make_format_args(args...)); | ||
| 135 | } | ||
| 136 | |||
| 137 | template<class... Args> | ||
| 138 | auto warn(std::format_string<Args...> fmt, Args&&... args) -> logger& { | ||
| 139 | return log_at<level::warn>(fmt.get(), std::make_format_args(args...)); | ||
| 140 | } | ||
| 141 | |||
| 142 | template<class... Args> | ||
| 143 | auto error(std::format_string<Args...> fmt, Args&&... args) -> logger& { | ||
| 144 | return log_at<level::error>(fmt.get(), std::make_format_args(args...)); | ||
| 145 | } | ||
| 146 | }; | ||
| 147 | |||
| 148 | } // namespace routemon::log | ||
diff --git a/server/src/main.cpp b/server/src/main.cpp new file mode 100644 index 0000000..a40b0b0 --- /dev/null +++ b/server/src/main.cpp | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | #include <malloc.h> // for malloc_trim(3) | ||
| 2 | #include <boost/asio.hpp> | ||
| 3 | |||
| 4 | import std; | ||
| 5 | import routemon; | ||
| 6 | |||
| 7 | namespace chrono = std::chrono; | ||
| 8 | namespace net = boost::asio; | ||
| 9 | |||
| 10 | enum class exit_status { | ||
| 11 | failure, | ||
| 12 | bad_usage, | ||
| 13 | }; | ||
| 14 | |||
| 15 | auto real_main(std::span<char const*> args) -> exit_status { | ||
| 16 | auto sink = routemon::log::make_sink(routemon::log::level::info); | ||
| 17 | auto l = routemon::log::logger{sink}; | ||
| 18 | |||
| 19 | if (args.size() != 2) { | ||
| 20 | l.error("Fatal: expected exactly one argument (the configuration file location), got {}", args.size() - 1); | ||
| 21 | return exit_status::bad_usage; | ||
| 22 | } | ||
| 23 | auto const* config_filename = args[1]; | ||
| 24 | |||
| 25 | auto lgen = routemon::locale::make_generator(); | ||
| 26 | auto default_locale = lgen->generate("en_US.UTF-8"); | ||
| 27 | auto locales = { | ||
| 28 | default_locale, | ||
| 29 | lgen->generate("nl_NL.UTF-8"), | ||
| 30 | lgen->generate("de_DE.UTF-8"), | ||
| 31 | lgen->generate("en_GB.UTF-8"), | ||
| 32 | }; | ||
| 33 | auto lsel = routemon::locale::selector{locales, default_locale, lgen}; | ||
| 34 | |||
| 35 | auto ioc = net::io_context{1 /* concurrency hint */}; | ||
| 36 | |||
| 37 | auto config = routemon::config::app{}; | ||
| 38 | try { | ||
| 39 | config = routemon::config::load_file(config_filename); | ||
| 40 | } catch (std::exception const& e) { | ||
| 41 | l.with("filename", std::string_view{config_filename}). | ||
| 42 | error("Failed to load configuration: {}", e.what()); | ||
| 43 | return exit_status::failure; | ||
| 44 | } | ||
| 45 | sink->set_level(config.logger.level); | ||
| 46 | // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, config.rwgps.auth_token}; | ||
| 47 | // for (auto route : rwgps_client.get_all_routes()) { | ||
| 48 | // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, route.name, route.url); | ||
| 49 | // } | ||
| 50 | |||
| 51 | auto dbc = std::shared_ptr<routemon::database::connection>{}; | ||
| 52 | try { | ||
| 53 | dbc = routemon::database::open(config.database.sqlite3_filename); | ||
| 54 | } catch (std::exception const& e) { | ||
| 55 | l.with("filename", config.database.sqlite3_filename). | ||
| 56 | error("Failed to open database: {}", e.what()); | ||
| 57 | return exit_status::failure; | ||
| 58 | } | ||
| 59 | |||
| 60 | l.with("filename", config.situations.datex2_filename). | ||
| 61 | info("Loading situations"); | ||
| 62 | auto const before_load = chrono::steady_clock::now(); | ||
| 63 | auto d2loader = routemon::datex2::loader{}; | ||
| 64 | auto pub = routemon::datex2::situation_publication{}; | ||
| 65 | try { | ||
| 66 | pub = d2loader.load_situation_publication(config.situations.datex2_filename); | ||
| 67 | } catch (std::exception const& e) { | ||
| 68 | l.error("Failed to load DATEX II situations publication: {}", e.what()); | ||
| 69 | return exit_status::failure; | ||
| 70 | } | ||
| 71 | if (!d2loader.warnings().empty()) { | ||
| 72 | auto const& warns = d2loader.warnings(); | ||
| 73 | l.warn("Encountered {} unique warnings while loading DATEX II situations publication", warns.size()); | ||
| 74 | auto i = 0uz; | ||
| 75 | for (auto it = warns.begin(); it != warns.end(); it = warns.upper_bound(*it)) { | ||
| 76 | l.warn("Warning {} (appeared {}×): {}", ++i, warns.count(*it), *it); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | // Processing the feed is by far the most memory-intensive operation | ||
| 80 | // during the run time of this application (at the moment), the | ||
| 81 | // resident set will likely never be this big again. So we ask libc | ||
| 82 | // to return as much memory as possible to the OS. | ||
| 83 | malloc_trim(0); | ||
| 84 | auto const after_load = chrono::steady_clock::now(); | ||
| 85 | auto const dur_load = chrono::duration_cast<chrono::milliseconds>(after_load - before_load); | ||
| 86 | l.info("Loading situations finished in {}", dur_load); | ||
| 87 | |||
| 88 | auto handler = routemon::api::handler{l, std::move(pub)}; | ||
| 89 | auto http_server = routemon::srv::server{l, std::move(lsel), std::move(handler)}; | ||
| 90 | http_server.spawn(ioc); | ||
| 91 | ioc.run(); | ||
| 92 | |||
| 93 | l.error("I/O context stopped"); | ||
| 94 | return exit_status::failure; | ||
| 95 | } | ||
| 96 | |||
| 97 | auto main(int argc, char* argv[]) -> int { | ||
| 98 | if (argc < 0) { | ||
| 99 | std::cout << "Fatal: argument count below zero" << std::endl; | ||
| 100 | return EXIT_FAILURE; | ||
| 101 | } | ||
| 102 | auto res = real_main(std::span{const_cast<char const**>(argv), static_cast<std::size_t>(argc)}); | ||
| 103 | switch (res) { | ||
| 104 | case exit_status::failure: | ||
| 105 | return EXIT_FAILURE; | ||
| 106 | case exit_status::bad_usage: | ||
| 107 | return 2; | ||
| 108 | } | ||
| 109 | } | ||
diff --git a/server/src/problem.cppm b/server/src/problem.cppm new file mode 100644 index 0000000..8764962 --- /dev/null +++ b/server/src/problem.cppm | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/http/message.hpp> | ||
| 4 | #include <boost/json.hpp> | ||
| 5 | #include <boost/locale/message.hpp> | ||
| 6 | |||
| 7 | export module routemon:problem; | ||
| 8 | |||
| 9 | import std; | ||
| 10 | import :locale; | ||
| 11 | |||
| 12 | namespace http = boost::beast::http; | ||
| 13 | namespace json = boost::json; | ||
| 14 | |||
| 15 | namespace routemon::problem { | ||
| 16 | |||
| 17 | export struct details { | ||
| 18 | http::status status; | ||
| 19 | blocale::message title; | ||
| 20 | std::string_view type_uri; | ||
| 21 | std::optional<blocale::message> detail = std::nullopt; | ||
| 22 | std::optional<std::string> instance = std::nullopt; | ||
| 23 | |||
| 24 | auto set_detail(blocale::message detail) -> details& { | ||
| 25 | this->detail = detail; | ||
| 26 | return *this; | ||
| 27 | } | ||
| 28 | |||
| 29 | auto set_instance(std::string&& instance) -> details& { | ||
| 30 | this->instance = instance; | ||
| 31 | return *this; | ||
| 32 | } | ||
| 33 | auto set_instance(std::string_view instance) -> details& { | ||
| 34 | this->instance = std::string{instance}; | ||
| 35 | return *this; | ||
| 36 | } | ||
| 37 | }; | ||
| 38 | |||
| 39 | export auto tag_invoke(json::value_from_tag, json::value& jv, details const& details, std::locale locale) -> void { | ||
| 40 | auto obj = json::object{ | ||
| 41 | {"type", details.type_uri}, | ||
| 42 | {"title", details.title.str(locale)}, | ||
| 43 | {"status", static_cast<unsigned>(details.status)}, | ||
| 44 | }; | ||
| 45 | if (details.detail) obj["detail"] = details.detail->str(locale); | ||
| 46 | if (details.instance) obj["instance"] = *details.instance; | ||
| 47 | jv = obj; | ||
| 48 | } | ||
| 49 | |||
| 50 | export struct tpl { | ||
| 51 | http::status status; | ||
| 52 | blocale::message title; | ||
| 53 | std::string_view type_uri; | ||
| 54 | |||
| 55 | auto instantiate() const -> details { | ||
| 56 | return details{status, title, type_uri}; | ||
| 57 | } | ||
| 58 | }; | ||
| 59 | |||
| 60 | } | ||
diff --git a/server/src/req_ctx.cppm b/server/src/req_ctx.cppm new file mode 100644 index 0000000..797c51d --- /dev/null +++ b/server/src/req_ctx.cppm | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/http/message.hpp> | ||
| 4 | #include <boost/beast/http/verb.hpp> | ||
| 5 | |||
| 6 | export module routemon:req_ctx; | ||
| 7 | |||
| 8 | import :http.common; | ||
| 9 | import :trace; | ||
| 10 | |||
| 11 | namespace routemon { | ||
| 12 | |||
| 13 | export class req_ctx { | ||
| 14 | trace::id tid_; | ||
| 15 | std::locale locale_; | ||
| 16 | http::verb_set route_verbs_; | ||
| 17 | bool keep_alive_; | ||
| 18 | bhttp::request_header<bhttp::fields> const& req_header_; | ||
| 19 | |||
| 20 | public: | ||
| 21 | explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bool keep_alive, bhttp::request_header<bhttp::fields> const& req_header) | ||
| 22 | : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, keep_alive_{keep_alive}, req_header_{req_header} | ||
| 23 | {} | ||
| 24 | |||
| 25 | template<typename ReqBody> | ||
| 26 | explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bhttp::request<ReqBody> const& req) | ||
| 27 | : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} | ||
| 28 | {} | ||
| 29 | |||
| 30 | auto trace_id() const -> trace::id { | ||
| 31 | return tid_; | ||
| 32 | } | ||
| 33 | |||
| 34 | auto locale() const -> std::locale { | ||
| 35 | return locale_; | ||
| 36 | } | ||
| 37 | |||
| 38 | auto route_verbs() const -> http::verb_set { | ||
| 39 | return route_verbs_; | ||
| 40 | } | ||
| 41 | |||
| 42 | auto keep_alive() const -> bool { | ||
| 43 | return keep_alive_; | ||
| 44 | } | ||
| 45 | |||
| 46 | auto req_header() const -> bhttp::request_header<bhttp::fields> const& { | ||
| 47 | return req_header_; | ||
| 48 | } | ||
| 49 | }; | ||
| 50 | |||
| 51 | } // namespace routemon | ||
diff --git a/server/src/routemon.cppm b/server/src/routemon.cppm new file mode 100644 index 0000000..62db02a --- /dev/null +++ b/server/src/routemon.cppm | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | export module routemon; | ||
| 2 | export import :api; | ||
| 3 | export import :config; | ||
| 4 | export import :database; | ||
| 5 | export import :datex2; | ||
| 6 | export import :gpx; | ||
| 7 | export import :locale; | ||
| 8 | export import :log; | ||
| 9 | export import :srv; | ||
| 10 | export import :rwgps; | ||
| 11 | export import :util; | ||
diff --git a/server/src/rwgps.cppm b/server/src/rwgps.cppm new file mode 100644 index 0000000..7ad6605 --- /dev/null +++ b/server/src/rwgps.cppm | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/beast/core.hpp> | ||
| 4 | #include <boost/beast/http.hpp> | ||
| 5 | #include <boost/json.hpp> | ||
| 6 | |||
| 7 | export module routemon:rwgps; | ||
| 8 | |||
| 9 | import std; | ||
| 10 | import :http.client; | ||
| 11 | import :log; | ||
| 12 | |||
| 13 | namespace beast = boost::beast; | ||
| 14 | namespace bhttp = beast::http; | ||
| 15 | namespace net = boost::asio; | ||
| 16 | namespace json = boost::json; | ||
| 17 | |||
| 18 | namespace routemon::rwgps { | ||
| 19 | |||
| 20 | export struct route_summary { | ||
| 21 | std::int64_t id; | ||
| 22 | std::int64_t user_id; | ||
| 23 | std::string url; | ||
| 24 | std::string name; | ||
| 25 | std::string description; | ||
| 26 | }; | ||
| 27 | |||
| 28 | struct pagination { | ||
| 29 | std::size_t record_count; | ||
| 30 | std::size_t page_count; | ||
| 31 | std::size_t page_size; | ||
| 32 | std::optional<std::string> next_page_url; | ||
| 33 | }; | ||
| 34 | |||
| 35 | struct get_routes_meta { | ||
| 36 | pagination pagination; | ||
| 37 | }; | ||
| 38 | |||
| 39 | struct get_routes_response { | ||
| 40 | std::vector<route_summary> routes; | ||
| 41 | get_routes_meta meta; | ||
| 42 | }; | ||
| 43 | |||
| 44 | auto tag_invoke(json::value_to_tag<route_summary> const&, json::value const& jv) -> route_summary { | ||
| 45 | return { | ||
| 46 | .id = json::value_to<std::int64_t>(jv.at("id")), | ||
| 47 | .user_id = json::value_to<std::int64_t>(jv.at("user_id")), | ||
| 48 | .url = json::value_to<std::string>(jv.at("url")), | ||
| 49 | .name = json::value_to<std::string>(jv.at("name")), | ||
| 50 | .description = json::value_to<std::string>(jv.at("description")), | ||
| 51 | }; | ||
| 52 | } | ||
| 53 | |||
| 54 | auto tag_invoke(json::value_to_tag<pagination> const&, json::value const& jv) -> pagination { | ||
| 55 | return { | ||
| 56 | .record_count = json::value_to<std::size_t>(jv.at("record_count")), | ||
| 57 | .page_count = json::value_to<std::size_t>(jv.at("page_count")), | ||
| 58 | .page_size = json::value_to<std::size_t>(jv.at("page_size")), | ||
| 59 | .next_page_url = json::value_to<std::optional<std::string>>(jv.at("next_page_url")), | ||
| 60 | }; | ||
| 61 | } | ||
| 62 | |||
| 63 | auto tag_invoke(json::value_to_tag<get_routes_meta> const&, json::value const& jv) -> get_routes_meta { | ||
| 64 | return { | ||
| 65 | .pagination = json::value_to<pagination>(jv.at("pagination")), | ||
| 66 | }; | ||
| 67 | } | ||
| 68 | |||
| 69 | auto tag_invoke(json::value_to_tag<get_routes_response> const&, json::value const& jv) -> get_routes_response { | ||
| 70 | return { | ||
| 71 | .routes = json::value_to<std::vector<route_summary>>(jv.at("routes")), | ||
| 72 | .meta = json::value_to<get_routes_meta>(jv.at("meta")), | ||
| 73 | }; | ||
| 74 | } | ||
| 75 | |||
| 76 | auto json_value_to_get_routes_response(json::value const& jv) -> get_routes_response { | ||
| 77 | return json::value_to<get_routes_response>(jv); | ||
| 78 | } | ||
| 79 | |||
| 80 | export class client { | ||
| 81 | log::logger l_; | ||
| 82 | http::client hc_; | ||
| 83 | std::string api_key_; | ||
| 84 | std::string auth_token_; | ||
| 85 | |||
| 86 | static constexpr std::string host = "ridewithgps.com"; | ||
| 87 | |||
| 88 | // TODO: handle failure appropriately | ||
| 89 | auto get_routes_page(std::size_t page) -> get_routes_response { | ||
| 90 | auto req = bhttp::request<bhttp::string_body>{ | ||
| 91 | bhttp::verb::get, | ||
| 92 | std::format("/api/v1/routes.json?page_size=200?page={}", page), | ||
| 93 | 11, // HTTP 1.1 | ||
| 94 | }; | ||
| 95 | req.set(bhttp::field::host, host); | ||
| 96 | req.set("x-rwgps-api-key", api_key_); | ||
| 97 | req.set("x-rwgps-auth-token", auth_token_); | ||
| 98 | |||
| 99 | auto rsp = hc_.do_request(req); | ||
| 100 | auto p = json::stream_parser{}; | ||
| 101 | for (auto const frag : rsp.body().cdata()) { | ||
| 102 | p.write(static_cast<char const*>(frag.data()), frag.size()); | ||
| 103 | } | ||
| 104 | assert(p.done()); | ||
| 105 | return json_value_to_get_routes_response(p.release()); | ||
| 106 | } | ||
| 107 | |||
| 108 | public: | ||
| 109 | explicit client(net::io_context& ioc, log::logger const& l, std::string api_key, std::string auth_token) | ||
| 110 | : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, auth_token_{std::move(auth_token)} | ||
| 111 | {} | ||
| 112 | |||
| 113 | auto get_all_routes() -> std::vector<route_summary> { | ||
| 114 | // TODO: make sure that there are no duplicates here. | ||
| 115 | // What does RWGPS sort on, by default? | ||
| 116 | // Consider using an associative container instead of a vector. | ||
| 117 | auto record_count = 0uz; | ||
| 118 | auto current_page = 0uz; | ||
| 119 | auto routes = std::vector<route_summary>{}; | ||
| 120 | |||
| 121 | while (true) { | ||
| 122 | auto rsp = get_routes_page(current_page); | ||
| 123 | if (rsp.meta.pagination.next_page_url) | ||
| 124 | l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); | ||
| 125 | routes.append_range(rsp.routes); | ||
| 126 | if (rsp.meta.pagination.record_count > 0) | ||
| 127 | record_count = rsp.meta.pagination.record_count; | ||
| 128 | if (rsp.routes.empty() || routes.size() >= record_count) { | ||
| 129 | break; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | return routes; | ||
| 134 | } | ||
| 135 | }; | ||
| 136 | |||
| 137 | } // namespace routemon::rwgps | ||
diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm new file mode 100644 index 0000000..f226c6b --- /dev/null +++ b/server/src/sqlite3.cppm | |||
| @@ -0,0 +1,257 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <sqlite3.h> | ||
| 4 | |||
| 5 | export module routemon:sqlite3; | ||
| 6 | |||
| 7 | import std; | ||
| 8 | import :util; | ||
| 9 | |||
| 10 | namespace routemon::sqlite3 { | ||
| 11 | |||
| 12 | class mutex_guard { | ||
| 13 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} { | ||
| 14 | ::sqlite3_mutex_enter(mut_); | ||
| 15 | } | ||
| 16 | |||
| 17 | friend auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())); | ||
| 18 | |||
| 19 | public: | ||
| 20 | mutex_guard(mutex_guard const&) = delete; | ||
| 21 | ~mutex_guard() { | ||
| 22 | ::sqlite3_mutex_leave(mut_); | ||
| 23 | } | ||
| 24 | |||
| 25 | private: | ||
| 26 | ::sqlite3_mutex* mut_; | ||
| 27 | }; | ||
| 28 | |||
| 29 | auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) { | ||
| 30 | return f(mutex_guard{mut}); | ||
| 31 | } | ||
| 32 | |||
| 33 | auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) { | ||
| 34 | return do_guarded(::sqlite3_db_mutex(dbc), f); | ||
| 35 | } | ||
| 36 | |||
| 37 | class error : public std::exception { | ||
| 38 | int code_; | ||
| 39 | std::string message_; | ||
| 40 | |||
| 41 | public: | ||
| 42 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc) | ||
| 43 | : code_{code}, message_{::sqlite3_errmsg(dbc)} | ||
| 44 | {} | ||
| 45 | |||
| 46 | explicit error(int code) | ||
| 47 | : code_{code}, message_{::sqlite3_errstr(code)} | ||
| 48 | {} | ||
| 49 | |||
| 50 | [[nodiscard]] auto what() const noexcept -> char const* override { | ||
| 51 | return message_.c_str(); | ||
| 52 | } | ||
| 53 | |||
| 54 | [[nodiscard]] auto code() const noexcept -> int { | ||
| 55 | return code_; | ||
| 56 | } | ||
| 57 | }; | ||
| 58 | |||
| 59 | template<class T, template<class U> concept C> | ||
| 60 | concept optional_of = requires { | ||
| 61 | typename T::value_type; | ||
| 62 | requires std::same_as<T, std::optional<typename T::value_type>>; | ||
| 63 | requires C<typename T::value_type>; | ||
| 64 | }; | ||
| 65 | |||
| 66 | template<class T> | ||
| 67 | concept scannable_prim = | ||
| 68 | std::same_as<T, std::string> || | ||
| 69 | std::same_as<T, double> || | ||
| 70 | std::same_as<T, std::int64_t>; | ||
| 71 | |||
| 72 | template<class T> | ||
| 73 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; | ||
| 74 | |||
| 75 | class statement { | ||
| 76 | ::sqlite3_stmt* stmt_; | ||
| 77 | |||
| 78 | public: | ||
| 79 | explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} | ||
| 80 | statement(statement const&) = delete; | ||
| 81 | statement(statement&& s) noexcept { | ||
| 82 | stmt_ = s.stmt_; | ||
| 83 | s.stmt_ = nullptr; | ||
| 84 | } | ||
| 85 | ~statement() { | ||
| 86 | ::sqlite3_finalize(stmt_); | ||
| 87 | } | ||
| 88 | auto get() -> ::sqlite3_stmt* { | ||
| 89 | return stmt_; | ||
| 90 | } | ||
| 91 | }; | ||
| 92 | |||
| 93 | class row_reader { | ||
| 94 | statement stmt_; | ||
| 95 | |||
| 96 | explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} | ||
| 97 | |||
| 98 | friend class connection; | ||
| 99 | |||
| 100 | void scan(int col, std::string& s) { | ||
| 101 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) | ||
| 102 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 103 | unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); | ||
| 104 | auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); | ||
| 105 | if (!size.has_value()) | ||
| 106 | throw std::logic_error{"unexpected negative amount of bytes in column"}; | ||
| 107 | s = std::string{reinterpret_cast<char const*>(chs), *size}; | ||
| 108 | } | ||
| 109 | |||
| 110 | void scan(int col, double& v) { | ||
| 111 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) | ||
| 112 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 113 | v = ::sqlite3_column_double(stmt_.get(), col); | ||
| 114 | } | ||
| 115 | |||
| 116 | void scan(int col, std::int64_t& v) { | ||
| 117 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) | ||
| 118 | throw std::invalid_argument{"invalid type for scan"}; | ||
| 119 | v = ::sqlite3_column_int64(stmt_.get(), col); | ||
| 120 | } | ||
| 121 | |||
| 122 | void scan(int col, optional_of<scannable> auto& v) { | ||
| 123 | if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) { | ||
| 124 | v.reset(); | ||
| 125 | } else { | ||
| 126 | typename std::remove_cvref_t<decltype(v)>::value_type tmp; | ||
| 127 | scan(col, tmp); | ||
| 128 | v = std::move(tmp); | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | public: | ||
| 133 | auto next() -> bool { | ||
| 134 | ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); | ||
| 135 | return do_guarded(dbc, [&](auto const& guard) -> bool { | ||
| 136 | auto const s = ::sqlite3_step(stmt_.get()); | ||
| 137 | if (s == SQLITE_ROW) | ||
| 138 | return true; | ||
| 139 | if (s == SQLITE_DONE) | ||
| 140 | return false; | ||
| 141 | throw error{guard, s, dbc}; | ||
| 142 | }); | ||
| 143 | } | ||
| 144 | |||
| 145 | auto scan(scannable auto&... args) -> void { | ||
| 146 | auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); | ||
| 147 | if (!ncols.has_value()) | ||
| 148 | throw std::logic_error{"got unexpected negative amount of columns"}; | ||
| 149 | if (sizeof...(args) > *ncols) | ||
| 150 | throw std::invalid_argument{"more scanning arguments provided than columns in result set"}; | ||
| 151 | auto col = 0; (..., scan(col++, args)); | ||
| 152 | } | ||
| 153 | |||
| 154 | auto scan_single(scannable auto&... args) -> void { | ||
| 155 | if (!next()) | ||
| 156 | throw std::logic_error{"no row in result set"}; | ||
| 157 | scan(args...); | ||
| 158 | if (next()) { | ||
| 159 | throw std::logic_error{"more than one row in result set"}; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | }; | ||
| 163 | |||
| 164 | class binder { | ||
| 165 | statement& stmt_; | ||
| 166 | |||
| 167 | explicit binder(statement& stmt) : stmt_{stmt} {} | ||
| 168 | |||
| 169 | friend class connection; | ||
| 170 | |||
| 171 | public: | ||
| 172 | auto text(std::string const& param_name, std::string_view str) -> void { | ||
| 173 | int const i = ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); | ||
| 174 | if (i == 0) | ||
| 175 | throw std::invalid_argument{std::format("bind: no parameter with name {} found", param_name)}; | ||
| 176 | auto str_size = util::int_from_size(str.size()); | ||
| 177 | if (!str_size.has_value()) | ||
| 178 | throw std::invalid_argument{"bind: provided text is too long"}; | ||
| 179 | if (auto s = ::sqlite3_bind_text(stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) { | ||
| 180 | throw error{s}; | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | static auto noop(binder&) -> void {} | ||
| 185 | }; | ||
| 186 | |||
| 187 | export class connection { | ||
| 188 | ::sqlite3* dbc_; | ||
| 189 | ::sqlite3_mutex* mut_; | ||
| 190 | |||
| 191 | explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} {} | ||
| 192 | |||
| 193 | friend auto open(std::string const& filename) -> connection; | ||
| 194 | |||
| 195 | public: | ||
| 196 | connection(connection const&) = delete; | ||
| 197 | connection(connection&& c) noexcept { | ||
| 198 | dbc_ = c.dbc_; | ||
| 199 | mut_ = c.mut_; | ||
| 200 | c.dbc_ = nullptr; | ||
| 201 | c.mut_ = nullptr; | ||
| 202 | } | ||
| 203 | |||
| 204 | [[nodiscard]] auto query(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> row_reader { | ||
| 205 | ::sqlite3_stmt* pstmt = nullptr; | ||
| 206 | char const* sql_tail = nullptr; | ||
| 207 | auto sql_size = util::int_from_size(sql.size()); | ||
| 208 | if (!sql_size.has_value() || *sql_size >= std::numeric_limits<int>::max() - 1) | ||
| 209 | throw std::invalid_argument{"provided input text too large"}; | ||
| 210 | do_guarded(mut_, [&](auto const& guard) -> void { | ||
| 211 | if (auto s = ::sqlite3_prepare_v2(dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) { | ||
| 212 | if (pstmt != nullptr) { | ||
| 213 | // Use contract_assert when having a compiler with contracts available | ||
| 214 | ::sqlite3_finalize(pstmt); | ||
| 215 | throw std::logic_error{"expected stmt to be null after failed preparation"}; | ||
| 216 | } | ||
| 217 | throw error{guard, s, dbc_}; | ||
| 218 | } | ||
| 219 | }); | ||
| 220 | if (!pstmt) | ||
| 221 | throw std::invalid_argument{"provided input text contains no SQL"}; | ||
| 222 | auto stmt = statement{pstmt}; | ||
| 223 | if (sql_tail && std::strlen(sql_tail) > 0) | ||
| 224 | throw std::invalid_argument{"provided input text contains more than one SQL statement"}; | ||
| 225 | auto b = binder{stmt}; bf(b); | ||
| 226 | return row_reader{std::move(stmt)}; | ||
| 227 | } | ||
| 228 | |||
| 229 | auto exec(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> void { | ||
| 230 | auto reader = query(sql, bf); | ||
| 231 | while (reader.next()); | ||
| 232 | } | ||
| 233 | |||
| 234 | ~connection() { | ||
| 235 | std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); | ||
| 236 | } | ||
| 237 | }; | ||
| 238 | |||
| 239 | export auto open(std::string const& filename) -> connection { | ||
| 240 | ::sqlite3* dbc = nullptr; | ||
| 241 | auto s = ::sqlite3_open_v2(filename.c_str(), &dbc, | ||
| 242 | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | | ||
| 243 | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE, | ||
| 244 | nullptr); | ||
| 245 | if (s != SQLITE_OK) { | ||
| 246 | if (dbc) { | ||
| 247 | do_guarded(dbc, [&](auto const& guard) -> void { | ||
| 248 | throw error{guard, s, dbc}; | ||
| 249 | }); | ||
| 250 | } else { | ||
| 251 | throw error{s}; | ||
| 252 | } | ||
| 253 | } | ||
| 254 | return connection{dbc}; | ||
| 255 | } | ||
| 256 | |||
| 257 | } // namespace routemon::sqlite3 | ||
diff --git a/server/src/srv.cppm b/server/src/srv.cppm new file mode 100644 index 0000000..2ca48c6 --- /dev/null +++ b/server/src/srv.cppm | |||
| @@ -0,0 +1,221 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/config.hpp> | ||
| 4 | #include <boost/asio/ip/tcp.hpp> | ||
| 5 | #include <boost/asio/as_tuple.hpp> | ||
| 6 | #include <boost/asio/awaitable.hpp> | ||
| 7 | #include <boost/asio/co_spawn.hpp> | ||
| 8 | #include <boost/beast/core.hpp> | ||
| 9 | #include <boost/beast/http.hpp> | ||
| 10 | #include <boost/json.hpp> | ||
| 11 | #include <boost/locale/generator.hpp> | ||
| 12 | |||
| 13 | #include <expat.h> | ||
| 14 | |||
| 15 | export module routemon:srv; | ||
| 16 | |||
| 17 | import std; | ||
| 18 | import :api; | ||
| 19 | import :config; | ||
| 20 | import :gpx; | ||
| 21 | import :http.server; | ||
| 22 | import :locale; | ||
| 23 | import :log; | ||
| 24 | import :problem; | ||
| 25 | import :req_ctx; | ||
| 26 | import :util; | ||
| 27 | |||
| 28 | namespace beast = boost::beast; | ||
| 29 | namespace json = boost::json; | ||
| 30 | namespace net = boost::asio; | ||
| 31 | using tcp = boost::asio::ip::tcp; | ||
| 32 | |||
| 33 | namespace routemon::srv { | ||
| 34 | |||
| 35 | class gpx_parse_error_category_impl : public std::error_category { | ||
| 36 | public: | ||
| 37 | char const* name() const noexcept override { return "gpx_parse"; } | ||
| 38 | auto message(int condition) const noexcept -> std::string override { | ||
| 39 | std::ignore = condition; | ||
| 40 | return "failed to parse GPX file"; | ||
| 41 | } | ||
| 42 | }; | ||
| 43 | auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& { | ||
| 44 | static auto const inst = gpx_parse_error_category_impl{}; | ||
| 45 | return inst; | ||
| 46 | } | ||
| 47 | auto gpx_parse_error() noexcept -> std::error_code { | ||
| 48 | return std::error_code{1, gpx_parse_error_category()}; | ||
| 49 | } | ||
| 50 | |||
| 51 | class gpx_parse_result { | ||
| 52 | std::variant<std::exception_ptr, gpx::file> res_; | ||
| 53 | |||
| 54 | public: | ||
| 55 | auto set_exception(std::exception_ptr ex) noexcept { | ||
| 56 | res_ = ex; | ||
| 57 | } | ||
| 58 | auto set_gpx_file(gpx::file&& f) noexcept { | ||
| 59 | res_ = std::move(f); | ||
| 60 | } | ||
| 61 | |||
| 62 | auto unwrap() -> gpx::file&& { | ||
| 63 | return std::visit(util::overloaded{ | ||
| 64 | [](std::exception_ptr ex) -> gpx::file&& { | ||
| 65 | if (ex) std::rethrow_exception(ex); | ||
| 66 | else throw std::runtime_error{"no GPX file parse result available"}; | ||
| 67 | }, | ||
| 68 | [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, | ||
| 69 | }, std::move(res_)); | ||
| 70 | } | ||
| 71 | }; | ||
| 72 | |||
| 73 | struct readable_gpx_body { | ||
| 74 | using value_type = gpx_parse_result; | ||
| 75 | |||
| 76 | class reader { | ||
| 77 | gpx::reader r_; | ||
| 78 | util::not_null<value_type*> res_; | ||
| 79 | |||
| 80 | public: | ||
| 81 | template<bool isRequest, bhttp::concepts::fields Fields> | ||
| 82 | explicit reader(bhttp::header<isRequest, Fields>&, value_type& v) | ||
| 83 | : res_{&v} | ||
| 84 | {} | ||
| 85 | |||
| 86 | // The following methods (which are called by Beast) are marked | ||
| 87 | // noexcept, since Beast does not ensure that exceptions thrown | ||
| 88 | // here are appropriately directed to the caller of | ||
| 89 | // (async_)read(_some), so throwing here might cause the program | ||
| 90 | // to crash. | ||
| 91 | |||
| 92 | auto init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept -> void { | ||
| 93 | try { | ||
| 94 | r_.init(); | ||
| 95 | ec = {}; | ||
| 96 | } catch (std::exception& ex) { | ||
| 97 | res_->set_exception(std::current_exception()); | ||
| 98 | ec = gpx_parse_error(); | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { | ||
| 103 | auto total = 0uz; | ||
| 104 | try { | ||
| 105 | for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { | ||
| 106 | r_.put(std::string_view{static_cast<char const*>(it->data()), it->size()}); | ||
| 107 | total += it->size(); | ||
| 108 | } | ||
| 109 | ec = {}; | ||
| 110 | } catch (std::exception& ex) { | ||
| 111 | res_->set_exception(std::current_exception()); | ||
| 112 | ec = gpx_parse_error(); | ||
| 113 | } | ||
| 114 | return total; | ||
| 115 | } | ||
| 116 | |||
| 117 | auto finish(beast::error_code& ec) noexcept { | ||
| 118 | try { | ||
| 119 | res_->set_gpx_file(r_.finish()); | ||
| 120 | ec = {}; | ||
| 121 | } catch (std::exception& ex) { | ||
| 122 | res_->set_exception(std::current_exception()); | ||
| 123 | ec = gpx_parse_error(); | ||
| 124 | } | ||
| 125 | } | ||
| 126 | }; | ||
| 127 | }; | ||
| 128 | static_assert(bhttp::concepts::body<readable_gpx_body>); | ||
| 129 | static_assert(bhttp::concepts::body_reader<readable_gpx_body>); | ||
| 130 | |||
| 131 | class handler { | ||
| 132 | api::handler inner_; | ||
| 133 | |||
| 134 | public: | ||
| 135 | using outer_ctx = http::trace_id_ctx<http::base_ctx>; | ||
| 136 | using l0_ctx = http::routed_ctx<outer_ctx>; | ||
| 137 | |||
| 138 | private: | ||
| 139 | auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> { | ||
| 140 | auto gpx_file = gpx::file{}; | ||
| 141 | try { | ||
| 142 | auto req = co_await http::read_request<readable_gpx_body>(ctx, std::move(r)); | ||
| 143 | gpx_file = std::move(req->body().unwrap()); | ||
| 144 | } catch (std::exception& ex) { | ||
| 145 | // TODO: more detailed problem reporting | ||
| 146 | auto tpl = problem::tpl{ | ||
| 147 | .status = bhttp::status::bad_request, | ||
| 148 | .title = translate("Failed to parse GPX file"), | ||
| 149 | .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", | ||
| 150 | }; | ||
| 151 | co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false}); | ||
| 152 | } | ||
| 153 | |||
| 154 | // TODO: catch handler exceptions and return 500 when raised? | ||
| 155 | // (keep-alive depends on whether whole request was read) | ||
| 156 | auto mres = inner_.process_gpx(std::move(gpx_file)); | ||
| 157 | if (!mres) { | ||
| 158 | auto tpl = problem::tpl{ | ||
| 159 | .status = bhttp::status::internal_server_error, | ||
| 160 | .title = translate("Internal server error"), | ||
| 161 | .type_uri = "https://routemon.fautchen.eu/problems/internal-server-error", | ||
| 162 | }; | ||
| 163 | co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{true}); | ||
| 164 | } | ||
| 165 | |||
| 166 | auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true}); | ||
| 167 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 168 | rsp.body() = json::serialize(json::value_from(*mres)); | ||
| 169 | rsp.prepare_payload(); | ||
| 170 | co_return rsp; | ||
| 171 | } | ||
| 172 | |||
| 173 | auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> { | ||
| 174 | auto req = co_await http::read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 175 | auto info = inner_.sysinfo(); | ||
| 176 | |||
| 177 | auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true}); | ||
| 178 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 179 | rsp.body() = json::serialize(json::value_from(info)); | ||
| 180 | rsp.prepare_payload(); | ||
| 181 | co_return rsp; | ||
| 182 | } | ||
| 183 | |||
| 184 | public: | ||
| 185 | handler(api::handler&& inner) : inner_{std::move(inner)} {} | ||
| 186 | |||
| 187 | auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> { | ||
| 188 | auto handler = [this]<class MemFn>(MemFn member) { | ||
| 189 | return std::bind_front(member, this); | ||
| 190 | }; | ||
| 191 | |||
| 192 | return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({ | ||
| 193 | {"gpx", http::dtree<l0_ctx>{{ | ||
| 194 | .post = handler(&handler::handle_process_gpx), | ||
| 195 | }}.no_subtrees()}, | ||
| 196 | {"sysinfo", http::dtree<l0_ctx>{{ | ||
| 197 | .get = handler(&handler::handle_sysinfo), | ||
| 198 | }}.no_subtrees()}, | ||
| 199 | }); | ||
| 200 | } | ||
| 201 | }; | ||
| 202 | |||
| 203 | export class server { | ||
| 204 | handler handler_; | ||
| 205 | http::server<handler::outer_ctx> srv_; | ||
| 206 | |||
| 207 | static auto make_global_middleware() -> http::middleware_t<http::base_ctx, handler::outer_ctx> { | ||
| 208 | return http::middleware_compose<http::base_ctx, http::trace_id_ctx<http::base_ctx>, http::trace_id_ctx<http::base_ctx>>(http::trace_id_middleware<http::base_ctx>, http::lax_cors_middleware<http::trace_id_ctx<http::base_ctx>>); | ||
| 209 | } | ||
| 210 | |||
| 211 | public: | ||
| 212 | server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) | ||
| 213 | : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} | ||
| 214 | {} | ||
| 215 | |||
| 216 | auto spawn(net::io_context& ioc) -> void { | ||
| 217 | srv_.spawn(ioc); | ||
| 218 | } | ||
| 219 | }; | ||
| 220 | |||
| 221 | } // namespace routemon::srv | ||
diff --git a/server/src/time.cppm b/server/src/time.cppm new file mode 100644 index 0000000..767883c --- /dev/null +++ b/server/src/time.cppm | |||
| @@ -0,0 +1,205 @@ | |||
| 1 | export module routemon:time; | ||
| 2 | |||
| 3 | import std; | ||
| 4 | |||
| 5 | export namespace routemon::time { | ||
| 6 | |||
| 7 | using timestamp = std::chrono::time_point<std::chrono::utc_clock>; | ||
| 8 | |||
| 9 | class period { | ||
| 10 | // Assuming [start, end). Unfortunately the DATEX II model is not | ||
| 11 | // clear about this. | ||
| 12 | timestamp start_; | ||
| 13 | timestamp end_; | ||
| 14 | |||
| 15 | public: | ||
| 16 | explicit period(timestamp start, timestamp end) | ||
| 17 | : start_{start}, end_{end} | ||
| 18 | { | ||
| 19 | if (start >= end) { | ||
| 20 | throw std::invalid_argument("period: start should be before end"); | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | [[nodiscard]] auto intersect(period other) const -> std::optional<period> { | ||
| 25 | auto const new_start = start_ < other.start() ? other.start() : start_; | ||
| 26 | auto const new_end = other.end() < end_ ? other.end() : end_; | ||
| 27 | return new_start < new_end ? std::make_optional(period{new_start, new_end}) : std::nullopt; | ||
| 28 | } | ||
| 29 | |||
| 30 | [[nodiscard]] auto except(period other) const -> std::pair<std::optional<period>, std::optional<period>> { | ||
| 31 | auto const before_start = start_; | ||
| 32 | auto const before_end = other.start(); | ||
| 33 | auto const after_start = end_; | ||
| 34 | auto const after_end = other.end(); | ||
| 35 | std::optional<period> before, after; | ||
| 36 | if (before_start < before_end) | ||
| 37 | before = period{before_start, before_end}; | ||
| 38 | if (after_start < after_end) | ||
| 39 | after = period{after_end, after_start}; | ||
| 40 | return std::make_pair(before, after); | ||
| 41 | } | ||
| 42 | |||
| 43 | [[nodiscard]] auto start() const -> timestamp { return start_; } | ||
| 44 | [[nodiscard]] auto end() const -> timestamp { return end_; } | ||
| 45 | }; | ||
| 46 | |||
| 47 | class period_seq { | ||
| 48 | std::vector<period> periods_; | ||
| 49 | |||
| 50 | // The way lt and ge are ordered makes a difference for how the sorting | ||
| 51 | // (insertion based on lower_bound) works. Do not carelessly reorder this. | ||
| 52 | enum lt_ge : std::uint8_t { | ||
| 53 | ge, // >= | ||
| 54 | lt, // < | ||
| 55 | }; | ||
| 56 | |||
| 57 | // O(n log n) | ||
| 58 | template<std::input_iterator I, std::sentinel_for<I> S> | ||
| 59 | requires std::same_as<std::iter_value_t<I>, period> | ||
| 60 | static auto consolidate(I begin, S end) -> std::vector<period> { | ||
| 61 | auto periods = std::vector<period>{}; | ||
| 62 | auto preds = std::vector<std::pair<timestamp, lt_ge>>{}; | ||
| 63 | |||
| 64 | for (auto it = begin; it != end; it++) { | ||
| 65 | auto const& period = *it; | ||
| 66 | |||
| 67 | auto const a = std::make_pair(period.start(), ge); | ||
| 68 | auto const b = std::make_pair(period.end(), lt); | ||
| 69 | preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); | ||
| 70 | preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); | ||
| 71 | } | ||
| 72 | |||
| 73 | if (preds.empty()) | ||
| 74 | return periods; | ||
| 75 | |||
| 76 | if (preds.size() < 2) | ||
| 77 | throw std::logic_error{"period_seq::consolidate: amount of predicates should be >= 2"}; | ||
| 78 | if (preds.front().second != ge) | ||
| 79 | throw std::logic_error{"period_seq::consolidate: first element of preds should be a ge-element"}; | ||
| 80 | if (preds.back().second != lt) | ||
| 81 | throw std::logic_error{"period_seq::consolidate: last element of preds should be an lt-element"}; | ||
| 82 | |||
| 83 | auto period_start = preds[0].first; | ||
| 84 | for (std::size_t i = 1; i < preds.size(); i++) { | ||
| 85 | if (preds[i].second == lt && (i + 1 == preds.size() || preds[i + 1].second == ge)) { | ||
| 86 | auto const period_end = preds[i].first; | ||
| 87 | if (!periods.empty() && periods.back().start() == period_start) | ||
| 88 | periods.back() = period{periods.back().end(), period_end}; | ||
| 89 | else | ||
| 90 | periods.emplace_back(period_start, period_end); | ||
| 91 | if (i + 1 != preds.size()) { | ||
| 92 | period_start = preds[i + 1].first; | ||
| 93 | i++; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | return periods; | ||
| 99 | } | ||
| 100 | |||
| 101 | explicit period_seq(std::vector<period> periods) | ||
| 102 | : periods_{std::move(periods)} | ||
| 103 | { | ||
| 104 | for (auto i = 0uz; i < periods_.size(); i++) { | ||
| 105 | if (i + 1 < periods_.size()) { | ||
| 106 | if (periods_[i].end() >= periods_[i + 1].start()) { | ||
| 107 | throw std::logic_error{"period_seq: vector provided to private constructor not ordered properly"}; | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | public: | ||
| 114 | template<std::input_iterator I, std::sentinel_for<I> S> | ||
| 115 | requires std::same_as<std::iter_value_t<I>, period> | ||
| 116 | explicit period_seq(I begin, S end) | ||
| 117 | : periods_{consolidate(begin, end)} | ||
| 118 | {} | ||
| 119 | |||
| 120 | explicit period_seq(period singleton) | ||
| 121 | : periods_{singleton} | ||
| 122 | {} | ||
| 123 | |||
| 124 | [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq { | ||
| 125 | auto it1 = periods_.begin(); auto end1 = periods_.end(); | ||
| 126 | auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); | ||
| 127 | |||
| 128 | auto res = std::vector<period>{}; | ||
| 129 | while (it1 != end1 && it2 != end2) { | ||
| 130 | auto overlap = it1->intersect(*it2); | ||
| 131 | if (overlap) { | ||
| 132 | res.push_back(*overlap); | ||
| 133 | if (it1->end() < it2->end()) { | ||
| 134 | it1++; | ||
| 135 | } else { | ||
| 136 | it2++; | ||
| 137 | } | ||
| 138 | } else { | ||
| 139 | if (it1->end() < it2->start()) { | ||
| 140 | it1++; | ||
| 141 | } else { | ||
| 142 | it2++; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | return period_seq{res}; | ||
| 148 | } | ||
| 149 | |||
| 150 | [[nodiscard]] auto except(period_seq const& other) const -> period_seq { | ||
| 151 | // This code was pretty tricky to write, I wouldn't be surprised if it has some bugs in it. | ||
| 152 | |||
| 153 | auto it1 = periods_.begin(); auto end1 = periods_.end(); | ||
| 154 | auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); | ||
| 155 | |||
| 156 | auto res = std::vector<period>{}; | ||
| 157 | if (it1 == end1) | ||
| 158 | return period_seq{res}; | ||
| 159 | if (it2 == end2) | ||
| 160 | return period_seq{periods_}; | ||
| 161 | auto period1 = period{*it1++}; | ||
| 162 | |||
| 163 | while (it1 != end1 && it2 != end2) { | ||
| 164 | if (period1.end() <= it2->start()) { | ||
| 165 | res.push_back(period1); | ||
| 166 | period1 = *it1++; | ||
| 167 | } else if (it2->end() <= period1.start()) { | ||
| 168 | it2++; | ||
| 169 | } else /* period1.begin() < it2->end() && it2->begin() < period1.end() */ { | ||
| 170 | auto const [mbefore, mafter] = period1.except(*it2); | ||
| 171 | if (mbefore) | ||
| 172 | res.push_back(*mbefore); | ||
| 173 | if (mafter) { | ||
| 174 | period1 = *mafter; | ||
| 175 | } else { | ||
| 176 | period1 = *it1++; | ||
| 177 | } | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | return period_seq{res}; | ||
| 182 | } | ||
| 183 | |||
| 184 | [[nodiscard]] auto periods() const -> std::vector<period> const& { | ||
| 185 | return periods_; | ||
| 186 | } | ||
| 187 | }; | ||
| 188 | |||
| 189 | auto operator<<(std::ostream& os, period const& p) -> std::ostream& { | ||
| 190 | return os << "[" << p.start() << ", " << p.end() << ")"; | ||
| 191 | } | ||
| 192 | |||
| 193 | auto operator<<(std::ostream &os, period_seq const& ps) -> std::ostream& { | ||
| 194 | os << "{"; | ||
| 195 | auto it = ps.periods().begin(); | ||
| 196 | while (it != ps.periods().end()) { | ||
| 197 | os << " " << *it; | ||
| 198 | if (++it != ps.periods().end()) { | ||
| 199 | os << ","; | ||
| 200 | } | ||
| 201 | } | ||
| 202 | return os << " }"; | ||
| 203 | } | ||
| 204 | |||
| 205 | } // namespace routemon::time | ||
diff --git a/server/src/trace.cppm b/server/src/trace.cppm new file mode 100644 index 0000000..00ecd05 --- /dev/null +++ b/server/src/trace.cppm | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | // Might as well since we're using OpenSSL | ||
| 4 | #include <openssl/err.h> | ||
| 5 | #include <openssl/rand.h> | ||
| 6 | |||
| 7 | export module routemon:trace; | ||
| 8 | |||
| 9 | import std; | ||
| 10 | import :util; | ||
| 11 | |||
| 12 | namespace routemon::trace { | ||
| 13 | |||
| 14 | class uuid7 { | ||
| 15 | std::uint64_t high_ = 0; | ||
| 16 | std::uint64_t low_ = 0; | ||
| 17 | |||
| 18 | public: | ||
| 19 | uuid7() { | ||
| 20 | namespace chrono = std::chrono; | ||
| 21 | auto const unix_time_ms_signed = static_cast<std::int64_t>(chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count()); | ||
| 22 | if (unix_time_ms_signed < 0) | ||
| 23 | throw std::runtime_error{"system time before UNIX epoch"}; | ||
| 24 | auto const unix_time_ms = static_cast<std::uint64_t>(unix_time_ms_signed); | ||
| 25 | if (std::countl_zero(unix_time_ms) < 16) | ||
| 26 | throw std::runtime_error{"system time too great"}; | ||
| 27 | |||
| 28 | auto rand = std::array<unsigned char, 10>{}; | ||
| 29 | int s = RAND_bytes(rand.data(), static_cast<int>(rand.size())); | ||
| 30 | if (s != 1) { | ||
| 31 | unsigned long e = ERR_get_error(); | ||
| 32 | throw std::runtime_error{std::format("failed to generate UUID(v7): {} ({}, code {})", ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; | ||
| 33 | } | ||
| 34 | |||
| 35 | auto version = std::uint64_t{0b0111}; | ||
| 36 | auto variant = std::uint64_t{0b10}; | ||
| 37 | |||
| 38 | high_ |= unix_time_ms << 16; | ||
| 39 | high_ |= version << 12; | ||
| 40 | high_ |= std::uint64_t{rand[0]} << 4; | ||
| 41 | high_ |= std::uint64_t{rand[1]}; | ||
| 42 | low_ |= variant << 62; | ||
| 43 | low_ |= std::uint64_t{rand[2]} << 54; | ||
| 44 | low_ |= std::uint64_t{rand[3]} << 48; | ||
| 45 | low_ |= std::uint64_t{rand[4]} << 40; | ||
| 46 | low_ |= std::uint64_t{rand[5]} << 32; | ||
| 47 | low_ |= std::uint64_t{rand[6]} << 24; | ||
| 48 | low_ |= std::uint64_t{rand[7]} << 16; | ||
| 49 | low_ |= std::uint64_t{rand[8]} << 8; | ||
| 50 | low_ |= std::uint64_t{rand[9]}; | ||
| 51 | } | ||
| 52 | |||
| 53 | auto format(std::array<char, 37>& target) -> void { | ||
| 54 | auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; | ||
| 55 | auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; | ||
| 56 | auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; | ||
| 57 | auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; | ||
| 58 | auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; | ||
| 59 | |||
| 60 | std::format_to(target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", | ||
| 61 | high_high, high_low_high, low_low_high, high_low, low_low); | ||
| 62 | target.back() = '\0'; | ||
| 63 | } | ||
| 64 | }; | ||
| 65 | |||
| 66 | export class id { | ||
| 67 | std::array<char, 37> chars_; | ||
| 68 | |||
| 69 | public: | ||
| 70 | id() { | ||
| 71 | uuid7{}.format(chars_); | ||
| 72 | } | ||
| 73 | |||
| 74 | auto as_string() const -> util::zstring_view { | ||
| 75 | return util::zstring_view{chars_.data(), chars_.size() - 1}; | ||
| 76 | } | ||
| 77 | }; | ||
| 78 | |||
| 79 | } // namespace routemon::trace | ||
diff --git a/server/src/util.cppm b/server/src/util.cppm new file mode 100644 index 0000000..65f4d67 --- /dev/null +++ b/server/src/util.cppm | |||
| @@ -0,0 +1,254 @@ | |||
| 1 | // Stuff that doesn't really have a place right now, but that is | ||
| 2 | // broadly useful. | ||
| 3 | export module routemon:util; | ||
| 4 | |||
| 5 | import std; | ||
| 6 | |||
| 7 | namespace routemon::util { | ||
| 8 | |||
| 9 | // For use with e.g. std::visit (on std::variant). | ||
| 10 | template<class... Ts> | ||
| 11 | struct overloaded : Ts... { | ||
| 12 | using Ts::operator()...; | ||
| 13 | }; | ||
| 14 | |||
| 15 | export constexpr auto parse_double(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional<double> { | ||
| 16 | auto x = 0.0; | ||
| 17 | auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); | ||
| 18 | if (ec == std::errc{}) { | ||
| 19 | return x; | ||
| 20 | } else { | ||
| 21 | return std::nullopt; | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | export constexpr auto parse_float(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional<float> { | ||
| 26 | auto x = 0.0; | ||
| 27 | auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); | ||
| 28 | if (ec == std::errc{}) { | ||
| 29 | return x; | ||
| 30 | } else { | ||
| 31 | return std::nullopt; | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | export template<class T> | ||
| 36 | class aolist : public std::enable_shared_from_this<aolist<T>> { | ||
| 37 | T v_; | ||
| 38 | std::shared_ptr<aolist<T> const> next_; | ||
| 39 | |||
| 40 | explicit aolist(T v, std::shared_ptr<aolist<T> const> next) | ||
| 41 | : v_{v}, next_{next} | ||
| 42 | {} | ||
| 43 | |||
| 44 | public: | ||
| 45 | static auto nil() -> std::shared_ptr<aolist<T>> { | ||
| 46 | return nullptr; | ||
| 47 | } | ||
| 48 | |||
| 49 | static auto cons(T v, std::shared_ptr<aolist<T> const> l) -> std::shared_ptr<aolist<T>> { | ||
| 50 | return std::shared_ptr<aolist<T>>{new aolist<T>{v, l}}; | ||
| 51 | } | ||
| 52 | |||
| 53 | auto next() const -> std::shared_ptr<aolist<T> const> { | ||
| 54 | return next_; | ||
| 55 | } | ||
| 56 | |||
| 57 | auto value() const noexcept -> T const& { | ||
| 58 | return v_; | ||
| 59 | } | ||
| 60 | }; | ||
| 61 | |||
| 62 | export constexpr auto size_from_int(int x) -> std::optional<std::size_t> { | ||
| 63 | static_assert(sizeof(int) <= sizeof(std::size_t), "cannot cast int to smaller size_t type"); | ||
| 64 | if (x < 0) | ||
| 65 | return std::nullopt; | ||
| 66 | return static_cast<std::size_t>(x); | ||
| 67 | } | ||
| 68 | |||
| 69 | export constexpr auto int_from_size(std::size_t x) -> std::optional<int> { | ||
| 70 | constexpr auto int_max = size_from_int(std::numeric_limits<int>::max()); | ||
| 71 | static_assert(int_max.has_value()); | ||
| 72 | if (x > *int_max) | ||
| 73 | return std::nullopt; | ||
| 74 | return static_cast<int>(x); | ||
| 75 | } | ||
| 76 | |||
| 77 | export class zstring_view { | ||
| 78 | char const* s_; | ||
| 79 | std::size_t length_; | ||
| 80 | |||
| 81 | public: | ||
| 82 | constexpr explicit zstring_view(char const* s, std::size_t length) : | ||
| 83 | s_{s}, length_{length} | ||
| 84 | {} | ||
| 85 | |||
| 86 | constexpr zstring_view(char const* s) : | ||
| 87 | zstring_view{s, std::char_traits<char>::length(s)} | ||
| 88 | {} | ||
| 89 | |||
| 90 | auto length() const -> std::size_t { | ||
| 91 | return length_; | ||
| 92 | } | ||
| 93 | |||
| 94 | auto c_str() const -> char const* { | ||
| 95 | return s_; | ||
| 96 | } | ||
| 97 | |||
| 98 | operator std::string_view() const { | ||
| 99 | return std::string_view{s_, length_}; | ||
| 100 | } | ||
| 101 | |||
| 102 | operator char const*() const { | ||
| 103 | return s_; | ||
| 104 | } | ||
| 105 | }; | ||
| 106 | |||
| 107 | // View for null-terminated strings for which we might not | ||
| 108 | // necessarily be interested in the length. String length is only | ||
| 109 | // calculated on demand, at most once on each thread (on more than | ||
| 110 | // one thread when racing). May be null. | ||
| 111 | // | ||
| 112 | // It is undefined behavior to assign to a lazy_zstring_view when | ||
| 113 | // it is in use by other threads. | ||
| 114 | export class lazy_zstring_view { | ||
| 115 | static constexpr auto unset_length = std::numeric_limits<std::size_t>::max(); | ||
| 116 | |||
| 117 | char const* s_; // nullable | ||
| 118 | mutable std::atomic<std::size_t> length_; | ||
| 119 | static_assert(decltype(length_)::is_always_lock_free); | ||
| 120 | |||
| 121 | public: | ||
| 122 | constexpr explicit lazy_zstring_view(char const* s) : | ||
| 123 | s_{s}, length_{s ? unset_length : 0} | ||
| 124 | {} | ||
| 125 | ~lazy_zstring_view() = default; | ||
| 126 | |||
| 127 | lazy_zstring_view(lazy_zstring_view const& sv) noexcept | ||
| 128 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | ||
| 129 | {} | ||
| 130 | lazy_zstring_view(lazy_zstring_view&& sv) noexcept | ||
| 131 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | ||
| 132 | {} | ||
| 133 | auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& { | ||
| 134 | if (this != &rhs) { | ||
| 135 | s_ = rhs.s_; | ||
| 136 | length_.store(rhs.length_.load(std::memory_order_acquire), std::memory_order_release); | ||
| 137 | } | ||
| 138 | return *this; | ||
| 139 | } | ||
| 140 | auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& { | ||
| 141 | return *this = rhs; // use the copy assignment operator | ||
| 142 | } | ||
| 143 | |||
| 144 | auto length() const noexcept -> std::size_t { | ||
| 145 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 146 | return v; | ||
| 147 | auto l = std::char_traits<char>::length(s_); | ||
| 148 | length_.store(l, std::memory_order_release); | ||
| 149 | return l; | ||
| 150 | } | ||
| 151 | |||
| 152 | auto c_str() const noexcept -> char const* { | ||
| 153 | return s_; | ||
| 154 | } | ||
| 155 | |||
| 156 | operator std::string_view() const noexcept { | ||
| 157 | return std::string_view{s_, length()}; | ||
| 158 | } | ||
| 159 | |||
| 160 | operator char const*() const noexcept { | ||
| 161 | return s_; | ||
| 162 | } | ||
| 163 | |||
| 164 | auto operator==(std::string_view sv) const noexcept -> bool { | ||
| 165 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 166 | if (sv.length() != v) | ||
| 167 | return false; | ||
| 168 | auto res = std::char_traits<char>::compare(s_, sv.data(), sv.length()); | ||
| 169 | if (res != 0) | ||
| 170 | return false; | ||
| 171 | // Strings are equal for sv.length() characters. | ||
| 172 | if (s_[sv.length()] != '\0') | ||
| 173 | return false; | ||
| 174 | // Strings are actually equal, and we have just found out the | ||
| 175 | // length of this string, so we might as well set it. | ||
| 176 | length_.store(sv.length(), std::memory_order_release); | ||
| 177 | return true; | ||
| 178 | } | ||
| 179 | }; | ||
| 180 | |||
| 181 | constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept -> zstring_view { | ||
| 182 | return zstring_view{s, length}; | ||
| 183 | } | ||
| 184 | |||
| 185 | auto operator==(zstring_view lhs, zstring_view rhs) -> bool { | ||
| 186 | return std::string_view{lhs} == std::string_view{rhs}; | ||
| 187 | } | ||
| 188 | |||
| 189 | export constexpr auto split_on(std::string_view s, char c) -> std::pair<std::string_view, std::optional<std::string_view>> { | ||
| 190 | if (auto i = s.find(c); i != std::string_view::npos) | ||
| 191 | return std::make_pair(s.substr(0, i), s.substr(i + 1)); | ||
| 192 | return std::make_pair(s, std::nullopt); | ||
| 193 | } | ||
| 194 | |||
| 195 | export template<class T> | ||
| 196 | class not_null; | ||
| 197 | |||
| 198 | export template<class T> | ||
| 199 | class not_null<T*> { | ||
| 200 | T* p_; | ||
| 201 | |||
| 202 | struct guaranteed_not_null_t {}; | ||
| 203 | explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} | ||
| 204 | |||
| 205 | public: | ||
| 206 | explicit not_null(T* p) | ||
| 207 | : p_{p} | ||
| 208 | { if (!p_) throw std::runtime_error{"not_null constructed with null pointer"}; } | ||
| 209 | ~not_null() = default; | ||
| 210 | |||
| 211 | not_null(not_null const& other) = default; | ||
| 212 | not_null(not_null&& other) noexcept = default; | ||
| 213 | auto operator=(not_null const& rhs) noexcept -> not_null& = default; | ||
| 214 | auto operator=(not_null&& rhs) noexcept -> not_null& = default; | ||
| 215 | |||
| 216 | friend auto make_not_null(T* p) noexcept -> std::optional<not_null> { | ||
| 217 | if (p) return not_null(p, guaranteed_not_null_t{}); | ||
| 218 | else return std::nullopt; | ||
| 219 | } | ||
| 220 | |||
| 221 | [[nodiscard]] auto get() const noexcept -> T* { | ||
| 222 | return p_; | ||
| 223 | } | ||
| 224 | |||
| 225 | auto operator*() const noexcept -> std::add_lvalue_reference_t<T> { | ||
| 226 | return *p_; | ||
| 227 | } | ||
| 228 | |||
| 229 | auto operator->() const noexcept -> T* { | ||
| 230 | return p_; | ||
| 231 | } | ||
| 232 | }; | ||
| 233 | export template<class T> explicit not_null(T*) -> not_null<T*>; | ||
| 234 | |||
| 235 | export template<> | ||
| 236 | class not_null<lazy_zstring_view> { | ||
| 237 | lazy_zstring_view s_; | ||
| 238 | |||
| 239 | public: | ||
| 240 | explicit not_null(lazy_zstring_view s) | ||
| 241 | : s_{std::move(s)} | ||
| 242 | { if (!s_) throw std::runtime_error{"not_null constructed with null pointer"}; } | ||
| 243 | |||
| 244 | [[nodiscard]] auto get() const noexcept -> lazy_zstring_view { | ||
| 245 | return s_; | ||
| 246 | } | ||
| 247 | |||
| 248 | operator lazy_zstring_view() const noexcept { return s_; } | ||
| 249 | operator std::string_view() const noexcept { return s_; } | ||
| 250 | operator char const*() const noexcept { return s_; } | ||
| 251 | }; | ||
| 252 | export explicit not_null(lazy_zstring_view s) -> not_null<lazy_zstring_view>; | ||
| 253 | |||
| 254 | } // namespace routemon::util | ||
diff --git a/server/src/xml.cpp b/server/src/xml.cpp new file mode 100644 index 0000000..cad42d1 --- /dev/null +++ b/server/src/xml.cpp | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <cassert> | ||
| 4 | #include <expat.h> | ||
| 5 | |||
| 6 | module routemon:xml$impl; | ||
| 7 | |||
| 8 | import :xml; | ||
| 9 | |||
| 10 | namespace routemon::xml { | ||
| 11 | |||
| 12 | auto qname_view::operator==(qname_view const& rhs) const -> bool { | ||
| 13 | return ns_uri == rhs.ns_uri && local == rhs.local; | ||
| 14 | } | ||
| 15 | |||
| 16 | executor::executor() : | ||
| 17 | p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)} | ||
| 18 | { | ||
| 19 | XML_SetUserData(p_, this); | ||
| 20 | XML_SetElementHandler(p_, handle_start_element, handle_end_element); | ||
| 21 | XML_SetCharacterDataHandler(p_, handle_character_data); | ||
| 22 | XML_SetProcessingInstructionHandler(p_, handle_processing_instructions); | ||
| 23 | XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref); | ||
| 24 | XML_SetNamespaceDeclHandler(p_, handle_start_namespace_decl, handle_end_namespace_decl); | ||
| 25 | XML_SetXmlDeclHandler(p_, handle_xml_decl); | ||
| 26 | } | ||
| 27 | |||
| 28 | executor::~executor() { | ||
| 29 | XML_ParserFree(p_); | ||
| 30 | } | ||
| 31 | |||
| 32 | auto executor::start() -> void { | ||
| 33 | continuation_.resume(); | ||
| 34 | if (ex_) std::rethrow_exception(ex_); | ||
| 35 | } | ||
| 36 | |||
| 37 | auto executor::read(std::string_view xml, bool is_final) -> void { | ||
| 38 | if (ex_) | ||
| 39 | throw std::runtime_error{"refusing to restart parser that was thrown in"}; | ||
| 40 | // TODO: narrow_cast | ||
| 41 | if (auto s = XML_Parse(p_, xml.data(), static_cast<int>(xml.size()), is_final); s != XML_STATUS_OK) { | ||
| 42 | auto errc = XML_GetErrorCode(p_); | ||
| 43 | if (errc == XML_ERROR_ABORTED) { | ||
| 44 | assert(ex_); | ||
| 45 | std::rethrow_exception(ex_); | ||
| 46 | } else { | ||
| 47 | throw std::runtime_error{std::format("failed to parse XML: {}", XML_ErrorString(errc))}; | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | auto executor::end() -> void { | ||
| 53 | if (ex_) | ||
| 54 | throw std::runtime_error{"refusing to restart parser that was thrown in"}; | ||
| 55 | ev_ = eof_event{}; | ||
| 56 | advance_ = false; | ||
| 57 | while (continuation_) continuation_.resume(); | ||
| 58 | if (ex_) std::rethrow_exception(ex_); | ||
| 59 | } | ||
| 60 | |||
| 61 | } // namespace routemon::xml | ||
diff --git a/server/src/xml.cppm b/server/src/xml.cppm new file mode 100644 index 0000000..957f149 --- /dev/null +++ b/server/src/xml.cppm | |||
| @@ -0,0 +1,632 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <cassert> | ||
| 4 | #include <expat.h> | ||
| 5 | |||
| 6 | export module routemon:xml; | ||
| 7 | |||
| 8 | import std; | ||
| 9 | import :util; | ||
| 10 | |||
| 11 | // XML parsing module. | ||
| 12 | // | ||
| 13 | // Makes heavy use of C++20 coroutines. Provides combinators to build | ||
| 14 | // XML (data format) parsers with. To keep overhead low (and allow for | ||
| 15 | // HALO/CoroElide), many non-polymorphic definitions are marked inline | ||
| 16 | // (which is not the default in module units). This also has the added | ||
| 17 | // benefit of allowing HALO across TU boundaries, which is practically | ||
| 18 | // necessary to reduce unnecessary allocations when building parsers | ||
| 19 | // using the provided combinators. | ||
| 20 | |||
| 21 | // Ensure that Expat is speaking UTF-8 | ||
| 22 | static_assert(std::is_same_v<XML_Char, char>); | ||
| 23 | |||
| 24 | namespace routemon::xml { | ||
| 25 | |||
| 26 | struct qname_view { | ||
| 27 | std::string_view ns_uri; | ||
| 28 | std::string_view local; | ||
| 29 | |||
| 30 | auto operator==(qname_view const& rhs) const -> bool; | ||
| 31 | }; | ||
| 32 | |||
| 33 | namespace detail { | ||
| 34 | |||
| 35 | constexpr auto qname_sep = '\xFF'; | ||
| 36 | auto split_name(char const* name) noexcept -> qname_view { | ||
| 37 | auto [l, r] = util::split_on(name, qname_sep); | ||
| 38 | if (r) return { .ns_uri = l, .local = *r }; | ||
| 39 | else return { .ns_uri = std::string_view{}, .local = l }; | ||
| 40 | } | ||
| 41 | |||
| 42 | } // namespace detail | ||
| 43 | |||
| 44 | class attribute_view_iterator { | ||
| 45 | std::string_view default_ns_uri_; | ||
| 46 | char const* const* attrs_; | ||
| 47 | |||
| 48 | auto advance() -> void { | ||
| 49 | attrs_ += 2; | ||
| 50 | } | ||
| 51 | |||
| 52 | public: | ||
| 53 | using difference_type = std::ptrdiff_t; | ||
| 54 | using value_type = std::pair<qname_view, char const*>; | ||
| 55 | |||
| 56 | struct sentinel { | ||
| 57 | friend constexpr auto operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool { | ||
| 58 | return !*it.attrs_; | ||
| 59 | } | ||
| 60 | }; | ||
| 61 | |||
| 62 | inline explicit attribute_view_iterator(std::string_view default_ns_uri, char const* const* attrs) | ||
| 63 | : default_ns_uri_{default_ns_uri}, attrs_{attrs} | ||
| 64 | {} | ||
| 65 | |||
| 66 | inline auto operator*() const -> std::pair<qname_view, char const*> { | ||
| 67 | if (!*attrs_) | ||
| 68 | throw std::runtime_error{"end of attribute list"}; | ||
| 69 | auto qname = detail::split_name(attrs_[0]); | ||
| 70 | if (qname.ns_uri.empty()) | ||
| 71 | qname.ns_uri = default_ns_uri_; | ||
| 72 | return std::make_pair(qname, attrs_[1]); | ||
| 73 | } | ||
| 74 | |||
| 75 | // Pre-increment | ||
| 76 | inline auto operator++() -> attribute_view_iterator& { | ||
| 77 | advance(); | ||
| 78 | return *this; | ||
| 79 | } | ||
| 80 | |||
| 81 | // Post-increment | ||
| 82 | inline auto operator++(int) -> attribute_view_iterator { | ||
| 83 | auto pre = *this; | ||
| 84 | advance(); | ||
| 85 | return pre; | ||
| 86 | } | ||
| 87 | }; | ||
| 88 | static_assert(std::input_iterator<attribute_view_iterator>); | ||
| 89 | |||
| 90 | class attribute_view : std::ranges::view_base { | ||
| 91 | std::string_view default_ns_uri_; | ||
| 92 | char const* const* attrs_; | ||
| 93 | |||
| 94 | public: | ||
| 95 | inline explicit attribute_view(std::string_view default_ns_uri, char const** attrs) | ||
| 96 | : default_ns_uri_{default_ns_uri}, attrs_{const_cast<char const* const*>(attrs)} | ||
| 97 | {} | ||
| 98 | |||
| 99 | [[nodiscard]] inline auto begin() const -> attribute_view_iterator { | ||
| 100 | return attribute_view_iterator{default_ns_uri_, attrs_}; | ||
| 101 | } | ||
| 102 | |||
| 103 | [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel { | ||
| 104 | return {}; | ||
| 105 | } | ||
| 106 | |||
| 107 | inline auto lookup(qname_view want) -> std::optional<util::not_null<util::lazy_zstring_view>> { | ||
| 108 | for (auto const& [name, v] : *this) { | ||
| 109 | if (name == want) { | ||
| 110 | return util::not_null{util::lazy_zstring_view{v}}; | ||
| 111 | } | ||
| 112 | } | ||
| 113 | return std::nullopt; | ||
| 114 | } | ||
| 115 | }; | ||
| 116 | static_assert(std::ranges::input_range<attribute_view>); | ||
| 117 | |||
| 118 | template<class T> class promise; | ||
| 119 | |||
| 120 | template<class T> | ||
| 121 | struct [[clang::coro_await_elidable, clang::coro_return_type]] parser { | ||
| 122 | using promise_type = promise<T>; | ||
| 123 | using result_type = promise_type::result_type; | ||
| 124 | using handle_type = std::coroutine_handle<promise_type>; | ||
| 125 | |||
| 126 | private: | ||
| 127 | handle_type h_; | ||
| 128 | |||
| 129 | public: | ||
| 130 | explicit parser(handle_type h) | ||
| 131 | : h_{h} | ||
| 132 | { assert(h); } | ||
| 133 | |||
| 134 | parser(const parser&) = delete; | ||
| 135 | parser(parser&& c) noexcept | ||
| 136 | : h_{std::exchange(c.h_, nullptr)} | ||
| 137 | {} | ||
| 138 | auto operator=(const parser&) -> parser& = delete; | ||
| 139 | auto operator=(parser&&) -> parser& = delete; | ||
| 140 | |||
| 141 | [[nodiscard]] auto promise() const -> promise_type& { | ||
| 142 | return h_.promise(); | ||
| 143 | } | ||
| 144 | |||
| 145 | ~parser() { | ||
| 146 | if (h_) h_.destroy(); | ||
| 147 | } | ||
| 148 | }; | ||
| 149 | |||
| 150 | struct start_element_event { | ||
| 151 | qname_view name; | ||
| 152 | attribute_view attrs; | ||
| 153 | }; | ||
| 154 | struct end_element_event { | ||
| 155 | qname_view name; | ||
| 156 | }; | ||
| 157 | struct character_data_event { | ||
| 158 | std::string_view data; | ||
| 159 | }; | ||
| 160 | struct processing_instructions_event { | ||
| 161 | util::lazy_zstring_view target; | ||
| 162 | util::lazy_zstring_view data; | ||
| 163 | }; | ||
| 164 | struct xml_decl_event { | ||
| 165 | util::lazy_zstring_view version; | ||
| 166 | util::lazy_zstring_view encoding; | ||
| 167 | std::optional<bool> standalone; | ||
| 168 | }; | ||
| 169 | struct eof_event {}; | ||
| 170 | using event = std::variant<start_element_event, | ||
| 171 | end_element_event, | ||
| 172 | character_data_event, | ||
| 173 | processing_instructions_event, | ||
| 174 | xml_decl_event, | ||
| 175 | eof_event>; | ||
| 176 | template<class T> | ||
| 177 | concept event_type = requires(event ev) { std::get<T>(ev); }; | ||
| 178 | |||
| 179 | class executor; | ||
| 180 | using executor_ref = util::not_null<executor*>; | ||
| 181 | |||
| 182 | struct current_event_t { | ||
| 183 | executor_ref executor; | ||
| 184 | }; | ||
| 185 | auto current_event(executor_ref executor) -> current_event_t { | ||
| 186 | return current_event_t{executor}; | ||
| 187 | } | ||
| 188 | |||
| 189 | class promise_base { | ||
| 190 | executor_ref executor_; | ||
| 191 | std::coroutine_handle<promise_base> continuation_ = nullptr; | ||
| 192 | |||
| 193 | public: | ||
| 194 | // Not having this constructor marked inline messes with coroutine | ||
| 195 | // HALO. (Hours 'wasted': many) | ||
| 196 | inline explicit promise_base(executor_ref executor) | ||
| 197 | : executor_{executor} | ||
| 198 | {} | ||
| 199 | |||
| 200 | [[nodiscard]] inline auto executor() const -> executor& { | ||
| 201 | return *executor_; | ||
| 202 | } | ||
| 203 | |||
| 204 | inline auto base_handle() -> std::coroutine_handle<promise_base> { | ||
| 205 | return std::coroutine_handle<promise_base>::from_promise(*this); | ||
| 206 | } | ||
| 207 | |||
| 208 | [[nodiscard]] inline auto continuation() const -> std::coroutine_handle<promise_base> { | ||
| 209 | return continuation_; | ||
| 210 | } | ||
| 211 | inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void { | ||
| 212 | continuation_ = c; | ||
| 213 | } | ||
| 214 | }; | ||
| 215 | |||
| 216 | struct position { | ||
| 217 | std::size_t line; | ||
| 218 | std::size_t col; | ||
| 219 | }; | ||
| 220 | |||
| 221 | class executor { | ||
| 222 | XML_Parser p_; | ||
| 223 | std::exception_ptr ex_ = nullptr; | ||
| 224 | std::coroutine_handle<promise_base> continuation_ = nullptr; | ||
| 225 | std::vector<std::optional<std::string>> default_namespace_; | ||
| 226 | std::unordered_map<std::string_view, std::vector<std::string>> namespaces_; | ||
| 227 | std::optional<event> ev_; | ||
| 228 | bool advance_ = true; | ||
| 229 | |||
| 230 | inline auto try_handle_event(event ev) noexcept -> void { | ||
| 231 | assert(!ex_); | ||
| 232 | |||
| 233 | try { | ||
| 234 | ev_ = std::move(ev); | ||
| 235 | } catch (...) { | ||
| 236 | ex_ = std::current_exception(); | ||
| 237 | return; | ||
| 238 | } | ||
| 239 | advance_ = false; | ||
| 240 | if (!continuation_) { | ||
| 241 | // Parser returned (all subparsers are done) and has set the continuation to nullptr. | ||
| 242 | if (auto s = XML_StopParser(p_, /* resumable */ false); s != XML_STATUS_OK) { | ||
| 243 | ex_ = std::make_exception_ptr(std::runtime_error{"unexpected error when stopping XML parser"}); | ||
| 244 | return; | ||
| 245 | } | ||
| 246 | ex_ = std::make_exception_ptr(std::runtime_error{"parser did not consume entire XML document"}); | ||
| 247 | return; | ||
| 248 | } | ||
| 249 | continuation_.resume(); | ||
| 250 | if (ex_) { | ||
| 251 | // Not sure if it's useful to report this error. | ||
| 252 | std::ignore = XML_StopParser(p_, /* resumable */ false); | ||
| 253 | } | ||
| 254 | } | ||
| 255 | |||
| 256 | static auto handle_start_element(void* ctx, char const* name, char const** attrs) noexcept -> void { | ||
| 257 | auto qname = detail::split_name(name); | ||
| 258 | static_cast<executor*>(ctx)->try_handle_event(start_element_event{ | ||
| 259 | .name = qname, | ||
| 260 | .attrs = attribute_view{qname.ns_uri, attrs}, | ||
| 261 | }); | ||
| 262 | } | ||
| 263 | static auto handle_end_element(void* ctx, char const* name) noexcept -> void { | ||
| 264 | static_cast<executor*>(ctx)->try_handle_event(end_element_event{ | ||
| 265 | .name = detail::split_name(name), | ||
| 266 | }); | ||
| 267 | } | ||
| 268 | static auto handle_character_data(void* ctx, char const* s, int len) noexcept -> void { | ||
| 269 | static_cast<executor*>(ctx)->try_handle_event(character_data_event{ | ||
| 270 | .data = std::string_view{s, static_cast<std::size_t>(len)}, | ||
| 271 | }); | ||
| 272 | } | ||
| 273 | static auto handle_processing_instructions(void* ctx, char const* target, char const* data) noexcept -> void { | ||
| 274 | static_cast<executor*>(ctx)->try_handle_event(processing_instructions_event{ | ||
| 275 | .target = util::lazy_zstring_view{target}, | ||
| 276 | .data = util::lazy_zstring_view{data}, | ||
| 277 | }); | ||
| 278 | } | ||
| 279 | static auto handle_external_entity_ref(XML_Parser, char const* /* context */, char const* /* base */, char const* /* system_id */, char const* /* public_id */) noexcept -> int { | ||
| 280 | return XML_STATUS_ERROR; | ||
| 281 | } | ||
| 282 | static auto handle_start_namespace_decl(void* ctx, char const* prefix, char const* uri) noexcept -> void { | ||
| 283 | if (prefix) { | ||
| 284 | static_cast<executor*>(ctx)->namespaces_[std::string_view{prefix}].emplace_back(uri); | ||
| 285 | } else { | ||
| 286 | static_cast<executor*>(ctx)->default_namespace_.push_back(uri ? std::make_optional<std::string>(uri) : std::nullopt); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept -> void { | ||
| 290 | if (prefix) { | ||
| 291 | static_cast<executor*>(ctx)->namespaces_[std::string_view{prefix}].pop_back(); | ||
| 292 | } else { | ||
| 293 | static_cast<executor*>(ctx)->default_namespace_.pop_back(); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | static auto handle_xml_decl(void * ctx, char const* version, char const* encoding, int standalone) noexcept -> void { | ||
| 297 | static_cast<executor*>(ctx)->try_handle_event(xml_decl_event{ | ||
| 298 | .version = util::lazy_zstring_view{version}, | ||
| 299 | .encoding = util::lazy_zstring_view{encoding}, | ||
| 300 | .standalone = standalone < 0 ? std::nullopt : std::make_optional(standalone > 0), | ||
| 301 | }); | ||
| 302 | } | ||
| 303 | |||
| 304 | inline auto advance_flag() -> bool { | ||
| 305 | return advance_; | ||
| 306 | } | ||
| 307 | inline auto set_exception(std::exception_ptr ex) -> void { | ||
| 308 | ex_ = std::move(ex); | ||
| 309 | } | ||
| 310 | [[nodiscard]] inline auto take_exception() -> std::exception_ptr { | ||
| 311 | return std::exchange(ex_, nullptr); | ||
| 312 | } | ||
| 313 | |||
| 314 | template<class T> friend class promise; | ||
| 315 | |||
| 316 | public: | ||
| 317 | executor(); | ||
| 318 | |||
| 319 | executor(executor const&) = delete; | ||
| 320 | executor(executor&&) = delete; | ||
| 321 | auto operator=(executor const&) -> executor& = delete; | ||
| 322 | auto operator=(executor&&) -> executor& = delete; | ||
| 323 | |||
| 324 | ~executor(); | ||
| 325 | |||
| 326 | inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void { | ||
| 327 | continuation_ = c; | ||
| 328 | } | ||
| 329 | inline auto set_advance_flag() -> void { | ||
| 330 | if (!ev_ || !std::holds_alternative<eof_event>(ev_.value())) { | ||
| 331 | advance_ = true; | ||
| 332 | } | ||
| 333 | } | ||
| 334 | inline auto event() const -> std::optional<event> const& { | ||
| 335 | return ev_; | ||
| 336 | } | ||
| 337 | inline auto resolve_namespace(std::string_view prefix) -> std::optional<std::string_view> { | ||
| 338 | if (auto it = namespaces_.find(prefix); it != namespaces_.end() && !it->second.empty()) | ||
| 339 | return it->second.back(); | ||
| 340 | return std::nullopt; | ||
| 341 | } | ||
| 342 | [[nodiscard]] inline auto position() -> position { | ||
| 343 | return { | ||
| 344 | .line = XML_GetCurrentLineNumber(p_), | ||
| 345 | .col = XML_GetCurrentColumnNumber(p_), | ||
| 346 | }; | ||
| 347 | } | ||
| 348 | |||
| 349 | auto start() -> void; | ||
| 350 | auto read(std::string_view xml, bool is_final) -> void; | ||
| 351 | auto end() -> void; | ||
| 352 | }; | ||
| 353 | |||
| 354 | template<class T> | ||
| 355 | class promise_returnable : public promise_base { | ||
| 356 | std::optional<T> returned_value_; | ||
| 357 | |||
| 358 | public: | ||
| 359 | using result_type = T; | ||
| 360 | using promise_base::promise_base; | ||
| 361 | |||
| 362 | template<class U> | ||
| 363 | auto return_value(U&& v) -> void { | ||
| 364 | returned_value_.emplace(std::forward<U>(v)); | ||
| 365 | } | ||
| 366 | auto returned_value() -> T&& { | ||
| 367 | if (!returned_value_) | ||
| 368 | throw std::runtime_error{"XML coroutine did not return"}; | ||
| 369 | return std::forward<T>(returned_value_.value()); | ||
| 370 | } | ||
| 371 | }; | ||
| 372 | |||
| 373 | template<> | ||
| 374 | class promise_returnable<void> : public promise_base { | ||
| 375 | public: | ||
| 376 | using result_type = void; | ||
| 377 | using promise_base::promise_base; | ||
| 378 | |||
| 379 | auto return_void() -> void {} | ||
| 380 | }; | ||
| 381 | |||
| 382 | template<class T> | ||
| 383 | class promise : public promise_returnable<T> { | ||
| 384 | public: | ||
| 385 | // Called with all the coroutine's arguments. | ||
| 386 | // Ignoring all but the first argument, which should be the executor. | ||
| 387 | template<class... Args> | ||
| 388 | explicit promise(executor_ref executor, Args&&...) | ||
| 389 | : promise_returnable<T>{executor} | ||
| 390 | {} | ||
| 391 | |||
| 392 | auto handle() -> std::coroutine_handle<promise<T>> { | ||
| 393 | return {parser<T>::handle_type::from_promise(*this)}; | ||
| 394 | } | ||
| 395 | |||
| 396 | auto get_return_object() -> parser<T> { | ||
| 397 | return parser<T>{handle()}; | ||
| 398 | } | ||
| 399 | |||
| 400 | auto initial_suspend() { | ||
| 401 | return std::suspend_always{}; | ||
| 402 | } | ||
| 403 | auto final_suspend() noexcept { | ||
| 404 | struct awaiter { | ||
| 405 | std::coroutine_handle<> h_; | ||
| 406 | |||
| 407 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } | ||
| 408 | auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> { return h_; } | ||
| 409 | constexpr auto await_resume() const noexcept -> void { return; } | ||
| 410 | }; | ||
| 411 | if (this->continuation()) { | ||
| 412 | return awaiter{this->continuation()}; | ||
| 413 | } else { | ||
| 414 | this->executor().set_continuation(nullptr); | ||
| 415 | return awaiter{std::noop_coroutine()}; | ||
| 416 | } | ||
| 417 | } | ||
| 418 | |||
| 419 | auto unhandled_exception() -> void { | ||
| 420 | this->executor().set_exception(std::current_exception()); | ||
| 421 | } | ||
| 422 | |||
| 423 | auto await_transform(current_event_t const& req) { | ||
| 424 | struct awaiter { | ||
| 425 | executor_ref executor_; | ||
| 426 | |||
| 427 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { | ||
| 428 | return !executor_->advance_flag(); | ||
| 429 | } | ||
| 430 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> void { | ||
| 431 | executor_->set_continuation(h.promise().base_handle()); | ||
| 432 | } | ||
| 433 | [[nodiscard]] auto await_resume() const -> event { | ||
| 434 | assert(executor_->event()); | ||
| 435 | return executor_->event().value(); | ||
| 436 | } | ||
| 437 | }; | ||
| 438 | return awaiter{req.executor}; | ||
| 439 | } | ||
| 440 | |||
| 441 | template<class U> | ||
| 442 | auto await_transform(parser<U> const& coro) { | ||
| 443 | struct [[clang::coro_await_elidable]] awaiter { | ||
| 444 | util::not_null<promise<U>*> next_; | ||
| 445 | |||
| 446 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } | ||
| 447 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> std::coroutine_handle<> { | ||
| 448 | // Passed coroutine handle will be the same as parser<T>::handle_type::from_promise(*this) | ||
| 449 | next_->set_continuation(h.promise().base_handle()); | ||
| 450 | return next_->handle(); | ||
| 451 | } | ||
| 452 | auto await_resume() -> U { | ||
| 453 | // Promise is still valid since coroutine frame is still alive (and suspended): | ||
| 454 | // control was transferred back to this coroutine via symmetric transfer in | ||
| 455 | // final_suspend(). Assuming that the destructor for coro still needs to run. | ||
| 456 | if (auto ex = next_->executor().take_exception()) { | ||
| 457 | std::rethrow_exception(ex); | ||
| 458 | } else { | ||
| 459 | if constexpr (!std::is_void_v<U>) { | ||
| 460 | return std::move(next_->returned_value()); | ||
| 461 | } | ||
| 462 | } | ||
| 463 | } | ||
| 464 | }; | ||
| 465 | return awaiter{util::not_null{&coro.promise()}}; | ||
| 466 | } | ||
| 467 | }; | ||
| 468 | |||
| 469 | // Helpers for handling XML documents. Non-polymorphic functions | ||
| 470 | // should be marked inline to allow HALO across TU boundaries. | ||
| 471 | |||
| 472 | template<class T> | ||
| 473 | concept unconstrained = true; | ||
| 474 | |||
| 475 | template<class T, template<class U> concept C> | ||
| 476 | concept parser_of = requires { | ||
| 477 | typename T::result_type; | ||
| 478 | requires std::same_as<parser<typename T::result_type>, T>; | ||
| 479 | requires C<typename T::result_type>; | ||
| 480 | }; | ||
| 481 | |||
| 482 | template<parser_of<unconstrained> T> | ||
| 483 | using parser_result_t = T::result_type; | ||
| 484 | |||
| 485 | template<class T, class... Args> | ||
| 486 | concept parser_invocable = std::invocable<T, Args...> && parser_of<std::invoke_result_t<T, Args...>, unconstrained>; | ||
| 487 | |||
| 488 | template<class Fn, class... Args> | ||
| 489 | requires parser_invocable<Fn, Args...> | ||
| 490 | using parser_invoke_result_t = parser_result_t<std::invoke_result_t<Fn, Args...>>; | ||
| 491 | |||
| 492 | template<event_type T> auto expect_event(executor_ref e) -> parser<T> { | ||
| 493 | auto ev = co_await current_event(e); | ||
| 494 | if (!std::holds_alternative<T>(ev)) { | ||
| 495 | auto pos = e->position(); | ||
| 496 | throw std::runtime_error{std::format("at {}:{}: unexpected event type, have {}", pos.line, pos.col, ev.index())}; | ||
| 497 | } | ||
| 498 | e->set_advance_flag(); | ||
| 499 | co_return std::get<T>(ev); | ||
| 500 | } | ||
| 501 | |||
| 502 | inline auto expect_start_element(executor_ref e, qname_view want) -> parser<attribute_view> { | ||
| 503 | auto ev = co_await expect_event<start_element_event>(e); | ||
| 504 | if (ev.name != want) { | ||
| 505 | auto pos = e->position(); | ||
| 506 | throw std::runtime_error{std::format("at {}:{}: unexpected element started", pos.line, pos.col)}; | ||
| 507 | } | ||
| 508 | co_return ev.attrs; | ||
| 509 | } | ||
| 510 | |||
| 511 | inline auto allow_start_element(executor_ref e, qname_view want) -> parser<std::optional<attribute_view>> { | ||
| 512 | auto ev = co_await current_event(e); | ||
| 513 | if (auto const* pev = std::get_if<start_element_event>(&ev)) { | ||
| 514 | if (pev->name == want) { | ||
| 515 | e->set_advance_flag(); | ||
| 516 | co_return pev->attrs; | ||
| 517 | } | ||
| 518 | } | ||
| 519 | co_return std::nullopt; | ||
| 520 | } | ||
| 521 | |||
| 522 | inline auto expect_end_element(executor_ref e, qname_view want) -> parser<void> { | ||
| 523 | auto ev = co_await expect_event<end_element_event>(e); | ||
| 524 | if (ev.name != want) { | ||
| 525 | throw std::runtime_error{"unexpected element ended"}; | ||
| 526 | } | ||
| 527 | } | ||
| 528 | |||
| 529 | inline auto ignore_whitespace(executor_ref e) -> parser<void> { | ||
| 530 | auto all_whitespace = [](std::string_view s) -> bool { | ||
| 531 | for (auto c : s) | ||
| 532 | if (c != ' ' && c != '\r' && c != '\n' && c != '\t') | ||
| 533 | return false; | ||
| 534 | return true; | ||
| 535 | }; | ||
| 536 | |||
| 537 | while (true) { | ||
| 538 | auto ev = co_await current_event(e); | ||
| 539 | if (auto const* pev = std::get_if<character_data_event>(&ev); pev && all_whitespace(pev->data)) { | ||
| 540 | e->set_advance_flag(); | ||
| 541 | } else { | ||
| 542 | co_return; | ||
| 543 | } | ||
| 544 | } | ||
| 545 | } | ||
| 546 | |||
| 547 | auto expect_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p) | ||
| 548 | -> parser<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> | ||
| 549 | { | ||
| 550 | co_await ignore_whitespace(e); | ||
| 551 | auto attrs = co_await expect_start_element(e, want); | ||
| 552 | auto&& res = co_await p(e, attrs); | ||
| 553 | co_await expect_end_element(e, want); | ||
| 554 | co_await ignore_whitespace(e); | ||
| 555 | co_return std::forward<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res); | ||
| 556 | } | ||
| 557 | |||
| 558 | auto allow_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p) | ||
| 559 | -> parser<std::optional<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>> | ||
| 560 | { | ||
| 561 | co_await ignore_whitespace(e); | ||
| 562 | if (auto mattrs = co_await allow_start_element(e, want)) { | ||
| 563 | auto&& res = co_await p(e, *mattrs); | ||
| 564 | co_await expect_end_element(e, want); | ||
| 565 | co_await ignore_whitespace(e); | ||
| 566 | co_return std::make_optional(std::forward<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res)); | ||
| 567 | } | ||
| 568 | co_return std::nullopt; | ||
| 569 | } | ||
| 570 | |||
| 571 | auto allow_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p) | ||
| 572 | -> parser<bool> | ||
| 573 | requires std::is_void_v<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> | ||
| 574 | { | ||
| 575 | co_await ignore_whitespace(e); | ||
| 576 | if (auto mattrs = co_await allow_start_element(e, want)) { | ||
| 577 | co_await p(e, *mattrs); | ||
| 578 | co_await expect_end_element(e, want); | ||
| 579 | co_await ignore_whitespace(e); | ||
| 580 | co_return true; | ||
| 581 | } | ||
| 582 | co_return false; | ||
| 583 | } | ||
| 584 | |||
| 585 | inline auto ignore_contents(executor_ref e, std::optional<qname_view> muntil = std::nullopt) -> parser<void> { | ||
| 586 | std::size_t depth = 0; | ||
| 587 | while (true) { | ||
| 588 | auto ev = co_await current_event(e); | ||
| 589 | if (auto* pev = std::get_if<start_element_event>(&ev)) { | ||
| 590 | if (depth == 0 && muntil && pev->name == *muntil) { | ||
| 591 | co_return; | ||
| 592 | } else { | ||
| 593 | depth++; | ||
| 594 | } | ||
| 595 | } else if (std::holds_alternative<end_element_event>(ev)) { | ||
| 596 | if (depth == 0) { | ||
| 597 | co_return; | ||
| 598 | } else { | ||
| 599 | depth--; | ||
| 600 | } | ||
| 601 | } | ||
| 602 | e->set_advance_flag(); | ||
| 603 | } | ||
| 604 | } | ||
| 605 | inline auto ignore_element_contents(executor_ref e, attribute_view) -> parser<void> { | ||
| 606 | co_await ignore_contents(e); | ||
| 607 | } | ||
| 608 | |||
| 609 | inline auto read_string(executor_ref e) -> parser<std::string> { | ||
| 610 | std::string s; | ||
| 611 | while (true) { | ||
| 612 | auto ev = co_await current_event(e); | ||
| 613 | if (auto* pev = std::get_if<character_data_event>(&ev)) { | ||
| 614 | e->set_advance_flag(); | ||
| 615 | s += pev->data; | ||
| 616 | } else { | ||
| 617 | co_return s; | ||
| 618 | } | ||
| 619 | } | ||
| 620 | } | ||
| 621 | inline auto read_string_contents(executor_ref e, attribute_view) -> parser<std::string> { | ||
| 622 | co_return co_await read_string(e); | ||
| 623 | } | ||
| 624 | |||
| 625 | template<auto f> | ||
| 626 | auto hohalo() { | ||
| 627 | return []<class... Args>(Args&&... args) -> std::invoke_result_t<decltype(f), Args...> { | ||
| 628 | co_return co_await f(std::forward<Args>(args)...); | ||
| 629 | }; | ||
| 630 | } | ||
| 631 | |||
| 632 | } // namespace routemon::xml | ||