diff options
Diffstat (limited to 'server/src')
| -rw-r--r-- | server/src/api.cpp | 383 | ||||
| -rw-r--r-- | server/src/api.cppm | 116 | ||||
| -rw-r--r-- | server/src/config.cpp | 323 | ||||
| -rw-r--r-- | server/src/config.cppm | 54 | ||||
| -rw-r--r-- | server/src/database.cppm | 52 | ||||
| -rw-r--r-- | server/src/datex2.cppm | 540 | ||||
| -rw-r--r-- | server/src/geo.cppm | 59 | ||||
| -rw-r--r-- | server/src/gpx.cpp | 341 | ||||
| -rw-r--r-- | server/src/gpx.cppm | 57 | ||||
| -rw-r--r-- | server/src/http_client.cppm | 74 | ||||
| -rw-r--r-- | server/src/http_common.cppm | 228 | ||||
| -rw-r--r-- | server/src/http_server.cppm | 1249 | ||||
| -rw-r--r-- | server/src/locale.cppm | 424 | ||||
| -rw-r--r-- | server/src/log.cppm | 235 | ||||
| -rw-r--r-- | server/src/main.cpp | 101 | ||||
| -rw-r--r-- | server/src/problem.cppm | 85 | ||||
| -rw-r--r-- | server/src/req_ctx.cppm | 65 | ||||
| -rw-r--r-- | server/src/rwgps.cppm | 211 | ||||
| -rw-r--r-- | server/src/sqlite3.cppm | 454 | ||||
| -rw-r--r-- | server/src/srv.cppm | 370 | ||||
| -rw-r--r-- | server/src/time.cppm | 353 | ||||
| -rw-r--r-- | server/src/trace.cppm | 120 | ||||
| -rw-r--r-- | server/src/util.cppm | 428 | ||||
| -rw-r--r-- | server/src/xml.cpp | 94 | ||||
| -rw-r--r-- | server/src/xml.cppm | 1166 |
25 files changed, 4367 insertions, 3215 deletions
diff --git a/server/src/api.cpp b/server/src/api.cpp index b3c8e31..a6eef23 100644 --- a/server/src/api.cpp +++ b/server/src/api.cpp | |||
| @@ -17,190 +17,259 @@ import :trace; | |||
| 17 | 17 | ||
| 18 | namespace { | 18 | namespace { |
| 19 | 19 | ||
| 20 | namespace chrono = std::chrono; | 20 | namespace chrono = std::chrono; |
| 21 | namespace json = boost::json; | 21 | namespace json = boost::json; |
| 22 | namespace views = std::views; | 22 | namespace views = std::views; |
| 23 | 23 | ||
| 24 | } // namespace <anonymous> | 24 | } // namespace |
| 25 | 25 | ||
| 26 | namespace routemon::api { | 26 | namespace routemon::api { |
| 27 | 27 | ||
| 28 | auto json_value_from_point(geo::point const& p) -> json::value { | 28 | auto json_value_from_point(geo::point const& p) -> json::value |
| 29 | return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; | 29 | { |
| 30 | } | 30 | return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; |
| 31 | auto json_value_from_linestring(geo::linestring const& ls) -> json::value { | 31 | } |
| 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 | 32 | ||
| 44 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void { | 33 | auto json_value_from_linestring(geo::linestring const& ls) -> json::value |
| 45 | jv = json::object{ | 34 | { |
| 35 | json::array a; | ||
| 36 | for (auto const& p : ls) | ||
| 37 | a.push_back(json_value_from_point(p)); | ||
| 38 | return a; | ||
| 39 | } | ||
| 40 | |||
| 41 | auto json_value_from_linestrings(std::vector<geo::linestring> const& lss) | ||
| 42 | -> json::value | ||
| 43 | { | ||
| 44 | json::array a; | ||
| 45 | for (auto const& ls : lss) | ||
| 46 | a.push_back(json_value_from_linestring(ls)); | ||
| 47 | return a; | ||
| 48 | } | ||
| 49 | |||
| 50 | auto tag_invoke( | ||
| 51 | json::value_from_tag, json::value& jv, relevant_road_closure const& clo) | ||
| 52 | -> void | ||
| 53 | { | ||
| 54 | jv = json::object{ | ||
| 46 | {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, | 55 | {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, |
| 47 | }; | 56 | }; |
| 48 | } | 57 | } |
| 49 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void { | 58 | |
| 50 | jv = json::object{ | 59 | auto tag_invoke( |
| 51 | {"id", json::value_from(sit.id)}, | 60 | json::value_from_tag, json::value& jv, relevant_situation const& sit) |
| 52 | {"location", sit.location ? json_value_from_point(*sit.location) : nullptr}, | 61 | -> void |
| 53 | {"comments", json::value_from(sit.comments)}, | 62 | { |
| 63 | jv = json::object{ | ||
| 64 | {"id", json::value_from(sit.id)}, | ||
| 65 | {"location", | ||
| 66 | sit.location ? json_value_from_point(*sit.location) : nullptr}, | ||
| 67 | {"comments", json::value_from(sit.comments)}, | ||
| 54 | {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, | 68 | {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, |
| 55 | }; | 69 | }; |
| 56 | } | 70 | } |
| 57 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void { | 71 | |
| 58 | jv = json::object{ | 72 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) |
| 73 | -> void | ||
| 74 | { | ||
| 75 | jv = json::object{ | ||
| 59 | {"points", json_value_from_linestring(seg.points)}, | 76 | {"points", json_value_from_linestring(seg.points)}, |
| 60 | }; | 77 | }; |
| 61 | } | 78 | } |
| 62 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void { | 79 | |
| 63 | jv = json::object{ | 80 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) |
| 81 | -> void | ||
| 82 | { | ||
| 83 | jv = json::object{ | ||
| 64 | {"segments", json::value_from(track.segments)}, | 84 | {"segments", json::value_from(track.segments)}, |
| 65 | }; | 85 | }; |
| 66 | } | 86 | } |
| 67 | auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void { | 87 | |
| 68 | jv = json::object{ | 88 | auto tag_invoke( |
| 69 | {"tracks", json::value_from(res.tracks)}, | 89 | json::value_from_tag, json::value& jv, process_gpx_result const& res) |
| 90 | -> void | ||
| 91 | { | ||
| 92 | jv = json::object{ | ||
| 93 | {"tracks", json::value_from(res.tracks)}, | ||
| 70 | {"relevant_situations", json::value_from(res.relevant_situations)}, | 94 | {"relevant_situations", json::value_from(res.relevant_situations)}, |
| 71 | }; | 95 | }; |
| 72 | } | 96 | } |
| 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 | 97 | ||
| 79 | handler::handler(log::logger const& l, datex2::situation_publication pub) | 98 | auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) |
| 80 | : l_{l.sub("handler")}, pub_{std::move(pub)} | 99 | -> void |
| 100 | { | ||
| 101 | jv = json::object{ | ||
| 102 | {"using_publication_of", | ||
| 103 | std::format("{:%FT%TZ}", info.using_publication_of)}, | ||
| 104 | }; | ||
| 105 | } | ||
| 106 | |||
| 107 | handler::handler(log::logger const& l, datex2::situation_publication pub) | ||
| 108 | : l_{l.sub("handler")}, pub_{std::move(pub)} | ||
| 109 | { | ||
| 110 | l_.info("Building indices"); | ||
| 111 | auto const before_build = chrono::steady_clock::now(); | ||
| 112 | for (auto const& sit : pub_.situations) | ||
| 81 | { | 113 | { |
| 82 | l_.info("Building indices"); | 114 | for (auto const& rc : sit->road_closures) |
| 83 | auto const before_build = chrono::steady_clock::now(); | 115 | { |
| 84 | for (auto const& sit : pub_.situations) { | 116 | for (auto const& ls : rc->relevant_line_strings) |
| 85 | for (auto const& rc : sit->road_closures) { | 117 | { |
| 86 | for (auto const& ls : rc->relevant_line_strings) { | 118 | auto box = geo::box{}; |
| 87 | auto box = geo::box{}; | 119 | bgeo::envelope(*ls, box); |
| 88 | bgeo::envelope(*ls, box); | 120 | lse_index_.insert(std::make_tuple(box, ls, rc)); |
| 89 | lse_index_.insert(std::make_tuple(box, ls, rc)); | 121 | } |
| 90 | } | 122 | for (auto p : rc->relevant_points) |
| 91 | for (auto p : rc->relevant_points) { | 123 | { |
| 92 | p_index_.insert(std::make_pair(p, rc)); | 124 | p_index_.insert(std::make_pair(p, rc)); |
| 93 | } | ||
| 94 | } | 125 | } |
| 95 | } | 126 | } |
| 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 | } | 127 | } |
| 128 | auto const after_build = chrono::steady_clock::now(); | ||
| 129 | auto const dur_build = | ||
| 130 | chrono::duration_cast<chrono::milliseconds>(after_build - before_build); | ||
| 131 | l_.info("Indices built in {}", dur_build); | ||
| 132 | l_.info("LSE index size: {}", lse_index_.size()); | ||
| 133 | l_.info("Point index size: {}", p_index_.size()); | ||
| 134 | } | ||
| 102 | 135 | ||
| 103 | auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result> { | 136 | auto handler::process_gpx(gpx::file&& gpx_file) |
| 104 | auto const now = chrono::utc_clock::now(); | 137 | -> std::optional<process_gpx_result> |
| 105 | auto const relevant = std::initializer_list<time::period>{time::period{now - chrono::days(7), now + chrono::days(7)}}; | 138 | { |
| 106 | auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; | 139 | auto const now = chrono::utc_clock::now(); |
| 140 | auto const relevant = std::initializer_list<time::period>{ | ||
| 141 | time::period{now - chrono::days(7), now + chrono::days(7)} | ||
| 142 | }; | ||
| 143 | auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; | ||
| 107 | 144 | ||
| 108 | auto splits_with_overlap_segments = std::vector<geo::linestring>{}; | 145 | auto splits_with_overlap_segments = std::vector<geo::linestring>{}; |
| 109 | for (auto const& track : gpx_file.tracks) | 146 | for (auto const& track : gpx_file.tracks) |
| 110 | for (auto const& seg : track.segments) | 147 | 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 */, | 148 | geo::split_linestring_with_overlap_segments( |
| 112 | splits_with_overlap_segments); | 149 | seg.waypoints, |
| 113 | auto const before_query = chrono::steady_clock::now(); | 150 | 5000 /* meters max total dist until a new split is forced */, |
| 151 | splits_with_overlap_segments); | ||
| 152 | auto const before_query = chrono::steady_clock::now(); | ||
| 114 | 153 | ||
| 115 | l_.debug("Querying for relevant situations"); | 154 | l_.debug("Querying for relevant situations"); |
| 116 | auto relevant_road_closures = std::unordered_set<std::shared_ptr<datex2::road_closure>>{}; | 155 | auto relevant_road_closures = |
| 117 | auto ls_checked = 0uz; | 156 | std::unordered_set<std::shared_ptr<datex2::road_closure>>{}; |
| 118 | auto p_checked = 0uz; | 157 | auto ls_checked = 0uz; |
| 119 | auto i = 0; | 158 | auto p_checked = 0uz; |
| 120 | for (geo::linestring const& part : splits_with_overlap_segments) { | 159 | auto i = 0; |
| 121 | l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); | 160 | for (geo::linestring const& part : splits_with_overlap_segments) |
| 161 | { | ||
| 162 | l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); | ||
| 122 | 163 | ||
| 123 | auto part_box = geo::box{}; | 164 | auto part_box = geo::box{}; |
| 124 | bgeo::envelope(part, part_box); | 165 | bgeo::envelope(part, part_box); |
| 125 | 166 | ||
| 126 | for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) { | 167 | for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); |
| 127 | // Cannot use structured bindings here, as boost::geometry::get interferes with ADL. | 168 | it != lse_index_.qend(); it++) |
| 128 | // It is a candidate as the namespace boost::geometry is part of the associated namespace set, | 169 | { |
| 129 | // which happens because geo::linestring ≡ boost::geometry::model::linestring<geo::point> is part | 170 | // Cannot use structured bindings here, as boost::geometry::get |
| 130 | // of the whole tuple type (lse_index_value) that is the value_type of the iterator. | 171 | // interferes with ADL. It is a candidate as the namespace |
| 131 | std::shared_ptr<geo::linestring> const& ls = std::get<1>(*it); | 172 | // boost::geometry is part of the associated namespace set, which |
| 132 | std::shared_ptr<datex2::road_closure> const& rc = std::get<2>(*it); | 173 | // happens because geo::linestring ≡ |
| 133 | if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) | 174 | // boost::geometry::model::linestring<geo::point> is part of the |
| 134 | continue; | 175 | // whole tuple type (lse_index_value) that is the value_type of the |
| 135 | if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) | 176 | // iterator. |
| 136 | relevant_road_closures.emplace(rc); | 177 | std::shared_ptr<geo::linestring> const& ls = std::get<1>(*it); |
| 137 | ls_checked++; | 178 | std::shared_ptr<datex2::road_closure> const& rc = std::get<2>(*it); |
| 138 | } | 179 | if (rc->validity |
| 139 | for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) { | 180 | && rc->validity->intersect(check_periods).periods().empty()) |
| 140 | // Cannot use structured bindings here for the same reason as above. | 181 | continue; |
| 141 | geo::point const& p = std::get<0>(*it); | 182 | if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) |
| 142 | std::shared_ptr<datex2::road_closure> const& rc = std::get<1>(*it); | 183 | relevant_road_closures.emplace(rc); |
| 143 | if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) | 184 | ls_checked++; |
| 144 | continue; | 185 | } |
| 145 | if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) | 186 | for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); |
| 146 | relevant_road_closures.emplace(rc); | 187 | it != p_index_.qend(); it++) |
| 147 | p_checked++; | 188 | { |
| 148 | } | 189 | // Cannot use structured bindings here for the same reason as above. |
| 190 | geo::point const& p = std::get<0>(*it); | ||
| 191 | std::shared_ptr<datex2::road_closure> const& rc = std::get<1>(*it); | ||
| 192 | if (rc->validity | ||
| 193 | && rc->validity->intersect(check_periods).periods().empty()) | ||
| 194 | continue; | ||
| 195 | if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) | ||
| 196 | relevant_road_closures.emplace(rc); | ||
| 197 | p_checked++; | ||
| 149 | } | 198 | } |
| 199 | } | ||
| 150 | 200 | ||
| 151 | auto const after_query = chrono::steady_clock::now(); | 201 | auto const after_query = chrono::steady_clock::now(); |
| 152 | l_.debug("Done (checked {} line string(s) and {} point(s)) in {}", | 202 | l_.debug( |
| 153 | ls_checked, p_checked, chrono::duration_cast<chrono::milliseconds>(after_query - before_query)); | 203 | "Done (checked {} line string(s) and {} point(s)) in {}", ls_checked, |
| 204 | p_checked, | ||
| 205 | chrono::duration_cast<chrono::milliseconds>(after_query - before_query)); | ||
| 154 | 206 | ||
| 155 | auto relevant_situations = std::unordered_set<std::shared_ptr<datex2::situation>>{}; | 207 | auto relevant_situations = |
| 156 | for (auto const& rc : relevant_road_closures) | 208 | std::unordered_set<std::shared_ptr<datex2::situation>>{}; |
| 157 | relevant_situations.emplace(rc->parent); | 209 | for (auto const& rc : relevant_road_closures) |
| 210 | relevant_situations.emplace(rc->parent); | ||
| 158 | 211 | ||
| 159 | l_.debug("Identified {} relevant road closure(s), part of {} unique situation(s)", | 212 | l_.debug( |
| 160 | relevant_road_closures.size(), relevant_situations.size()); | 213 | "Identified {} relevant road closure(s), part of {} unique " |
| 161 | for (auto const& sit : relevant_situations) | 214 | "situation(s)", |
| 162 | l_.debug("Relevant situation: {}", sit->id); | 215 | relevant_road_closures.size(), relevant_situations.size()); |
| 216 | for (auto const& sit : relevant_situations) | ||
| 217 | l_.debug("Relevant situation: {}", sit->id); | ||
| 163 | 218 | ||
| 164 | return process_gpx_result{ | 219 | return process_gpx_result{ |
| 165 | .tracks = gpx_file.tracks | 220 | .tracks = gpx_file.tracks |
| 166 | | views::transform([](auto const& trk) -> track { | 221 | | views::transform( |
| 167 | return { | 222 | [](auto const& trk) -> track |
| 168 | .segments = trk.segments | 223 | { |
| 169 | | views::transform([](auto const& seg) -> track_segment { | 224 | return { |
| 170 | return {.points = seg.waypoints}; | 225 | .segments = |
| 171 | }) | 226 | trk.segments |
| 172 | | std::ranges::to<std::vector<track_segment>>(), | 227 | | views::transform( |
| 173 | }; | 228 | [](auto const& seg) -> track_segment |
| 174 | }) | 229 | { return {.points = seg.waypoints}; }) |
| 175 | | std::ranges::to<std::vector<track>>(), | 230 | | std::ranges::to<std::vector<track_segment>>(), |
| 176 | .relevant_situations = relevant_situations | 231 | }; |
| 177 | | views::transform([&](std::shared_ptr<datex2::situation> sit) -> relevant_situation { | 232 | }) |
| 178 | return { | 233 | | std::ranges::to<std::vector<track>>(), |
| 179 | .id = sit->id, | 234 | .relevant_situations = |
| 180 | .location = sit->location, | 235 | relevant_situations |
| 181 | .comments = sit->comments, | 236 | | views::transform( |
| 182 | .relevant_road_closures = relevant_road_closures | 237 | [&](std::shared_ptr<datex2::situation> sit) -> relevant_situation |
| 183 | | views::filter([&](std::shared_ptr<datex2::road_closure> const& rc) -> bool { | 238 | { |
| 184 | return std::shared_ptr{rc->parent} == sit; | 239 | return { |
| 185 | }) | 240 | .id = sit->id, |
| 186 | | views::transform([](std::shared_ptr<datex2::road_closure> const& rc) -> relevant_road_closure { | 241 | .location = sit->location, |
| 187 | return { | 242 | .comments = sit->comments, |
| 188 | .relevant_lss = rc->relevant_line_strings | 243 | .relevant_road_closures = |
| 189 | | views::transform([](auto const& lsp) -> geo::linestring { | 244 | relevant_road_closures |
| 190 | return *lsp; | 245 | | views::filter( |
| 246 | [&](std::shared_ptr<datex2::road_closure> const& rc) | ||
| 247 | -> bool | ||
| 248 | { return std::shared_ptr{rc->parent} == sit; }) | ||
| 249 | | views::transform( | ||
| 250 | [](std::shared_ptr<datex2::road_closure> const& rc) | ||
| 251 | -> relevant_road_closure | ||
| 252 | { | ||
| 253 | return { | ||
| 254 | .relevant_lss = | ||
| 255 | rc->relevant_line_strings | ||
| 256 | | views::transform( | ||
| 257 | [](auto const& lsp) -> geo::linestring | ||
| 258 | { return *lsp; }) | ||
| 259 | | std::ranges::to< | ||
| 260 | std::vector<geo::linestring>>(), | ||
| 261 | }; | ||
| 262 | }) | ||
| 263 | | std::ranges::to<std::vector<relevant_road_closure>>(), | ||
| 264 | }; | ||
| 191 | }) | 265 | }) |
| 192 | | std::ranges::to<std::vector<geo::linestring>>(), | 266 | | std::ranges::to<std::vector<relevant_situation>>(), |
| 193 | }; | 267 | }; |
| 194 | }) | 268 | } |
| 195 | | std::ranges::to<std::vector<relevant_road_closure>>(), | ||
| 196 | }; | ||
| 197 | }) | ||
| 198 | | std::ranges::to<std::vector<relevant_situation>>(), | ||
| 199 | }; | ||
| 200 | } | ||
| 201 | 269 | ||
| 202 | auto handler::sysinfo() -> struct sysinfo { | 270 | auto handler::sysinfo() -> struct sysinfo |
| 203 | return {.using_publication_of = pub_.publication_time}; | 271 | { |
| 204 | } | 272 | return {.using_publication_of = pub_.publication_time}; |
| 273 | } | ||
| 205 | 274 | ||
| 206 | } // namespace routemon::api | 275 | } // namespace routemon::api |
diff --git a/server/src/api.cppm b/server/src/api.cppm index caeb44c..66e8f45 100644 --- a/server/src/api.cppm +++ b/server/src/api.cppm | |||
| @@ -15,65 +15,85 @@ import :trace; | |||
| 15 | 15 | ||
| 16 | namespace { | 16 | namespace { |
| 17 | 17 | ||
| 18 | namespace chrono = std::chrono; | 18 | namespace chrono = std::chrono; |
| 19 | namespace json = boost::json; | 19 | namespace json = boost::json; |
| 20 | namespace views = std::views; | 20 | namespace views = std::views; |
| 21 | 21 | ||
| 22 | } // namespace <anonymous> | 22 | } // namespace |
| 23 | 23 | ||
| 24 | export | 24 | export namespace routemon::api { |
| 25 | namespace routemon::api { | ||
| 26 | 25 | ||
| 27 | struct relevant_road_closure { | 26 | struct relevant_road_closure |
| 28 | std::vector<geo::linestring> relevant_lss; | 27 | { |
| 29 | }; | 28 | std::vector<geo::linestring> relevant_lss; |
| 30 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void; | 29 | }; |
| 31 | 30 | ||
| 32 | struct relevant_situation { | 31 | struct relevant_situation |
| 33 | std::string id; | 32 | { |
| 34 | std::optional<geo::point> location; | 33 | std::string id; |
| 35 | std::vector<std::string> comments; | 34 | std::optional<geo::point> location; |
| 36 | std::vector<relevant_road_closure> relevant_road_closures; | 35 | std::vector<std::string> comments; |
| 37 | }; | 36 | std::vector<relevant_road_closure> relevant_road_closures; |
| 38 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void; | 37 | }; |
| 39 | 38 | ||
| 40 | struct track_segment { | 39 | struct track_segment |
| 41 | geo::linestring points; | 40 | { |
| 42 | }; | 41 | geo::linestring points; |
| 43 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void; | 42 | }; |
| 44 | 43 | ||
| 45 | struct track { | 44 | struct track |
| 46 | std::vector<track_segment> segments; | 45 | { |
| 47 | }; | 46 | std::vector<track_segment> segments; |
| 48 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void; | 47 | }; |
| 49 | 48 | ||
| 50 | struct process_gpx_result { | 49 | struct process_gpx_result |
| 51 | std::vector<track> tracks; | 50 | { |
| 52 | std::vector<relevant_situation> relevant_situations; | 51 | std::vector<track> tracks; |
| 53 | }; | 52 | std::vector<relevant_situation> relevant_situations; |
| 54 | auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void; | 53 | }; |
| 55 | 54 | ||
| 56 | struct sysinfo { | 55 | struct sysinfo |
| 57 | time::timestamp using_publication_of; | 56 | { |
| 58 | }; | 57 | time::timestamp using_publication_of; |
| 59 | auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void; | 58 | }; |
| 60 | 59 | ||
| 61 | class handler { | 60 | auto tag_invoke( |
| 62 | using lse_index_value = std::tuple<geo::box, std::shared_ptr<geo::linestring>, std::shared_ptr<datex2::road_closure>>; | 61 | json::value_from_tag, json::value& jv, relevant_road_closure const& clo) |
| 63 | using p_index_value = std::pair<geo::point, std::shared_ptr<datex2::road_closure>>; | 62 | -> void; |
| 64 | using lse_index = bgeo::index::rtree<lse_index_value, bgeo::index::quadratic<16>>; | 63 | auto tag_invoke( |
| 65 | using p_index = bgeo::index::rtree<p_index_value, bgeo::index::quadratic<16>>; | 64 | json::value_from_tag, json::value& jv, relevant_situation const& sit) |
| 65 | -> void; | ||
| 66 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) | ||
| 67 | -> void; | ||
| 68 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) | ||
| 69 | -> void; | ||
| 70 | auto tag_invoke( | ||
| 71 | json::value_from_tag, json::value& jv, process_gpx_result const& res) | ||
| 72 | -> void; | ||
| 73 | auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) | ||
| 74 | -> void; | ||
| 66 | 75 | ||
| 67 | log::logger l_; | 76 | class handler |
| 68 | datex2::situation_publication pub_; | 77 | { |
| 69 | lse_index lse_index_; | 78 | using lse_index_value = std::tuple< |
| 70 | p_index p_index_; | 79 | geo::box, std::shared_ptr<geo::linestring>, |
| 80 | std::shared_ptr<datex2::road_closure>>; | ||
| 81 | using p_index_value = | ||
| 82 | std::pair<geo::point, std::shared_ptr<datex2::road_closure>>; | ||
| 83 | using lse_index = | ||
| 84 | bgeo::index::rtree<lse_index_value, bgeo::index::quadratic<16>>; | ||
| 85 | using p_index = bgeo::index::rtree<p_index_value, bgeo::index::quadratic<16>>; | ||
| 71 | 86 | ||
| 72 | public: | 87 | log::logger l_; |
| 73 | explicit handler(log::logger const& l, datex2::situation_publication pub); | 88 | datex2::situation_publication pub_; |
| 89 | lse_index lse_index_; | ||
| 90 | p_index p_index_; | ||
| 74 | 91 | ||
| 75 | auto process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result>; | 92 | public: |
| 76 | auto sysinfo() -> sysinfo; | 93 | explicit handler(log::logger const& l, datex2::situation_publication pub); |
| 77 | }; | 94 | |
| 95 | auto process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result>; | ||
| 96 | auto sysinfo() -> sysinfo; | ||
| 97 | }; | ||
| 78 | 98 | ||
| 79 | } // namespace routemon::api | 99 | } // namespace routemon::api |
diff --git a/server/src/config.cpp b/server/src/config.cpp index 3b17691..84c436a 100644 --- a/server/src/config.cpp +++ b/server/src/config.cpp | |||
| @@ -14,154 +14,225 @@ namespace json = boost::json; | |||
| 14 | 14 | ||
| 15 | namespace routemon::config { | 15 | namespace routemon::config { |
| 16 | 16 | ||
| 17 | class location { | 17 | class location |
| 18 | std::optional<std::pair<std::string_view, util::not_null<location const*>>> next_; | 18 | { |
| 19 | std::optional<std::pair<std::string_view, util::not_null<location const*>>> | ||
| 20 | next_; | ||
| 19 | 21 | ||
| 20 | auto append_to(std::string& s) const -> void { | 22 | auto append_to(std::string& s) const -> void |
| 21 | if (next_) { | 23 | { |
| 22 | s += next_->first; | 24 | if (next_) |
| 23 | s += "."; | 25 | { |
| 24 | next_->second->append_to(s); | 26 | s += next_->first; |
| 25 | } | 27 | s += "."; |
| 28 | next_->second->append_to(s); | ||
| 26 | } | 29 | } |
| 30 | } | ||
| 27 | 31 | ||
| 28 | public: | 32 | public: |
| 29 | location() = default; | 33 | location() = default; |
| 30 | 34 | ||
| 31 | explicit location(location const& next, std::string_view entry) | 35 | explicit location(location const& next, std::string_view entry) |
| 32 | : next_{std::make_pair(entry, util::not_null{&next})} | 36 | : next_{std::make_pair(entry, util::not_null{&next})} |
| 33 | {} | 37 | { |
| 38 | } | ||
| 34 | 39 | ||
| 35 | [[nodiscard]] auto to_string() const -> std::string { | 40 | [[nodiscard]] auto to_string() const -> std::string |
| 36 | if (!next_) | 41 | { |
| 37 | return ""; | 42 | if (!next_) |
| 38 | auto s = std::string{next_->first}; | 43 | return ""; |
| 39 | next_->second->append_to(s); | 44 | auto s = std::string{next_->first}; |
| 40 | return s; | 45 | next_->second->append_to(s); |
| 41 | } | 46 | return s; |
| 47 | } | ||
| 42 | 48 | ||
| 43 | auto sub(std::string_view entry) const& -> location { | 49 | auto sub(std::string_view entry) const& -> location |
| 44 | return location{*this, entry}; | 50 | { |
| 45 | } | 51 | return location{*this, entry}; |
| 46 | }; | 52 | } |
| 53 | }; | ||
| 47 | 54 | ||
| 48 | class object_reader { | 55 | class object_reader |
| 49 | location loc_; | 56 | { |
| 50 | json::object const& obj_; | 57 | location loc_; |
| 51 | std::unordered_set<std::string> visited_; | 58 | json::object const& obj_; |
| 59 | std::unordered_set<std::string> visited_; | ||
| 52 | 60 | ||
| 53 | public: | 61 | public: |
| 54 | object_reader(json::value const& jv, location loc) | 62 | object_reader(json::value const& jv, location loc) |
| 55 | try : loc_{std::move(loc)}, obj_{jv.as_object()} | 63 | try : loc_{std::move(loc)}, obj_{jv.as_object()} {} |
| 56 | {} catch (boost::system::system_error const&) { | 64 | catch (boost::system::system_error const&) |
| 57 | throw std::runtime_error{std::format("expected an object at {}", loc.to_string())}; | 65 | { |
| 58 | } | 66 | throw std::runtime_error{std::format( |
| 67 | "expected an object at {}", loc.to_string())}; | ||
| 68 | } | ||
| 59 | 69 | ||
| 60 | auto check_unused() const -> void { | 70 | auto check_unused() const -> void |
| 61 | for (auto const& kv : obj_) { | 71 | { |
| 62 | if (!visited_.contains(std::string{kv.key()})) { | 72 | for (auto const& kv : obj_) |
| 63 | throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())}; | 73 | { |
| 64 | } | 74 | if (!visited_.contains(std::string{kv.key()})) |
| 75 | { | ||
| 76 | throw std::runtime_error{std::format( | ||
| 77 | "unexpected key {}", loc_.sub(kv.key()).to_string())}; | ||
| 65 | } | 78 | } |
| 66 | } | 79 | } |
| 80 | } | ||
| 67 | 81 | ||
| 68 | template<class T> | 82 | template <class T> |
| 69 | auto expect_at(std::string_view key) -> T { | 83 | auto expect_at(std::string_view key) -> T |
| 70 | visited_.emplace(key); | 84 | { |
| 71 | auto const loc = loc_.sub(key); | 85 | visited_.emplace(key); |
| 72 | if (auto const jv = obj_.try_at(key)) { | 86 | auto const loc = loc_.sub(key); |
| 73 | try { | 87 | if (auto const jv = obj_.try_at(key)) |
| 74 | return json::value_to<T>(*jv, loc); | 88 | { |
| 75 | } catch (boost::system::system_error const& e) { | 89 | try |
| 76 | throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())}; | 90 | { |
| 77 | } | 91 | return json::value_to<T>(*jv, loc); |
| 78 | } else { | 92 | } |
| 79 | throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())}; | 93 | catch (boost::system::system_error const& e) |
| 94 | { | ||
| 95 | throw std::runtime_error{std::format( | ||
| 96 | "failed to read {}: {}", loc.to_string(), e.code().message())}; | ||
| 80 | } | 97 | } |
| 81 | } | 98 | } |
| 82 | }; | 99 | else |
| 83 | 100 | { | |
| 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&>())) { | 101 | throw std::runtime_error{std::format( |
| 85 | auto r = object_reader{jv, loc}; | 102 | "did not find expected key {}", loc.to_string())}; |
| 86 | auto&& v = f(r); | 103 | } |
| 87 | r.check_unused(); | ||
| 88 | return std::forward<decltype(f(r))>(v); | ||
| 89 | } | 104 | } |
| 105 | }; | ||
| 90 | 106 | ||
| 91 | auto tag_invoke(json::value_to_tag<rwgps> const&, json::value const& jv, location const& loc) -> rwgps { | 107 | auto as_checked_object( |
| 92 | return as_checked_object(jv, loc, [](object_reader& r) -> rwgps { | 108 | json::value const& jv, location const& loc, |
| 93 | return { | 109 | std::invocable<object_reader&> auto const& f) |
| 94 | .api_key = r.expect_at<std::string>("api_key"), | 110 | -> decltype(f(std::declval<object_reader&>())) |
| 95 | .auth_token = r.expect_at<std::string>("auth_token"), | 111 | { |
| 96 | }; | 112 | auto r = object_reader{jv, loc}; |
| 97 | }); | 113 | auto&& v = f(r); |
| 98 | } | 114 | r.check_unused(); |
| 115 | return std::forward<decltype(f(r))>(v); | ||
| 116 | } | ||
| 99 | 117 | ||
| 100 | auto tag_invoke(json::value_to_tag<situations> const&, json::value const& jv, location const& loc) -> situations { | 118 | auto tag_invoke( |
| 101 | return as_checked_object(jv, loc, [](object_reader& r) -> situations { | 119 | json::value_to_tag<rwgps> const&, json::value const& jv, |
| 102 | return { | 120 | location const& loc) -> rwgps |
| 103 | .datex2_filename = r.expect_at<std::string>("datex2_filename"), | 121 | { |
| 104 | }; | 122 | return as_checked_object( |
| 105 | }); | 123 | jv, loc, |
| 106 | } | 124 | [](object_reader& r) -> rwgps |
| 125 | { | ||
| 126 | return { | ||
| 127 | .api_key = r.expect_at<std::string>("api_key"), | ||
| 128 | .auth_token = r.expect_at<std::string>("auth_token"), | ||
| 129 | }; | ||
| 130 | }); | ||
| 131 | } | ||
| 107 | 132 | ||
| 108 | auto tag_invoke(json::value_to_tag<database> const&, json::value const& jv, location const& loc) -> database { | 133 | auto tag_invoke( |
| 109 | return as_checked_object(jv, loc, [](object_reader& r) -> database { | 134 | json::value_to_tag<situations> const&, json::value const& jv, |
| 110 | return { | 135 | location const& loc) -> situations |
| 111 | .sqlite3_filename = r.expect_at<std::string>("sqlite3_filename"), | 136 | { |
| 112 | }; | 137 | return as_checked_object( |
| 113 | }); | 138 | jv, loc, |
| 114 | } | 139 | [](object_reader& r) -> situations |
| 140 | { | ||
| 141 | return { | ||
| 142 | .datex2_filename = r.expect_at<std::string>("datex2_filename"), | ||
| 143 | }; | ||
| 144 | }); | ||
| 145 | } | ||
| 115 | 146 | ||
| 116 | auto tag_invoke(json::value_to_tag<http_server> const&, json::value const& jv, location const& loc) -> http_server { | 147 | auto tag_invoke( |
| 117 | return as_checked_object(jv, loc, [](object_reader& r) -> http_server { | 148 | json::value_to_tag<database> const&, json::value const& jv, |
| 118 | return { | 149 | location const& loc) -> database |
| 119 | .lax_cors = r.expect_at<bool>("lax_cors"), | 150 | { |
| 120 | }; | 151 | return as_checked_object( |
| 121 | }); | 152 | jv, loc, |
| 122 | } | 153 | [](object_reader& r) -> database |
| 154 | { | ||
| 155 | return { | ||
| 156 | .sqlite3_filename = r.expect_at<std::string>("sqlite3_filename"), | ||
| 157 | }; | ||
| 158 | }); | ||
| 159 | } | ||
| 123 | 160 | ||
| 124 | auto tag_invoke(json::value_to_tag<logger> const&, json::value const& jv, location const& loc) -> logger { | 161 | auto tag_invoke( |
| 125 | return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger { | 162 | json::value_to_tag<http_server> const&, json::value const& jv, |
| 126 | auto const level_str = r.expect_at<std::string>("level"); | 163 | location const& loc) -> http_server |
| 127 | auto level = log::level{}; | 164 | { |
| 128 | if (level_str == "debug") | 165 | return as_checked_object( |
| 129 | level = log::level::debug; | 166 | jv, loc, |
| 130 | else if (level_str == "info") | 167 | [](object_reader& r) -> http_server |
| 131 | level = log::level::info; | 168 | { |
| 132 | else if (level_str == "warn") | 169 | return { |
| 133 | level = log::level::warn; | 170 | .lax_cors = r.expect_at<bool>("lax_cors"), |
| 134 | else if (level_str == "error") | 171 | }; |
| 135 | level = log::level::error; | 172 | }); |
| 136 | else | 173 | } |
| 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 | 174 | ||
| 142 | auto json_value_to_app(json::value const& jv) -> app { | 175 | auto tag_invoke( |
| 143 | return as_checked_object(jv, location{}, [](object_reader& r) -> app { | 176 | json::value_to_tag<logger> const&, json::value const& jv, |
| 144 | return { | 177 | location const& loc) -> logger |
| 145 | .rwgps = r.expect_at<rwgps>("rwgps"), | 178 | { |
| 146 | .situations = r.expect_at<situations>("situations"), | 179 | return as_checked_object( |
| 147 | .database = r.expect_at<database>("database"), | 180 | jv, loc, |
| 148 | .http_server = r.expect_at<http_server>("http_server"), | 181 | [obj_loc = loc](object_reader& r) -> logger |
| 149 | .logger = r.expect_at<logger>("logger"), | 182 | { |
| 150 | }; | 183 | auto const level_str = r.expect_at<std::string>("level"); |
| 151 | }); | 184 | auto level = log::level{}; |
| 152 | } | 185 | if (level_str == "debug") |
| 186 | level = log::level::debug; | ||
| 187 | else if (level_str == "info") | ||
| 188 | level = log::level::info; | ||
| 189 | else if (level_str == "warn") | ||
| 190 | level = log::level::warn; | ||
| 191 | else if (level_str == "error") | ||
| 192 | level = log::level::error; | ||
| 193 | else | ||
| 194 | throw std::runtime_error{std::format( | ||
| 195 | "unable to parse log level {:?} at {}: expected one " | ||
| 196 | "of {{debug, info, warn, error}}", | ||
| 197 | level_str, obj_loc.sub("level").to_string())}; | ||
| 198 | return logger{level}; | ||
| 199 | }); | ||
| 200 | } | ||
| 153 | 201 | ||
| 154 | auto load_file(std::string const& filename) -> app { | 202 | auto json_value_to_app(json::value const& jv) -> app |
| 155 | auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode? | 203 | { |
| 156 | if (!f.is_open()) | 204 | return as_checked_object( |
| 157 | throw std::runtime_error{std::format("failed to open {}", filename)}; | 205 | jv, location{}, |
| 158 | auto jv = json::value{}; | 206 | [](object_reader& r) -> app |
| 159 | try { | 207 | { |
| 160 | jv = json::parse(f); | 208 | return { |
| 161 | } catch (boost::system::system_error const& e) { | 209 | .rwgps = r.expect_at<rwgps>("rwgps"), |
| 162 | throw std::runtime_error{std::format("failed to parse: {}", e.code().message())}; | 210 | .situations = r.expect_at<situations>("situations"), |
| 163 | } | 211 | .database = r.expect_at<database>("database"), |
| 164 | return json_value_to_app(jv); | 212 | .http_server = r.expect_at<http_server>("http_server"), |
| 213 | .logger = r.expect_at<logger>("logger"), | ||
| 214 | }; | ||
| 215 | }); | ||
| 216 | } | ||
| 217 | |||
| 218 | auto load_file(std::string const& filename) -> app | ||
| 219 | { | ||
| 220 | auto f = std::ifstream{ | ||
| 221 | filename | ||
| 222 | }; // TODO: ensure that we are opening in binary mode? | ||
| 223 | if (!f.is_open()) | ||
| 224 | throw std::runtime_error{std::format("failed to open {}", filename)}; | ||
| 225 | auto jv = json::value{}; | ||
| 226 | try | ||
| 227 | { | ||
| 228 | jv = json::parse(f); | ||
| 229 | } | ||
| 230 | catch (boost::system::system_error const& e) | ||
| 231 | { | ||
| 232 | throw std::runtime_error{std::format( | ||
| 233 | "failed to parse: {}", e.code().message())}; | ||
| 165 | } | 234 | } |
| 235 | return json_value_to_app(jv); | ||
| 236 | } | ||
| 166 | 237 | ||
| 167 | } // namespace routemon::config | 238 | } // namespace routemon::config |
diff --git a/server/src/config.cppm b/server/src/config.cppm index ef827a6..e5b48f4 100644 --- a/server/src/config.cppm +++ b/server/src/config.cppm | |||
| @@ -5,35 +5,41 @@ import :log; | |||
| 5 | 5 | ||
| 6 | namespace routemon::config { | 6 | namespace routemon::config { |
| 7 | 7 | ||
| 8 | export struct rwgps { | 8 | export struct rwgps |
| 9 | std::string api_key; | 9 | { |
| 10 | std::string auth_token; | 10 | std::string api_key; |
| 11 | }; | 11 | std::string auth_token; |
| 12 | }; | ||
| 12 | 13 | ||
| 13 | export struct situations { | 14 | export struct situations |
| 14 | std::string datex2_filename; | 15 | { |
| 15 | }; | 16 | std::string datex2_filename; |
| 17 | }; | ||
| 16 | 18 | ||
| 17 | export struct database { | 19 | export struct database |
| 18 | std::string sqlite3_filename; | 20 | { |
| 19 | }; | 21 | std::string sqlite3_filename; |
| 22 | }; | ||
| 20 | 23 | ||
| 21 | export struct http_server { | 24 | export struct http_server |
| 22 | bool lax_cors; | 25 | { |
| 23 | }; | 26 | bool lax_cors; |
| 27 | }; | ||
| 24 | 28 | ||
| 25 | export struct logger { | 29 | export struct logger |
| 26 | log::level level; | 30 | { |
| 27 | }; | 31 | log::level level; |
| 32 | }; | ||
| 28 | 33 | ||
| 29 | export struct app { | 34 | export struct app |
| 30 | rwgps rwgps; | 35 | { |
| 31 | situations situations; | 36 | rwgps rwgps; |
| 32 | database database; | 37 | situations situations; |
| 33 | http_server http_server; | 38 | database database; |
| 34 | logger logger; | 39 | http_server http_server; |
| 35 | }; | 40 | logger logger; |
| 41 | }; | ||
| 36 | 42 | ||
| 37 | export auto load_file(std::string const& filename) -> app; | 43 | export auto load_file(std::string const& filename) -> app; |
| 38 | 44 | ||
| 39 | } // namespace routemon::config | 45 | } // namespace routemon::config |
diff --git a/server/src/database.cppm b/server/src/database.cppm index 0312dda..86ed0d7 100644 --- a/server/src/database.cppm +++ b/server/src/database.cppm | |||
| @@ -5,33 +5,43 @@ import :sqlite3; | |||
| 5 | 5 | ||
| 6 | namespace routemon::database { | 6 | namespace routemon::database { |
| 7 | 7 | ||
| 8 | static constexpr std::int64_t expected_database_version = 1; | 8 | static constexpr std::int64_t expected_database_version = 1; |
| 9 | 9 | ||
| 10 | export class connection { | 10 | export class connection |
| 11 | sqlite3::connection dbc_; | 11 | { |
| 12 | sqlite3::connection dbc_; | ||
| 12 | 13 | ||
| 13 | explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {} | 14 | explicit connection(sqlite3::connection dbc) : dbc_{std::move(dbc)} {} |
| 14 | 15 | ||
| 15 | friend auto open(std::string const& filename) -> std::shared_ptr<connection>; | 16 | friend auto open(std::string const& filename) -> std::shared_ptr<connection>; |
| 16 | 17 | ||
| 17 | public: | 18 | public: |
| 18 | // Nothing here yet | 19 | // Nothing here yet |
| 19 | }; | 20 | }; |
| 20 | 21 | ||
| 21 | export auto open(std::string const& filename) -> std::shared_ptr<connection> { | 22 | export auto open(std::string const& filename) -> std::shared_ptr<connection> |
| 22 | auto dbc = sqlite3::open(filename); | 23 | { |
| 23 | try { | 24 | auto dbc = sqlite3::open(filename); |
| 24 | auto version = std::optional<std::int64_t>{}; | 25 | try |
| 25 | dbc.query("SELECT version FROM migration;").scan_single(version); | 26 | { |
| 26 | if (!version) | 27 | auto version = std::optional<std::int64_t>{}; |
| 27 | throw std::runtime_error{"failed to fetch database migration version"}; | 28 | dbc.query("SELECT version FROM migration;").scan_single(version); |
| 28 | if (version != expected_database_version) { | 29 | if (!version) |
| 29 | throw std::runtime_error{std::format("database migration version ({}) does not match expected version ({}), consider running migrations", *version, expected_database_version)}; | 30 | throw std::runtime_error{"failed to fetch database migration version"}; |
| 30 | } | 31 | if (version != expected_database_version) |
| 31 | } catch (std::exception const& e) { | 32 | { |
| 32 | throw std::runtime_error{std::format("failed to query database version: {}", e.what())}; | 33 | throw std::runtime_error{std::format( |
| 34 | "database migration version ({}) does not match expected " | ||
| 35 | "version ({}), consider running migrations", | ||
| 36 | *version, expected_database_version)}; | ||
| 33 | } | 37 | } |
| 34 | return std::shared_ptr<connection>{new connection{std::move(dbc)}}; | ||
| 35 | } | 38 | } |
| 39 | catch (std::exception const& e) | ||
| 40 | { | ||
| 41 | throw std::runtime_error{std::format( | ||
| 42 | "failed to query database version: {}", e.what())}; | ||
| 43 | } | ||
| 44 | return std::shared_ptr<connection>{new connection{std::move(dbc)}}; | ||
| 45 | } | ||
| 36 | 46 | ||
| 37 | } // namespace routemon::database | 47 | } // namespace routemon::database |
diff --git a/server/src/datex2.cppm b/server/src/datex2.cppm index b507e6f..b4c795f 100644 --- a/server/src/datex2.cppm +++ b/server/src/datex2.cppm | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | module; | 1 | module; |
| 2 | 2 | ||
| 3 | #include <boost/geometry/algorithms/is_empty.hpp> | 3 | #include <boost/geometry/algorithms/is_empty.hpp> |
| 4 | #include <boost/geometry/srs/transformation.hpp> | ||
| 5 | #include <boost/geometry/srs/epsg.hpp> | 4 | #include <boost/geometry/srs/epsg.hpp> |
| 5 | #include <boost/geometry/srs/transformation.hpp> | ||
| 6 | 6 | ||
| 7 | #include <pugixml.hpp> | 7 | #include <pugixml.hpp> |
| 8 | 8 | ||
| @@ -17,205 +17,293 @@ using namespace std::literals::string_view_literals; | |||
| 17 | 17 | ||
| 18 | namespace routemon::datex2 { | 18 | namespace routemon::datex2 { |
| 19 | 19 | ||
| 20 | export struct situation; | 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 | 21 | ||
| 36 | export struct situation_publication { | 22 | export struct road_closure |
| 37 | time::timestamp publication_time; | 23 | { |
| 38 | std::vector<std::shared_ptr<situation>> situations; | 24 | std::weak_ptr<situation> parent; |
| 39 | }; | 25 | std::optional<time::period_seq> validity; |
| 26 | std::vector<geo::point> relevant_points = {}; | ||
| 27 | std::vector<std::shared_ptr<geo::linestring>> relevant_line_strings = {}; | ||
| 28 | }; | ||
| 40 | 29 | ||
| 41 | auto parse_timestamp(char const* in) -> std::optional<time::timestamp> { | 30 | export struct situation |
| 42 | auto res = time::timestamp{}; | 31 | { |
| 43 | auto is = std::istringstream{in}; | 32 | std::string id; |
| 44 | is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); | 33 | std::optional<geo::point> location = |
| 45 | return is.fail() ? std::nullopt : std::make_optional(res); | 34 | std::nullopt; // as shown on the map, not used for querying |
| 46 | } | 35 | std::vector<std::string> comments = {}; |
| 36 | std::vector<std::shared_ptr<road_closure>> road_closures = {}; | ||
| 37 | }; | ||
| 47 | 38 | ||
| 48 | export class loader { | 39 | export struct situation_publication |
| 49 | // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) | 40 | { |
| 50 | bgeo::srs::transformation<bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> etrs89_to_wgs84_{}; | 41 | time::timestamp publication_time; |
| 42 | std::vector<std::shared_ptr<situation>> situations; | ||
| 43 | }; | ||
| 51 | 44 | ||
| 52 | std::multiset<std::string> warnings_; | 45 | auto parse_timestamp(char const* in) -> std::optional<time::timestamp> |
| 46 | { | ||
| 47 | auto res = time::timestamp{}; | ||
| 48 | auto is = std::istringstream{in}; | ||
| 49 | is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res); | ||
| 50 | return is.fail() ? std::nullopt : std::make_optional(res); | ||
| 51 | } | ||
| 53 | 52 | ||
| 54 | auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) -> void { | 53 | export class loader |
| 55 | auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; | 54 | { |
| 56 | if (loc_xml_type == "loc:ItineraryByIndexedLocations") { | 55 | // ETRS 89 (EPSG:4258) -> WGS 84 (EPSG:4326) |
| 57 | for (auto const loc_cont_xml : loc_xml.children("loc:locationContainedInItinerary")) { | 56 | bgeo::srs::transformation< |
| 58 | add_location_from_xml(rc, loc_cont_xml.child("loc:location")); | 57 | bgeo::srs::static_epsg<4258>, bgeo::srs::static_epsg<4326>> |
| 59 | } | 58 | etrs89_to_wgs84_{}; |
| 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 | 59 | ||
| 65 | auto const srs_name = std::string_view{loc_gml_xml.attribute("srsName").value()}; | 60 | std::multiset<std::string> warnings_; |
| 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 | 61 | ||
| 73 | auto ls = std::make_shared<geo::linestring>(); | 62 | auto add_location_from_xml(road_closure& rc, pugi::xml_node const& loc_xml) |
| 63 | -> void | ||
| 64 | { | ||
| 65 | auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()}; | ||
| 66 | if (loc_xml_type == "loc:ItineraryByIndexedLocations") | ||
| 67 | { | ||
| 68 | for (auto const loc_cont_xml : | ||
| 69 | loc_xml.children("loc:locationContainedInItinerary")) | ||
| 70 | { | ||
| 71 | add_location_from_xml(rc, loc_cont_xml.child("loc:location")); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | else if (loc_xml_type == "loc:LinearLocation" | ||
| 75 | || loc_xml_type == "loc:SingleRoadLinearLocation") | ||
| 76 | { | ||
| 77 | auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString"); | ||
| 78 | if (!loc_gml_xml) | ||
| 79 | return; | ||
| 74 | 80 | ||
| 75 | auto lat_set = false; | 81 | auto const srs_name = |
| 76 | auto lat = 0.0; | 82 | std::string_view{loc_gml_xml.attribute("srsName").value()}; |
| 77 | for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) { | 83 | if (srs_name != "WGS 84"sv) |
| 78 | auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str}); | 84 | { |
| 79 | if (!mlat_or_long) { | 85 | warnings_.insert( |
| 80 | warnings_.insert(std::format("failed to parse coordinate {:?}", std::string_view{lat_or_long_str})); | 86 | std::format("don't now how to handle the CRS {}", srs_name)); |
| 81 | return; | 87 | return; |
| 82 | } | 88 | } |
| 89 | auto const pos_list_str = | ||
| 90 | std::string_view{loc_gml_xml.child_value("loc:posList")}; | ||
| 91 | // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn | ||
| 83 | 92 | ||
| 84 | if (!lat_set) { | 93 | auto ls = std::make_shared<geo::linestring>(); |
| 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 | ||
| 94 | if (bgeo::is_empty(*ls)) { | 95 | auto lat_set = false; |
| 95 | warnings_.emplace("empty line string in data set"); | 96 | auto lat = 0.0; |
| 97 | for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv)) | ||
| 98 | { | ||
| 99 | auto mlat_or_long = | ||
| 100 | util::parse_double(std::string_view{lat_or_long_str}); | ||
| 101 | if (!mlat_or_long) | ||
| 102 | { | ||
| 103 | warnings_.insert( | ||
| 104 | std::format( | ||
| 105 | "failed to parse coordinate {:?}", | ||
| 106 | std::string_view{lat_or_long_str})); | ||
| 96 | return; | 107 | return; |
| 97 | } | 108 | } |
| 98 | 109 | ||
| 99 | rc.relevant_line_strings.push_back(ls); | 110 | if (!lat_set) |
| 100 | } else if (loc_xml_type == "loc:PointLocation") { | 111 | { |
| 101 | auto const& coords_xml = loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); | 112 | lat = *mlat_or_long; |
| 102 | if (!coords_xml) | 113 | lat_set = true; |
| 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 | } | 114 | } |
| 115 | else | ||
| 116 | { | ||
| 117 | bgeo::append(*ls, geo::point{*mlat_or_long, lat}); | ||
| 118 | lat = 0; | ||
| 119 | lat_set = false; | ||
| 120 | } | ||
| 121 | } | ||
| 111 | 122 | ||
| 112 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | 123 | if (bgeo::is_empty(*ls)) |
| 113 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | 124 | { |
| 114 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | 125 | warnings_.emplace("empty line string in data set"); |
| 115 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | 126 | return; |
| 127 | } | ||
| 116 | 128 | ||
| 117 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | 129 | rc.relevant_line_strings.push_back(ls); |
| 118 | auto coords_wgs84 = geo::point{}; | 130 | } |
| 119 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | 131 | else if (loc_xml_type == "loc:PointLocation") |
| 132 | { | ||
| 133 | auto const& coords_xml = | ||
| 134 | loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates"); | ||
| 135 | if (!coords_xml) | ||
| 136 | return; | ||
| 120 | 137 | ||
| 121 | rc.relevant_points.push_back(coords_wgs84); | 138 | auto mlat = util::parse_double(coords_xml.child_value("loc:latitude")); |
| 122 | } else { | 139 | auto mlon = util::parse_double(coords_xml.child_value("loc:longitude")); |
| 123 | warnings_.insert(std::format("don't know how to hande location of type {}, ignoring", loc_xml.attribute("xsi:type").value())); | 140 | if (!mlat || !mlon) |
| 141 | { | ||
| 142 | warnings_.emplace("failed to parse PointLocation coordinates"); | ||
| 124 | return; | 143 | return; |
| 125 | } | 144 | } |
| 145 | |||
| 146 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | ||
| 147 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 148 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | ||
| 149 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 150 | |||
| 151 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | ||
| 152 | auto coords_wgs84 = geo::point{}; | ||
| 153 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | ||
| 154 | |||
| 155 | rc.relevant_points.push_back(coords_wgs84); | ||
| 156 | } | ||
| 157 | else | ||
| 158 | { | ||
| 159 | warnings_.insert( | ||
| 160 | std::format( | ||
| 161 | "don't know how to hande location of type {}, ignoring", | ||
| 162 | loc_xml.attribute("xsi:type").value())); | ||
| 163 | return; | ||
| 126 | } | 164 | } |
| 165 | } | ||
| 127 | 166 | ||
| 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>> { | 167 | auto handle_road_or_carriageway_or_lane_management( |
| 129 | auto const type = std::string_view{record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value()}; | 168 | pugi::xml_node const& record_xml, std::weak_ptr<situation> parent) |
| 130 | if (type != "carriagewayClosures" && type != "roadClosed") | 169 | -> std::optional<std::shared_ptr<road_closure>> |
| 131 | // TODO: checken of er nog andere types fietsers de doorgang zouden kunnen blokkeren? | 170 | { |
| 132 | return std::nullopt; | 171 | auto const type = |
| 172 | std::string_view{record_xml | ||
| 173 | .child("sit:roadOrCarriagewayOrLaneManagementType") | ||
| 174 | .child_value()}; | ||
| 175 | if (type != "carriagewayClosures" && type != "roadClosed") | ||
| 176 | // TODO: checken of er nog andere types fietsers de doorgang zouden | ||
| 177 | // kunnen blokkeren? | ||
| 178 | return std::nullopt; | ||
| 133 | 179 | ||
| 134 | auto const& restricted_vehicle_types_xml = record_xml.child("sit:forVehiclesWithCharacteristicsOf"); | 180 | auto const& restricted_vehicle_types_xml = |
| 135 | bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); | 181 | record_xml.child("sit:forVehiclesWithCharacteristicsOf"); |
| 136 | for (auto const vehicle_type_xml : restricted_vehicle_types_xml.children("com:vehicleType")) { | 182 | bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty(); |
| 137 | auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; | 183 | for (auto const vehicle_type_xml : |
| 138 | if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" || | 184 | restricted_vehicle_types_xml.children("com:vehicleType")) |
| 139 | vehicle_type == "unknown" || vehicle_type == "other") { | 185 | { |
| 140 | likely_restriction_for_bikes = true; | 186 | auto vehicle_type = std::string_view{vehicle_type_xml.child_value()}; |
| 141 | } | 187 | if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle" |
| 188 | || vehicle_type == "unknown" || vehicle_type == "other") | ||
| 189 | { | ||
| 190 | likely_restriction_for_bikes = true; | ||
| 142 | } | 191 | } |
| 143 | if (!likely_restriction_for_bikes) | 192 | } |
| 144 | return std::nullopt; | 193 | if (!likely_restriction_for_bikes) |
| 194 | return std::nullopt; | ||
| 145 | 195 | ||
| 146 | //---- Check if within the defined validity period | 196 | //---- Check if within the defined validity period |
| 147 | 197 | ||
| 148 | auto validity = std::optional<time::period_seq>{}; | 198 | auto validity = std::optional<time::period_seq>{}; |
| 149 | auto const& validity_xml = record_xml.child("sit:validity"); | 199 | auto const& validity_xml = record_xml.child("sit:validity"); |
| 150 | if (validity_xml && validity_xml.child_value("com:validityStatus") == "definedByValidityTimeSpec"sv) { | 200 | if (validity_xml |
| 151 | auto const& validity_spec_xml = validity_xml.child("com:validityTimeSpecification"); | 201 | && validity_xml.child_value("com:validityStatus") |
| 202 | == "definedByValidityTimeSpec"sv) | ||
| 203 | { | ||
| 204 | auto const& validity_spec_xml = | ||
| 205 | validity_xml.child("com:validityTimeSpecification"); | ||
| 152 | 206 | ||
| 153 | auto valid_periods = std::vector<time::period>{}; | 207 | auto valid_periods = std::vector<time::period>{}; |
| 154 | auto exception_periods = std::vector<time::period>{}; | 208 | auto exception_periods = std::vector<time::period>{}; |
| 155 | 209 | ||
| 156 | // TODO: com:overallEndTime may be missing (according to the DATEX II v3 data model) | 210 | // TODO: com:overallEndTime may be missing (according to the DATEX |
| 157 | auto const overall_start_time = parse_timestamp(validity_spec_xml.child_value("com:overallStartTime")); | 211 | // II v3 data model) |
| 158 | auto const overall_end_time = parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); | 212 | auto const overall_start_time = parse_timestamp( |
| 159 | if (overall_start_time && overall_end_time && *overall_start_time < *overall_end_time) { | 213 | validity_spec_xml.child_value("com:overallStartTime")); |
| 160 | valid_periods.emplace_back(*overall_start_time, *overall_end_time); | 214 | auto const overall_end_time = |
| 215 | parse_timestamp(validity_spec_xml.child_value("com:overallEndTime")); | ||
| 216 | if (overall_start_time && overall_end_time | ||
| 217 | && *overall_start_time < *overall_end_time) | ||
| 218 | { | ||
| 219 | valid_periods.emplace_back(*overall_start_time, *overall_end_time); | ||
| 161 | 220 | ||
| 162 | for (auto const valid_period_xml : validity_xml.children("com:validPeriod")) { | 221 | for (auto const valid_period_xml : |
| 163 | auto const start_of_period = parse_timestamp(valid_period_xml.child_value("com:startOfPeriod")); | 222 | validity_xml.children("com:validPeriod")) |
| 164 | auto const end_of_period = parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); | 223 | { |
| 165 | if (start_of_period && end_of_period && *start_of_period < *end_of_period) { | 224 | auto const start_of_period = parse_timestamp( |
| 166 | valid_periods.emplace_back(*start_of_period, *end_of_period); | 225 | valid_period_xml.child_value("com:startOfPeriod")); |
| 167 | } | 226 | auto const end_of_period = |
| 227 | parse_timestamp(valid_period_xml.child_value("com:endOfPeriod")); | ||
| 228 | if (start_of_period && end_of_period | ||
| 229 | && *start_of_period < *end_of_period) | ||
| 230 | { | ||
| 231 | valid_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 168 | } | 232 | } |
| 169 | for (auto const exception_period_xml : validity_xml.children("com:exceptionPeriod")) { | 233 | } |
| 170 | auto const start_of_period = parse_timestamp(exception_period_xml.child_value("com:startOfPeriod")); | 234 | for (auto const exception_period_xml : |
| 171 | auto const end_of_period = parse_timestamp(exception_period_xml.child_value("com:endOfPeriod")); | 235 | validity_xml.children("com:exceptionPeriod")) |
| 172 | if (start_of_period && end_of_period && *start_of_period < *end_of_period) { | 236 | { |
| 173 | exception_periods.emplace_back(*start_of_period, *end_of_period); | 237 | auto const start_of_period = parse_timestamp( |
| 174 | } | 238 | exception_period_xml.child_value("com:startOfPeriod")); |
| 239 | auto const end_of_period = parse_timestamp( | ||
| 240 | exception_period_xml.child_value("com:endOfPeriod")); | ||
| 241 | if (start_of_period && end_of_period | ||
| 242 | && *start_of_period < *end_of_period) | ||
| 243 | { | ||
| 244 | exception_periods.emplace_back(*start_of_period, *end_of_period); | ||
| 175 | } | 245 | } |
| 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 | } | 246 | } |
| 247 | |||
| 248 | validity = | ||
| 249 | time::period_seq{valid_periods.begin(), valid_periods.end()}.except( | ||
| 250 | time::period_seq{ | ||
| 251 | exception_periods.begin(), exception_periods.end() | ||
| 252 | }); | ||
| 185 | } | 253 | } |
| 254 | else | ||
| 255 | { | ||
| 256 | warnings_.insert( | ||
| 257 | std::format( | ||
| 258 | "invalid overall start / end time (start time: {}, end " | ||
| 259 | "time: {})", | ||
| 260 | validity_spec_xml.child_value("com:overallStartTime"), | ||
| 261 | validity_spec_xml.child_value("com:overallEndTime"))); | ||
| 262 | return std::nullopt; | ||
| 263 | } | ||
| 264 | } | ||
| 186 | 265 | ||
| 187 | //---- Try to extract the location info | 266 | //---- Try to extract the location info |
| 188 | 267 | ||
| 189 | auto rc = std::make_shared<road_closure>(std::move(parent), validity); | 268 | auto rc = std::make_shared<road_closure>(std::move(parent), validity); |
| 190 | add_location_from_xml(*rc, record_xml.child("sit:locationReference")); | 269 | add_location_from_xml(*rc, record_xml.child("sit:locationReference")); |
| 191 | return rc; | 270 | return rc; |
| 192 | } | 271 | } |
| 193 | 272 | ||
| 194 | public: | 273 | public: |
| 195 | [[nodiscard]] auto load_situation_publication(std::string const& filename) -> situation_publication { | 274 | [[nodiscard]] auto load_situation_publication(std::string const& filename) |
| 196 | auto doc = pugi::xml_document{}; | 275 | -> situation_publication |
| 197 | if (auto result = doc.load_file(filename.c_str()); !result) { | 276 | { |
| 198 | throw std::runtime_error{result.description()}; | 277 | auto doc = pugi::xml_document{}; |
| 199 | } | 278 | if (auto result = doc.load_file(filename.c_str()); !result) |
| 200 | auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); | 279 | { |
| 201 | auto mpublication_time = parse_timestamp(payload_xml.child_value("com:publicationTime")); | 280 | throw std::runtime_error{result.description()}; |
| 202 | if (!mpublication_time) | 281 | } |
| 203 | throw std::runtime_error{"provided publication does not name publication time"}; | 282 | auto payload_xml = doc.child("mc:messageContainer").child("mc:payload"); |
| 283 | auto mpublication_time = | ||
| 284 | parse_timestamp(payload_xml.child_value("com:publicationTime")); | ||
| 285 | if (!mpublication_time) | ||
| 286 | throw std::runtime_error{ | ||
| 287 | "provided publication does not name publication time" | ||
| 288 | }; | ||
| 204 | 289 | ||
| 205 | auto situations = std::vector<std::shared_ptr<situation>>{}; | 290 | auto situations = std::vector<std::shared_ptr<situation>>{}; |
| 206 | for (auto const sit_xml : payload_xml.children("sit:situation")) { | 291 | for (auto const sit_xml : payload_xml.children("sit:situation")) |
| 207 | auto id = std::string_view{sit_xml.attribute("id").value()}; | 292 | { |
| 293 | auto id = std::string_view{sit_xml.attribute("id").value()}; | ||
| 208 | 294 | ||
| 209 | auto const sit = std::make_shared<situation>(std::string{id}); | 295 | auto const sit = std::make_shared<situation>(std::string{id}); |
| 210 | situations.push_back(sit); | 296 | situations.push_back(sit); |
| 211 | 297 | ||
| 212 | auto const& header_info_xml = sit_xml.child("sit:headerInformation"); | 298 | auto const& header_info_xml = sit_xml.child("sit:headerInformation"); |
| 213 | if (header_info_xml.child_value("com:informationStatus") != "real"sv) | 299 | if (header_info_xml.child_value("com:informationStatus") != "real"sv) |
| 214 | continue; | 300 | continue; |
| 215 | 301 | ||
| 216 | for (auto const record_xml : sit_xml.children("sit:situationRecord")) { | 302 | for (auto const record_xml : sit_xml.children("sit:situationRecord")) |
| 217 | auto const record_type = std::string_view{record_xml.attribute("xsi:type").value()}; | 303 | { |
| 218 | auto const primary_record_types = std::unordered_set<std::string_view>{ | 304 | auto const record_type = |
| 305 | std::string_view{record_xml.attribute("xsi:type").value()}; | ||
| 306 | auto const primary_record_types = std::unordered_set<std::string_view>{ | ||
| 219 | "sit:Roadworks", | 307 | "sit:Roadworks", |
| 220 | /* { */ "sit:MaintenanceWorks", | 308 | /* { */ "sit:MaintenanceWorks", |
| 221 | /* | */ "sit:ConstructionWorks", | 309 | /* | */ "sit:ConstructionWorks", |
| @@ -228,52 +316,84 @@ namespace routemon::datex2 { | |||
| 228 | "sit:Activity", | 316 | "sit:Activity", |
| 229 | /* { */ "sit:PublicEvent", | 317 | /* { */ "sit:PublicEvent", |
| 230 | /* } */ | 318 | /* } */ |
| 231 | }; | 319 | }; |
| 232 | 320 | ||
| 233 | if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") { | 321 | if (record_type == "sit:RoadOrCarriagewayOrLaneManagement") |
| 234 | if (auto rc = handle_road_or_carriageway_or_lane_management(record_xml, sit)) { | 322 | { |
| 235 | sit->road_closures.push_back(*rc); | 323 | if (auto rc = handle_road_or_carriageway_or_lane_management( |
| 324 | record_xml, sit)) | ||
| 325 | { | ||
| 326 | sit->road_closures.push_back(*rc); | ||
| 327 | } | ||
| 328 | } | ||
| 329 | else if (primary_record_types.contains(record_type)) | ||
| 330 | { | ||
| 331 | for (auto const comment_xml : | ||
| 332 | record_xml.children("sit:generalPublicComment")) | ||
| 333 | { | ||
| 334 | // if | ||
| 335 | // (comment_xml.child_value("sit:commentType") | ||
| 336 | // == "internalNote"sv) { | ||
| 337 | auto candidate = std::optional<std::pair< | ||
| 338 | std::string_view, std::string_view>>{}; // (text, language) | ||
| 339 | for (auto const comment_value_xml : comment_xml.child("sit:comment") | ||
| 340 | .child("com:values") | ||
| 341 | .children("com:value")) | ||
| 342 | { | ||
| 343 | if (!candidate | ||
| 344 | || comment_value_xml.attribute("lang").value() == "nl"sv | ||
| 345 | || (candidate->second != "nl"sv | ||
| 346 | && comment_value_xml.attribute("lang").value() == "nl"sv)) | ||
| 347 | { | ||
| 348 | candidate = std::make_pair( | ||
| 349 | comment_value_xml.child_value(), | ||
| 350 | comment_value_xml.attribute("lang").value()); | ||
| 351 | } | ||
| 236 | } | 352 | } |
| 237 | } else if (primary_record_types.contains(record_type)) { | 353 | if (candidate) |
| 238 | for (auto const comment_xml : record_xml.children("sit:generalPublicComment")) { | 354 | { |
| 239 | // if (comment_xml.child_value("sit:commentType") == "internalNote"sv) { | 355 | auto already_present = false; |
| 240 | auto candidate = std::optional<std::pair<std::string_view, std::string_view>>{}; // (text, language) | 356 | for (auto const& comment : sit->comments) |
| 241 | for (auto const comment_value_xml : comment_xml.child("sit:comment").child("com:values").children("com:value")) { | 357 | already_present = |
| 242 | if (!candidate || | 358 | already_present || comment == candidate->first; |
| 243 | comment_value_xml.attribute("lang").value() == "nl"sv || | 359 | if (!already_present) |
| 244 | (candidate->second != "nl"sv && comment_value_xml.attribute("lang").value() == "nl"sv)) { | 360 | { |
| 245 | candidate = std::make_pair(comment_value_xml.child_value(), comment_value_xml.attribute("lang").value()); | 361 | sit->comments.emplace_back(candidate->first); |
| 246 | } | 362 | } |
| 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 | } | 363 | } |
| 364 | // } | ||
| 365 | } | ||
| 258 | 366 | ||
| 259 | if (auto const location_ref_xml = record_xml.child("sit:locationReference")) { | 367 | if (auto const location_ref_xml = |
| 260 | if (location_ref_xml.attribute("xsi:type").value() == "loc:PointLocation"sv) { | 368 | record_xml.child("sit:locationReference")) |
| 261 | if (auto const coords_xml = location_ref_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates")) { | 369 | { |
| 262 | auto const mlat = util::parse_double(coords_xml.child_value("loc:latitude")); | 370 | if (location_ref_xml.attribute("xsi:type").value() |
| 263 | auto const mlon = util::parse_double(coords_xml.child_value("loc:longitude")); | 371 | == "loc:PointLocation"sv) |
| 264 | if (mlat && mlon) { | 372 | { |
| 265 | // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat: | 373 | if (auto const coords_xml = |
| 266 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | 374 | location_ref_xml.child("loc:pointByCoordinates") |
| 267 | // maar heeft het UML-model van DATEX II v3 het over ETRS 89: | 375 | .child("loc:pointCoordinates")) |
| 268 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | 376 | { |
| 377 | auto const mlat = | ||
| 378 | util::parse_double(coords_xml.child_value("loc:latitude")); | ||
| 379 | auto const mlon = | ||
| 380 | util::parse_double(coords_xml.child_value("loc:longitude")); | ||
| 381 | if (mlat && mlon) | ||
| 382 | { | ||
| 383 | // Vaag genoeg zegt NDW dat het hier om WGS | ||
| 384 | // 84 gaat: | ||
| 385 | // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/ | ||
| 386 | // maar heeft het UML-model van DATEX II v3 | ||
| 387 | // het over ETRS 89: | ||
| 388 | // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm | ||
| 269 | 389 | ||
| 270 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; | 390 | auto const coords_etrs89 = geo::point{*mlon, *mlat}; |
| 271 | auto coords_wgs84 = geo::point{}; | 391 | auto coords_wgs84 = geo::point{}; |
| 272 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); | 392 | etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84); |
| 273 | 393 | ||
| 274 | if (!sit->location) { | 394 | if (!sit->location) |
| 275 | sit->location = coords_wgs84; | 395 | { |
| 276 | } | 396 | sit->location = coords_wgs84; |
| 277 | } | 397 | } |
| 278 | } | 398 | } |
| 279 | } | 399 | } |
| @@ -281,16 +401,18 @@ namespace routemon::datex2 { | |||
| 281 | } | 401 | } |
| 282 | } | 402 | } |
| 283 | } | 403 | } |
| 404 | } | ||
| 284 | 405 | ||
| 285 | return { | 406 | return { |
| 286 | .publication_time = *mpublication_time, | 407 | .publication_time = *mpublication_time, |
| 287 | .situations = situations, | 408 | .situations = situations, |
| 288 | }; | 409 | }; |
| 289 | } | 410 | } |
| 290 | 411 | ||
| 291 | [[nodiscard]] auto warnings() const -> std::multiset<std::string> const& { | 412 | [[nodiscard]] auto warnings() const -> std::multiset<std::string> const& |
| 292 | return warnings_; | 413 | { |
| 293 | } | 414 | return warnings_; |
| 294 | }; | 415 | } |
| 416 | }; | ||
| 295 | 417 | ||
| 296 | } // namespace routemon::datex2 | 418 | } // namespace routemon::datex2 |
diff --git a/server/src/geo.cppm b/server/src/geo.cppm index 5cdbdd9..b599ad4 100644 --- a/server/src/geo.cppm +++ b/server/src/geo.cppm | |||
| @@ -8,33 +8,42 @@ export namespace bgeo = boost::geometry; | |||
| 8 | 8 | ||
| 9 | export namespace routemon::geo { | 9 | export namespace routemon::geo { |
| 10 | 10 | ||
| 11 | using point = bgeo::model::point<double, 2, bgeo::cs::spherical_equatorial<bgeo::degree>>; | 11 | using point = |
| 12 | using linestring = bgeo::model::linestring<point>; | 12 | bgeo::model::point<double, 2, bgeo::cs::spherical_equatorial<bgeo::degree>>; |
| 13 | using box = bgeo::model::box<point>; | 13 | using linestring = bgeo::model::linestring<point>; |
| 14 | using stype = bgeo::srs::spheroid<double>; | 14 | using box = bgeo::model::box<point>; |
| 15 | using vincenty_strategy = bgeo::strategy::distance::vincenty<stype>; | 15 | using stype = bgeo::srs::spheroid<double>; |
| 16 | using vincenty_strategy = bgeo::strategy::distance::vincenty<stype>; | ||
| 16 | 17 | ||
| 17 | auto split_linestring_with_overlap_segments(linestring const& ls, double max_split_distance_m, std::vector<linestring>& append_to) -> void { | 18 | auto split_linestring_with_overlap_segments( |
| 18 | if (bgeo::is_empty(ls)) | 19 | linestring const& ls, double max_split_distance_m, |
| 19 | return; | 20 | std::vector<linestring>& append_to) -> void |
| 21 | { | ||
| 22 | if (bgeo::is_empty(ls)) | ||
| 23 | return; | ||
| 20 | 24 | ||
| 21 | auto current_ls = linestring{}; | 25 | auto current_ls = linestring{}; |
| 22 | auto current_ls_length = 0.0; | 26 | auto current_ls_length = 0.0; |
| 23 | auto previous = std::optional<point>{}; | 27 | auto previous = std::optional<point>{}; |
| 24 | bgeo::for_each_point(ls, [&](point p) -> void { | 28 | bgeo::for_each_point( |
| 25 | bgeo::append(current_ls, p); | 29 | ls, |
| 26 | if (previous) { | 30 | [&](point p) -> void |
| 27 | auto d = bgeo::distance(*previous, p, vincenty_strategy()); | 31 | { |
| 28 | current_ls_length += d; | 32 | bgeo::append(current_ls, p); |
| 29 | if (current_ls_length > max_split_distance_m) { | 33 | if (previous) |
| 30 | append_to.push_back(std::move(current_ls)); | 34 | { |
| 31 | current_ls = linestring{*previous, p}; | 35 | auto d = bgeo::distance(*previous, p, vincenty_strategy()); |
| 32 | current_ls_length = d; | 36 | current_ls_length += d; |
| 37 | if (current_ls_length > max_split_distance_m) | ||
| 38 | { | ||
| 39 | append_to.push_back(std::move(current_ls)); | ||
| 40 | current_ls = linestring{*previous, p}; | ||
| 41 | current_ls_length = d; | ||
| 42 | } | ||
| 33 | } | 43 | } |
| 34 | } | 44 | previous = p; |
| 35 | previous = p; | 45 | }); |
| 36 | }); | 46 | append_to.emplace_back(std::move(current_ls)); |
| 37 | append_to.emplace_back(std::move(current_ls)); | 47 | } |
| 38 | } | ||
| 39 | 48 | ||
| 40 | } // namespace routemon::geo | 49 | } // namespace routemon::geo |
diff --git a/server/src/gpx.cpp b/server/src/gpx.cpp index 62dccff..44a6bb2 100644 --- a/server/src/gpx.cpp +++ b/server/src/gpx.cpp | |||
| @@ -13,177 +13,226 @@ using namespace std::literals::string_view_literals; | |||
| 13 | 13 | ||
| 14 | namespace routemon::gpx { | 14 | namespace routemon::gpx { |
| 15 | 15 | ||
| 16 | namespace v10 { | 16 | namespace v10 { |
| 17 | 17 | ||
| 18 | constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; | 18 | constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; |
| 19 | auto qname(std::string_view local) -> xml::qname_view { | 19 | auto qname(std::string_view local) -> xml::qname_view |
| 20 | return {.ns_uri = xmlns, .local = local}; | 20 | { |
| 21 | } | 21 | return {.ns_uri = xmlns, .local = local}; |
| 22 | } | ||
| 22 | 23 | ||
| 23 | auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<geo::point> { | 24 | auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) |
| 24 | auto parse_xml_double = [](std::string_view sv) -> std::optional<double> { | 25 | -> xml::parser<geo::point> |
| 25 | return util::parse_double(sv, std::chars_format::fixed); | 26 | { |
| 26 | }; | 27 | auto parse_xml_double = [](std::string_view sv) -> std::optional<double> |
| 27 | auto mlat = std::optional<double>{}; | 28 | { return util::parse_double(sv, std::chars_format::fixed); }; |
| 28 | auto mlon = std::optional<double>{}; | 29 | auto mlat = std::optional<double>{}; |
| 29 | for (auto const& [name, value] : attrs) { | 30 | auto mlon = std::optional<double>{}; |
| 30 | if (name == qname("lat")) { | 31 | for (auto const& [name, value] : attrs) |
| 31 | mlat = parse_xml_double(value); | 32 | { |
| 32 | } else if (name == qname("lon")) { | 33 | if (name == qname("lat")) |
| 33 | mlon = parse_xml_double(value); | 34 | { |
| 34 | } | 35 | mlat = parse_xml_double(value); |
| 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 | } | 36 | } |
| 41 | 37 | else if (name == qname("lon")) | |
| 42 | auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser<track_segment> { | 38 | { |
| 43 | auto s = track_segment{}; | 39 | mlon = parse_xml_double(value); |
| 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 | } | 40 | } |
| 41 | } | ||
| 42 | if (!mlat || !mlon) | ||
| 43 | throw std::runtime_error{ | ||
| 44 | "expected valid latitude and longitude for waypoint" | ||
| 45 | }; | ||
| 46 | co_await xml::ignore_contents(e); | ||
| 47 | co_return geo::point{*mlon, *mlat}; | ||
| 48 | } | ||
| 48 | 49 | ||
| 49 | auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> { | 50 | auto parse_trkseg(xml::executor_ref e, xml::attribute_view) |
| 50 | auto t = track{}; | 51 | -> xml::parser<track_segment> |
| 51 | t.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | 52 | { |
| 52 | co_await allow_element(e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>()); | 53 | auto s = track_segment{}; |
| 53 | t.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | 54 | while (auto mwpt = co_await allow_element( |
| 54 | co_await xml::ignore_contents(e, /* until */ qname("trkseg")); | 55 | e, qname("trkpt"), xml::hohalo<parse_wpt>())) |
| 55 | while (auto mseg = co_await allow_element(e, qname("trkseg"), xml::hohalo<parse_trkseg>())) | 56 | bgeo::append(s.waypoints, *mwpt); |
| 56 | t.segments.push_back(std::move(*mseg)); | 57 | co_return std::move(s); |
| 57 | co_return std::move(t); | 58 | } |
| 58 | } | ||
| 59 | 59 | ||
| 60 | auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<file> { | 60 | auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> |
| 61 | auto f = file{}; | 61 | { |
| 62 | if (attrs.lookup(qname("version")) != "1.0"sv) | 62 | auto t = track{}; |
| 63 | throw std::runtime_error{"expected GPX version to be 1.0"}; | 63 | t.name = co_await allow_element( |
| 64 | if (auto mcreator = attrs.lookup(qname("creator"))) | 64 | e, qname("name"), xml::hohalo<xml::read_string_contents>()); |
| 65 | f.creator = *mcreator; | 65 | co_await allow_element( |
| 66 | else | 66 | e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>()); |
| 67 | throw std::runtime_error{"expected GPX file to have creator"}; | 67 | t.desc = co_await allow_element( |
| 68 | e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | ||
| 69 | co_await xml::ignore_contents(e, /* until */ qname("trkseg")); | ||
| 70 | while (auto mseg = co_await allow_element( | ||
| 71 | e, qname("trkseg"), xml::hohalo<parse_trkseg>())) | ||
| 72 | t.segments.push_back(std::move(*mseg)); | ||
| 73 | co_return std::move(t); | ||
| 74 | } | ||
| 68 | 75 | ||
| 69 | f.meta.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | 76 | auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) |
| 70 | f.meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | 77 | -> xml::parser<file> |
| 78 | { | ||
| 79 | auto f = file{}; | ||
| 80 | if (attrs.lookup(qname("version")) != "1.0"sv) | ||
| 81 | throw std::runtime_error{"expected GPX version to be 1.0"}; | ||
| 82 | if (auto mcreator = attrs.lookup(qname("creator"))) | ||
| 83 | f.creator = *mcreator; | ||
| 84 | else | ||
| 85 | throw std::runtime_error{"expected GPX file to have creator"}; | ||
| 71 | 86 | ||
| 72 | co_await xml::ignore_contents(e, /* until */ qname("trk")); | 87 | f.meta.name = co_await allow_element( |
| 73 | while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>())) | 88 | e, qname("name"), xml::hohalo<xml::read_string_contents>()); |
| 74 | f.tracks.push_back(std::move(*mtrk)); | 89 | f.meta.desc = co_await allow_element( |
| 75 | co_await xml::ignore_contents(e); | 90 | e, qname("desc"), xml::hohalo<xml::read_string_contents>()); |
| 76 | 91 | ||
| 77 | co_return std::move(f); | 92 | co_await xml::ignore_contents(e, /* until */ qname("trk")); |
| 78 | } | 93 | while (auto mtrk = |
| 94 | co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>())) | ||
| 95 | f.tracks.push_back(std::move(*mtrk)); | ||
| 96 | co_await xml::ignore_contents(e); | ||
| 79 | 97 | ||
| 80 | } // namespace v10 | 98 | co_return std::move(f); |
| 99 | } | ||
| 81 | 100 | ||
| 82 | namespace v11 { | 101 | } // namespace v10 |
| 83 | 102 | ||
| 84 | constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv; | 103 | namespace v11 { |
| 85 | auto qname(std::string_view local) -> xml::qname_view { | ||
| 86 | return {.ns_uri = xmlns, .local = local}; | ||
| 87 | } | ||
| 88 | 104 | ||
| 89 | auto parse_metadata(xml::executor_ref e, xml::attribute_view) -> xml::parser<metadata> { | 105 | constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv; |
| 90 | auto meta = metadata{}; | 106 | auto qname(std::string_view local) -> xml::qname_view |
| 91 | meta.name = co_await allow_element(e, qname("name"), xml::hohalo<xml::read_string_contents>()); | 107 | { |
| 92 | meta.desc = co_await allow_element(e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | 108 | return {.ns_uri = xmlns, .local = local}; |
| 93 | co_await xml::ignore_contents(e); | 109 | } |
| 94 | co_return meta; | ||
| 95 | } | ||
| 96 | 110 | ||
| 97 | auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<geo::point> { | 111 | auto parse_metadata(xml::executor_ref e, xml::attribute_view) |
| 98 | auto parse_xml_double = [](std::string_view sv) -> std::optional<double> { | 112 | -> xml::parser<metadata> |
| 99 | return util::parse_double(sv, std::chars_format::fixed); | 113 | { |
| 100 | }; | 114 | auto meta = metadata{}; |
| 101 | auto mlat = std::optional<double>{}; | 115 | meta.name = co_await allow_element( |
| 102 | auto mlon = std::optional<double>{}; | 116 | e, qname("name"), xml::hohalo<xml::read_string_contents>()); |
| 103 | for (auto const& [name, value] : attrs) { | 117 | meta.desc = co_await allow_element( |
| 104 | if (name == qname("lat")) { | 118 | e, qname("desc"), xml::hohalo<xml::read_string_contents>()); |
| 105 | mlat = parse_xml_double(value); | 119 | co_await xml::ignore_contents(e); |
| 106 | } else if (name == qname("lon")) { | 120 | co_return meta; |
| 107 | mlon = parse_xml_double(value); | 121 | } |
| 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 | 122 | ||
| 116 | auto parse_trkseg(xml::executor_ref e, xml::attribute_view) -> xml::parser<track_segment> { | 123 | auto parse_wpt(xml::executor_ref e, xml::attribute_view attrs) |
| 117 | auto s = track_segment{}; | 124 | -> xml::parser<geo::point> |
| 118 | while (auto mwpt = co_await allow_element(e, qname("trkpt"), xml::hohalo<parse_wpt>())) | 125 | { |
| 119 | bgeo::append(s.waypoints, *mwpt); | 126 | auto parse_xml_double = [](std::string_view sv) -> std::optional<double> |
| 120 | co_await allow_element(e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>()); | 127 | { return util::parse_double(sv, std::chars_format::fixed); }; |
| 121 | co_return std::move(s); | 128 | auto mlat = std::optional<double>{}; |
| 129 | auto mlon = std::optional<double>{}; | ||
| 130 | for (auto const& [name, value] : attrs) | ||
| 131 | { | ||
| 132 | if (name == qname("lat")) | ||
| 133 | { | ||
| 134 | mlat = parse_xml_double(value); | ||
| 122 | } | 135 | } |
| 123 | 136 | else if (name == qname("lon")) | |
| 124 | auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> { | 137 | { |
| 125 | auto t = track{}; | 138 | mlon = parse_xml_double(value); |
| 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 | } | 139 | } |
| 140 | } | ||
| 141 | if (!mlat || !mlon) | ||
| 142 | throw std::runtime_error{ | ||
| 143 | "expected valid latitude and longitude for waypoint" | ||
| 144 | }; | ||
| 145 | co_await xml::ignore_contents(e); | ||
| 146 | co_return geo::point{*mlon, *mlat}; | ||
| 147 | } | ||
| 134 | 148 | ||
| 135 | auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) -> xml::parser<file> { | 149 | auto parse_trkseg(xml::executor_ref e, xml::attribute_view) |
| 136 | auto f = file{}; | 150 | -> xml::parser<track_segment> |
| 137 | if (attrs.lookup(qname("version")) != "1.1"sv) | 151 | { |
| 138 | throw std::runtime_error{"expected GPX version to be 1.1"}; | 152 | auto s = track_segment{}; |
| 139 | if (auto mcreator = attrs.lookup(qname("creator"))) | 153 | while (auto mwpt = co_await allow_element( |
| 140 | f.creator = *mcreator; | 154 | e, qname("trkpt"), xml::hohalo<parse_wpt>())) |
| 141 | else | 155 | bgeo::append(s.waypoints, *mwpt); |
| 142 | throw std::runtime_error{"expected GPX file to have creator"}; | 156 | co_await allow_element( |
| 157 | e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 158 | co_return std::move(s); | ||
| 159 | } | ||
| 143 | 160 | ||
| 144 | if (auto mmeta = co_await allow_element(e, qname("metadata"), xml::hohalo<parse_metadata>())) | 161 | auto parse_trk(xml::executor_ref e, xml::attribute_view) -> xml::parser<track> |
| 145 | f.meta = *mmeta; | 162 | { |
| 146 | co_await xml::ignore_contents(e, /* until */ qname("trk")); | 163 | auto t = track{}; |
| 147 | while (auto mtrk = co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>())) | 164 | t.name = co_await allow_element( |
| 148 | f.tracks.push_back(std::move(*mtrk)); | 165 | e, qname("name"), xml::hohalo<xml::read_string_contents>()); |
| 149 | co_await allow_element(e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>()); | 166 | co_await allow_element( |
| 167 | e, qname("cmt"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 168 | t.desc = co_await allow_element( | ||
| 169 | e, qname("desc"), xml::hohalo<xml::read_string_contents>()); | ||
| 170 | co_await xml::ignore_contents(e, /* until */ qname("trkseg")); | ||
| 171 | while (auto mseg = co_await allow_element( | ||
| 172 | e, qname("trkseg"), xml::hohalo<parse_trkseg>())) | ||
| 173 | t.segments.push_back(std::move(*mseg)); | ||
| 174 | co_return std::move(t); | ||
| 175 | } | ||
| 150 | 176 | ||
| 151 | co_return std::move(f); | 177 | auto parse_gpx(xml::executor_ref e, xml::attribute_view attrs) |
| 152 | } | 178 | -> xml::parser<file> |
| 179 | { | ||
| 180 | auto f = file{}; | ||
| 181 | if (attrs.lookup(qname("version")) != "1.1"sv) | ||
| 182 | throw std::runtime_error{"expected GPX version to be 1.1"}; | ||
| 183 | if (auto mcreator = attrs.lookup(qname("creator"))) | ||
| 184 | f.creator = *mcreator; | ||
| 185 | else | ||
| 186 | throw std::runtime_error{"expected GPX file to have creator"}; | ||
| 153 | 187 | ||
| 154 | } // namespace v11 | 188 | if (auto mmeta = co_await allow_element( |
| 189 | e, qname("metadata"), xml::hohalo<parse_metadata>())) | ||
| 190 | f.meta = *mmeta; | ||
| 191 | co_await xml::ignore_contents(e, /* until */ qname("trk")); | ||
| 192 | while (auto mtrk = | ||
| 193 | co_await allow_element(e, qname("trk"), xml::hohalo<parse_trk>())) | ||
| 194 | f.tracks.push_back(std::move(*mtrk)); | ||
| 195 | co_await allow_element( | ||
| 196 | e, qname("extensions"), xml::hohalo<xml::ignore_element_contents>()); | ||
| 155 | 197 | ||
| 156 | auto parse_file(xml::executor_ref e) -> xml::parser<file> { | 198 | co_return std::move(f); |
| 157 | auto decl = co_await expect_event<xml::xml_decl_event>(e); | 199 | } |
| 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 | 200 | ||
| 169 | reader::reader() | 201 | } // namespace v11 |
| 170 | : p_{parse_file(util::not_null{&e_})} | ||
| 171 | { e_.set_continuation(p_.promise().base_handle()); } | ||
| 172 | 202 | ||
| 173 | auto reader::init() -> void { | 203 | auto parse_file(xml::executor_ref e) -> xml::parser<file> |
| 174 | e_.start(); | 204 | { |
| 175 | } | 205 | auto decl = co_await expect_event<xml::xml_decl_event>(e); |
| 206 | if (decl.version != "1.0"sv) | ||
| 207 | throw std::runtime_error{std::format( | ||
| 208 | "unsupported XML version, got {}", std::string_view{decl.version})}; | ||
| 209 | if (decl.encoding != "UTF-8"sv) | ||
| 210 | throw std::runtime_error{"unsupported encoding"}; | ||
| 211 | if (auto mf = co_await allow_element( | ||
| 212 | e, v10::qname("gpx"), xml::hohalo<v10::parse_gpx>())) | ||
| 213 | co_return std::move(*mf); | ||
| 214 | if (auto mf = co_await allow_element( | ||
| 215 | e, v11::qname("gpx"), xml::hohalo<v11::parse_gpx>())) | ||
| 216 | co_return std::move(*mf); | ||
| 217 | throw std::runtime_error{"no supported GPX document found"}; | ||
| 218 | } | ||
| 176 | 219 | ||
| 177 | auto reader::put(std::string_view buf) -> void { | 220 | reader::reader() : p_{parse_file(util::not_null{&e_})} |
| 178 | e_.read(buf, false); | 221 | { |
| 179 | } | 222 | e_.set_continuation(p_.promise().base_handle()); |
| 223 | } | ||
| 180 | 224 | ||
| 181 | auto reader::finish() -> gpx::file { | 225 | auto reader::init() -> void { e_.start(); } |
| 182 | e_.read(std::string_view{}, true); | 226 | |
| 183 | e_.end(); | 227 | auto reader::put(std::string_view buf) -> void { e_.read(buf, false); } |
| 184 | // Promise is still alive since the last coroutine performs a | 228 | |
| 185 | // symmetric transfer to std::noop_coroutine() in final_suspend(). | 229 | auto reader::finish() -> gpx::file |
| 186 | return std::move(p_.promise().returned_value()); | 230 | { |
| 187 | } | 231 | e_.read(std::string_view{}, true); |
| 232 | e_.end(); | ||
| 233 | // Promise is still alive since the last coroutine performs a | ||
| 234 | // symmetric transfer to std::noop_coroutine() in final_suspend(). | ||
| 235 | return std::move(p_.promise().returned_value()); | ||
| 236 | } | ||
| 188 | 237 | ||
| 189 | } // namespace routemon::gpx | 238 | } // namespace routemon::gpx |
diff --git a/server/src/gpx.cppm b/server/src/gpx.cppm index 20e6242..5872805 100644 --- a/server/src/gpx.cppm +++ b/server/src/gpx.cppm | |||
| @@ -7,37 +7,42 @@ import :xml; | |||
| 7 | 7 | ||
| 8 | namespace routemon::gpx { | 8 | namespace routemon::gpx { |
| 9 | 9 | ||
| 10 | struct metadata { | 10 | struct metadata |
| 11 | std::optional<std::string> name; | 11 | { |
| 12 | std::optional<std::string> desc; | 12 | std::optional<std::string> name; |
| 13 | }; | 13 | std::optional<std::string> desc; |
| 14 | }; | ||
| 14 | 15 | ||
| 15 | struct track_segment { | 16 | struct track_segment |
| 16 | geo::linestring waypoints; | 17 | { |
| 17 | }; | 18 | geo::linestring waypoints; |
| 19 | }; | ||
| 18 | 20 | ||
| 19 | struct track { | 21 | struct track |
| 20 | std::optional<std::string> name; | 22 | { |
| 21 | std::optional<std::string> desc; | 23 | std::optional<std::string> name; |
| 22 | std::vector<track_segment> segments; | 24 | std::optional<std::string> desc; |
| 23 | }; | 25 | std::vector<track_segment> segments; |
| 26 | }; | ||
| 24 | 27 | ||
| 25 | struct file { | 28 | struct file |
| 26 | std::string creator; | 29 | { |
| 27 | metadata meta; | 30 | std::string creator; |
| 28 | std::vector<track> tracks; | 31 | metadata meta; |
| 29 | }; | 32 | std::vector<track> tracks; |
| 33 | }; | ||
| 30 | 34 | ||
| 31 | class reader { | 35 | class reader |
| 32 | xml::executor e_; | 36 | { |
| 33 | xml::parser<file> p_; | 37 | xml::executor e_; |
| 38 | xml::parser<file> p_; | ||
| 34 | 39 | ||
| 35 | public: | 40 | public: |
| 36 | explicit reader(); | 41 | explicit reader(); |
| 37 | 42 | ||
| 38 | auto init() -> void; | 43 | auto init() -> void; |
| 39 | auto put(std::string_view buf) -> void; | 44 | auto put(std::string_view buf) -> void; |
| 40 | [[nodiscard]] auto finish() -> gpx::file; | 45 | [[nodiscard]] auto finish() -> gpx::file; |
| 41 | }; | 46 | }; |
| 42 | 47 | ||
| 43 | } // namespace routemon::gpx | 48 | } // namespace routemon::gpx |
diff --git a/server/src/http_client.cppm b/server/src/http_client.cppm index d5316d6..bea5384 100644 --- a/server/src/http_client.cppm +++ b/server/src/http_client.cppm | |||
| @@ -16,47 +16,51 @@ using tcp = net::ip::tcp; | |||
| 16 | 16 | ||
| 17 | namespace routemon::http { | 17 | namespace routemon::http { |
| 18 | 18 | ||
| 19 | export class client { | 19 | export class client |
| 20 | net::io_context& ioc_; | 20 | { |
| 21 | ssl::context sslc_{ssl::context::tlsv12_client}; | 21 | net::io_context& ioc_; |
| 22 | tcp::resolver resolver_; | 22 | ssl::context sslc_{ssl::context::tlsv12_client}; |
| 23 | tcp::resolver resolver_; | ||
| 23 | 24 | ||
| 24 | public: | 25 | public: |
| 25 | explicit client(net::io_context& ioc) | 26 | explicit client(net::io_context& ioc) : ioc_{ioc}, resolver_{ioc} |
| 26 | : ioc_{ioc}, resolver_{ioc} | 27 | { |
| 27 | { | 28 | sslc_.set_default_verify_paths(); |
| 28 | sslc_.set_default_verify_paths(); | 29 | sslc_.set_verify_mode( |
| 29 | sslc_.set_verify_mode(net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); | 30 | net::ssl::verify_peer | net::ssl::verify_fail_if_no_peer_cert); |
| 30 | } | 31 | } |
| 31 | 32 | ||
| 32 | template<class ReqBody> | 33 | template <class ReqBody> |
| 33 | auto do_request(bhttp::request<ReqBody>& req) -> bhttp::response<bhttp::dynamic_body> { | 34 | auto do_request(bhttp::request<ReqBody>& req) |
| 34 | auto stream = ssl::stream<beast::tcp_stream>{ioc_, sslc_}; | 35 | -> bhttp::response<bhttp::dynamic_body> |
| 36 | { | ||
| 37 | auto stream = ssl::stream<beast::tcp_stream>{ioc_, sslc_}; | ||
| 35 | 38 | ||
| 36 | auto host = std::string{req.at(bhttp::field::host)}; | 39 | auto host = std::string{req.at(bhttp::field::host)}; |
| 37 | if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { | 40 | if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) |
| 38 | throw beast::system_error(static_cast<int>(::ERR_get_error()), | 41 | { |
| 39 | net::error::get_ssl_category()); | 42 | throw beast::system_error( |
| 40 | } | 43 | static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()); |
| 41 | stream.set_verify_callback(ssl::host_name_verification(host)); | 44 | } |
| 42 | auto const results = resolver_.resolve(host, "443"); | 45 | stream.set_verify_callback(ssl::host_name_verification(host)); |
| 43 | beast::get_lowest_layer(stream).connect(results); | 46 | auto const results = resolver_.resolve(host, "443"); |
| 44 | stream.handshake(ssl::stream_base::client); | 47 | beast::get_lowest_layer(stream).connect(results); |
| 48 | stream.handshake(ssl::stream_base::client); | ||
| 45 | 49 | ||
| 46 | req.set(bhttp::field::user_agent, "routemon/1.0"); | 50 | req.set(bhttp::field::user_agent, "routemon/1.0"); |
| 47 | bhttp::write(stream, req); | 51 | bhttp::write(stream, req); |
| 48 | 52 | ||
| 49 | auto buffer = beast::flat_buffer{}; | 53 | auto buffer = beast::flat_buffer{}; |
| 50 | auto res = bhttp::response<bhttp::dynamic_body>{}; | 54 | auto res = bhttp::response<bhttp::dynamic_body>{}; |
| 51 | bhttp::read(stream, buffer, res); | 55 | bhttp::read(stream, buffer, res); |
| 52 | 56 | ||
| 53 | auto ec = beast::error_code{}; | 57 | auto ec = beast::error_code{}; |
| 54 | stream.shutdown(ec); | 58 | stream.shutdown(ec); |
| 55 | if (ec != net::ssl::error::stream_truncated) | 59 | if (ec != net::ssl::error::stream_truncated) |
| 56 | throw beast::system_error{ec}; | 60 | throw beast::system_error{ec}; |
| 57 | 61 | ||
| 58 | return res; | 62 | return res; |
| 59 | } | 63 | } |
| 60 | }; | 64 | }; |
| 61 | 65 | ||
| 62 | } // namespace routemon::http | 66 | } // namespace routemon::http |
diff --git a/server/src/http_common.cppm b/server/src/http_common.cppm index 3c38da7..606a5d5 100644 --- a/server/src/http_common.cppm +++ b/server/src/http_common.cppm | |||
| @@ -10,130 +10,160 @@ export namespace bhttp = beast::http; | |||
| 10 | 10 | ||
| 11 | namespace boost::beast { | 11 | namespace boost::beast { |
| 12 | 12 | ||
| 13 | namespace concepts { | 13 | namespace concepts { |
| 14 | 14 | ||
| 15 | template<class T> | 15 | template <class T> |
| 16 | concept buffers_generator = beast::is_buffers_generator<T>::value; | 16 | concept buffers_generator = beast::is_buffers_generator<T>::value; |
| 17 | 17 | ||
| 18 | template<class T> | 18 | template <class T> |
| 19 | concept const_buffer_sequence = beast::is_const_buffer_sequence<T>::value; | 19 | concept const_buffer_sequence = beast::is_const_buffer_sequence<T>::value; |
| 20 | 20 | ||
| 21 | } // namespace concepts | 21 | } // namespace concepts |
| 22 | 22 | ||
| 23 | namespace http::concepts { | 23 | namespace http::concepts { |
| 24 | 24 | ||
| 25 | template<class T> | 25 | template <class T> |
| 26 | concept fields = is_fields<T>::value; | 26 | concept fields = is_fields<T>::value; |
| 27 | 27 | ||
| 28 | template<class T> | 28 | template <class T> |
| 29 | concept body = is_body<T>::value; | 29 | concept body = is_body<T>::value; |
| 30 | 30 | ||
| 31 | template<class T> | 31 | template <class T> |
| 32 | concept body_reader = is_body_reader<T>::value; | 32 | concept body_reader = is_body_reader<T>::value; |
| 33 | 33 | ||
| 34 | } // namespace http::concepts | 34 | } // namespace http::concepts |
| 35 | 35 | ||
| 36 | } // namespace boost::beast | 36 | } // namespace boost::beast |
| 37 | 37 | ||
| 38 | namespace routemon::http { | 38 | namespace routemon::http { |
| 39 | 39 | ||
| 40 | struct supported_verb { | 40 | struct supported_verb |
| 41 | enum supported_verb_t : std::uint8_t { | 41 | { |
| 42 | options, | 42 | enum supported_verb_t : std::uint8_t |
| 43 | delete_, | 43 | { |
| 44 | get, | 44 | options, |
| 45 | head, | 45 | delete_, |
| 46 | post, | 46 | get, |
| 47 | put, | 47 | head, |
| 48 | }; | 48 | post, |
| 49 | put, | ||
| 50 | }; | ||
| 49 | 51 | ||
| 50 | supported_verb_t value; | 52 | supported_verb_t value; |
| 51 | 53 | ||
| 52 | supported_verb(supported_verb_t value) : value{value} {} | 54 | supported_verb(supported_verb_t value) : value{value} {} |
| 53 | 55 | ||
| 54 | static auto from(bhttp::verb v) -> std::optional<supported_verb> { | 56 | static auto from(bhttp::verb v) -> std::optional<supported_verb> |
| 55 | switch (v) { | 57 | { |
| 56 | case bhttp::verb::options: return supported_verb::options; | 58 | switch (v) |
| 57 | case bhttp::verb::delete_: return supported_verb::delete_; | 59 | { |
| 58 | case bhttp::verb::get: return supported_verb::get; | 60 | case bhttp::verb::options: |
| 59 | case bhttp::verb::head: return supported_verb::head; | 61 | return supported_verb::options; |
| 60 | case bhttp::verb::post: return supported_verb::post; | 62 | case bhttp::verb::delete_: |
| 61 | case bhttp::verb::put: return supported_verb::put; | 63 | return supported_verb::delete_; |
| 62 | default: return std::nullopt; | 64 | case bhttp::verb::get: |
| 63 | } | 65 | return supported_verb::get; |
| 66 | case bhttp::verb::head: | ||
| 67 | return supported_verb::head; | ||
| 68 | case bhttp::verb::post: | ||
| 69 | return supported_verb::post; | ||
| 70 | case bhttp::verb::put: | ||
| 71 | return supported_verb::put; | ||
| 72 | default: | ||
| 73 | return std::nullopt; | ||
| 64 | } | 74 | } |
| 75 | } | ||
| 65 | 76 | ||
| 66 | operator bhttp::verb() const { | 77 | operator bhttp::verb() const |
| 67 | switch (value) { | 78 | { |
| 68 | case supported_verb::options: return bhttp::verb::options; | 79 | switch (value) |
| 69 | case supported_verb::delete_: return bhttp::verb::delete_; | 80 | { |
| 70 | case supported_verb::get: return bhttp::verb::get; | 81 | case supported_verb::options: |
| 71 | case supported_verb::head: return bhttp::verb::head; | 82 | return bhttp::verb::options; |
| 72 | case supported_verb::post: return bhttp::verb::post; | 83 | case supported_verb::delete_: |
| 73 | case supported_verb::put: return bhttp::verb::put; | 84 | return bhttp::verb::delete_; |
| 74 | } | 85 | case supported_verb::get: |
| 86 | return bhttp::verb::get; | ||
| 87 | case supported_verb::head: | ||
| 88 | return bhttp::verb::head; | ||
| 89 | case supported_verb::post: | ||
| 90 | return bhttp::verb::post; | ||
| 91 | case supported_verb::put: | ||
| 92 | return bhttp::verb::put; | ||
| 75 | } | 93 | } |
| 76 | }; | 94 | } |
| 95 | }; | ||
| 77 | 96 | ||
| 78 | export struct verb_set { | 97 | export struct verb_set |
| 79 | bool delete_ : 1 = false; | 98 | { |
| 80 | bool get : 1 = false; | 99 | bool delete_ : 1 = false; |
| 81 | bool head : 1 = false; | 100 | bool get : 1 = false; |
| 82 | bool post : 1 = false; | 101 | bool head : 1 = false; |
| 83 | bool put : 1 = false; | 102 | bool post : 1 = false; |
| 84 | bool options : 1 = false; | 103 | bool put : 1 = false; |
| 104 | bool options : 1 = false; | ||
| 85 | 105 | ||
| 86 | auto enable(supported_verb v) -> void { | 106 | auto enable(supported_verb v) -> void |
| 87 | switch (v.value) { | 107 | { |
| 88 | case supported_verb::delete_: | 108 | switch (v.value) |
| 89 | delete_ = true; | 109 | { |
| 90 | break; | 110 | case supported_verb::delete_: |
| 91 | case supported_verb::get: | 111 | delete_ = true; |
| 92 | get = true; | 112 | break; |
| 93 | break; | 113 | case supported_verb::get: |
| 94 | case supported_verb::head: | 114 | get = true; |
| 95 | head = true; | 115 | break; |
| 96 | break; | 116 | case supported_verb::head: |
| 97 | case supported_verb::post: | 117 | head = true; |
| 98 | post = true; | 118 | break; |
| 99 | break; | 119 | case supported_verb::post: |
| 100 | case supported_verb::put: | 120 | post = true; |
| 101 | put = true; | 121 | break; |
| 102 | break; | 122 | case supported_verb::put: |
| 103 | case supported_verb::options: | 123 | put = true; |
| 104 | options = true; | 124 | break; |
| 105 | break; | 125 | case supported_verb::options: |
| 106 | default:; | 126 | options = true; |
| 107 | } | 127 | break; |
| 128 | default:; | ||
| 108 | } | 129 | } |
| 130 | } | ||
| 109 | 131 | ||
| 110 | auto operator==(verb_set const& rhs) const noexcept -> bool = default; | 132 | auto operator==(verb_set const& rhs) const noexcept -> bool = default; |
| 111 | 133 | ||
| 112 | auto empty() const -> bool { | 134 | auto empty() const -> bool { return *this == verb_set{}; } |
| 113 | return *this == verb_set{}; | ||
| 114 | } | ||
| 115 | 135 | ||
| 116 | verb_set(std::initializer_list<supported_verb> vs) { | 136 | verb_set(std::initializer_list<supported_verb> vs) |
| 117 | for (auto const v : vs) enable(v); | 137 | { |
| 118 | } | 138 | for (auto const v : vs) |
| 139 | enable(v); | ||
| 140 | } | ||
| 119 | 141 | ||
| 120 | auto to_string() const -> std::string { | 142 | auto to_string() const -> std::string |
| 121 | std::ostringstream ss; | 143 | { |
| 122 | bool wrote = false; | 144 | std::ostringstream ss; |
| 123 | auto write = [&](bhttp::verb v) { | 145 | bool wrote = false; |
| 124 | if (wrote) | 146 | auto write = [&](bhttp::verb v) |
| 125 | ss << ", "; | 147 | { |
| 126 | ss << v; | 148 | if (wrote) |
| 127 | wrote = true; | 149 | ss << ", "; |
| 128 | }; | 150 | ss << v; |
| 129 | if (delete_) write(bhttp::verb::delete_); | 151 | wrote = true; |
| 130 | if (get) write(bhttp::verb::get); | 152 | }; |
| 131 | if (head) write(bhttp::verb::head); | 153 | if (delete_) |
| 132 | if (post) write(bhttp::verb::post); | 154 | write(bhttp::verb::delete_); |
| 133 | if (put) write(bhttp::verb::put); | 155 | if (get) |
| 134 | if (options) write(bhttp::verb::options); | 156 | write(bhttp::verb::get); |
| 135 | return ss.str(); | 157 | if (head) |
| 136 | } | 158 | write(bhttp::verb::head); |
| 137 | }; | 159 | if (post) |
| 160 | write(bhttp::verb::post); | ||
| 161 | if (put) | ||
| 162 | write(bhttp::verb::put); | ||
| 163 | if (options) | ||
| 164 | write(bhttp::verb::options); | ||
| 165 | return ss.str(); | ||
| 166 | } | ||
| 167 | }; | ||
| 138 | 168 | ||
| 139 | } // namespace routemon::http | 169 | } // namespace routemon::http |
diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index 0770d97..0f7fda7 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | module; | 1 | module; |
| 2 | 2 | ||
| 3 | #include <boost/config.hpp> | ||
| 4 | #include <boost/asio/as_tuple.hpp> | 3 | #include <boost/asio/as_tuple.hpp> |
| 5 | #include <boost/asio/awaitable.hpp> | 4 | #include <boost/asio/awaitable.hpp> |
| 6 | #include <boost/asio/co_spawn.hpp> | 5 | #include <boost/asio/co_spawn.hpp> |
| 7 | #include <boost/asio/ip/tcp.hpp> | 6 | #include <boost/asio/ip/tcp.hpp> |
| 8 | #include <boost/beast/core.hpp> | 7 | #include <boost/beast/core.hpp> |
| 9 | #include <boost/beast/http.hpp> | 8 | #include <boost/beast/http.hpp> |
| 9 | #include <boost/config.hpp> | ||
| 10 | #include <boost/json/serialize.hpp> | 10 | #include <boost/json/serialize.hpp> |
| 11 | #include <boost/url.hpp> | 11 | #include <boost/url.hpp> |
| 12 | 12 | ||
| @@ -23,639 +23,836 @@ using tcp = net::ip::tcp; | |||
| 23 | 23 | ||
| 24 | namespace routemon::http { | 24 | namespace routemon::http { |
| 25 | 25 | ||
| 26 | struct readable_request { | 26 | struct readable_request |
| 27 | util::not_null<bhttp::request_parser<bhttp::empty_body>*> p; | 27 | { |
| 28 | util::not_null<beast::tcp_stream*> strm; | 28 | util::not_null<bhttp::request_parser<bhttp::empty_body>*> p; |
| 29 | util::not_null<beast::flat_buffer*> buf; | 29 | util::not_null<beast::tcp_stream*> strm; |
| 30 | }; | 30 | util::not_null<beast::flat_buffer*> buf; |
| 31 | }; | ||
| 31 | 32 | ||
| 32 | class presponse { | 33 | class presponse |
| 33 | public: | 34 | { |
| 34 | using const_buffers_type = beast::span<net::const_buffer>; | 35 | public: |
| 36 | using const_buffers_type = beast::span<net::const_buffer>; | ||
| 35 | 37 | ||
| 36 | private: | 38 | private: |
| 37 | struct impl_base { | 39 | struct impl_base |
| 38 | virtual ~impl_base() = default; | 40 | { |
| 39 | virtual auto header() -> bhttp::response_header<bhttp::fields>& = 0; | 41 | virtual ~impl_base() = default; |
| 40 | virtual auto header() const -> bhttp::response_header<bhttp::fields> const& = 0; | 42 | virtual auto header() -> bhttp::response_header<bhttp::fields>& = 0; |
| 41 | virtual auto is_done() const -> bool = 0; | 43 | virtual auto header() const |
| 42 | virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; | 44 | -> bhttp::response_header<bhttp::fields> const& = 0; |
| 43 | virtual auto consume(std::size_t n) -> void = 0; | 45 | virtual auto is_done() const -> bool = 0; |
| 44 | virtual auto keep_alive() const -> bool = 0; | 46 | virtual auto prepare(beast::error_code&) -> const_buffers_type = 0; |
| 45 | }; | 47 | virtual auto consume(std::size_t n) -> void = 0; |
| 46 | std::unique_ptr<impl_base> impl_; | 48 | virtual auto keep_alive() const -> bool = 0; |
| 49 | }; | ||
| 50 | std::unique_ptr<impl_base> impl_; | ||
| 47 | 51 | ||
| 48 | template<bhttp::concepts::body Body> | 52 | template <bhttp::concepts::body Body> |
| 49 | class impl : public impl_base { | 53 | class impl : public impl_base |
| 50 | // Initializes in the response state. | 54 | { |
| 51 | // At the first call to prepare, we switch to the message generator state. | 55 | // Initializes in the response state. |
| 52 | // After that point, header may not be called anymore (it will throw). | 56 | // At the first call to prepare, we switch to the message generator |
| 53 | std::variant<bhttp::response<Body>, bhttp::message_generator> state_; | 57 | // state. After that point, header may not be called anymore (it will |
| 58 | // throw). | ||
| 59 | std::variant<bhttp::response<Body>, bhttp::message_generator> state_; | ||
| 54 | 60 | ||
| 55 | auto ensure_message_generator() -> bhttp::message_generator& { | 61 | auto ensure_message_generator() -> bhttp::message_generator& |
| 56 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) { | 62 | { |
| 57 | auto rsp = bhttp::response<Body>{std::move(*prsp)}; | 63 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) |
| 58 | state_.template emplace<bhttp::message_generator>(std::move(rsp)); | 64 | { |
| 59 | } | 65 | auto rsp = bhttp::response<Body>{std::move(*prsp)}; |
| 60 | return std::get<bhttp::message_generator>(state_); | 66 | state_.template emplace<bhttp::message_generator>(std::move(rsp)); |
| 61 | } | 67 | } |
| 68 | return std::get<bhttp::message_generator>(state_); | ||
| 69 | } | ||
| 62 | 70 | ||
| 63 | public: | 71 | public: |
| 64 | explicit impl(bhttp::response<Body>&& rsp) : state_{std::move(rsp)} {} | 72 | explicit impl(bhttp::response<Body>&& rsp) : state_{std::move(rsp)} {} |
| 65 | 73 | ||
| 66 | auto header() -> bhttp::response_header<bhttp::fields>& override { | 74 | auto header() -> bhttp::response_header<bhttp::fields>& override |
| 67 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) { | 75 | { |
| 68 | return prsp->base(); | 76 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) |
| 69 | } else { | 77 | { |
| 70 | // TODO: define custom exception type presponse::bad_header_access | 78 | return prsp->base(); |
| 71 | throw std::logic_error{"header() may not be called after prepare()"}; | ||
| 72 | } | ||
| 73 | } | 79 | } |
| 74 | auto header() const -> bhttp::response_header<bhttp::fields> const& override { | 80 | else |
| 75 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) { | 81 | { |
| 76 | return prsp->base(); | 82 | // TODO: define custom exception type |
| 77 | } else { | 83 | // presponse::bad_header_access |
| 78 | throw std::logic_error{"header() may not be called after prepare()"}; | 84 | throw std::logic_error{"header() may not be called after prepare()"}; |
| 79 | } | ||
| 80 | } | 85 | } |
| 81 | 86 | } | |
| 82 | auto is_done() const -> bool override { | 87 | auto header() const -> bhttp::response_header<bhttp::fields> const& override |
| 83 | if (auto pgen = std::get_if<bhttp::message_generator>(&state_)) { | 88 | { |
| 84 | return pgen->is_done(); | 89 | if (auto prsp = std::get_if<bhttp::response<Body>>(&state_)) |
| 85 | } else /* still in the response state */ { | 90 | { |
| 86 | return false; | 91 | return prsp->base(); |
| 87 | } | ||
| 88 | } | 92 | } |
| 89 | 93 | else | |
| 90 | auto prepare(beast::error_code& ec) -> const_buffers_type override { | 94 | { |
| 91 | return ensure_message_generator().prepare(ec); | 95 | throw std::logic_error{"header() may not be called after prepare()"}; |
| 92 | } | 96 | } |
| 97 | } | ||
| 93 | 98 | ||
| 94 | auto consume(std::size_t n) -> void override { | 99 | auto is_done() const -> bool override |
| 95 | ensure_message_generator().consume(n); | 100 | { |
| 101 | if (auto pgen = std::get_if<bhttp::message_generator>(&state_)) | ||
| 102 | { | ||
| 103 | return pgen->is_done(); | ||
| 96 | } | 104 | } |
| 97 | 105 | else /* still in the response state */ | |
| 98 | auto keep_alive() const noexcept -> bool override { | 106 | { |
| 99 | return state_.visit(util::overloaded{ | 107 | return false; |
| 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 | } |
| 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 | } | 109 | } |
| 126 | 110 | ||
| 127 | auto prepare(beast::error_code& ec) -> const_buffers_type { | 111 | auto prepare(beast::error_code& ec) -> const_buffers_type override |
| 128 | return impl_->prepare(ec); | 112 | { |
| 113 | return ensure_message_generator().prepare(ec); | ||
| 129 | } | 114 | } |
| 130 | 115 | ||
| 131 | auto consume(std::size_t n) -> void { | 116 | auto consume(std::size_t n) -> void override |
| 132 | return impl_->consume(n); | 117 | { |
| 118 | ensure_message_generator().consume(n); | ||
| 133 | } | 119 | } |
| 134 | 120 | ||
| 135 | auto keep_alive() const noexcept -> bool { | 121 | auto keep_alive() const noexcept -> bool override |
| 136 | return impl_->keep_alive(); | 122 | { |
| 123 | return state_.visit( | ||
| 124 | util::overloaded{ | ||
| 125 | [](bhttp::response<Body> const& rsp) -> bool | ||
| 126 | { return rsp.keep_alive(); }, | ||
| 127 | [](bhttp::message_generator const& gen) -> bool | ||
| 128 | { return gen.keep_alive(); }, | ||
| 129 | }); | ||
| 137 | } | 130 | } |
| 138 | }; | 131 | }; |
| 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 | 132 | ||
| 144 | template<class OuterCtx, class InnerCtx> | 133 | public: |
| 145 | using middleware_t = std::function<auto(OuterCtx, bhttp::request_header<bhttp::fields>&, next_handler_t<InnerCtx>) -> net::awaitable<presponse>>; | 134 | template <bhttp::concepts::body Body> |
| 135 | explicit presponse(bhttp::response<Body>&& rsp) | ||
| 136 | : impl_{new impl{std::move(rsp)}} | ||
| 137 | { | ||
| 138 | } | ||
| 146 | 139 | ||
| 147 | template<class Ctx> | 140 | auto header() -> bhttp::response_header<bhttp::fields>& |
| 148 | auto lax_cors_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>& req_hdr, next_handler_t<Ctx> next) -> net::awaitable<presponse> { | 141 | { |
| 149 | std::ignore = req_hdr; | 142 | return impl_->header(); |
| 150 | auto prersp = co_await next(ctx); | 143 | } |
| 151 | prersp.header().set(bhttp::field::access_control_allow_origin, "*"); | 144 | auto header() const -> bhttp::response_header<bhttp::fields> const& |
| 152 | co_return std::move(prersp); | 145 | { |
| 146 | return impl_->header(); | ||
| 153 | } | 147 | } |
| 154 | 148 | ||
| 155 | template<class InnerCtx> | 149 | auto is_done() const -> bool { return impl_->is_done(); } |
| 156 | struct trace_id_ctx : InnerCtx { | ||
| 157 | trace::id trace_id = {}; | ||
| 158 | }; | ||
| 159 | 150 | ||
| 160 | template<class OuterCtx> | 151 | auto prepare(beast::error_code& ec) -> const_buffers_type |
| 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> { | 152 | { |
| 162 | std::ignore = req_hdr; | 153 | return impl_->prepare(ec); |
| 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 | } | 154 | } |
| 169 | 155 | ||
| 170 | struct base_ctx { | 156 | auto consume(std::size_t n) -> void { return impl_->consume(n); } |
| 171 | std::locale locale; | ||
| 172 | }; | ||
| 173 | 157 | ||
| 174 | template<class Ctx> | 158 | auto keep_alive() const noexcept -> bool { return impl_->keep_alive(); } |
| 175 | using basic_route_handler_fn_t = std::function<auto(Ctx, readable_request, std::vector<std::string> const& matches) -> net::awaitable<presponse>>; | 159 | }; |
| 160 | static_assert(beast::concepts::buffers_generator<presponse>); | ||
| 176 | 161 | ||
| 177 | struct keep_alive { | 162 | template <class Ctx> |
| 178 | bool value; | 163 | using next_handler_t = std::function<auto(Ctx)->net::awaitable<presponse>>; |
| 179 | 164 | ||
| 180 | explicit keep_alive(bool value) : value{value} {} | 165 | template <class OuterCtx, class InnerCtx> |
| 181 | }; | 166 | using middleware_t = |
| 167 | std::function<auto( | ||
| 168 | OuterCtx, bhttp::request_header<bhttp::fields>&, | ||
| 169 | next_handler_t<InnerCtx>) | ||
| 170 | ->net::awaitable<presponse>>; | ||
| 182 | 171 | ||
| 183 | template<bhttp::concepts::body Body> | 172 | template <class Ctx> |
| 184 | auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> { | 173 | auto lax_cors_middleware( |
| 185 | auto rsp = bhttp::response<Body>{}; // HTTP version gets set later | 174 | Ctx ctx, bhttp::request_header<bhttp::fields>& req_hdr, |
| 186 | rsp.result(status); | 175 | next_handler_t<Ctx> next) -> net::awaitable<presponse> |
| 187 | rsp.keep_alive(ka.value); | 176 | { |
| 188 | return rsp; | 177 | std::ignore = req_hdr; |
| 189 | } | 178 | auto prersp = co_await next(ctx); |
| 179 | prersp.header().set(bhttp::field::access_control_allow_origin, "*"); | ||
| 180 | co_return std::move(prersp); | ||
| 181 | } | ||
| 190 | 182 | ||
| 191 | auto problem_rsp(base_ctx const& ctx, problem::details const& problem, keep_alive ka) -> presponse { | 183 | template <class InnerCtx> |
| 192 | auto rsp = make_rsp<bhttp::string_body>(problem.status, ka); | 184 | struct trace_id_ctx : InnerCtx |
| 193 | rsp.set(bhttp::field::content_type, "application/problem+json"); | 185 | { |
| 194 | rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); | 186 | trace::id trace_id = {}; |
| 195 | rsp.prepare_payload(); | 187 | }; |
| 196 | return presponse{std::move(rsp)}; | ||
| 197 | } | ||
| 198 | 188 | ||
| 199 | struct preflight_response { | 189 | template <class OuterCtx> |
| 200 | verb_set allow_methods; | 190 | auto trace_id_middleware( |
| 201 | std::vector<bhttp::field> allow_headers; | 191 | OuterCtx ctx0, bhttp::request_header<bhttp::fields>& req_hdr, |
| 202 | }; | 192 | next_handler_t<trace_id_ctx<OuterCtx>> next) -> net::awaitable<presponse> |
| 203 | auto make_preflight_rsp(preflight_response res, keep_alive ka) -> bhttp::response<bhttp::empty_body> { | 193 | { |
| 204 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka); | 194 | std::ignore = req_hdr; |
| 205 | auto allow_headers_str = res.allow_headers | 195 | auto ctx = trace_id_ctx{std::move(ctx0)}; |
| 206 | | std::views::transform([](auto const& field) -> std::string_view { return bhttp::to_string(field); }) | 196 | auto prersp = co_await next(std::move(ctx)); |
| 207 | | std::views::join_with(std::string_view{", "}) | 197 | prersp.header().set( |
| 208 | | std::ranges::to<std::string>(); | 198 | "X-Routemon-Trace-Id", std::string_view{ctx.trace_id.as_string()}); |
| 209 | rsp.set(bhttp::field::access_control_allow_methods, res.allow_methods.to_string()); | 199 | prersp.header().insert( |
| 210 | rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); | 200 | bhttp::field::access_control_expose_headers, "X-Routemon-Trace-Id"); |
| 211 | rsp.prepare_payload(); | 201 | co_return std::move(prersp); |
| 212 | return rsp; | 202 | } |
| 213 | } | ||
| 214 | 203 | ||
| 215 | // Using base_ctx instead of a template here since that saves you | 204 | struct base_ctx |
| 216 | // typing on invocation (and we do not care about the context type | 205 | { |
| 217 | // anyway, but all context types should derive from base_ctx). | 206 | std::locale locale; |
| 218 | template<bhttp::concepts::body_reader Body> | 207 | }; |
| 219 | auto read_request(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<Body>, presponse>> { | 208 | |
| 220 | std::ignore = ctx; | 209 | template <class Ctx> |
| 221 | auto p = bhttp::request_parser<Body>{std::move(*r.p)}; | 210 | using basic_route_handler_fn_t = std::function< |
| 222 | co_await bhttp::async_read(*r.strm, *r.buf, p); | 211 | auto(Ctx, readable_request, std::vector<std::string> const& matches) |
| 223 | co_return std::move(p.release()); | 212 | ->net::awaitable<presponse>>; |
| 224 | } | 213 | |
| 214 | struct keep_alive | ||
| 215 | { | ||
| 216 | bool value; | ||
| 225 | 217 | ||
| 226 | template<> | 218 | explicit keep_alive(bool value) : value{value} {} |
| 227 | auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) -> net::awaitable<std::expected<bhttp::request<bhttp::empty_body>, presponse>> { | 219 | }; |
| 228 | auto [ec, _] = co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); | 220 | |
| 229 | if (ec == bhttp::error::unexpected_body) { | 221 | template <bhttp::concepts::body Body> |
| 230 | auto tpl = problem::tpl{ | 222 | auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> |
| 231 | .status = bhttp::status::bad_request, | 223 | { |
| 232 | .title = translate("No body expected for this request"), | 224 | auto rsp = bhttp::response<Body>{}; // HTTP version gets set later |
| 225 | rsp.result(status); | ||
| 226 | rsp.keep_alive(ka.value); | ||
| 227 | return rsp; | ||
| 228 | } | ||
| 229 | |||
| 230 | auto problem_rsp( | ||
| 231 | base_ctx const& ctx, problem::details const& problem, keep_alive ka) | ||
| 232 | -> presponse | ||
| 233 | { | ||
| 234 | auto rsp = make_rsp<bhttp::string_body>(problem.status, ka); | ||
| 235 | rsp.set(bhttp::field::content_type, "application/problem+json"); | ||
| 236 | rsp.body() = json::serialize(json::value_from(problem, ctx.locale)); | ||
| 237 | rsp.prepare_payload(); | ||
| 238 | return presponse{std::move(rsp)}; | ||
| 239 | } | ||
| 240 | |||
| 241 | struct preflight_response | ||
| 242 | { | ||
| 243 | verb_set allow_methods; | ||
| 244 | std::vector<bhttp::field> allow_headers; | ||
| 245 | }; | ||
| 246 | auto make_preflight_rsp(preflight_response res, keep_alive ka) | ||
| 247 | -> bhttp::response<bhttp::empty_body> | ||
| 248 | { | ||
| 249 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, ka); | ||
| 250 | auto allow_headers_str = res.allow_headers | ||
| 251 | | std::views::transform( | ||
| 252 | [](auto const& field) -> std::string_view | ||
| 253 | { return bhttp::to_string(field); }) | ||
| 254 | | std::views::join_with(std::string_view{", "}) | ||
| 255 | | std::ranges::to<std::string>(); | ||
| 256 | rsp.set( | ||
| 257 | bhttp::field::access_control_allow_methods, | ||
| 258 | res.allow_methods.to_string()); | ||
| 259 | rsp.set(bhttp::field::access_control_allow_headers, allow_headers_str); | ||
| 260 | rsp.prepare_payload(); | ||
| 261 | return rsp; | ||
| 262 | } | ||
| 263 | |||
| 264 | // Using base_ctx instead of a template here since that saves you | ||
| 265 | // typing on invocation (and we do not care about the context type | ||
| 266 | // anyway, but all context types should derive from base_ctx). | ||
| 267 | template <bhttp::concepts::body_reader Body> | ||
| 268 | auto read_request(base_ctx const& ctx, readable_request&& r) | ||
| 269 | -> net::awaitable<std::expected<bhttp::request<Body>, presponse>> | ||
| 270 | { | ||
| 271 | std::ignore = ctx; | ||
| 272 | auto p = bhttp::request_parser<Body>{std::move(*r.p)}; | ||
| 273 | co_await bhttp::async_read(*r.strm, *r.buf, p); | ||
| 274 | co_return std::move(p.release()); | ||
| 275 | } | ||
| 276 | |||
| 277 | template <> | ||
| 278 | auto read_request<bhttp::empty_body>(base_ctx const& ctx, readable_request&& r) | ||
| 279 | -> net::awaitable< | ||
| 280 | std::expected<bhttp::request<bhttp::empty_body>, presponse>> | ||
| 281 | { | ||
| 282 | auto [ec, _] = | ||
| 283 | co_await bhttp::async_read(*r.strm, *r.buf, *r.p, net::as_tuple); | ||
| 284 | if (ec == bhttp::error::unexpected_body) | ||
| 285 | { | ||
| 286 | auto tpl = problem::tpl{ | ||
| 287 | .status = bhttp::status::bad_request, | ||
| 288 | .title = translate("No body expected for this request"), | ||
| 233 | .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body", | 289 | .type_uri = "https://routemon.fautchen.eu/problems/unexpected-body", |
| 234 | }; | 290 | }; |
| 235 | co_return std::unexpected{problem_rsp(ctx, tpl.instantiate(), keep_alive{false})}; | 291 | co_return std::unexpected{problem_rsp( |
| 236 | } else if (ec) { | 292 | ctx, tpl.instantiate(), keep_alive{false})}; |
| 237 | throw boost::system::system_error{ec}; | ||
| 238 | } | ||
| 239 | co_return r.p->release(); | ||
| 240 | } | 293 | } |
| 294 | else if (ec) | ||
| 295 | { | ||
| 296 | throw boost::system::system_error{ec}; | ||
| 297 | } | ||
| 298 | co_return r.p->release(); | ||
| 299 | } | ||
| 241 | 300 | ||
| 242 | template<class InnerCtx> | 301 | template <class InnerCtx> |
| 243 | struct routed_ctx : InnerCtx { | 302 | struct routed_ctx : InnerCtx |
| 244 | verb_set route_methods; | 303 | { |
| 245 | }; | 304 | verb_set route_methods; |
| 305 | }; | ||
| 246 | 306 | ||
| 247 | template<class Ctx> | 307 | template <class Ctx> |
| 248 | requires requires(Ctx ctx) { | 308 | requires requires(Ctx ctx) { |
| 249 | // Ctx must be derived from an instantiation of routed_ctx | 309 | // Ctx must be derived from an instantiation of routed_ctx |
| 250 | []<class InnerCtx>(routed_ctx<InnerCtx> const&) {}(ctx); | 310 | []<class InnerCtx>(routed_ctx<InnerCtx> const&) {}(ctx); |
| 251 | } | 311 | } |
| 252 | auto default_options_handler(Ctx const& ctx, readable_request r, std::vector<std::string> const&) -> net::awaitable<presponse> { | 312 | auto default_options_handler( |
| 253 | auto mreq = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); | 313 | Ctx const& ctx, readable_request r, std::vector<std::string> const&) |
| 254 | if (!mreq) | 314 | -> net::awaitable<presponse> |
| 255 | co_return std::move(mreq.error()); | 315 | { |
| 316 | auto mreq = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 317 | if (!mreq) | ||
| 318 | co_return std::move(mreq.error()); | ||
| 256 | 319 | ||
| 257 | if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) { | 320 | if (mreq->find(bhttp::field::access_control_request_method) != mreq->end()) |
| 258 | // CORS preflight request | 321 | { |
| 259 | co_return make_preflight_rsp(preflight_response{ | 322 | // CORS preflight request |
| 260 | // TODO: should access-control-allow-methods contain OPTIONS? | 323 | co_return make_preflight_rsp( |
| 261 | .allow_methods = ctx.route_methods, | 324 | preflight_response{ |
| 262 | .allow_headers = {bhttp::field::content_type}, | 325 | // TODO: should access-control-allow-methods contain OPTIONS? |
| 263 | }, keep_alive{mreq->keep_alive()}); | 326 | .allow_methods = ctx.route_methods, |
| 264 | } else { | 327 | .allow_headers = {bhttp::field::content_type}, |
| 265 | // Normal OPTIONS request | 328 | }, |
| 266 | auto rsp = make_rsp<bhttp::empty_body>(bhttp::status::no_content, keep_alive{mreq->keep_alive()}); | 329 | 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 | } | 330 | } |
| 272 | 331 | else | |
| 273 | auto global_options_handler(base_ctx const& ctx, readable_request r) -> net::awaitable<presponse> { | 332 | { |
| 274 | // TODO: switch to "small (4KB) discarded" body type, similar to what Go does? | 333 | // Normal OPTIONS request |
| 275 | // Same goes for default_options_handler? Not sure. | 334 | auto rsp = make_rsp<bhttp::empty_body>( |
| 276 | if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); !res) | 335 | bhttp::status::no_content, keep_alive{mreq->keep_alive()}); |
| 277 | co_return std::move(res.error()); | 336 | rsp.set(bhttp::field::allow, ctx.route_methods.to_string()); |
| 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(); | 337 | rsp.prepare_payload(); |
| 281 | co_return std::move(rsp); | 338 | co_return std::move(rsp); |
| 282 | } | 339 | } |
| 340 | } | ||
| 283 | 341 | ||
| 284 | template<class Ctx> | 342 | auto global_options_handler(base_ctx const& ctx, readable_request r) |
| 285 | auto id_middleware(Ctx ctx, bhttp::request_header<bhttp::fields>&, next_handler_t<Ctx> next) -> net::awaitable<presponse> { | 343 | -> net::awaitable<presponse> |
| 286 | co_return co_await next(std::move(ctx)); | 344 | { |
| 287 | } | 345 | // TODO: switch to "small (4KB) discarded" body type, similar to what Go |
| 346 | // does? Same goes for default_options_handler? Not sure. | ||
| 347 | if (auto res = co_await read_request<bhttp::empty_body>(ctx, std::move(r)); | ||
| 348 | !res) | ||
| 349 | co_return std::move(res.error()); | ||
| 350 | auto req = r.p->release(); | ||
| 351 | auto rsp = make_rsp<bhttp::empty_body>( | ||
| 352 | bhttp::status::no_content, keep_alive{req.keep_alive()}); | ||
| 353 | rsp.prepare_payload(); | ||
| 354 | co_return std::move(rsp); | ||
| 355 | } | ||
| 288 | 356 | ||
| 289 | template<class A, class B, class C> | 357 | template <class Ctx> |
| 290 | auto middleware_compose(middleware_t<A, B> ab, middleware_t<B, C> bc) -> middleware_t<A, C> { | 358 | auto id_middleware( |
| 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> { | 359 | Ctx ctx, bhttp::request_header<bhttp::fields>&, next_handler_t<Ctx> next) |
| 292 | co_return co_await ab(std::move(a), header, [&](B b) -> net::awaitable<presponse> { | 360 | -> net::awaitable<presponse> |
| 293 | co_return co_await bc(std::move(b), header, next); | 361 | { |
| 294 | }); | 362 | co_return co_await next(std::move(ctx)); |
| 295 | }; | 363 | } |
| 296 | } | ||
| 297 | 364 | ||
| 298 | template<class A, class B> | 365 | template <class A, class B, class C> |
| 299 | auto middleware_wrap_fn(middleware_t<A, B> ab, basic_route_handler_fn_t<B> fn) -> basic_route_handler_fn_t<A> { | 366 | auto middleware_compose(middleware_t<A, B> ab, middleware_t<B, C> bc) |
| 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> { | 367 | -> middleware_t<A, C> |
| 301 | co_return co_await ab(std::move(a_ctx), r.p->get().base(), [&](B b_ctx) -> net::awaitable<presponse> { | 368 | { |
| 302 | co_return co_await fn(std::move(b_ctx), r, matches); | 369 | return [ab = std::move(ab), bc = std::move(bc)]( |
| 303 | }); | 370 | A a, bhttp::request_header<bhttp::fields>& header, |
| 304 | }; | 371 | next_handler_t<C> next) -> net::awaitable<presponse> |
| 305 | } | 372 | { |
| 373 | co_return co_await ab( | ||
| 374 | std::move(a), header, [&](B b) -> net::awaitable<presponse> | ||
| 375 | { co_return co_await bc(std::move(b), header, next); }); | ||
| 376 | }; | ||
| 377 | } | ||
| 378 | |||
| 379 | template <class A, class B> | ||
| 380 | auto middleware_wrap_fn(middleware_t<A, B> ab, basic_route_handler_fn_t<B> fn) | ||
| 381 | -> basic_route_handler_fn_t<A> | ||
| 382 | { | ||
| 383 | return | ||
| 384 | [ab = std::move(ab), fn = std::move(fn)]( | ||
| 385 | A a_ctx, readable_request r, | ||
| 386 | std::vector<std::string> const& matches) -> net::awaitable<presponse> | ||
| 387 | { | ||
| 388 | co_return co_await ab( | ||
| 389 | std::move(a_ctx), r.p->get().base(), | ||
| 390 | [&](B b_ctx) -> net::awaitable<presponse> | ||
| 391 | { co_return co_await fn(std::move(b_ctx), r, matches); }); | ||
| 392 | }; | ||
| 393 | } | ||
| 306 | 394 | ||
| 307 | template<std::default_initializable V> | 395 | template <std::default_initializable V> |
| 308 | requires requires(V v) { | 396 | requires requires(V v) { |
| 309 | { static_cast<bool>(v) }; | 397 | { static_cast<bool>(v) }; |
| 310 | } | 398 | } |
| 311 | struct handler_map { | 399 | struct handler_map |
| 312 | V options = {}; | 400 | { |
| 313 | V delete_ = {}; | 401 | V options = {}; |
| 314 | V get = {}; | 402 | V delete_ = {}; |
| 315 | V head = {}; | 403 | V get = {}; |
| 316 | V post = {}; | 404 | V head = {}; |
| 317 | V put = {}; | 405 | V post = {}; |
| 406 | V put = {}; | ||
| 318 | 407 | ||
| 319 | template<class Self> | 408 | template <class Self> |
| 320 | auto lookup(this Self&& self, supported_verb v) -> auto&& { | 409 | auto lookup(this Self&& self, supported_verb v) -> auto&& |
| 321 | switch (v.value) { | 410 | { |
| 322 | case supported_verb::options: return std::forward<Self>(self).options; | 411 | switch (v.value) |
| 323 | case supported_verb::delete_: return std::forward<Self>(self).delete_; | 412 | { |
| 324 | case supported_verb::get: return std::forward<Self>(self).get; | 413 | case supported_verb::options: |
| 325 | case supported_verb::head: return std::forward<Self>(self).head; | 414 | return std::forward<Self>(self).options; |
| 326 | case supported_verb::post: return std::forward<Self>(self).post; | 415 | case supported_verb::delete_: |
| 327 | case supported_verb::put: return std::forward<Self>(self).put; | 416 | return std::forward<Self>(self).delete_; |
| 328 | } | 417 | case supported_verb::get: |
| 418 | return std::forward<Self>(self).get; | ||
| 419 | case supported_verb::head: | ||
| 420 | return std::forward<Self>(self).head; | ||
| 421 | case supported_verb::post: | ||
| 422 | return std::forward<Self>(self).post; | ||
| 423 | case supported_verb::put: | ||
| 424 | return std::forward<Self>(self).put; | ||
| 329 | } | 425 | } |
| 426 | } | ||
| 330 | 427 | ||
| 331 | auto verbs() const -> verb_set { | 428 | auto verbs() const -> verb_set |
| 332 | auto set = verb_set{}; | 429 | { |
| 333 | if (static_cast<bool>(options)) | 430 | auto set = verb_set{}; |
| 334 | set.enable(supported_verb::options); | 431 | if (static_cast<bool>(options)) |
| 335 | if (static_cast<bool>(delete_)) | 432 | set.enable(supported_verb::options); |
| 336 | set.enable(supported_verb::delete_); | 433 | if (static_cast<bool>(delete_)) |
| 337 | if (static_cast<bool>(get)) | 434 | set.enable(supported_verb::delete_); |
| 338 | set.enable(supported_verb::get); | 435 | if (static_cast<bool>(get)) |
| 339 | if (static_cast<bool>(head)) | 436 | set.enable(supported_verb::get); |
| 340 | set.enable(supported_verb::head); | 437 | if (static_cast<bool>(head)) |
| 341 | if (static_cast<bool>(post)) | 438 | set.enable(supported_verb::head); |
| 342 | set.enable(supported_verb::post); | 439 | if (static_cast<bool>(post)) |
| 343 | if (static_cast<bool>(put)) | 440 | set.enable(supported_verb::post); |
| 344 | set.enable(supported_verb::put); | 441 | if (static_cast<bool>(put)) |
| 345 | return set; | 442 | set.enable(supported_verb::put); |
| 346 | } | 443 | return set; |
| 444 | } | ||
| 347 | 445 | ||
| 348 | auto empty() const -> bool { | 446 | auto empty() const -> bool { return verbs().empty(); } |
| 349 | return verbs().empty(); | ||
| 350 | } | ||
| 351 | 447 | ||
| 352 | template<std::default_initializable U> | 448 | template <std::default_initializable U> |
| 353 | auto map(std::invocable<V const&> auto f) const -> handler_map<U> | 449 | 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&>> | 450 | requires std::assignable_from< |
| 355 | { | 451 | U&, std::invoke_result_t<decltype(f), V const&>> |
| 356 | return { | 452 | { |
| 453 | return { | ||
| 357 | .options = static_cast<bool>(options) ? f(options) : U{}, | 454 | .options = static_cast<bool>(options) ? f(options) : U{}, |
| 358 | .delete_ = static_cast<bool>(delete_) ? f(delete_) : U{}, | 455 | .delete_ = static_cast<bool>(delete_) ? f(delete_) : U{}, |
| 359 | .get = static_cast<bool>(get) ? f(get) : U{}, | 456 | .get = static_cast<bool>(get) ? f(get) : U{}, |
| 360 | .head = static_cast<bool>(head) ? f(head) : U{}, | 457 | .head = static_cast<bool>(head) ? f(head) : U{}, |
| 361 | .post = static_cast<bool>(post) ? f(post) : U{}, | 458 | .post = static_cast<bool>(post) ? f(post) : U{}, |
| 362 | .put = static_cast<bool>(put) ? f(put) : U{}, | 459 | .put = static_cast<bool>(put) ? f(put) : U{}, |
| 363 | }; | 460 | }; |
| 364 | } | 461 | } |
| 365 | }; | 462 | }; |
| 366 | 463 | ||
| 367 | template<class Ctx> | 464 | template <class Ctx> |
| 368 | struct route_tree { | 465 | struct route_tree |
| 369 | using leaves = handler_map<basic_route_handler_fn_t<Ctx>>; | 466 | { |
| 370 | using named_subtrees = std::unordered_map<std::string, route_tree>; | 467 | using leaves = handler_map<basic_route_handler_fn_t<Ctx>>; |
| 371 | using wildcard_subtree = std::indirect<route_tree>; | 468 | using named_subtrees = std::unordered_map<std::string, route_tree>; |
| 469 | using wildcard_subtree = std::indirect<route_tree>; | ||
| 372 | 470 | ||
| 373 | leaves here; | 471 | leaves here; |
| 374 | // TODO: consider making the first alternative a radix tree | 472 | // TODO: consider making the first alternative a radix tree |
| 375 | // Note: the map is the first variant here; the variant will be | 473 | // Note: the map is the first variant here; the variant will be |
| 376 | // default-constructed with the default-constructed first | 474 | // default-constructed with the default-constructed first |
| 377 | // alternative. The empty map denotes a lack of subtrees. | 475 | // alternative. The empty map denotes a lack of subtrees. |
| 378 | std::variant<named_subtrees, wildcard_subtree> sub; | 476 | std::variant<named_subtrees, wildcard_subtree> sub; |
| 379 | }; | 477 | }; |
| 380 | 478 | ||
| 381 | template<class OuterCtx, class InnerCtx> | 479 | template <class OuterCtx, class InnerCtx> |
| 382 | auto middleware_wrap_tree(middleware_t<OuterCtx, InnerCtx> mw, route_tree<InnerCtx> const& tree) -> route_tree<OuterCtx> { | 480 | auto middleware_wrap_tree( |
| 383 | auto new_leaves = tree.here.template map<basic_route_handler_fn_t<OuterCtx>>(std::bind_front(middleware_wrap_fn<OuterCtx, InnerCtx>, mw)); | 481 | middleware_t<OuterCtx, InnerCtx> mw, route_tree<InnerCtx> const& tree) |
| 384 | auto new_sub = tree.sub.visit(util::overloaded{ | 482 | -> route_tree<OuterCtx> |
| 385 | [&mw](route_tree<InnerCtx>::named_subtrees const& subtrees) -> decltype(route_tree<OuterCtx>::sub) { | 483 | { |
| 386 | auto new_subtrees = typename route_tree<OuterCtx>::named_subtrees{}; | 484 | auto new_leaves = tree.here.template map<basic_route_handler_fn_t<OuterCtx>>( |
| 387 | for (auto [seg, subtree] : subtrees) | 485 | std::bind_front(middleware_wrap_fn<OuterCtx, InnerCtx>, mw)); |
| 388 | new_subtrees[seg] = middleware_wrap_tree(mw, subtree); | 486 | auto new_sub = tree.sub.visit( |
| 389 | return new_subtrees; | 487 | util::overloaded{ |
| 390 | }, | 488 | [&mw](route_tree<InnerCtx>::named_subtrees const& subtrees) |
| 391 | [&mw](route_tree<InnerCtx>::wildcard_subtree const& subtree) -> decltype(route_tree<OuterCtx>::sub) { | 489 | -> decltype(route_tree<OuterCtx>::sub) |
| 392 | return typename route_tree<OuterCtx>::wildcard_subtree{middleware_wrap_tree(mw, *subtree)}; | 490 | { |
| 393 | }, | 491 | auto new_subtrees = typename route_tree<OuterCtx>::named_subtrees{}; |
| 492 | for (auto [seg, subtree] : subtrees) | ||
| 493 | new_subtrees[seg] = middleware_wrap_tree(mw, subtree); | ||
| 494 | return new_subtrees; | ||
| 495 | }, | ||
| 496 | [&mw](route_tree<InnerCtx>::wildcard_subtree const& subtree) | ||
| 497 | -> decltype(route_tree<OuterCtx>::sub) | ||
| 498 | { | ||
| 499 | return typename route_tree<OuterCtx>::wildcard_subtree{ | ||
| 500 | middleware_wrap_tree(mw, *subtree) | ||
| 501 | }; | ||
| 502 | }, | ||
| 394 | }); | 503 | }); |
| 395 | return {.here = new_leaves, .sub = new_sub}; | 504 | return {.here = new_leaves, .sub = new_sub}; |
| 396 | } | 505 | } |
| 397 | 506 | ||
| 398 | template<class T> | 507 | template <class T> |
| 399 | concept match_arg = std::constructible_from<T, std::string const&>; | 508 | concept match_arg = std::constructible_from<T, std::string const&>; |
| 400 | 509 | ||
| 401 | template<class Ctx, match_arg... MatchArgs> | 510 | template <class Ctx, match_arg... MatchArgs> |
| 402 | using route_handler_fn_t = std::function<auto(Ctx, readable_request, MatchArgs...) -> net::awaitable<presponse>>; | 511 | using route_handler_fn_t = std::function< |
| 512 | auto(Ctx, readable_request, MatchArgs...)->net::awaitable<presponse>>; | ||
| 403 | 513 | ||
| 404 | template<class Ctx, match_arg... MatchArgs> | 514 | template <class Ctx, match_arg... MatchArgs> |
| 405 | auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) -> basic_route_handler_fn_t<Ctx> { | 515 | auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) |
| 406 | return [fn = std::move(fn)](Ctx ctx, readable_request r, std::vector<std::string> const& matches) -> net::awaitable<presponse> { | 516 | -> basic_route_handler_fn_t<Ctx> |
| 407 | if (sizeof...(MatchArgs) != matches.size()) | 517 | { |
| 408 | throw std::runtime_error{"got unexpected amount of matches"}; | 518 | return |
| 409 | auto it = matches.begin(); | 519 | [fn = std::move(fn)]( |
| 410 | co_return co_await fn(std::move(ctx), r, MatchArgs{static_cast<std::string const&>(*it++)}...); | 520 | Ctx ctx, readable_request r, |
| 411 | }; | 521 | std::vector<std::string> const& matches) -> net::awaitable<presponse> |
| 412 | } | 522 | { |
| 413 | 523 | if (sizeof...(MatchArgs) != matches.size()) | |
| 414 | template<class Ctx, match_arg... MatchArgs> | 524 | throw std::runtime_error{"got unexpected amount of matches"}; |
| 415 | struct ctree : route_tree<Ctx> { | 525 | auto it = matches.begin(); |
| 416 | template<class OuterCtx> | 526 | co_return co_await fn( |
| 417 | auto wrap(middleware_t<OuterCtx, Ctx> mw) const -> ctree<OuterCtx, MatchArgs...> { | 527 | std::move(ctx), r, |
| 418 | return {middleware_wrap_tree(std::move(mw), *this)}; | 528 | MatchArgs{static_cast<std::string const&>(*it++)}...); |
| 419 | } | ||
| 420 | }; | 529 | }; |
| 530 | } | ||
| 421 | 531 | ||
| 422 | template<class Ctx, match_arg... MatchArgs> | 532 | template <class Ctx, match_arg... MatchArgs> |
| 423 | struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> { | 533 | struct ctree : route_tree<Ctx> |
| 424 | [[nodiscard]] auto to_leaves() const -> typename route_tree<Ctx>::leaves { | 534 | { |
| 425 | auto here = this->template map<basic_route_handler_fn_t<Ctx>>(degen_route_handler<Ctx, MatchArgs...>); | 535 | template <class OuterCtx> |
| 426 | if (!here.verbs().empty() && !static_cast<bool>(this->options)) | 536 | auto wrap(middleware_t<OuterCtx, Ctx> mw) const |
| 427 | here.options = default_options_handler<Ctx>; | 537 | -> ctree<OuterCtx, MatchArgs...> |
| 428 | return here; | 538 | { |
| 429 | } | 539 | return {middleware_wrap_tree(std::move(mw), *this)}; |
| 540 | } | ||
| 541 | }; | ||
| 430 | 542 | ||
| 431 | [[nodiscard]] auto named_subtrees(std::initializer_list<std::pair<std::string, ctree<Ctx, MatchArgs...>>> subtrees) const -> ctree<Ctx, MatchArgs...> { | 543 | template <class Ctx, match_arg... MatchArgs> |
| 432 | auto sub = typename route_tree<Ctx>::named_subtrees{ | 544 | struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> |
| 433 | std::from_range, | 545 | { |
| 434 | subtrees | std::views::transform([](auto const& p) { | 546 | [[nodiscard]] auto to_leaves() const -> typename route_tree<Ctx>::leaves |
| 435 | return std::make_pair(p.first, static_cast<route_tree<Ctx>>(p.second)); | 547 | { |
| 436 | }) | 548 | auto here = this->template map<basic_route_handler_fn_t<Ctx>>( |
| 437 | }; | 549 | degen_route_handler<Ctx, MatchArgs...>); |
| 438 | return {route_tree<Ctx>{.here = to_leaves(), .sub = sub}}; | 550 | if (!here.verbs().empty() && !static_cast<bool>(this->options)) |
| 439 | } | 551 | here.options = default_options_handler<Ctx>; |
| 552 | return here; | ||
| 553 | } | ||
| 440 | 554 | ||
| 441 | template<match_arg MatchArg> | 555 | [[nodiscard]] auto named_subtrees( |
| 442 | [[nodiscard]] auto wildcard_subtree(ctree<Ctx, MatchArgs..., MatchArg> subtree) -> ctree<Ctx, MatchArgs...> { | 556 | std::initializer_list<std::pair<std::string, ctree<Ctx, MatchArgs...>>> |
| 443 | return {route_tree<Ctx>{.here = to_leaves(), .sub = typename route_tree<Ctx>::wildcard_subtree{static_cast<route_tree<Ctx>>(subtree)}}}; | 557 | subtrees) const -> ctree<Ctx, MatchArgs...> |
| 444 | } | 558 | { |
| 559 | auto sub = typename route_tree<Ctx>::named_subtrees{ | ||
| 560 | std::from_range, subtrees | ||
| 561 | | std::views::transform( | ||
| 562 | [](auto const& p) | ||
| 563 | { | ||
| 564 | return std::make_pair( | ||
| 565 | p.first, | ||
| 566 | static_cast<route_tree<Ctx>>(p.second)); | ||
| 567 | }) | ||
| 568 | }; | ||
| 569 | return {route_tree<Ctx>{.here = to_leaves(), .sub = sub}}; | ||
| 570 | } | ||
| 445 | 571 | ||
| 446 | [[nodiscard]] auto no_subtrees() const -> ctree<Ctx, MatchArgs...> { | 572 | template <match_arg MatchArg> |
| 447 | return {route_tree<Ctx>{.here = to_leaves(), .sub = {}}}; | 573 | [[nodiscard]] auto |
| 448 | } | 574 | wildcard_subtree(ctree<Ctx, MatchArgs..., MatchArg> subtree) |
| 449 | }; | 575 | -> ctree<Ctx, MatchArgs...> |
| 576 | { | ||
| 577 | return {route_tree<Ctx>{ | ||
| 578 | .here = to_leaves(), | ||
| 579 | .sub = typename route_tree<Ctx>::wildcard_subtree{ | ||
| 580 | static_cast<route_tree<Ctx>>(subtree) | ||
| 581 | } | ||
| 582 | }}; | ||
| 583 | } | ||
| 450 | 584 | ||
| 451 | template<std::derived_from<base_ctx> PreRouteCtx> | 585 | [[nodiscard]] auto no_subtrees() const -> ctree<Ctx, MatchArgs...> |
| 452 | class server { | 586 | { |
| 453 | log::logger l_; | 587 | return {route_tree<Ctx>{.here = to_leaves(), .sub = {}}}; |
| 454 | locale::selector lsel_; | 588 | } |
| 455 | middleware_t<base_ctx, PreRouteCtx> global_middleware_; | 589 | }; |
| 456 | route_tree<routed_ctx<PreRouteCtx>> routes_; | ||
| 457 | 590 | ||
| 458 | public: | 591 | template <std::derived_from<base_ctx> PreRouteCtx> |
| 459 | explicit server(log::logger const& l, locale::selector&& lsel, middleware_t<base_ctx, PreRouteCtx> global_middleware, route_tree<routed_ctx<PreRouteCtx>> routes) | 592 | class server |
| 460 | : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, global_middleware_{std::move(global_middleware)}, routes_{std::move(routes)} | 593 | { |
| 461 | {} | 594 | log::logger l_; |
| 595 | locale::selector lsel_; | ||
| 596 | middleware_t<base_ctx, PreRouteCtx> global_middleware_; | ||
| 597 | route_tree<routed_ctx<PreRouteCtx>> routes_; | ||
| 462 | 598 | ||
| 463 | struct match_result { | 599 | public: |
| 464 | util::not_null<handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const*> route_handlers; | 600 | explicit server( |
| 465 | std::vector<std::string> wildcard_matches; | 601 | log::logger const& l, locale::selector&& lsel, |
| 602 | middleware_t<base_ctx, PreRouteCtx> global_middleware, | ||
| 603 | route_tree<routed_ctx<PreRouteCtx>> routes) | ||
| 604 | : l_{l.sub("http_server")}, lsel_{std::move(lsel)}, | ||
| 605 | global_middleware_{std::move(global_middleware)}, | ||
| 606 | routes_{std::move(routes)} | ||
| 607 | { | ||
| 608 | } | ||
| 466 | 609 | ||
| 467 | auto allowed_methods() const -> verb_set { | 610 | struct match_result |
| 468 | return route_handlers->verbs(); | 611 | { |
| 469 | } | 612 | util::not_null< |
| 470 | }; | 613 | handler_map<basic_route_handler_fn_t<routed_ctx<PreRouteCtx>>> const*> |
| 614 | route_handlers; | ||
| 615 | std::vector<std::string> wildcard_matches; | ||
| 471 | 616 | ||
| 472 | auto match(boost::urls::segments_view segments) const -> std::optional<match_result> { | 617 | auto allowed_methods() const -> verb_set { return route_handlers->verbs(); } |
| 473 | auto const* tree = &routes_; | 618 | }; |
| 474 | auto wildcard_matches = std::vector<std::string>{}; | 619 | |
| 475 | for (auto const& seg : segments) { | 620 | auto match(boost::urls::segments_view segments) const |
| 476 | tree->sub.visit(util::overloaded{ | 621 | -> std::optional<match_result> |
| 477 | [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& subtrees) { | 622 | { |
| 478 | auto it = subtrees.find(seg); | 623 | auto const* tree = &routes_; |
| 479 | tree = it == subtrees.end() ? nullptr : &it->second; | 624 | auto wildcard_matches = std::vector<std::string>{}; |
| 480 | }, | 625 | for (auto const& seg : segments) |
| 481 | [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& wildcard_subtree) { | 626 | { |
| 482 | wildcard_matches.push_back(seg); | 627 | tree->sub.visit( |
| 483 | tree = &*wildcard_subtree; | 628 | util::overloaded{ |
| 484 | }, | 629 | [&](route_tree<routed_ctx<PreRouteCtx>>::named_subtrees const& |
| 630 | subtrees) | ||
| 631 | { | ||
| 632 | auto it = subtrees.find(seg); | ||
| 633 | tree = it == subtrees.end() ? nullptr : &it->second; | ||
| 634 | }, | ||
| 635 | [&](route_tree<routed_ctx<PreRouteCtx>>::wildcard_subtree const& | ||
| 636 | wildcard_subtree) | ||
| 637 | { | ||
| 638 | wildcard_matches.push_back(seg); | ||
| 639 | tree = &*wildcard_subtree; | ||
| 640 | }, | ||
| 485 | }); | 641 | }); |
| 486 | if (!tree) return std::nullopt; | 642 | if (!tree) |
| 487 | } | 643 | return std::nullopt; |
| 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 | } | 644 | } |
| 645 | if (tree->here.empty()) | ||
| 646 | return std::nullopt; | ||
| 647 | return match_result{ | ||
| 648 | .route_handlers = util::not_null{&tree->here}, | ||
| 649 | .wildcard_matches = wildcard_matches, | ||
| 650 | }; | ||
| 651 | } | ||
| 494 | 652 | ||
| 495 | auto route_request(PreRouteCtx ctx, readable_request r) const -> net::awaitable<presponse> { | 653 | auto route_request(PreRouteCtx ctx, readable_request r) const |
| 496 | auto req_base = r.p->get().base(); | 654 | -> net::awaitable<presponse> |
| 655 | { | ||
| 656 | auto req_base = r.p->get().base(); | ||
| 497 | 657 | ||
| 498 | auto const bad_request_tpl = problem::tpl{ | 658 | auto const bad_request_tpl = problem::tpl{ |
| 499 | .status = bhttp::status::bad_request, | 659 | .status = bhttp::status::bad_request, |
| 500 | .title = translate("Bad request"), | 660 | .title = translate("Bad request"), |
| 501 | .type_uri = "https://routemon.fautchen.eu/problems/bad-request", | 661 | .type_uri = "https://routemon.fautchen.eu/problems/bad-request", |
| 502 | }; | 662 | }; |
| 503 | 663 | ||
| 504 | if (req_base.target() == "*") { | 664 | if (req_base.target() == "*") |
| 505 | // request-target is in asterisk-form (RFC 9112, § 3.2.4), | 665 | { |
| 506 | // so the request must be a server-wide OPTIONS request. | 666 | // request-target is in asterisk-form (RFC 9112, § 3.2.4), |
| 667 | // so the request must be a server-wide OPTIONS request. | ||
| 507 | 668 | ||
| 508 | if (req_base.method() != bhttp::verb::options) { | 669 | if (req_base.method() != bhttp::verb::options) |
| 509 | auto tpl = problem::tpl{ | 670 | { |
| 510 | .status = bhttp::status::method_not_allowed, | 671 | auto tpl = problem::tpl{ |
| 511 | .title = translate("Method not allowed"), | 672 | .status = bhttp::status::method_not_allowed, |
| 512 | .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", | 673 | .title = translate("Method not allowed"), |
| 513 | }; | 674 | .type_uri = "https://routemon.fautchen.eu/problems/" |
| 514 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | 675 | "method-not-allowed", |
| 515 | } | 676 | }; |
| 677 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 678 | } | ||
| 516 | 679 | ||
| 517 | co_return co_await global_options_handler(ctx, r); | 680 | co_return co_await global_options_handler(ctx, r); |
| 518 | } else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) { | 681 | } |
| 519 | // request-target is in origin-form (RFC 9112, § 3.2.1), | 682 | else if (auto mreq_url0 = boost::urls::parse_origin_form(req_base.target())) |
| 520 | // so it must be a normal request (not a CONNECT or | 683 | { |
| 521 | // server-wide OPTIONS request). | 684 | // request-target is in origin-form (RFC 9112, § 3.2.1), |
| 685 | // so it must be a normal request (not a CONNECT or | ||
| 686 | // server-wide OPTIONS request). | ||
| 522 | 687 | ||
| 523 | auto req_url = boost::urls::url{*mreq_url0}; | 688 | auto req_url = boost::urls::url{*mreq_url0}; |
| 524 | req_url.normalize(); | 689 | req_url.normalize(); |
| 525 | if (!req_url.is_path_absolute()) { | 690 | if (!req_url.is_path_absolute()) |
| 526 | auto problem = bad_request_tpl.instantiate(). | 691 | { |
| 527 | set_detail(translate("Path of normalized (RFC 3986, § 6) " | 692 | auto problem = bad_request_tpl.instantiate().set_detail(translate( |
| 528 | "origin-form request-target (RFC " | 693 | "Path of normalized (RFC 3986, § 6) " |
| 529 | "9112, § 3.2.1) should be " | 694 | "origin-form request-target (RFC " |
| 530 | "absolute")); | 695 | "9112, § 3.2.1) should be " |
| 531 | co_return problem_rsp(ctx, problem, keep_alive{false}); | 696 | "absolute")); |
| 532 | } | 697 | co_return problem_rsp(ctx, problem, keep_alive{false}); |
| 698 | } | ||
| 533 | 699 | ||
| 534 | auto mres = match(req_url.segments()); | 700 | auto mres = match(req_url.segments()); |
| 535 | if (!mres) { | 701 | if (!mres) |
| 536 | auto tpl = problem::tpl{ | 702 | { |
| 537 | .status = bhttp::status::not_found, | 703 | auto tpl = problem::tpl{ |
| 538 | .title = translate("Not found"), | 704 | .status = bhttp::status::not_found, |
| 705 | .title = translate("Not found"), | ||
| 539 | .type_uri = "https://routemon.fautchen.eu/problems/not-found", | 706 | .type_uri = "https://routemon.fautchen.eu/problems/not-found", |
| 540 | }; | 707 | }; |
| 541 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | 708 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); |
| 542 | } | 709 | } |
| 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 | 710 | ||
| 555 | if (auto mhdl = mres->route_handlers->lookup(*mverb)) { | 711 | auto mverb = supported_verb::from(req_base.method()); |
| 556 | auto new_ctx = routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()}; | 712 | if (!mverb) |
| 557 | co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); | 713 | { |
| 558 | } else { | 714 | // Method not implemented. |
| 559 | // Path recognized, but method not allowed. | 715 | auto tpl = problem::tpl{ |
| 560 | auto tpl = problem::tpl{ | 716 | .status = bhttp::status::not_implemented, |
| 561 | .status = bhttp::status::method_not_allowed, | 717 | .title = translate("Method not implemented"), |
| 562 | .title = translate("Method not allowed"), | 718 | .type_uri = "https://routemon.fautchen.eu/problems/" |
| 563 | .type_uri = "https://routemon.fautchen.eu/problems/method-not-allowed", | 719 | "method-not-implemented", |
| 564 | }; | 720 | }; |
| 565 | auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | 721 | co_return problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); |
| 566 | rsp.header().set(bhttp::field::allow, mres->allowed_methods().to_string()); | 722 | } |
| 567 | co_return std::move(rsp); | ||
| 568 | } | ||
| 569 | } else { | ||
| 570 | // We do not accept any other request-target forms. | ||
| 571 | 723 | ||
| 572 | auto problem = bad_request_tpl.instantiate(). | 724 | if (auto mhdl = mres->route_handlers->lookup(*mverb)) |
| 573 | set_detail(translate("Invalid request-target, expected " | 725 | { |
| 574 | "asterisk-form or origin-form " | 726 | auto new_ctx = |
| 575 | "(see RFC 9112, § 3.2)")); | 727 | routed_ctx<PreRouteCtx>{std::move(ctx), mres->allowed_methods()}; |
| 576 | co_return problem_rsp(ctx, problem, keep_alive{false}); | 728 | co_return co_await mhdl(std::move(new_ctx), r, mres->wildcard_matches); |
| 729 | } | ||
| 730 | else | ||
| 731 | { | ||
| 732 | // Path recognized, but method not allowed. | ||
| 733 | auto tpl = problem::tpl{ | ||
| 734 | .status = bhttp::status::method_not_allowed, | ||
| 735 | .title = translate("Method not allowed"), | ||
| 736 | .type_uri = "https://routemon.fautchen.eu/problems/" | ||
| 737 | "method-not-allowed", | ||
| 738 | }; | ||
| 739 | auto rsp = problem_rsp(ctx, tpl.instantiate(), keep_alive{false}); | ||
| 740 | rsp.header().set( | ||
| 741 | bhttp::field::allow, mres->allowed_methods().to_string()); | ||
| 742 | co_return std::move(rsp); | ||
| 577 | } | 743 | } |
| 578 | } | 744 | } |
| 745 | else | ||
| 746 | { | ||
| 747 | // We do not accept any other request-target forms. | ||
| 579 | 748 | ||
| 580 | auto handle_request(readable_request r) const -> net::awaitable<presponse> { | 749 | auto problem = bad_request_tpl.instantiate().set_detail(translate( |
| 581 | auto header = r.p->get().base(); | 750 | "Invalid request-target, expected " |
| 582 | auto locale = lsel_.select(header[bhttp::field::accept_language]); | 751 | "asterisk-form or origin-form " |
| 583 | auto ctx0 = base_ctx{.locale = locale}; | 752 | "(see RFC 9112, § 3.2)")); |
| 584 | 753 | co_return problem_rsp(ctx, problem, keep_alive{false}); | |
| 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 | } | 754 | } |
| 755 | } | ||
| 589 | 756 | ||
| 590 | auto do_session(beast::tcp_stream strm) -> net::awaitable<void> { | 757 | auto handle_request(readable_request r) const -> net::awaitable<presponse> |
| 591 | auto buf = beast::flat_buffer{}; | 758 | { |
| 759 | auto header = r.p->get().base(); | ||
| 760 | auto locale = lsel_.select(header[bhttp::field::accept_language]); | ||
| 761 | auto ctx0 = base_ctx{.locale = locale}; | ||
| 592 | 762 | ||
| 593 | while (true) { | 763 | co_return co_await global_middleware_( |
| 594 | auto p0 = bhttp::request_parser<bhttp::empty_body>{}; | 764 | std::move(ctx0), header, |
| 595 | p0.body_limit(boost::none); | 765 | [&](PreRouteCtx ctx) -> net::awaitable<presponse> |
| 596 | auto [ec, _] = co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); | 766 | { co_return co_await route_request(std::move(ctx), std::move(r)); }); |
| 597 | if (ec == bhttp::error::end_of_stream) { | 767 | } |
| 598 | break; | ||
| 599 | } else if (ec) { | ||
| 600 | throw boost::system::system_error{ec}; | ||
| 601 | } | ||
| 602 | 768 | ||
| 603 | auto http_version = p0.get().version(); | 769 | auto do_session(beast::tcp_stream strm) -> net::awaitable<void> |
| 604 | auto&& rsp = co_await handle_request(readable_request{ | 770 | { |
| 605 | .p = util::not_null{&p0}, | 771 | auto buf = beast::flat_buffer{}; |
| 606 | .strm = util::not_null{&strm}, | 772 | |
| 607 | .buf = util::not_null{&buf}, | 773 | while (true) |
| 774 | { | ||
| 775 | auto p0 = bhttp::request_parser<bhttp::empty_body>{}; | ||
| 776 | p0.body_limit(boost::none); | ||
| 777 | auto [ec, _] = | ||
| 778 | co_await bhttp::async_read_header(strm, buf, p0, net::as_tuple); | ||
| 779 | if (ec == bhttp::error::end_of_stream) | ||
| 780 | break; | ||
| 781 | else if (ec) | ||
| 782 | throw boost::system::system_error{ec}; | ||
| 783 | |||
| 784 | auto http_version = p0.get().version(); | ||
| 785 | auto&& rsp = co_await handle_request( | ||
| 786 | readable_request{ | ||
| 787 | .p = util::not_null{&p0}, | ||
| 788 | .strm = util::not_null{&strm}, | ||
| 789 | .buf = util::not_null{&buf}, | ||
| 608 | }); | 790 | }); |
| 609 | rsp.header().version(http_version); | 791 | rsp.header().version(http_version); |
| 610 | bool keep_alive = rsp.keep_alive(); | 792 | bool keep_alive = rsp.keep_alive(); |
| 611 | co_await beast::async_write(strm, std::move(rsp)); | 793 | co_await beast::async_write(strm, std::move(rsp)); |
| 612 | if (!keep_alive) { | 794 | if (!keep_alive) |
| 613 | break; | 795 | { |
| 614 | } | 796 | break; |
| 615 | } | 797 | } |
| 616 | |||
| 617 | strm.socket().shutdown(tcp::socket::shutdown_send); | ||
| 618 | } | 798 | } |
| 619 | 799 | ||
| 620 | auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void> { | 800 | strm.socket().shutdown(tcp::socket::shutdown_send); |
| 621 | auto executor = co_await net::this_coro::executor; | 801 | } |
| 622 | auto acceptor = tcp::acceptor{executor, endpoint}; | 802 | |
| 803 | auto do_listen(tcp::endpoint endpoint) -> net::awaitable<void> | ||
| 804 | { | ||
| 805 | auto executor = co_await net::this_coro::executor; | ||
| 806 | auto acceptor = tcp::acceptor{executor, endpoint}; | ||
| 623 | 807 | ||
| 624 | l_.with("endpoint", endpoint.address().to_string()). | 808 | l_.with("endpoint", endpoint.address().to_string()) |
| 625 | with("port", std::to_string(endpoint.port())). | 809 | .with("port", std::to_string(endpoint.port())) |
| 626 | info("Serving"); | 810 | .info("Serving"); |
| 627 | while (true) { | 811 | while (true) |
| 628 | net::co_spawn(executor, | 812 | { |
| 629 | do_session(beast::tcp_stream{co_await acceptor.async_accept()}), | 813 | net::co_spawn( |
| 630 | [this](std::exception_ptr e) { | 814 | executor, |
| 631 | if (e) { | 815 | do_session(beast::tcp_stream{co_await acceptor.async_accept()}), |
| 632 | try { | 816 | [this](std::exception_ptr e) |
| 633 | std::rethrow_exception(e); | 817 | { |
| 634 | } catch (std::exception const& e) { | 818 | if (e) |
| 635 | l_.error("Error in session: {}", e.what()); | 819 | { |
| 636 | } | 820 | try |
| 637 | } | 821 | { |
| 638 | }); | 822 | std::rethrow_exception(e); |
| 639 | } | 823 | } |
| 824 | catch (std::exception const& e) | ||
| 825 | { | ||
| 826 | l_.error("Error in session: {}", e.what()); | ||
| 827 | } | ||
| 828 | } | ||
| 829 | }); | ||
| 640 | } | 830 | } |
| 831 | } | ||
| 641 | 832 | ||
| 642 | auto spawn(net::io_context& ioc) -> void { | 833 | auto spawn(net::io_context& ioc) -> void |
| 643 | auto const addr = net::ip::make_address("0.0.0.0"); | 834 | { |
| 644 | auto const endpoint = tcp::endpoint{addr, 8284}; | 835 | auto const addr = net::ip::make_address("0.0.0.0"); |
| 836 | auto const endpoint = tcp::endpoint{addr, 8284}; | ||
| 645 | 837 | ||
| 646 | // TODO: make exception handling as nice as in srv.cpp | 838 | // TODO: make exception handling as nice as in srv.cpp |
| 647 | net::co_spawn(ioc, | 839 | net::co_spawn( |
| 648 | do_listen(endpoint), | 840 | ioc, do_listen(endpoint), |
| 649 | [this](std::exception_ptr e) { | 841 | [this](std::exception_ptr e) |
| 650 | if (e) { | 842 | { |
| 651 | try { | 843 | if (e) |
| 652 | std::rethrow_exception(e); | 844 | { |
| 653 | } catch (std::exception const& e) { | 845 | try |
| 654 | l_.error("Error: {}", e.what()); | 846 | { |
| 655 | } | 847 | std::rethrow_exception(e); |
| 656 | } | 848 | } |
| 657 | }); | 849 | catch (std::exception const& e) |
| 658 | } | 850 | { |
| 659 | }; | 851 | l_.error("Error: {}", e.what()); |
| 852 | } | ||
| 853 | } | ||
| 854 | }); | ||
| 855 | } | ||
| 856 | }; | ||
| 660 | 857 | ||
| 661 | } // namespace routemon::http | 858 | } // namespace routemon::http |
diff --git a/server/src/locale.cppm b/server/src/locale.cppm index 65ae2ea..8fd867b 100644 --- a/server/src/locale.cppm +++ b/server/src/locale.cppm | |||
| @@ -11,235 +11,285 @@ import :util; | |||
| 11 | export namespace blocale = boost::locale; | 11 | export namespace blocale = boost::locale; |
| 12 | 12 | ||
| 13 | export namespace routemon { | 13 | export namespace routemon { |
| 14 | using lformat = blocale::format; | 14 | using lformat = blocale::format; |
| 15 | using blocale::translate; | 15 | using blocale::gettext; |
| 16 | using blocale::gettext; | 16 | using blocale::translate; |
| 17 | } // namespace routemon | 17 | } // namespace routemon |
| 18 | 18 | ||
| 19 | namespace routemon::locale { | 19 | namespace routemon::locale { |
| 20 | 20 | ||
| 21 | struct locale_priority { | 21 | struct locale_priority |
| 22 | float weight; | 22 | { |
| 23 | std::size_t original_index; | 23 | float weight; |
| 24 | }; | 24 | std::size_t original_index; |
| 25 | }; | ||
| 25 | 26 | ||
| 26 | auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool { | 27 | auto operator<(locale_priority const& lhs, locale_priority const& rhs) -> bool |
| 27 | if (lhs.weight != rhs.weight) | 28 | { |
| 28 | return lhs.weight > rhs.weight; | 29 | if (lhs.weight != rhs.weight) |
| 29 | return lhs.original_index < rhs.original_index; | 30 | return lhs.weight > rhs.weight; |
| 30 | } | 31 | return lhs.original_index < rhs.original_index; |
| 32 | } | ||
| 31 | 33 | ||
| 32 | struct icu_locale_hash { | 34 | struct icu_locale_hash |
| 33 | std::size_t operator()(icu::Locale const& l) const noexcept { | 35 | { |
| 34 | static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); | 36 | std::size_t operator()(icu::Locale const& l) const noexcept |
| 35 | std::int32_t hash = l.hashCode(); | 37 | { |
| 36 | if (hash < 0) { | 38 | static_assert(sizeof(std::int32_t) < sizeof(std::size_t)); |
| 37 | return static_cast<std::size_t>(std::numeric_limits<int>::max()) + static_cast<std::size_t>(-hash) + 1; | 39 | std::int32_t hash = l.hashCode(); |
| 38 | } else { | 40 | if (hash < 0) |
| 39 | return static_cast<std::size_t>(hash); | 41 | { |
| 40 | } | 42 | return static_cast<std::size_t>(std::numeric_limits<int>::max()) |
| 43 | + static_cast<std::size_t>(-hash) + 1; | ||
| 44 | } | ||
| 45 | else | ||
| 46 | { | ||
| 47 | return static_cast<std::size_t>(hash); | ||
| 41 | } | 48 | } |
| 42 | }; | 49 | } |
| 50 | }; | ||
| 43 | 51 | ||
| 44 | using icu_locale_priority_map = std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>; | 52 | using icu_locale_priority_map = |
| 45 | using icu_priority_locale = std::pair<icu::Locale, locale_priority>; | 53 | std::unordered_map<icu::Locale, locale_priority, icu_locale_hash>; |
| 54 | using icu_priority_locale = std::pair<icu::Locale, locale_priority>; | ||
| 46 | 55 | ||
| 47 | auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) -> bool { | 56 | auto operator<(icu_priority_locale const& lhs, icu_priority_locale const& rhs) |
| 48 | return lhs.second < rhs.second; | 57 | -> bool |
| 49 | } | 58 | { |
| 59 | return lhs.second < rhs.second; | ||
| 60 | } | ||
| 50 | 61 | ||
| 51 | class icu_priority_locale_vec_iterator : public icu::Locale::Iterator { | 62 | class icu_priority_locale_vec_iterator : public icu::Locale::Iterator |
| 52 | std::size_t i_ = 0uz; | 63 | { |
| 53 | std::vector<icu_priority_locale> ls_; | 64 | std::size_t i_ = 0uz; |
| 65 | std::vector<icu_priority_locale> ls_; | ||
| 54 | 66 | ||
| 55 | public: | 67 | public: |
| 56 | explicit icu_priority_locale_vec_iterator(std::vector<icu_priority_locale>&& ls) | 68 | explicit icu_priority_locale_vec_iterator( |
| 57 | : ls_(std::move(ls)) | 69 | std::vector<icu_priority_locale>&& ls) |
| 58 | {} | 70 | : ls_(std::move(ls)) |
| 71 | { | ||
| 72 | } | ||
| 59 | 73 | ||
| 60 | auto hasNext() const -> UBool override { | 74 | auto hasNext() const -> UBool override { return i_ < ls_.size(); } |
| 61 | return i_ < ls_.size(); | ||
| 62 | } | ||
| 63 | 75 | ||
| 64 | auto next() -> icu::Locale const& override { | 76 | auto next() -> icu::Locale const& override { return ls_[i_++].first; } |
| 65 | return ls_[i_++].first; | ||
| 66 | } | ||
| 67 | 77 | ||
| 68 | ~icu_priority_locale_vec_iterator() override = default; | 78 | ~icu_priority_locale_vec_iterator() override = default; |
| 69 | }; | 79 | }; |
| 70 | 80 | ||
| 71 | export template<class T> | 81 | export template <class T> |
| 72 | concept locale_input_range = | 82 | concept locale_input_range = |
| 73 | std::ranges::input_range<T> && | 83 | std::ranges::input_range<T> |
| 74 | std::same_as<std::locale const&, std::ranges::range_const_reference_t<T>>; | 84 | && std::same_as< |
| 85 | std::locale const&, std::ranges::range_const_reference_t<T>>; | ||
| 75 | 86 | ||
| 76 | // Helps select a locale based on the Accept-Language header in an | 87 | // Helps select a locale based on the Accept-Language header in an |
| 77 | // HTTP request. | 88 | // HTTP request. |
| 78 | export class selector { | 89 | export class selector |
| 79 | std::locale default_; | 90 | { |
| 80 | icu::LocaleMatcher matcher_; | 91 | std::locale default_; |
| 81 | std::shared_ptr<blocale::generator const> lgen_; | 92 | icu::LocaleMatcher matcher_; |
| 93 | std::shared_ptr<blocale::generator const> lgen_; | ||
| 82 | 94 | ||
| 83 | auto make_matcher(locale_input_range auto supported_locales, std::locale default_locale) { | 95 | auto make_matcher( |
| 84 | auto builder = icu::LocaleMatcher::Builder{}; | 96 | locale_input_range auto supported_locales, std::locale default_locale) |
| 85 | for (auto const& supported_locale : supported_locales) { | 97 | { |
| 86 | auto const& supported_locale_info = std::use_facet<blocale::info>(supported_locale); | 98 | auto builder = icu::LocaleMatcher::Builder{}; |
| 87 | auto supported_icu_locale = icu::Locale{supported_locale_info.name().c_str()}; | 99 | for (auto const& supported_locale : supported_locales) |
| 88 | if (supported_icu_locale.isBogus()) | 100 | { |
| 89 | throw std::runtime_error{"supported locale gives rise to bogus ICU locale"}; | 101 | auto const& supported_locale_info = |
| 90 | builder.addSupportedLocale(supported_icu_locale); | 102 | std::use_facet<blocale::info>(supported_locale); |
| 91 | } | 103 | auto supported_icu_locale = |
| 92 | auto const& default_locale_info = std::use_facet<blocale::info>(default_locale); | 104 | icu::Locale{supported_locale_info.name().c_str()}; |
| 93 | auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; | 105 | if (supported_icu_locale.isBogus()) |
| 94 | if (default_icu_locale.isBogus()) | 106 | throw std::runtime_error{ |
| 95 | throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; | 107 | "supported locale gives rise to bogus ICU locale" |
| 96 | builder.setDefaultLocale(&default_icu_locale); | 108 | }; |
| 97 | auto ec = UErrorCode::U_ZERO_ERROR; | 109 | builder.addSupportedLocale(supported_icu_locale); |
| 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 | } | 110 | } |
| 111 | auto const& default_locale_info = | ||
| 112 | std::use_facet<blocale::info>(default_locale); | ||
| 113 | auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; | ||
| 114 | if (default_icu_locale.isBogus()) | ||
| 115 | throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; | ||
| 116 | builder.setDefaultLocale(&default_icu_locale); | ||
| 117 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 118 | auto matcher = builder.build(ec); | ||
| 119 | if (U_FAILURE(ec)) | ||
| 120 | throw std::runtime_error{"failed to build icu::LocaleMatcher"}; | ||
| 121 | return matcher; | ||
| 122 | } | ||
| 123 | |||
| 124 | // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. | ||
| 125 | static auto ltrim_ows(std::string_view s) -> std::string_view | ||
| 126 | { | ||
| 127 | if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) | ||
| 128 | s.remove_prefix(i); | ||
| 129 | return s; | ||
| 130 | } | ||
| 131 | static auto rtrim_ows(std::string_view s) -> std::string_view | ||
| 132 | { | ||
| 133 | if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) | ||
| 134 | return s.substr(0, i + 1); | ||
| 135 | return s; | ||
| 136 | } | ||
| 137 | static auto trim_ows(std::string_view s) -> std::string_view | ||
| 138 | { | ||
| 139 | return rtrim_ows(ltrim_ows(s)); | ||
| 140 | } | ||
| 103 | 141 | ||
| 104 | // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. | 142 | auto from_icu_locale(icu::Locale const& l) const -> std::locale |
| 105 | static auto ltrim_ows(std::string_view s) -> std::string_view { | 143 | { |
| 106 | if (auto i = s.find_first_not_of(" \t"); i != std::string_view::npos) | 144 | auto posix_name = std::string{l.getLanguage()}; |
| 107 | s.remove_prefix(i); | 145 | if (l.getScript() && std::strlen(l.getScript()) > 0) |
| 108 | return s; | 146 | { |
| 147 | posix_name += "_"; | ||
| 148 | posix_name += l.getScript(); | ||
| 109 | } | 149 | } |
| 110 | static auto rtrim_ows(std::string_view s) -> std::string_view { | 150 | if (l.getCountry() && std::strlen(l.getCountry()) > 0) |
| 111 | if (auto i = s.find_last_not_of(" \t"); i != std::string_view::npos) | 151 | { |
| 112 | return s.substr(0, i + 1); | 152 | posix_name += "_"; |
| 113 | return s; | 153 | posix_name += l.getCountry(); |
| 114 | } | 154 | } |
| 115 | static auto trim_ows(std::string_view s) -> std::string_view { | 155 | posix_name += ".UTF-8"; |
| 116 | return rtrim_ows(ltrim_ows(s)); | 156 | auto added_at = false; |
| 157 | if (l.getVariant() && std::strlen(l.getVariant()) > 0) | ||
| 158 | { | ||
| 159 | added_at = true; | ||
| 160 | posix_name += "@"; | ||
| 161 | posix_name += l.getVariant(); | ||
| 117 | } | 162 | } |
| 118 | 163 | auto ec = UErrorCode::U_ZERO_ERROR; | |
| 119 | auto from_icu_locale(icu::Locale const& l) const -> std::locale { | 164 | auto* keywords = l.createKeywords(ec); |
| 120 | auto posix_name = std::string{l.getLanguage()}; | 165 | if (U_FAILURE(ec)) |
| 121 | if (l.getScript() && std::strlen(l.getScript()) > 0) { | 166 | throw std::runtime_error{"failed to create keywords"}; |
| 122 | posix_name += "_"; | 167 | if (keywords) |
| 123 | posix_name += l.getScript(); | 168 | { |
| 124 | } | 169 | std::int32_t kw_len = 0; |
| 125 | if (l.getCountry() && std::strlen(l.getCountry()) > 0) { | 170 | char const* kw = nullptr; |
| 126 | posix_name += "_"; | 171 | while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) |
| 127 | posix_name += l.getCountry(); | 172 | { |
| 128 | } | 173 | auto value = |
| 129 | posix_name += ".UTF-8"; | 174 | l.getKeywordValue<std::string>(icu::StringPiece(kw, kw_len), ec); |
| 130 | auto added_at = false; | 175 | if (!added_at) |
| 131 | if (l.getVariant() && std::strlen(l.getVariant()) > 0) { | 176 | { |
| 132 | added_at = true; | 177 | posix_name += "@"; |
| 133 | posix_name += "@"; | 178 | added_at = true; |
| 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 | } | 179 | } |
| 155 | if (U_FAILURE(ec)) | 180 | else |
| 156 | throw std::runtime_error{"failed to iterate over keywords"}; | 181 | { |
| 157 | delete keywords; | 182 | posix_name += ";"; |
| 183 | } | ||
| 184 | posix_name += kw; | ||
| 185 | posix_name += "="; | ||
| 186 | posix_name += value; | ||
| 158 | } | 187 | } |
| 159 | return lgen_->generate(posix_name); | 188 | if (U_FAILURE(ec)) |
| 189 | throw std::runtime_error{"failed to iterate over keywords"}; | ||
| 190 | delete keywords; | ||
| 160 | } | 191 | } |
| 192 | return lgen_->generate(posix_name); | ||
| 193 | } | ||
| 161 | 194 | ||
| 162 | public: | 195 | public: |
| 163 | // Note: lgen must live at least as long as the selector constructed here! | 196 | // 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. | 197 | // 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) | 198 | explicit selector( |
| 166 | : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} | 199 | locale_input_range auto locales, std::locale default_, |
| 167 | {} | 200 | std::shared_ptr<blocale::generator const> lgen) |
| 201 | : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} | ||
| 202 | { | ||
| 203 | } | ||
| 168 | 204 | ||
| 169 | auto select(std::string_view accept_language) const -> std::locale { | 205 | auto select(std::string_view accept_language) const -> std::locale |
| 170 | using namespace std::literals::string_view_literals; | 206 | { |
| 171 | // NOTE: can also contain a *;q=0.1 | 207 | using namespace std::literals::string_view_literals; |
| 172 | // q should have at most 3 digits after period | 208 | // NOTE: can also contain a *;q=0.1 |
| 173 | auto dlpm = icu_locale_priority_map{}; | 209 | // q should have at most 3 digits after period |
| 174 | for (auto const [i, lang_prio] : accept_language | std::views::split(","sv) | std::views::enumerate) { | 210 | auto dlpm = icu_locale_priority_map{}; |
| 175 | auto [lang_range_ut, mweight_ut] = util::split_on(std::string_view{lang_prio}, ';'); | 211 | for (auto const [i, lang_prio] : |
| 176 | auto lang_range_str = trim_ows(lang_range_ut); | 212 | accept_language | std::views::split(","sv) | std::views::enumerate) |
| 177 | auto mweight_str = mweight_ut.transform(trim_ows); | 213 | { |
| 178 | if (lang_range_str == "*") | 214 | auto [lang_range_ut, mweight_ut] = |
| 179 | break; | 215 | util::split_on(std::string_view{lang_prio}, ';'); |
| 216 | auto lang_range_str = trim_ows(lang_range_ut); | ||
| 217 | auto mweight_str = mweight_ut.transform(trim_ows); | ||
| 218 | if (lang_range_str == "*") | ||
| 219 | break; | ||
| 180 | 220 | ||
| 181 | auto ec = UErrorCode::U_ZERO_ERROR; | 221 | auto ec = UErrorCode::U_ZERO_ERROR; |
| 182 | auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); | 222 | auto icu_locale = icu::Locale::forLanguageTag(lang_range_str, ec); |
| 183 | if (U_FAILURE(ec) || icu_locale.isBogus()) | 223 | if (U_FAILURE(ec) || icu_locale.isBogus()) |
| 184 | continue; // ignore this locale | 224 | continue; // ignore this locale |
| 185 | 225 | ||
| 186 | auto weight = 1.0f; | 226 | auto weight = 1.0f; |
| 187 | if (mweight_str && mweight_str->starts_with("q=")) { | 227 | if (mweight_str && mweight_str->starts_with("q=")) |
| 188 | auto weight_str = mweight_str->substr(2, 4); | 228 | { |
| 189 | if (auto mweight = util::parse_float(weight_str, std::chars_format::fixed); | 229 | auto weight_str = mweight_str->substr(2, 4); |
| 190 | mweight && 0.0f < *mweight && *mweight < 1.0f) { | 230 | if (auto mweight = |
| 191 | weight = *mweight; | 231 | util::parse_float(weight_str, std::chars_format::fixed); |
| 192 | } | 232 | mweight && 0.0f < *mweight && *mweight < 1.0f) |
| 233 | { | ||
| 234 | weight = *mweight; | ||
| 193 | } | 235 | } |
| 236 | } | ||
| 194 | 237 | ||
| 195 | if (weight > 0.0f) { | 238 | if (weight > 0.0f) |
| 196 | dlpm[icu_locale] = { | 239 | { |
| 240 | dlpm[icu_locale] = { | ||
| 197 | .weight = weight, | 241 | .weight = weight, |
| 198 | .original_index = static_cast<std::size_t>(i), | 242 | .original_index = static_cast<std::size_t>(i), |
| 199 | }; | 243 | }; |
| 200 | } else { | 244 | } |
| 201 | dlpm.erase(icu_locale); | 245 | else |
| 202 | } | 246 | { |
| 247 | dlpm.erase(icu_locale); | ||
| 203 | } | 248 | } |
| 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 | } | 249 | } |
| 217 | }; | ||
| 218 | 250 | ||
| 219 | export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> { | 251 | auto desired_locales = |
| 220 | auto const& locale_info = std::use_facet<blocale::info>(locale); | 252 | std::vector<icu_priority_locale>{dlpm.begin(), dlpm.end()}; |
| 253 | std::sort(desired_locales.begin(), desired_locales.end()); | ||
| 254 | auto it = icu_priority_locale_vec_iterator{std::move(desired_locales)}; | ||
| 221 | auto ec = UErrorCode::U_ZERO_ERROR; | 255 | auto ec = UErrorCode::U_ZERO_ERROR; |
| 222 | auto bcp47_lang_tag = icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec); | 256 | auto res = matcher_.getBestMatchResult(it, ec); |
| 257 | if (U_FAILURE(ec)) | ||
| 258 | return default_; | ||
| 259 | auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? | ||
| 223 | if (U_FAILURE(ec)) | 260 | if (U_FAILURE(ec)) |
| 224 | return std::nullopt; | 261 | return from_icu_locale(*res.getSupportedLocale()); |
| 225 | return bcp47_lang_tag; | 262 | return from_icu_locale(resolved); |
| 226 | } | 263 | } |
| 264 | }; | ||
| 265 | |||
| 266 | export auto to_bcp47_lang_tag(std::locale locale) -> std::optional<std::string> | ||
| 267 | { | ||
| 268 | auto const& locale_info = std::use_facet<blocale::info>(locale); | ||
| 269 | auto ec = UErrorCode::U_ZERO_ERROR; | ||
| 270 | auto bcp47_lang_tag = | ||
| 271 | icu::Locale{locale_info.name().c_str()}.toLanguageTag<std::string>(ec); | ||
| 272 | if (U_FAILURE(ec)) | ||
| 273 | return std::nullopt; | ||
| 274 | return bcp47_lang_tag; | ||
| 275 | } | ||
| 227 | 276 | ||
| 228 | #ifdef LOCALEDIR | 277 | #ifdef LOCALEDIR |
| 229 | # define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) | 278 | #define LOCALEDIR_AUX_XSTR(s) LOCALEDIR_AUX_STR(s) |
| 230 | # define LOCALEDIR_AUX_STR(s) #s | 279 | #define LOCALEDIR_AUX_STR(s) #s |
| 231 | constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; | 280 | constexpr auto messages_path = std::string_view{LOCALEDIR_AUX_XSTR(LOCALEDIR)}; |
| 232 | # undef LOCALEDIR_AUX_STR | 281 | #undef LOCALEDIR_AUX_STR |
| 233 | # undef LOCALEDIR_AUX_XSTR | 282 | #undef LOCALEDIR_AUX_XSTR |
| 234 | #else // ifdef LOCALEDIR | 283 | #else // ifdef LOCALEDIR |
| 235 | constexpr auto messages_path = std::string_view{"locale/dev"}; | 284 | constexpr auto messages_path = std::string_view{"locale/dev"}; |
| 236 | #endif // ifdef LOCALEDIR | 285 | #endif // ifdef LOCALEDIR |
| 237 | 286 | ||
| 238 | export auto make_generator() -> std::shared_ptr<blocale::generator const> { | 287 | export auto make_generator() -> std::shared_ptr<blocale::generator const> |
| 239 | auto lgen = std::make_shared<blocale::generator>(); | 288 | { |
| 240 | lgen->add_messages_path(std::string{messages_path}); | 289 | auto lgen = std::make_shared<blocale::generator>(); |
| 241 | lgen->add_messages_domain("routemon"); | 290 | lgen->add_messages_path(std::string{messages_path}); |
| 242 | return std::static_pointer_cast<blocale::generator const>(lgen); | 291 | lgen->add_messages_domain("routemon"); |
| 243 | } | 292 | return std::static_pointer_cast<blocale::generator const>(lgen); |
| 293 | } | ||
| 244 | 294 | ||
| 245 | } // namespace routemon::locale | 295 | } // namespace routemon::locale |
diff --git a/server/src/log.cppm b/server/src/log.cppm index d7e2bff..0bfdf7d 100644 --- a/server/src/log.cppm +++ b/server/src/log.cppm | |||
| @@ -12,137 +12,160 @@ import std; | |||
| 12 | 12 | ||
| 13 | namespace routemon::log { | 13 | namespace routemon::log { |
| 14 | 14 | ||
| 15 | export enum class level : std::uint8_t { | 15 | export enum class level : std::uint8_t { |
| 16 | debug, | 16 | debug, |
| 17 | info, | 17 | info, |
| 18 | warn, | 18 | warn, |
| 19 | error, | 19 | error, |
| 20 | }; | 20 | }; |
| 21 | 21 | ||
| 22 | namespace { | 22 | namespace { |
| 23 | 23 | ||
| 24 | auto operator<<(std::ostream& os, level lvl) -> std::ostream& { | 24 | auto operator<<(std::ostream& os, level lvl) -> std::ostream& |
| 25 | switch (lvl) { | 25 | { |
| 26 | case level::debug: os << "dbg"; break; | 26 | switch (lvl) |
| 27 | case level::info: os << "inf"; break; | 27 | { |
| 28 | case level::warn: os << "wrn"; break; | 28 | case level::debug: |
| 29 | case level::error: os << "err"; break; | 29 | os << "dbg"; |
| 30 | } | 30 | break; |
| 31 | return os; | 31 | case level::info: |
| 32 | } | 32 | os << "inf"; |
| 33 | break; | ||
| 34 | case level::warn: | ||
| 35 | os << "wrn"; | ||
| 36 | break; | ||
| 37 | case level::error: | ||
| 38 | os << "err"; | ||
| 39 | break; | ||
| 40 | } | ||
| 41 | return os; | ||
| 42 | } | ||
| 33 | 43 | ||
| 34 | } // namespace (unique) | 44 | } // namespace |
| 35 | 45 | ||
| 36 | export class sink { | 46 | export class sink |
| 37 | std::atomic<enum level> lvl_; | 47 | { |
| 38 | std::ostream& os_ = std::cout; | 48 | std::atomic<enum level> lvl_; |
| 49 | std::ostream& os_ = std::cout; | ||
| 39 | 50 | ||
| 40 | struct tmp_message { | 51 | struct tmp_message |
| 41 | level lvl; | 52 | { |
| 42 | std::string_view component; | 53 | level lvl; |
| 43 | std::string_view txt; | 54 | std::string_view component; |
| 44 | std::map<std::string, std::string> const& attrs; | 55 | std::string_view txt; |
| 45 | }; | 56 | std::map<std::string, std::string> const& attrs; |
| 57 | }; | ||
| 46 | 58 | ||
| 47 | auto write(tmp_message msg) -> void { | 59 | auto write(tmp_message msg) -> void |
| 48 | auto sos = std::osyncstream{os_}; | 60 | { |
| 49 | sos << "[" << msg.lvl; | 61 | auto sos = std::osyncstream{os_}; |
| 50 | if (!msg.component.empty()) | 62 | sos << "[" << msg.lvl; |
| 51 | sos << " " << msg.component; | 63 | if (!msg.component.empty()) |
| 52 | sos << "] " << msg.txt; | 64 | sos << " " << msg.component; |
| 53 | for (auto const& [k, v] : msg.attrs) { | 65 | sos << "] " << msg.txt; |
| 54 | sos << " " << k << "=" << std::quoted(v); | 66 | for (auto const& [k, v] : msg.attrs) |
| 55 | } | 67 | { |
| 56 | sos << '\n'; | 68 | sos << " " << k << "=" << std::quoted(v); |
| 57 | } | 69 | } |
| 70 | sos << '\n'; | ||
| 71 | } | ||
| 58 | 72 | ||
| 59 | explicit sink(level lvl) | 73 | explicit sink(level lvl) : lvl_{lvl} {} |
| 60 | : lvl_{lvl} | ||
| 61 | {} | ||
| 62 | 74 | ||
| 63 | friend auto make_sink(level lvl) -> std::shared_ptr<sink>; | 75 | friend auto make_sink(level lvl) -> std::shared_ptr<sink>; |
| 64 | friend class logger; | 76 | friend class logger; |
| 65 | 77 | ||
| 66 | public: | 78 | public: |
| 67 | [[nodiscard]] auto level() const -> enum level { | 79 | [[nodiscard]] auto level() const -> enum level { return lvl_; } |
| 68 | return lvl_; | ||
| 69 | } | ||
| 70 | 80 | ||
| 71 | auto set_level(enum level lvl) -> void { | 81 | auto set_level(enum level lvl) -> void { lvl_ = lvl; } |
| 72 | lvl_ = lvl; | 82 | }; |
| 73 | } | ||
| 74 | }; | ||
| 75 | 83 | ||
| 76 | export auto make_sink(level lvl) -> std::shared_ptr<sink> { | 84 | export auto make_sink(level lvl) -> std::shared_ptr<sink> |
| 77 | return std::shared_ptr<sink>{new sink{lvl}}; | 85 | { |
| 78 | } | 86 | return std::shared_ptr<sink>{new sink{lvl}}; |
| 87 | } | ||
| 79 | 88 | ||
| 80 | export class logger { | 89 | export class logger |
| 81 | std::shared_ptr<sink> sink_; | 90 | { |
| 82 | std::string component_; | 91 | std::shared_ptr<sink> sink_; |
| 83 | std::map<std::string, std::string> attrs_; | 92 | std::string component_; |
| 93 | std::map<std::string, std::string> attrs_; | ||
| 84 | 94 | ||
| 85 | template<log::level lvl> | 95 | template <log::level lvl> |
| 86 | auto log_at(std::string_view fmt, std::format_args args) -> logger& { | 96 | auto log_at(std::string_view fmt, std::format_args args) -> logger& |
| 87 | if (sink_->level() <= lvl) | 97 | { |
| 88 | sink_->write(sink::tmp_message{ | 98 | if (sink_->level() <= lvl) |
| 89 | .lvl = lvl, | 99 | sink_->write( |
| 90 | .component = component_, | 100 | sink::tmp_message{ |
| 91 | .txt = std::vformat(fmt, args), | 101 | .lvl = lvl, |
| 92 | .attrs = attrs_, | 102 | .component = component_, |
| 103 | .txt = std::vformat(fmt, args), | ||
| 104 | .attrs = attrs_, | ||
| 93 | }); | 105 | }); |
| 94 | return *this; | 106 | return *this; |
| 95 | } | 107 | } |
| 96 | 108 | ||
| 97 | public: | 109 | public: |
| 98 | explicit logger(std::shared_ptr<sink> const& sink) | 110 | explicit logger(std::shared_ptr<sink> const& sink) : sink_{sink} |
| 99 | : sink_{sink} | 111 | { |
| 112 | if (!sink) | ||
| 100 | { | 113 | { |
| 101 | if (!sink) { | 114 | throw std::invalid_argument{"logger sink may not be null"}; |
| 102 | throw std::invalid_argument{"logger sink may not be null"}; | ||
| 103 | } | ||
| 104 | } | 115 | } |
| 116 | } | ||
| 105 | 117 | ||
| 106 | [[nodiscard]] auto sub(std::string_view component) const -> logger { | 118 | [[nodiscard]] auto sub(std::string_view component) const -> logger |
| 107 | auto l = *this; | 119 | { |
| 108 | if (l.component_.empty()) { | 120 | auto l = *this; |
| 109 | l.component_ = component; | 121 | if (l.component_.empty()) |
| 110 | } else { | 122 | { |
| 111 | l.component_ += "."; | 123 | l.component_ = component; |
| 112 | l.component_ += component; | ||
| 113 | } | ||
| 114 | return l; | ||
| 115 | } | 124 | } |
| 116 | 125 | else | |
| 117 | [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger { | 126 | { |
| 118 | auto l = *this; | 127 | l.component_ += "."; |
| 119 | l.attrs_[k] = std::move(v); | 128 | l.component_ += component; |
| 120 | return l; | ||
| 121 | } | 129 | } |
| 130 | return l; | ||
| 131 | } | ||
| 122 | 132 | ||
| 123 | [[nodiscard]] auto with(std::string const& k, std::string_view v) const -> logger { | 133 | [[nodiscard]] auto with(std::string const& k, std::string&& v) const -> logger |
| 124 | return with(k, std::string{v}); | 134 | { |
| 125 | } | 135 | auto l = *this; |
| 136 | l.attrs_[k] = std::move(v); | ||
| 137 | return l; | ||
| 138 | } | ||
| 126 | 139 | ||
| 127 | template<class... Args> | 140 | [[nodiscard]] auto with(std::string const& k, std::string_view v) const |
| 128 | auto debug(std::format_string<Args...> fmt, Args&&... args) -> logger& { | 141 | -> logger |
| 129 | return log_at<level::debug>(fmt.get(), std::make_format_args(args...)); | 142 | { |
| 130 | } | 143 | return with(k, std::string{v}); |
| 144 | } | ||
| 131 | 145 | ||
| 132 | template<class... Args> | 146 | template <class... Args> |
| 133 | auto info(std::format_string<Args...> fmt, Args&&... args) -> logger& { | 147 | auto debug(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 134 | return log_at<level::info>(fmt.get(), std::make_format_args(args...)); | 148 | { |
| 135 | } | 149 | return log_at<level::debug>(fmt.get(), std::make_format_args(args...)); |
| 150 | } | ||
| 136 | 151 | ||
| 137 | template<class... Args> | 152 | template <class... Args> |
| 138 | auto warn(std::format_string<Args...> fmt, Args&&... args) -> logger& { | 153 | auto info(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 139 | return log_at<level::warn>(fmt.get(), std::make_format_args(args...)); | 154 | { |
| 140 | } | 155 | return log_at<level::info>(fmt.get(), std::make_format_args(args...)); |
| 156 | } | ||
| 141 | 157 | ||
| 142 | template<class... Args> | 158 | template <class... Args> |
| 143 | auto error(std::format_string<Args...> fmt, Args&&... args) -> logger& { | 159 | auto warn(std::format_string<Args...> fmt, Args&&... args) -> logger& |
| 144 | return log_at<level::error>(fmt.get(), std::make_format_args(args...)); | 160 | { |
| 145 | } | 161 | return log_at<level::warn>(fmt.get(), std::make_format_args(args...)); |
| 146 | }; | 162 | } |
| 163 | |||
| 164 | template <class... Args> | ||
| 165 | auto error(std::format_string<Args...> fmt, Args&&... args) -> logger& | ||
| 166 | { | ||
| 167 | return log_at<level::error>(fmt.get(), std::make_format_args(args...)); | ||
| 168 | } | ||
| 169 | }; | ||
| 147 | 170 | ||
| 148 | } // namespace routemon::log | 171 | } // namespace routemon::log |
diff --git a/server/src/main.cpp b/server/src/main.cpp index a40b0b0..0a7a784 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | #include <malloc.h> // for malloc_trim(3) | ||
| 2 | #include <boost/asio.hpp> | 1 | #include <boost/asio.hpp> |
| 2 | #include <malloc.h> // for malloc_trim(3) | ||
| 3 | 3 | ||
| 4 | import std; | 4 | import std; |
| 5 | import routemon; | 5 | import routemon; |
| @@ -7,17 +7,23 @@ import routemon; | |||
| 7 | namespace chrono = std::chrono; | 7 | namespace chrono = std::chrono; |
| 8 | namespace net = boost::asio; | 8 | namespace net = boost::asio; |
| 9 | 9 | ||
| 10 | enum class exit_status { | 10 | enum class exit_status |
| 11 | { | ||
| 11 | failure, | 12 | failure, |
| 12 | bad_usage, | 13 | bad_usage, |
| 13 | }; | 14 | }; |
| 14 | 15 | ||
| 15 | auto real_main(std::span<char const*> args) -> exit_status { | 16 | auto real_main(std::span<char const*> args) -> exit_status |
| 17 | { | ||
| 16 | auto sink = routemon::log::make_sink(routemon::log::level::info); | 18 | auto sink = routemon::log::make_sink(routemon::log::level::info); |
| 17 | auto l = routemon::log::logger{sink}; | 19 | auto l = routemon::log::logger{sink}; |
| 18 | 20 | ||
| 19 | if (args.size() != 2) { | 21 | if (args.size() != 2) |
| 20 | l.error("Fatal: expected exactly one argument (the configuration file location), got {}", args.size() - 1); | 22 | { |
| 23 | l.error( | ||
| 24 | "Fatal: expected exactly one argument (the configuration file " | ||
| 25 | "location), got {}", | ||
| 26 | args.size() - 1); | ||
| 21 | return exit_status::bad_usage; | 27 | return exit_status::bad_usage; |
| 22 | } | 28 | } |
| 23 | auto const* config_filename = args[1]; | 29 | auto const* config_filename = args[1]; |
| @@ -25,54 +31,73 @@ auto real_main(std::span<char const*> args) -> exit_status { | |||
| 25 | auto lgen = routemon::locale::make_generator(); | 31 | auto lgen = routemon::locale::make_generator(); |
| 26 | auto default_locale = lgen->generate("en_US.UTF-8"); | 32 | auto default_locale = lgen->generate("en_US.UTF-8"); |
| 27 | auto locales = { | 33 | auto locales = { |
| 28 | default_locale, | 34 | default_locale, |
| 29 | lgen->generate("nl_NL.UTF-8"), | 35 | lgen->generate("nl_NL.UTF-8"), |
| 30 | lgen->generate("de_DE.UTF-8"), | 36 | lgen->generate("de_DE.UTF-8"), |
| 31 | lgen->generate("en_GB.UTF-8"), | 37 | lgen->generate("en_GB.UTF-8"), |
| 32 | }; | 38 | }; |
| 33 | auto lsel = routemon::locale::selector{locales, default_locale, lgen}; | 39 | auto lsel = routemon::locale::selector{locales, default_locale, lgen}; |
| 34 | 40 | ||
| 35 | auto ioc = net::io_context{1 /* concurrency hint */}; | 41 | auto ioc = net::io_context{1 /* concurrency hint */}; |
| 36 | 42 | ||
| 37 | auto config = routemon::config::app{}; | 43 | auto config = routemon::config::app{}; |
| 38 | try { | 44 | try |
| 45 | { | ||
| 39 | config = routemon::config::load_file(config_filename); | 46 | config = routemon::config::load_file(config_filename); |
| 40 | } catch (std::exception const& e) { | 47 | } |
| 41 | l.with("filename", std::string_view{config_filename}). | 48 | catch (std::exception const& e) |
| 42 | error("Failed to load configuration: {}", e.what()); | 49 | { |
| 50 | l.with("filename", std::string_view{config_filename}) | ||
| 51 | .error("Failed to load configuration: {}", e.what()); | ||
| 43 | return exit_status::failure; | 52 | return exit_status::failure; |
| 44 | } | 53 | } |
| 45 | sink->set_level(config.logger.level); | 54 | sink->set_level(config.logger.level); |
| 46 | // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, config.rwgps.auth_token}; | 55 | // auto rwgps_client = routemon::rwgps::client{ioc, l, config.rwgps.api_key, |
| 47 | // for (auto route : rwgps_client.get_all_routes()) { | 56 | // config.rwgps.auth_token}; for (auto route : |
| 48 | // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, route.name, route.url); | 57 | // rwgps_client.get_all_routes()) |
| 58 | // { | ||
| 59 | // l.info("Route {} (user {}): {} @ {}", route.id, route.user_id, | ||
| 60 | // route.name, route.url); | ||
| 49 | // } | 61 | // } |
| 50 | 62 | ||
| 51 | auto dbc = std::shared_ptr<routemon::database::connection>{}; | 63 | auto dbc = std::shared_ptr<routemon::database::connection>{}; |
| 52 | try { | 64 | try |
| 65 | { | ||
| 53 | dbc = routemon::database::open(config.database.sqlite3_filename); | 66 | dbc = routemon::database::open(config.database.sqlite3_filename); |
| 54 | } catch (std::exception const& e) { | 67 | } |
| 55 | l.with("filename", config.database.sqlite3_filename). | 68 | catch (std::exception const& e) |
| 56 | error("Failed to open database: {}", e.what()); | 69 | { |
| 70 | l.with("filename", config.database.sqlite3_filename) | ||
| 71 | .error("Failed to open database: {}", e.what()); | ||
| 57 | return exit_status::failure; | 72 | return exit_status::failure; |
| 58 | } | 73 | } |
| 59 | 74 | ||
| 60 | l.with("filename", config.situations.datex2_filename). | 75 | l.with("filename", config.situations.datex2_filename) |
| 61 | info("Loading situations"); | 76 | .info("Loading situations"); |
| 62 | auto const before_load = chrono::steady_clock::now(); | 77 | auto const before_load = chrono::steady_clock::now(); |
| 63 | auto d2loader = routemon::datex2::loader{}; | 78 | auto d2loader = routemon::datex2::loader{}; |
| 64 | auto pub = routemon::datex2::situation_publication{}; | 79 | auto pub = routemon::datex2::situation_publication{}; |
| 65 | try { | 80 | try |
| 66 | pub = d2loader.load_situation_publication(config.situations.datex2_filename); | 81 | { |
| 67 | } catch (std::exception const& e) { | 82 | pub = |
| 83 | d2loader.load_situation_publication(config.situations.datex2_filename); | ||
| 84 | } | ||
| 85 | catch (std::exception const& e) | ||
| 86 | { | ||
| 68 | l.error("Failed to load DATEX II situations publication: {}", e.what()); | 87 | l.error("Failed to load DATEX II situations publication: {}", e.what()); |
| 69 | return exit_status::failure; | 88 | return exit_status::failure; |
| 70 | } | 89 | } |
| 71 | if (!d2loader.warnings().empty()) { | 90 | if (!d2loader.warnings().empty()) |
| 91 | { | ||
| 72 | auto const& warns = d2loader.warnings(); | 92 | auto const& warns = d2loader.warnings(); |
| 73 | l.warn("Encountered {} unique warnings while loading DATEX II situations publication", warns.size()); | 93 | l.warn( |
| 94 | "Encountered {} unique warnings while loading DATEX II situations " | ||
| 95 | "publication", | ||
| 96 | warns.size()); | ||
| 74 | auto i = 0uz; | 97 | auto i = 0uz; |
| 75 | for (auto it = warns.begin(); it != warns.end(); it = warns.upper_bound(*it)) { | 98 | for (auto it = warns.begin(); it != warns.end(); |
| 99 | it = warns.upper_bound(*it)) | ||
| 100 | { | ||
| 76 | l.warn("Warning {} (appeared {}×): {}", ++i, warns.count(*it), *it); | 101 | l.warn("Warning {} (appeared {}×): {}", ++i, warns.count(*it), *it); |
| 77 | } | 102 | } |
| 78 | } | 103 | } |
| @@ -82,11 +107,13 @@ auto real_main(std::span<char const*> args) -> exit_status { | |||
| 82 | // to return as much memory as possible to the OS. | 107 | // to return as much memory as possible to the OS. |
| 83 | malloc_trim(0); | 108 | malloc_trim(0); |
| 84 | auto const after_load = chrono::steady_clock::now(); | 109 | auto const after_load = chrono::steady_clock::now(); |
| 85 | auto const dur_load = chrono::duration_cast<chrono::milliseconds>(after_load - before_load); | 110 | auto const dur_load = |
| 111 | chrono::duration_cast<chrono::milliseconds>(after_load - before_load); | ||
| 86 | l.info("Loading situations finished in {}", dur_load); | 112 | l.info("Loading situations finished in {}", dur_load); |
| 87 | 113 | ||
| 88 | auto handler = routemon::api::handler{l, std::move(pub)}; | 114 | auto handler = routemon::api::handler{l, std::move(pub)}; |
| 89 | auto http_server = routemon::srv::server{l, std::move(lsel), std::move(handler)}; | 115 | auto http_server = |
| 116 | routemon::srv::server{l, std::move(lsel), std::move(handler)}; | ||
| 90 | http_server.spawn(ioc); | 117 | http_server.spawn(ioc); |
| 91 | ioc.run(); | 118 | ioc.run(); |
| 92 | 119 | ||
| @@ -94,13 +121,19 @@ auto real_main(std::span<char const*> args) -> exit_status { | |||
| 94 | return exit_status::failure; | 121 | return exit_status::failure; |
| 95 | } | 122 | } |
| 96 | 123 | ||
| 97 | auto main(int argc, char* argv[]) -> int { | 124 | auto main(int argc, char* argv[]) -> int |
| 98 | if (argc < 0) { | 125 | { |
| 126 | if (argc < 0) | ||
| 127 | { | ||
| 99 | std::cout << "Fatal: argument count below zero" << std::endl; | 128 | std::cout << "Fatal: argument count below zero" << std::endl; |
| 100 | return EXIT_FAILURE; | 129 | return EXIT_FAILURE; |
| 101 | } | 130 | } |
| 102 | auto res = real_main(std::span{const_cast<char const**>(argv), static_cast<std::size_t>(argc)}); | 131 | auto res = real_main( |
| 103 | switch (res) { | 132 | std::span{ |
| 133 | const_cast<char const**>(argv), static_cast<std::size_t>(argc) | ||
| 134 | }); | ||
| 135 | switch (res) | ||
| 136 | { | ||
| 104 | case exit_status::failure: | 137 | case exit_status::failure: |
| 105 | return EXIT_FAILURE; | 138 | return EXIT_FAILURE; |
| 106 | case exit_status::bad_usage: | 139 | case exit_status::bad_usage: |
diff --git a/server/src/problem.cppm b/server/src/problem.cppm index 8764962..a124312 100644 --- a/server/src/problem.cppm +++ b/server/src/problem.cppm | |||
| @@ -14,47 +14,58 @@ namespace json = boost::json; | |||
| 14 | 14 | ||
| 15 | namespace routemon::problem { | 15 | namespace routemon::problem { |
| 16 | 16 | ||
| 17 | export struct details { | 17 | export struct details |
| 18 | http::status status; | 18 | { |
| 19 | blocale::message title; | 19 | http::status status; |
| 20 | std::string_view type_uri; | 20 | blocale::message title; |
| 21 | std::optional<blocale::message> detail = std::nullopt; | 21 | std::string_view type_uri; |
| 22 | std::optional<std::string> instance = std::nullopt; | 22 | std::optional<blocale::message> detail = std::nullopt; |
| 23 | std::optional<std::string> instance = std::nullopt; | ||
| 23 | 24 | ||
| 24 | auto set_detail(blocale::message detail) -> details& { | 25 | auto set_detail(blocale::message detail) -> details& |
| 25 | this->detail = detail; | 26 | { |
| 26 | return *this; | 27 | this->detail = detail; |
| 27 | } | 28 | return *this; |
| 29 | } | ||
| 28 | 30 | ||
| 29 | auto set_instance(std::string&& instance) -> details& { | 31 | auto set_instance(std::string&& instance) -> details& |
| 30 | this->instance = instance; | 32 | { |
| 31 | return *this; | 33 | this->instance = instance; |
| 32 | } | 34 | return *this; |
| 33 | auto set_instance(std::string_view instance) -> details& { | 35 | } |
| 34 | this->instance = std::string{instance}; | 36 | auto set_instance(std::string_view instance) -> details& |
| 35 | return *this; | 37 | { |
| 36 | } | 38 | this->instance = std::string{instance}; |
| 37 | }; | 39 | return *this; |
| 40 | } | ||
| 41 | }; | ||
| 38 | 42 | ||
| 39 | export auto tag_invoke(json::value_from_tag, json::value& jv, details const& details, std::locale locale) -> void { | 43 | export auto tag_invoke( |
| 40 | auto obj = json::object{ | 44 | json::value_from_tag, json::value& jv, details const& details, |
| 41 | {"type", details.type_uri}, | 45 | std::locale locale) -> void |
| 42 | {"title", details.title.str(locale)}, | 46 | { |
| 47 | auto obj = json::object{ | ||
| 48 | {"type", details.type_uri}, | ||
| 49 | {"title", details.title.str(locale)}, | ||
| 43 | {"status", static_cast<unsigned>(details.status)}, | 50 | {"status", static_cast<unsigned>(details.status)}, |
| 44 | }; | 51 | }; |
| 45 | if (details.detail) obj["detail"] = details.detail->str(locale); | 52 | if (details.detail) |
| 46 | if (details.instance) obj["instance"] = *details.instance; | 53 | obj["detail"] = details.detail->str(locale); |
| 47 | jv = obj; | 54 | if (details.instance) |
| 48 | } | 55 | obj["instance"] = *details.instance; |
| 56 | jv = obj; | ||
| 57 | } | ||
| 49 | 58 | ||
| 50 | export struct tpl { | 59 | export struct tpl |
| 51 | http::status status; | 60 | { |
| 52 | blocale::message title; | 61 | http::status status; |
| 53 | std::string_view type_uri; | 62 | blocale::message title; |
| 63 | std::string_view type_uri; | ||
| 54 | 64 | ||
| 55 | auto instantiate() const -> details { | 65 | auto instantiate() const -> details |
| 56 | return details{status, title, type_uri}; | 66 | { |
| 57 | } | 67 | return details{status, title, type_uri}; |
| 58 | }; | 68 | } |
| 69 | }; | ||
| 59 | 70 | ||
| 60 | } | 71 | } // namespace routemon::problem |
diff --git a/server/src/req_ctx.cppm b/server/src/req_ctx.cppm index 797c51d..4a64d7d 100644 --- a/server/src/req_ctx.cppm +++ b/server/src/req_ctx.cppm | |||
| @@ -10,42 +10,39 @@ import :trace; | |||
| 10 | 10 | ||
| 11 | namespace routemon { | 11 | namespace routemon { |
| 12 | 12 | ||
| 13 | export class req_ctx { | 13 | export class req_ctx |
| 14 | trace::id tid_; | 14 | { |
| 15 | std::locale locale_; | 15 | trace::id tid_; |
| 16 | http::verb_set route_verbs_; | 16 | std::locale locale_; |
| 17 | bool keep_alive_; | 17 | http::verb_set route_verbs_; |
| 18 | bhttp::request_header<bhttp::fields> const& req_header_; | 18 | bool keep_alive_; |
| 19 | bhttp::request_header<bhttp::fields> const& req_header_; | ||
| 19 | 20 | ||
| 20 | public: | 21 | 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 | explicit req_ctx( |
| 22 | : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, keep_alive_{keep_alive}, req_header_{req_header} | 23 | trace::id tid, std::locale locale, http::verb_set route_verbs, |
| 23 | {} | 24 | bool keep_alive, bhttp::request_header<bhttp::fields> const& req_header) |
| 25 | : tid_{tid}, locale_{locale}, route_verbs_{route_verbs}, | ||
| 26 | keep_alive_{keep_alive}, req_header_{req_header} | ||
| 27 | { | ||
| 28 | } | ||
| 24 | 29 | ||
| 25 | template<typename ReqBody> | 30 | template <typename ReqBody> |
| 26 | explicit req_ctx(trace::id tid, std::locale locale, http::verb_set route_verbs, bhttp::request<ReqBody> const& req) | 31 | explicit req_ctx( |
| 27 | : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} | 32 | trace::id tid, std::locale locale, http::verb_set route_verbs, |
| 28 | {} | 33 | bhttp::request<ReqBody> const& req) |
| 34 | : req_ctx{tid, locale, route_verbs, req.keep_alive(), req.base()} | ||
| 35 | { | ||
| 36 | } | ||
| 29 | 37 | ||
| 30 | auto trace_id() const -> trace::id { | 38 | auto trace_id() const -> trace::id { return tid_; } |
| 31 | return tid_; | 39 | auto locale() const -> std::locale { return locale_; } |
| 32 | } | 40 | auto route_verbs() const -> http::verb_set { return route_verbs_; } |
| 33 | 41 | auto keep_alive() const -> bool { return keep_alive_; } | |
| 34 | auto locale() const -> std::locale { | 42 | auto req_header() const -> bhttp::request_header<bhttp::fields> const& |
| 35 | return locale_; | 43 | { |
| 36 | } | 44 | return req_header_; |
| 37 | 45 | } | |
| 38 | auto route_verbs() const -> http::verb_set { | 46 | }; |
| 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 | 47 | ||
| 51 | } // namespace routemon | 48 | } // namespace routemon |
diff --git a/server/src/rwgps.cppm b/server/src/rwgps.cppm index 7ad6605..8269ee8 100644 --- a/server/src/rwgps.cppm +++ b/server/src/rwgps.cppm | |||
| @@ -17,121 +17,148 @@ namespace json = boost::json; | |||
| 17 | 17 | ||
| 18 | namespace routemon::rwgps { | 18 | namespace routemon::rwgps { |
| 19 | 19 | ||
| 20 | export struct route_summary { | 20 | export struct route_summary |
| 21 | std::int64_t id; | 21 | { |
| 22 | std::int64_t user_id; | 22 | std::int64_t id; |
| 23 | std::string url; | 23 | std::int64_t user_id; |
| 24 | std::string name; | 24 | std::string url; |
| 25 | std::string description; | 25 | std::string name; |
| 26 | }; | 26 | std::string description; |
| 27 | }; | ||
| 27 | 28 | ||
| 28 | struct pagination { | 29 | struct pagination |
| 29 | std::size_t record_count; | 30 | { |
| 30 | std::size_t page_count; | 31 | std::size_t record_count; |
| 31 | std::size_t page_size; | 32 | std::size_t page_count; |
| 32 | std::optional<std::string> next_page_url; | 33 | std::size_t page_size; |
| 33 | }; | 34 | std::optional<std::string> next_page_url; |
| 35 | }; | ||
| 34 | 36 | ||
| 35 | struct get_routes_meta { | 37 | struct get_routes_meta |
| 36 | pagination pagination; | 38 | { |
| 37 | }; | 39 | pagination pagination; |
| 40 | }; | ||
| 38 | 41 | ||
| 39 | struct get_routes_response { | 42 | struct get_routes_response |
| 40 | std::vector<route_summary> routes; | 43 | { |
| 41 | get_routes_meta meta; | 44 | std::vector<route_summary> routes; |
| 42 | }; | 45 | get_routes_meta meta; |
| 46 | }; | ||
| 43 | 47 | ||
| 44 | auto tag_invoke(json::value_to_tag<route_summary> const&, json::value const& jv) -> route_summary { | 48 | auto tag_invoke(json::value_to_tag<route_summary> const&, json::value const& jv) |
| 45 | return { | 49 | -> route_summary |
| 46 | .id = json::value_to<std::int64_t>(jv.at("id")), | 50 | { |
| 47 | .user_id = json::value_to<std::int64_t>(jv.at("user_id")), | 51 | return { |
| 48 | .url = json::value_to<std::string>(jv.at("url")), | 52 | .id = json::value_to<std::int64_t>(jv.at("id")), |
| 49 | .name = json::value_to<std::string>(jv.at("name")), | 53 | .user_id = json::value_to<std::int64_t>(jv.at("user_id")), |
| 54 | .url = json::value_to<std::string>(jv.at("url")), | ||
| 55 | .name = json::value_to<std::string>(jv.at("name")), | ||
| 50 | .description = json::value_to<std::string>(jv.at("description")), | 56 | .description = json::value_to<std::string>(jv.at("description")), |
| 51 | }; | 57 | }; |
| 52 | } | 58 | } |
| 53 | 59 | ||
| 54 | auto tag_invoke(json::value_to_tag<pagination> const&, json::value const& jv) -> pagination { | 60 | auto tag_invoke(json::value_to_tag<pagination> const&, json::value const& jv) |
| 55 | return { | 61 | -> pagination |
| 56 | .record_count = json::value_to<std::size_t>(jv.at("record_count")), | 62 | { |
| 57 | .page_count = json::value_to<std::size_t>(jv.at("page_count")), | 63 | return { |
| 58 | .page_size = json::value_to<std::size_t>(jv.at("page_size")), | 64 | .record_count = json::value_to<std::size_t>(jv.at("record_count")), |
| 59 | .next_page_url = json::value_to<std::optional<std::string>>(jv.at("next_page_url")), | 65 | .page_count = json::value_to<std::size_t>(jv.at("page_count")), |
| 60 | }; | 66 | .page_size = json::value_to<std::size_t>(jv.at("page_size")), |
| 61 | } | 67 | .next_page_url = |
| 68 | json::value_to<std::optional<std::string>>(jv.at("next_page_url")), | ||
| 69 | }; | ||
| 70 | } | ||
| 62 | 71 | ||
| 63 | auto tag_invoke(json::value_to_tag<get_routes_meta> const&, json::value const& jv) -> get_routes_meta { | 72 | auto tag_invoke( |
| 64 | return { | 73 | json::value_to_tag<get_routes_meta> const&, json::value const& jv) |
| 74 | -> get_routes_meta | ||
| 75 | { | ||
| 76 | return { | ||
| 65 | .pagination = json::value_to<pagination>(jv.at("pagination")), | 77 | .pagination = json::value_to<pagination>(jv.at("pagination")), |
| 66 | }; | 78 | }; |
| 67 | } | 79 | } |
| 68 | 80 | ||
| 69 | auto tag_invoke(json::value_to_tag<get_routes_response> const&, json::value const& jv) -> get_routes_response { | 81 | auto tag_invoke( |
| 70 | return { | 82 | json::value_to_tag<get_routes_response> const&, json::value const& jv) |
| 83 | -> get_routes_response | ||
| 84 | { | ||
| 85 | return { | ||
| 71 | .routes = json::value_to<std::vector<route_summary>>(jv.at("routes")), | 86 | .routes = json::value_to<std::vector<route_summary>>(jv.at("routes")), |
| 72 | .meta = json::value_to<get_routes_meta>(jv.at("meta")), | 87 | .meta = json::value_to<get_routes_meta>(jv.at("meta")), |
| 73 | }; | 88 | }; |
| 74 | } | 89 | } |
| 75 | 90 | ||
| 76 | auto json_value_to_get_routes_response(json::value const& jv) -> get_routes_response { | 91 | auto json_value_to_get_routes_response(json::value const& jv) |
| 77 | return json::value_to<get_routes_response>(jv); | 92 | -> get_routes_response |
| 78 | } | 93 | { |
| 94 | return json::value_to<get_routes_response>(jv); | ||
| 95 | } | ||
| 79 | 96 | ||
| 80 | export class client { | 97 | export class client |
| 81 | log::logger l_; | 98 | { |
| 82 | http::client hc_; | 99 | log::logger l_; |
| 83 | std::string api_key_; | 100 | http::client hc_; |
| 84 | std::string auth_token_; | 101 | std::string api_key_; |
| 102 | std::string auth_token_; | ||
| 85 | 103 | ||
| 86 | static constexpr std::string host = "ridewithgps.com"; | 104 | static constexpr std::string host = "ridewithgps.com"; |
| 87 | 105 | ||
| 88 | // TODO: handle failure appropriately | 106 | // TODO: handle failure appropriately |
| 89 | auto get_routes_page(std::size_t page) -> get_routes_response { | 107 | auto get_routes_page(std::size_t page) -> get_routes_response |
| 90 | auto req = bhttp::request<bhttp::string_body>{ | 108 | { |
| 109 | auto req = bhttp::request<bhttp::string_body>{ | ||
| 91 | bhttp::verb::get, | 110 | bhttp::verb::get, |
| 92 | std::format("/api/v1/routes.json?page_size=200?page={}", page), | 111 | std::format("/api/v1/routes.json?page_size=200?page={}", page), |
| 93 | 11, // HTTP 1.1 | 112 | 11, // HTTP 1.1 |
| 94 | }; | 113 | }; |
| 95 | req.set(bhttp::field::host, host); | 114 | req.set(bhttp::field::host, host); |
| 96 | req.set("x-rwgps-api-key", api_key_); | 115 | req.set("x-rwgps-api-key", api_key_); |
| 97 | req.set("x-rwgps-auth-token", auth_token_); | 116 | req.set("x-rwgps-auth-token", auth_token_); |
| 98 | 117 | ||
| 99 | auto rsp = hc_.do_request(req); | 118 | auto rsp = hc_.do_request(req); |
| 100 | auto p = json::stream_parser{}; | 119 | auto p = json::stream_parser{}; |
| 101 | for (auto const frag : rsp.body().cdata()) { | 120 | for (auto const frag : rsp.body().cdata()) |
| 102 | p.write(static_cast<char const*>(frag.data()), frag.size()); | 121 | { |
| 103 | } | 122 | p.write(static_cast<char const*>(frag.data()), frag.size()); |
| 104 | assert(p.done()); | ||
| 105 | return json_value_to_get_routes_response(p.release()); | ||
| 106 | } | 123 | } |
| 124 | assert(p.done()); | ||
| 125 | return json_value_to_get_routes_response(p.release()); | ||
| 126 | } | ||
| 107 | 127 | ||
| 108 | public: | 128 | public: |
| 109 | explicit client(net::io_context& ioc, log::logger const& l, std::string api_key, std::string auth_token) | 129 | explicit client( |
| 110 | : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, auth_token_{std::move(auth_token)} | 130 | net::io_context& ioc, log::logger const& l, std::string api_key, |
| 111 | {} | 131 | std::string auth_token) |
| 132 | : l_{l.sub("rwgps-client")}, hc_{ioc}, api_key_{std::move(api_key)}, | ||
| 133 | auth_token_{std::move(auth_token)} | ||
| 134 | { | ||
| 135 | } | ||
| 112 | 136 | ||
| 113 | auto get_all_routes() -> std::vector<route_summary> { | 137 | auto get_all_routes() -> std::vector<route_summary> |
| 114 | // TODO: make sure that there are no duplicates here. | 138 | { |
| 115 | // What does RWGPS sort on, by default? | 139 | // TODO: make sure that there are no duplicates here. |
| 116 | // Consider using an associative container instead of a vector. | 140 | // What does RWGPS sort on, by default? |
| 117 | auto record_count = 0uz; | 141 | // Consider using an associative container instead of a vector. |
| 118 | auto current_page = 0uz; | 142 | auto record_count = 0uz; |
| 119 | auto routes = std::vector<route_summary>{}; | 143 | auto current_page = 0uz; |
| 144 | auto routes = std::vector<route_summary>{}; | ||
| 120 | 145 | ||
| 121 | while (true) { | 146 | while (true) |
| 122 | auto rsp = get_routes_page(current_page); | 147 | { |
| 123 | if (rsp.meta.pagination.next_page_url) | 148 | auto rsp = get_routes_page(current_page); |
| 124 | l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); | 149 | if (rsp.meta.pagination.next_page_url) |
| 125 | routes.append_range(rsp.routes); | 150 | l_.debug("Next page URL: {}", *rsp.meta.pagination.next_page_url); |
| 126 | if (rsp.meta.pagination.record_count > 0) | 151 | routes.append_range(rsp.routes); |
| 127 | record_count = rsp.meta.pagination.record_count; | 152 | if (rsp.meta.pagination.record_count > 0) |
| 128 | if (rsp.routes.empty() || routes.size() >= record_count) { | 153 | record_count = rsp.meta.pagination.record_count; |
| 129 | break; | 154 | if (rsp.routes.empty() || routes.size() >= record_count) |
| 130 | } | 155 | { |
| 156 | break; | ||
| 131 | } | 157 | } |
| 132 | |||
| 133 | return routes; | ||
| 134 | } | 158 | } |
| 135 | }; | 159 | |
| 160 | return routes; | ||
| 161 | } | ||
| 162 | }; | ||
| 136 | 163 | ||
| 137 | } // namespace routemon::rwgps | 164 | } // namespace routemon::rwgps |
diff --git a/server/src/sqlite3.cppm b/server/src/sqlite3.cppm index f226c6b..70f58c0 100644 --- a/server/src/sqlite3.cppm +++ b/server/src/sqlite3.cppm | |||
| @@ -9,249 +9,303 @@ import :util; | |||
| 9 | 9 | ||
| 10 | namespace routemon::sqlite3 { | 10 | namespace routemon::sqlite3 { |
| 11 | 11 | ||
| 12 | class mutex_guard { | 12 | class mutex_guard |
| 13 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} { | 13 | { |
| 14 | ::sqlite3_mutex_enter(mut_); | 14 | explicit mutex_guard(::sqlite3_mutex* mut) noexcept : mut_{mut} |
| 15 | } | 15 | { |
| 16 | ::sqlite3_mutex_enter(mut_); | ||
| 17 | } | ||
| 16 | 18 | ||
| 17 | friend auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())); | 19 | friend auto |
| 20 | do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) | ||
| 21 | -> decltype(f(std::declval<mutex_guard const&>())); | ||
| 18 | 22 | ||
| 19 | public: | 23 | public: |
| 20 | mutex_guard(mutex_guard const&) = delete; | 24 | mutex_guard(mutex_guard const&) = delete; |
| 21 | ~mutex_guard() { | 25 | ~mutex_guard() { ::sqlite3_mutex_leave(mut_); } |
| 22 | ::sqlite3_mutex_leave(mut_); | ||
| 23 | } | ||
| 24 | 26 | ||
| 25 | private: | 27 | private: |
| 26 | ::sqlite3_mutex* mut_; | 28 | ::sqlite3_mutex* mut_; |
| 27 | }; | 29 | }; |
| 28 | 30 | ||
| 29 | auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) { | 31 | auto do_guarded(::sqlite3_mutex* mut, std::invocable<mutex_guard const&> auto f) |
| 30 | return f(mutex_guard{mut}); | 32 | -> decltype(f(std::declval<mutex_guard const&>())) |
| 31 | } | 33 | { |
| 34 | return f(mutex_guard{mut}); | ||
| 35 | } | ||
| 32 | 36 | ||
| 33 | auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) -> decltype(f(std::declval<mutex_guard const&>())) { | 37 | auto do_guarded(::sqlite3* dbc, std::invocable<mutex_guard const&> auto f) |
| 34 | return do_guarded(::sqlite3_db_mutex(dbc), f); | 38 | -> decltype(f(std::declval<mutex_guard const&>())) |
| 35 | } | 39 | { |
| 40 | return do_guarded(::sqlite3_db_mutex(dbc), f); | ||
| 41 | } | ||
| 36 | 42 | ||
| 37 | class error : public std::exception { | 43 | class error : public std::exception |
| 38 | int code_; | 44 | { |
| 39 | std::string message_; | 45 | int code_; |
| 46 | std::string message_; | ||
| 40 | 47 | ||
| 41 | public: | 48 | public: |
| 42 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc) | 49 | explicit error(mutex_guard const&, int code, ::sqlite3* dbc) |
| 43 | : code_{code}, message_{::sqlite3_errmsg(dbc)} | 50 | : code_{code}, message_{::sqlite3_errmsg(dbc)} |
| 44 | {} | 51 | { |
| 52 | } | ||
| 45 | 53 | ||
| 46 | explicit error(int code) | 54 | explicit error(int code) : code_{code}, message_{::sqlite3_errstr(code)} {} |
| 47 | : code_{code}, message_{::sqlite3_errstr(code)} | ||
| 48 | {} | ||
| 49 | 55 | ||
| 50 | [[nodiscard]] auto what() const noexcept -> char const* override { | 56 | [[nodiscard]] auto what() const noexcept -> char const* override |
| 51 | return message_.c_str(); | 57 | { |
| 52 | } | 58 | return message_.c_str(); |
| 59 | } | ||
| 53 | 60 | ||
| 54 | [[nodiscard]] auto code() const noexcept -> int { | 61 | [[nodiscard]] auto code() const noexcept -> int { return code_; } |
| 55 | return code_; | 62 | }; |
| 56 | } | ||
| 57 | }; | ||
| 58 | 63 | ||
| 59 | template<class T, template<class U> concept C> | 64 | template <class T, template <class U> concept C> |
| 60 | concept optional_of = requires { | 65 | concept optional_of = requires { |
| 61 | typename T::value_type; | 66 | typename T::value_type; |
| 62 | requires std::same_as<T, std::optional<typename T::value_type>>; | 67 | requires std::same_as<T, std::optional<typename T::value_type>>; |
| 63 | requires C<typename T::value_type>; | 68 | requires C<typename T::value_type>; |
| 64 | }; | 69 | }; |
| 65 | 70 | ||
| 66 | template<class T> | 71 | template <class T> |
| 67 | concept scannable_prim = | 72 | concept scannable_prim = std::same_as<T, std::string> || std::same_as<T, double> |
| 68 | std::same_as<T, std::string> || | 73 | || std::same_as<T, std::int64_t>; |
| 69 | std::same_as<T, double> || | ||
| 70 | std::same_as<T, std::int64_t>; | ||
| 71 | 74 | ||
| 72 | template<class T> | 75 | template <class T> |
| 73 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; | 76 | concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; |
| 74 | 77 | ||
| 75 | class statement { | 78 | class statement |
| 76 | ::sqlite3_stmt* stmt_; | 79 | { |
| 80 | ::sqlite3_stmt* stmt_; | ||
| 77 | 81 | ||
| 78 | public: | 82 | public: |
| 79 | explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} | 83 | explicit statement(::sqlite3_stmt* stmt) : stmt_{stmt} {} |
| 80 | statement(statement const&) = delete; | 84 | statement(statement const&) = delete; |
| 81 | statement(statement&& s) noexcept { | 85 | statement(statement&& s) noexcept |
| 82 | stmt_ = s.stmt_; | 86 | { |
| 83 | s.stmt_ = nullptr; | 87 | stmt_ = s.stmt_; |
| 84 | } | 88 | s.stmt_ = nullptr; |
| 85 | ~statement() { | 89 | } |
| 86 | ::sqlite3_finalize(stmt_); | 90 | ~statement() { ::sqlite3_finalize(stmt_); } |
| 87 | } | 91 | auto get() -> ::sqlite3_stmt* { return stmt_; } |
| 88 | auto get() -> ::sqlite3_stmt* { | 92 | }; |
| 89 | return stmt_; | ||
| 90 | } | ||
| 91 | }; | ||
| 92 | 93 | ||
| 93 | class row_reader { | 94 | class row_reader |
| 94 | statement stmt_; | 95 | { |
| 96 | statement stmt_; | ||
| 95 | 97 | ||
| 96 | explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} | 98 | explicit row_reader(statement stmt) : stmt_{std::move(stmt)} {} |
| 97 | 99 | ||
| 98 | friend class connection; | 100 | friend class connection; |
| 99 | 101 | ||
| 100 | void scan(int col, std::string& s) { | 102 | void scan(int col, std::string& s) |
| 101 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) | 103 | { |
| 102 | throw std::invalid_argument{"invalid type for scan"}; | 104 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_TEXT) |
| 103 | unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); | 105 | throw std::invalid_argument{"invalid type for scan"}; |
| 104 | auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); | 106 | unsigned char const* chs = ::sqlite3_column_text(stmt_.get(), col); |
| 105 | if (!size.has_value()) | 107 | auto size = util::size_from_int(::sqlite3_column_bytes(stmt_.get(), col)); |
| 106 | throw std::logic_error{"unexpected negative amount of bytes in column"}; | 108 | if (!size.has_value()) |
| 107 | s = std::string{reinterpret_cast<char const*>(chs), *size}; | 109 | throw std::logic_error{"unexpected negative amount of bytes in column"}; |
| 108 | } | 110 | s = std::string{reinterpret_cast<char const*>(chs), *size}; |
| 111 | } | ||
| 109 | 112 | ||
| 110 | void scan(int col, double& v) { | 113 | void scan(int col, double& v) |
| 111 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) | 114 | { |
| 112 | throw std::invalid_argument{"invalid type for scan"}; | 115 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_FLOAT) |
| 113 | v = ::sqlite3_column_double(stmt_.get(), col); | 116 | throw std::invalid_argument{"invalid type for scan"}; |
| 114 | } | 117 | v = ::sqlite3_column_double(stmt_.get(), col); |
| 118 | } | ||
| 115 | 119 | ||
| 116 | void scan(int col, std::int64_t& v) { | 120 | void scan(int col, std::int64_t& v) |
| 117 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) | 121 | { |
| 118 | throw std::invalid_argument{"invalid type for scan"}; | 122 | if (::sqlite3_column_type(stmt_.get(), col) != SQLITE_INTEGER) |
| 119 | v = ::sqlite3_column_int64(stmt_.get(), col); | 123 | throw std::invalid_argument{"invalid type for scan"}; |
| 120 | } | 124 | v = ::sqlite3_column_int64(stmt_.get(), col); |
| 125 | } | ||
| 121 | 126 | ||
| 122 | void scan(int col, optional_of<scannable> auto& v) { | 127 | void scan(int col, optional_of<scannable> auto& v) |
| 123 | if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) { | 128 | { |
| 124 | v.reset(); | 129 | if (::sqlite3_column_type(stmt_.get(), col) == SQLITE_NULL) |
| 125 | } else { | 130 | { |
| 126 | typename std::remove_cvref_t<decltype(v)>::value_type tmp; | 131 | v.reset(); |
| 127 | scan(col, tmp); | ||
| 128 | v = std::move(tmp); | ||
| 129 | } | ||
| 130 | } | 132 | } |
| 131 | 133 | else | |
| 132 | public: | 134 | { |
| 133 | auto next() -> bool { | 135 | typename std::remove_cvref_t<decltype(v)>::value_type tmp; |
| 134 | ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); | 136 | scan(col, tmp); |
| 135 | return do_guarded(dbc, [&](auto const& guard) -> bool { | 137 | v = std::move(tmp); |
| 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 | } | 138 | } |
| 139 | } | ||
| 144 | 140 | ||
| 145 | auto scan(scannable auto&... args) -> void { | 141 | public: |
| 146 | auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); | 142 | auto next() -> bool |
| 147 | if (!ncols.has_value()) | 143 | { |
| 148 | throw std::logic_error{"got unexpected negative amount of columns"}; | 144 | ::sqlite3* dbc = ::sqlite3_db_handle(stmt_.get()); |
| 149 | if (sizeof...(args) > *ncols) | 145 | return do_guarded( |
| 150 | throw std::invalid_argument{"more scanning arguments provided than columns in result set"}; | 146 | dbc, |
| 151 | auto col = 0; (..., scan(col++, args)); | 147 | [&](auto const& guard) -> bool |
| 152 | } | 148 | { |
| 149 | auto const s = ::sqlite3_step(stmt_.get()); | ||
| 150 | if (s == SQLITE_ROW) | ||
| 151 | return true; | ||
| 152 | if (s == SQLITE_DONE) | ||
| 153 | return false; | ||
| 154 | throw error{guard, s, dbc}; | ||
| 155 | }); | ||
| 156 | } | ||
| 157 | |||
| 158 | auto scan(scannable auto&... args) -> void | ||
| 159 | { | ||
| 160 | auto const ncols = util::size_from_int(::sqlite3_data_count(stmt_.get())); | ||
| 161 | if (!ncols.has_value()) | ||
| 162 | throw std::logic_error{"got unexpected negative amount of columns"}; | ||
| 163 | if (sizeof...(args) > *ncols) | ||
| 164 | throw std::invalid_argument{ | ||
| 165 | "more scanning arguments provided than columns in result set" | ||
| 166 | }; | ||
| 167 | auto col = 0; | ||
| 168 | (..., scan(col++, args)); | ||
| 169 | } | ||
| 153 | 170 | ||
| 154 | auto scan_single(scannable auto&... args) -> void { | 171 | auto scan_single(scannable auto&... args) -> void |
| 155 | if (!next()) | 172 | { |
| 156 | throw std::logic_error{"no row in result set"}; | 173 | if (!next()) |
| 157 | scan(args...); | 174 | throw std::logic_error{"no row in result set"}; |
| 158 | if (next()) { | 175 | scan(args...); |
| 159 | throw std::logic_error{"more than one row in result set"}; | 176 | if (next()) |
| 160 | } | 177 | { |
| 178 | throw std::logic_error{"more than one row in result set"}; | ||
| 161 | } | 179 | } |
| 162 | }; | 180 | } |
| 181 | }; | ||
| 163 | 182 | ||
| 164 | class binder { | 183 | class binder |
| 165 | statement& stmt_; | 184 | { |
| 185 | statement& stmt_; | ||
| 166 | 186 | ||
| 167 | explicit binder(statement& stmt) : stmt_{stmt} {} | 187 | explicit binder(statement& stmt) : stmt_{stmt} {} |
| 168 | 188 | ||
| 169 | friend class connection; | 189 | friend class connection; |
| 170 | 190 | ||
| 171 | public: | 191 | public: |
| 172 | auto text(std::string const& param_name, std::string_view str) -> void { | 192 | 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()); | 193 | { |
| 174 | if (i == 0) | 194 | int const i = |
| 175 | throw std::invalid_argument{std::format("bind: no parameter with name {} found", param_name)}; | 195 | ::sqlite3_bind_parameter_index(stmt_.get(), param_name.c_str()); |
| 176 | auto str_size = util::int_from_size(str.size()); | 196 | if (i == 0) |
| 177 | if (!str_size.has_value()) | 197 | throw std::invalid_argument{std::format( |
| 178 | throw std::invalid_argument{"bind: provided text is too long"}; | 198 | "bind: no parameter with name {} found", param_name)}; |
| 179 | if (auto s = ::sqlite3_bind_text(stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); s != SQLITE_OK) { | 199 | auto str_size = util::int_from_size(str.size()); |
| 180 | throw error{s}; | 200 | if (!str_size.has_value()) |
| 181 | } | 201 | throw std::invalid_argument{"bind: provided text is too long"}; |
| 202 | if (auto s = ::sqlite3_bind_text( | ||
| 203 | stmt_.get(), i, str.data(), *str_size, SQLITE_TRANSIENT); | ||
| 204 | s != SQLITE_OK) | ||
| 205 | { | ||
| 206 | throw error{s}; | ||
| 182 | } | 207 | } |
| 208 | } | ||
| 183 | 209 | ||
| 184 | static auto noop(binder&) -> void {} | 210 | static auto noop(binder&) -> void {} |
| 185 | }; | 211 | }; |
| 186 | 212 | ||
| 187 | export class connection { | 213 | export class connection |
| 188 | ::sqlite3* dbc_; | 214 | { |
| 189 | ::sqlite3_mutex* mut_; | 215 | ::sqlite3* dbc_; |
| 216 | ::sqlite3_mutex* mut_; | ||
| 190 | 217 | ||
| 191 | explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} {} | 218 | explicit connection(::sqlite3* dbc) : dbc_{dbc}, mut_{::sqlite3_db_mutex(dbc)} |
| 219 | { | ||
| 220 | } | ||
| 192 | 221 | ||
| 193 | friend auto open(std::string const& filename) -> connection; | 222 | friend auto open(std::string const& filename) -> connection; |
| 194 | 223 | ||
| 195 | public: | 224 | public: |
| 196 | connection(connection const&) = delete; | 225 | connection(connection const&) = delete; |
| 197 | connection(connection&& c) noexcept { | 226 | connection(connection&& c) noexcept |
| 198 | dbc_ = c.dbc_; | 227 | { |
| 199 | mut_ = c.mut_; | 228 | dbc_ = c.dbc_; |
| 200 | c.dbc_ = nullptr; | 229 | mut_ = c.mut_; |
| 201 | c.mut_ = nullptr; | 230 | c.dbc_ = nullptr; |
| 202 | } | 231 | c.mut_ = nullptr; |
| 232 | } | ||
| 203 | 233 | ||
| 204 | [[nodiscard]] auto query(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> row_reader { | 234 | [[nodiscard]] auto query( |
| 205 | ::sqlite3_stmt* pstmt = nullptr; | 235 | std::string const& sql, |
| 206 | char const* sql_tail = nullptr; | 236 | std::function<void(binder&)> const& bf = binder::noop) -> row_reader |
| 207 | auto sql_size = util::int_from_size(sql.size()); | 237 | { |
| 208 | if (!sql_size.has_value() || *sql_size >= std::numeric_limits<int>::max() - 1) | 238 | ::sqlite3_stmt* pstmt = nullptr; |
| 209 | throw std::invalid_argument{"provided input text too large"}; | 239 | char const* sql_tail = nullptr; |
| 210 | do_guarded(mut_, [&](auto const& guard) -> void { | 240 | auto sql_size = util::int_from_size(sql.size()); |
| 211 | if (auto s = ::sqlite3_prepare_v2(dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); s != SQLITE_OK) { | 241 | if (!sql_size.has_value() |
| 212 | if (pstmt != nullptr) { | 242 | || *sql_size >= std::numeric_limits<int>::max() - 1) |
| 213 | // Use contract_assert when having a compiler with contracts available | 243 | throw std::invalid_argument{"provided input text too large"}; |
| 214 | ::sqlite3_finalize(pstmt); | 244 | do_guarded( |
| 215 | throw std::logic_error{"expected stmt to be null after failed preparation"}; | 245 | mut_, |
| 246 | [&](auto const& guard) -> void | ||
| 247 | { | ||
| 248 | if (auto s = ::sqlite3_prepare_v2( | ||
| 249 | dbc_, sql.data(), *sql_size + 1, &pstmt, &sql_tail); | ||
| 250 | s != SQLITE_OK) | ||
| 251 | { | ||
| 252 | if (pstmt != nullptr) | ||
| 253 | { | ||
| 254 | // Use contract_assert when having a compiler with | ||
| 255 | // contracts available | ||
| 256 | ::sqlite3_finalize(pstmt); | ||
| 257 | throw std::logic_error{ | ||
| 258 | "expected stmt to be null after failed preparation" | ||
| 259 | }; | ||
| 260 | } | ||
| 261 | throw error{guard, s, dbc_}; | ||
| 216 | } | 262 | } |
| 217 | throw error{guard, s, dbc_}; | 263 | }); |
| 218 | } | 264 | if (!pstmt) |
| 219 | }); | 265 | throw std::invalid_argument{"provided input text contains no SQL"}; |
| 220 | if (!pstmt) | 266 | auto stmt = statement{pstmt}; |
| 221 | throw std::invalid_argument{"provided input text contains no SQL"}; | 267 | if (sql_tail && std::strlen(sql_tail) > 0) |
| 222 | auto stmt = statement{pstmt}; | 268 | throw std::invalid_argument{ |
| 223 | if (sql_tail && std::strlen(sql_tail) > 0) | 269 | "provided input text contains more than one SQL statement" |
| 224 | throw std::invalid_argument{"provided input text contains more than one SQL statement"}; | 270 | }; |
| 225 | auto b = binder{stmt}; bf(b); | 271 | auto b = binder{stmt}; |
| 226 | return row_reader{std::move(stmt)}; | 272 | bf(b); |
| 227 | } | 273 | return row_reader{std::move(stmt)}; |
| 274 | } | ||
| 228 | 275 | ||
| 229 | auto exec(std::string const& sql, std::function<void(binder&)> const& bf = binder::noop) -> void { | 276 | auto exec( |
| 230 | auto reader = query(sql, bf); | 277 | std::string const& sql, |
| 231 | while (reader.next()); | 278 | std::function<void(binder&)> const& bf = binder::noop) -> void |
| 232 | } | 279 | { |
| 280 | auto reader = query(sql, bf); | ||
| 281 | while (reader.next()) | ||
| 282 | ; | ||
| 283 | } | ||
| 233 | 284 | ||
| 234 | ~connection() { | 285 | ~connection() { std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); } |
| 235 | std::ignore = ::sqlite3_close(std::exchange(dbc_, nullptr)); | 286 | }; |
| 236 | } | ||
| 237 | }; | ||
| 238 | 287 | ||
| 239 | export auto open(std::string const& filename) -> connection { | 288 | export auto open(std::string const& filename) -> connection |
| 240 | ::sqlite3* dbc = nullptr; | 289 | { |
| 241 | auto s = ::sqlite3_open_v2(filename.c_str(), &dbc, | 290 | ::sqlite3* dbc = nullptr; |
| 242 | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | | 291 | auto s = ::sqlite3_open_v2( |
| 243 | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_EXRESCODE, | 292 | filename.c_str(), &dbc, |
| 244 | nullptr); | 293 | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX |
| 245 | if (s != SQLITE_OK) { | 294 | | SQLITE_OPEN_EXRESCODE, |
| 246 | if (dbc) { | 295 | nullptr); |
| 247 | do_guarded(dbc, [&](auto const& guard) -> void { | 296 | if (s != SQLITE_OK) |
| 248 | throw error{guard, s, dbc}; | 297 | { |
| 249 | }); | 298 | if (dbc) |
| 250 | } else { | 299 | { |
| 251 | throw error{s}; | 300 | do_guarded( |
| 252 | } | 301 | dbc, [&](auto const& guard) -> void { throw error{guard, s, dbc}; }); |
| 302 | } | ||
| 303 | else | ||
| 304 | { | ||
| 305 | throw error{s}; | ||
| 253 | } | 306 | } |
| 254 | return connection{dbc}; | ||
| 255 | } | 307 | } |
| 308 | return connection{dbc}; | ||
| 309 | } | ||
| 256 | 310 | ||
| 257 | } // namespace routemon::sqlite3 | 311 | } // namespace routemon::sqlite3 |
diff --git a/server/src/srv.cppm b/server/src/srv.cppm index 2ca48c6..d9ff880 100644 --- a/server/src/srv.cppm +++ b/server/src/srv.cppm | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | module; | 1 | module; |
| 2 | 2 | ||
| 3 | #include <boost/config.hpp> | ||
| 4 | #include <boost/asio/ip/tcp.hpp> | ||
| 5 | #include <boost/asio/as_tuple.hpp> | 3 | #include <boost/asio/as_tuple.hpp> |
| 6 | #include <boost/asio/awaitable.hpp> | 4 | #include <boost/asio/awaitable.hpp> |
| 7 | #include <boost/asio/co_spawn.hpp> | 5 | #include <boost/asio/co_spawn.hpp> |
| 6 | #include <boost/asio/ip/tcp.hpp> | ||
| 8 | #include <boost/beast/core.hpp> | 7 | #include <boost/beast/core.hpp> |
| 9 | #include <boost/beast/http.hpp> | 8 | #include <boost/beast/http.hpp> |
| 9 | #include <boost/config.hpp> | ||
| 10 | #include <boost/json.hpp> | 10 | #include <boost/json.hpp> |
| 11 | #include <boost/locale/generator.hpp> | 11 | #include <boost/locale/generator.hpp> |
| 12 | 12 | ||
| @@ -32,190 +32,248 @@ using tcp = boost::asio::ip::tcp; | |||
| 32 | 32 | ||
| 33 | namespace routemon::srv { | 33 | namespace routemon::srv { |
| 34 | 34 | ||
| 35 | class gpx_parse_error_category_impl : public std::error_category { | 35 | class gpx_parse_error_category_impl : public std::error_category |
| 36 | public: | 36 | { |
| 37 | char const* name() const noexcept override { return "gpx_parse"; } | 37 | public: |
| 38 | auto message(int condition) const noexcept -> std::string override { | 38 | char const* name() const noexcept override { return "gpx_parse"; } |
| 39 | std::ignore = condition; | 39 | auto message(int condition) const noexcept -> std::string override |
| 40 | return "failed to parse GPX file"; | 40 | { |
| 41 | } | 41 | std::ignore = condition; |
| 42 | }; | 42 | return "failed to parse GPX file"; |
| 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 | } | 43 | } |
| 44 | }; | ||
| 45 | auto gpx_parse_error_category() noexcept -> gpx_parse_error_category_impl const& | ||
| 46 | { | ||
| 47 | static auto const inst = gpx_parse_error_category_impl{}; | ||
| 48 | return inst; | ||
| 49 | } | ||
| 50 | auto gpx_parse_error() noexcept -> std::error_code | ||
| 51 | { | ||
| 52 | return std::error_code{1, gpx_parse_error_category()}; | ||
| 53 | } | ||
| 50 | 54 | ||
| 51 | class gpx_parse_result { | 55 | class gpx_parse_result |
| 52 | std::variant<std::exception_ptr, gpx::file> res_; | 56 | { |
| 57 | std::variant<std::exception_ptr, gpx::file> res_; | ||
| 53 | 58 | ||
| 54 | public: | 59 | public: |
| 55 | auto set_exception(std::exception_ptr ex) noexcept { | 60 | auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } |
| 56 | res_ = ex; | 61 | auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } |
| 57 | } | ||
| 58 | auto set_gpx_file(gpx::file&& f) noexcept { | ||
| 59 | res_ = std::move(f); | ||
| 60 | } | ||
| 61 | 62 | ||
| 62 | auto unwrap() -> gpx::file&& { | 63 | auto unwrap() -> gpx::file&& |
| 63 | return std::visit(util::overloaded{ | 64 | { |
| 64 | [](std::exception_ptr ex) -> gpx::file&& { | 65 | return std::visit( |
| 65 | if (ex) std::rethrow_exception(ex); | 66 | util::overloaded{ |
| 66 | else throw std::runtime_error{"no GPX file parse result available"}; | 67 | [](std::exception_ptr ex) -> gpx::file&& |
| 67 | }, | 68 | { |
| 68 | [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, | 69 | if (ex) |
| 69 | }, std::move(res_)); | 70 | std::rethrow_exception(ex); |
| 70 | } | 71 | else |
| 71 | }; | 72 | throw std::runtime_error{"no GPX file parse result available"}; |
| 73 | }, | ||
| 74 | [](gpx::file&& f) -> gpx::file&& { return std::move(f); }, | ||
| 75 | }, | ||
| 76 | std::move(res_)); | ||
| 77 | } | ||
| 78 | }; | ||
| 72 | 79 | ||
| 73 | struct readable_gpx_body { | 80 | struct readable_gpx_body |
| 74 | using value_type = gpx_parse_result; | 81 | { |
| 82 | using value_type = gpx_parse_result; | ||
| 75 | 83 | ||
| 76 | class reader { | 84 | class reader |
| 77 | gpx::reader r_; | 85 | { |
| 78 | util::not_null<value_type*> res_; | 86 | gpx::reader r_; |
| 87 | util::not_null<value_type*> res_; | ||
| 79 | 88 | ||
| 80 | public: | 89 | public: |
| 81 | template<bool isRequest, bhttp::concepts::fields Fields> | 90 | template <bool isRequest, bhttp::concepts::fields Fields> |
| 82 | explicit reader(bhttp::header<isRequest, Fields>&, value_type& v) | 91 | explicit reader(bhttp::header<isRequest, Fields>&, value_type& v) : res_{&v} |
| 83 | : res_{&v} | 92 | { |
| 84 | {} | 93 | } |
| 85 | 94 | ||
| 86 | // The following methods (which are called by Beast) are marked | 95 | // The following methods (which are called by Beast) are marked |
| 87 | // noexcept, since Beast does not ensure that exceptions thrown | 96 | // noexcept, since Beast does not ensure that exceptions thrown |
| 88 | // here are appropriately directed to the caller of | 97 | // here are appropriately directed to the caller of |
| 89 | // (async_)read(_some), so throwing here might cause the program | 98 | // (async_)read(_some), so throwing here might cause the program |
| 90 | // to crash. | 99 | // to crash. |
| 91 | 100 | ||
| 92 | auto init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept -> void { | 101 | auto |
| 93 | try { | 102 | init(boost::optional<std::uint64_t> /* n */, beast::error_code& ec) noexcept |
| 94 | r_.init(); | 103 | -> void |
| 95 | ec = {}; | 104 | { |
| 96 | } catch (std::exception& ex) { | 105 | try |
| 97 | res_->set_exception(std::current_exception()); | 106 | { |
| 98 | ec = gpx_parse_error(); | 107 | r_.init(); |
| 99 | } | 108 | ec = {}; |
| 109 | } | ||
| 110 | catch (std::exception& ex) | ||
| 111 | { | ||
| 112 | res_->set_exception(std::current_exception()); | ||
| 113 | ec = gpx_parse_error(); | ||
| 100 | } | 114 | } |
| 115 | } | ||
| 101 | 116 | ||
| 102 | auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { | 117 | auto |
| 103 | auto total = 0uz; | 118 | put(beast::concepts::const_buffer_sequence auto b, |
| 104 | try { | 119 | beast::error_code& ec) noexcept -> std::size_t |
| 105 | for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { | 120 | { |
| 106 | r_.put(std::string_view{static_cast<char const*>(it->data()), it->size()}); | 121 | auto total = 0uz; |
| 107 | total += it->size(); | 122 | try |
| 108 | } | 123 | { |
| 109 | ec = {}; | 124 | for (auto it = net::buffer_sequence_begin(b); |
| 110 | } catch (std::exception& ex) { | 125 | it != net::buffer_sequence_end(b); it++) |
| 111 | res_->set_exception(std::current_exception()); | 126 | { |
| 112 | ec = gpx_parse_error(); | 127 | r_.put( |
| 128 | std::string_view{ | ||
| 129 | static_cast<char const*>(it->data()), it->size() | ||
| 130 | }); | ||
| 131 | total += it->size(); | ||
| 113 | } | 132 | } |
| 114 | return total; | 133 | ec = {}; |
| 134 | } | ||
| 135 | catch (std::exception& ex) | ||
| 136 | { | ||
| 137 | res_->set_exception(std::current_exception()); | ||
| 138 | ec = gpx_parse_error(); | ||
| 115 | } | 139 | } |
| 140 | return total; | ||
| 141 | } | ||
| 116 | 142 | ||
| 117 | auto finish(beast::error_code& ec) noexcept { | 143 | auto finish(beast::error_code& ec) noexcept |
| 118 | try { | 144 | { |
| 119 | res_->set_gpx_file(r_.finish()); | 145 | try |
| 120 | ec = {}; | 146 | { |
| 121 | } catch (std::exception& ex) { | 147 | res_->set_gpx_file(r_.finish()); |
| 122 | res_->set_exception(std::current_exception()); | 148 | ec = {}; |
| 123 | ec = gpx_parse_error(); | 149 | } |
| 124 | } | 150 | catch (std::exception& ex) |
| 151 | { | ||
| 152 | res_->set_exception(std::current_exception()); | ||
| 153 | ec = gpx_parse_error(); | ||
| 125 | } | 154 | } |
| 126 | }; | 155 | } |
| 127 | }; | 156 | }; |
| 128 | static_assert(bhttp::concepts::body<readable_gpx_body>); | 157 | }; |
| 129 | static_assert(bhttp::concepts::body_reader<readable_gpx_body>); | 158 | static_assert(bhttp::concepts::body<readable_gpx_body>); |
| 159 | static_assert(bhttp::concepts::body_reader<readable_gpx_body>); | ||
| 130 | 160 | ||
| 131 | class handler { | 161 | class handler |
| 132 | api::handler inner_; | 162 | { |
| 163 | api::handler inner_; | ||
| 133 | 164 | ||
| 134 | public: | 165 | public: |
| 135 | using outer_ctx = http::trace_id_ctx<http::base_ctx>; | 166 | using outer_ctx = http::trace_id_ctx<http::base_ctx>; |
| 136 | using l0_ctx = http::routed_ctx<outer_ctx>; | 167 | using l0_ctx = http::routed_ctx<outer_ctx>; |
| 137 | 168 | ||
| 138 | private: | 169 | private: |
| 139 | auto handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> { | 170 | auto handle_process_gpx(l0_ctx ctx, http::readable_request r) |
| 140 | auto gpx_file = gpx::file{}; | 171 | -> net::awaitable<http::presponse> |
| 141 | try { | 172 | { |
| 142 | auto req = co_await http::read_request<readable_gpx_body>(ctx, std::move(r)); | 173 | auto gpx_file = gpx::file{}; |
| 143 | gpx_file = std::move(req->body().unwrap()); | 174 | try |
| 144 | } catch (std::exception& ex) { | 175 | { |
| 145 | // TODO: more detailed problem reporting | 176 | auto req = |
| 146 | auto tpl = problem::tpl{ | 177 | co_await http::read_request<readable_gpx_body>(ctx, std::move(r)); |
| 147 | .status = bhttp::status::bad_request, | 178 | gpx_file = std::move(req->body().unwrap()); |
| 148 | .title = translate("Failed to parse GPX file"), | 179 | } |
| 180 | catch (std::exception& ex) | ||
| 181 | { | ||
| 182 | // TODO: more detailed problem reporting | ||
| 183 | auto tpl = problem::tpl{ | ||
| 184 | .status = bhttp::status::bad_request, | ||
| 185 | .title = translate("Failed to parse GPX file"), | ||
| 149 | .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", | 186 | .type_uri = "https://routemon.fautchen.eu/problems/gpx-parse-failed", |
| 150 | }; | 187 | }; |
| 151 | co_return http::problem_rsp(ctx, tpl.instantiate(), http::keep_alive{false}); | 188 | co_return http::problem_rsp( |
| 152 | } | 189 | ctx, tpl.instantiate(), http::keep_alive{false}); |
| 153 | 190 | } | |
| 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 | 191 | ||
| 166 | auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true}); | 192 | // TODO: catch handler exceptions and return 500 when raised? |
| 167 | rsp.set(bhttp::field::content_type, "application/json"); | 193 | // (keep-alive depends on whether whole request was read) |
| 168 | rsp.body() = json::serialize(json::value_from(*mres)); | 194 | auto mres = inner_.process_gpx(std::move(gpx_file)); |
| 169 | rsp.prepare_payload(); | 195 | if (!mres) |
| 170 | co_return rsp; | 196 | { |
| 197 | auto tpl = problem::tpl{ | ||
| 198 | .status = bhttp::status::internal_server_error, | ||
| 199 | .title = translate("Internal server error"), | ||
| 200 | .type_uri = "https://routemon.fautchen.eu/problems/" | ||
| 201 | "internal-server-error", | ||
| 202 | }; | ||
| 203 | co_return http::problem_rsp( | ||
| 204 | ctx, tpl.instantiate(), http::keep_alive{true}); | ||
| 171 | } | 205 | } |
| 172 | 206 | ||
| 173 | auto handle_sysinfo(l0_ctx ctx, http::readable_request r) -> net::awaitable<http::presponse> { | 207 | auto rsp = http::make_rsp<bhttp::string_body>( |
| 174 | auto req = co_await http::read_request<bhttp::empty_body>(ctx, std::move(r)); | 208 | bhttp::status::ok, http::keep_alive{true}); |
| 175 | auto info = inner_.sysinfo(); | 209 | rsp.set(bhttp::field::content_type, "application/json"); |
| 210 | rsp.body() = json::serialize(json::value_from(*mres)); | ||
| 211 | rsp.prepare_payload(); | ||
| 212 | co_return rsp; | ||
| 213 | } | ||
| 176 | 214 | ||
| 177 | auto rsp = http::make_rsp<bhttp::string_body>(bhttp::status::ok, http::keep_alive{true}); | 215 | auto handle_sysinfo(l0_ctx ctx, http::readable_request r) |
| 178 | rsp.set(bhttp::field::content_type, "application/json"); | 216 | -> net::awaitable<http::presponse> |
| 179 | rsp.body() = json::serialize(json::value_from(info)); | 217 | { |
| 180 | rsp.prepare_payload(); | 218 | auto req = |
| 181 | co_return rsp; | 219 | co_await http::read_request<bhttp::empty_body>(ctx, std::move(r)); |
| 182 | } | 220 | auto info = inner_.sysinfo(); |
| 183 | 221 | ||
| 184 | public: | 222 | auto rsp = http::make_rsp<bhttp::string_body>( |
| 185 | handler(api::handler&& inner) : inner_{std::move(inner)} {} | 223 | bhttp::status::ok, http::keep_alive{true}); |
| 224 | rsp.set(bhttp::field::content_type, "application/json"); | ||
| 225 | rsp.body() = json::serialize(json::value_from(info)); | ||
| 226 | rsp.prepare_payload(); | ||
| 227 | co_return rsp; | ||
| 228 | } | ||
| 186 | 229 | ||
| 187 | auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> { | 230 | public: |
| 188 | auto handler = [this]<class MemFn>(MemFn member) { | 231 | handler(api::handler&& inner) : inner_{std::move(inner)} {} |
| 189 | return std::bind_front(member, this); | ||
| 190 | }; | ||
| 191 | 232 | ||
| 192 | return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({ | 233 | auto make_routes() -> http::route_tree<http::routed_ctx<outer_ctx>> |
| 193 | {"gpx", http::dtree<l0_ctx>{{ | 234 | { |
| 194 | .post = handler(&handler::handle_process_gpx), | 235 | auto handler = [this]<class MemFn>(MemFn member) |
| 195 | }}.no_subtrees()}, | 236 | { return std::bind_front(member, this); }; |
| 196 | {"sysinfo", http::dtree<l0_ctx>{{ | ||
| 197 | .get = handler(&handler::handle_sysinfo), | ||
| 198 | }}.no_subtrees()}, | ||
| 199 | }); | ||
| 200 | } | ||
| 201 | }; | ||
| 202 | 237 | ||
| 203 | export class server { | 238 | return http::dtree<http::routed_ctx<outer_ctx>>{}.named_subtrees({ |
| 204 | handler handler_; | 239 | {"gpx", |
| 205 | http::server<handler::outer_ctx> srv_; | 240 | http::dtree<l0_ctx>{ |
| 241 | { | ||
| 242 | .post = handler(&handler::handle_process_gpx), | ||
| 243 | } | ||
| 244 | }.no_subtrees()}, | ||
| 245 | {"sysinfo", http::dtree<l0_ctx>{ | ||
| 246 | { | ||
| 247 | .get = handler(&handler::handle_sysinfo), | ||
| 248 | } | ||
| 249 | }.no_subtrees()}, | ||
| 250 | }); | ||
| 251 | } | ||
| 252 | }; | ||
| 206 | 253 | ||
| 207 | static auto make_global_middleware() -> http::middleware_t<http::base_ctx, handler::outer_ctx> { | 254 | export class server |
| 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>>); | 255 | { |
| 209 | } | 256 | handler handler_; |
| 257 | http::server<handler::outer_ctx> srv_; | ||
| 210 | 258 | ||
| 211 | public: | 259 | static auto make_global_middleware() |
| 212 | server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) | 260 | -> http::middleware_t<http::base_ctx, handler::outer_ctx> |
| 213 | : handler_{std::move(inner)}, srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} | 261 | { |
| 214 | {} | 262 | return http::middleware_compose< |
| 263 | http::base_ctx, http::trace_id_ctx<http::base_ctx>, | ||
| 264 | http::trace_id_ctx<http::base_ctx>>( | ||
| 265 | http::trace_id_middleware<http::base_ctx>, | ||
| 266 | http::lax_cors_middleware<http::trace_id_ctx<http::base_ctx>>); | ||
| 267 | } | ||
| 215 | 268 | ||
| 216 | auto spawn(net::io_context& ioc) -> void { | 269 | public: |
| 217 | srv_.spawn(ioc); | 270 | server(log::logger const& l, locale::selector&& lsel, api::handler&& inner) |
| 218 | } | 271 | : handler_{std::move(inner)}, |
| 219 | }; | 272 | srv_{l, std::move(lsel), make_global_middleware(), handler_.make_routes()} |
| 273 | { | ||
| 274 | } | ||
| 275 | |||
| 276 | auto spawn(net::io_context& ioc) -> void { srv_.spawn(ioc); } | ||
| 277 | }; | ||
| 220 | 278 | ||
| 221 | } // namespace routemon::srv | 279 | } // namespace routemon::srv |
diff --git a/server/src/time.cppm b/server/src/time.cppm index 767883c..907cec4 100644 --- a/server/src/time.cppm +++ b/server/src/time.cppm | |||
| @@ -4,202 +4,253 @@ import std; | |||
| 4 | 4 | ||
| 5 | export namespace routemon::time { | 5 | export namespace routemon::time { |
| 6 | 6 | ||
| 7 | using timestamp = std::chrono::time_point<std::chrono::utc_clock>; | 7 | using timestamp = std::chrono::time_point<std::chrono::utc_clock>; |
| 8 | 8 | ||
| 9 | class period { | 9 | class period |
| 10 | // Assuming [start, end). Unfortunately the DATEX II model is not | 10 | { |
| 11 | // clear about this. | 11 | // Assuming [start, end). Unfortunately the DATEX II model is not |
| 12 | timestamp start_; | 12 | // clear about this. |
| 13 | timestamp end_; | 13 | timestamp start_; |
| 14 | timestamp end_; | ||
| 14 | 15 | ||
| 15 | public: | 16 | public: |
| 16 | explicit period(timestamp start, timestamp end) | 17 | explicit period(timestamp start, timestamp end) : start_{start}, end_{end} |
| 17 | : start_{start}, end_{end} | 18 | { |
| 19 | if (start >= end) | ||
| 18 | { | 20 | { |
| 19 | if (start >= end) { | 21 | throw std::invalid_argument("period: start should be before end"); |
| 20 | throw std::invalid_argument("period: start should be before end"); | ||
| 21 | } | ||
| 22 | } | 22 | } |
| 23 | } | ||
| 23 | 24 | ||
| 24 | [[nodiscard]] auto intersect(period other) const -> std::optional<period> { | 25 | [[nodiscard]] auto intersect(period other) const -> std::optional<period> |
| 25 | auto const new_start = start_ < other.start() ? other.start() : start_; | 26 | { |
| 26 | auto const new_end = other.end() < end_ ? other.end() : end_; | 27 | auto const new_start = start_ < other.start() ? other.start() : start_; |
| 27 | return new_start < new_end ? std::make_optional(period{new_start, new_end}) : std::nullopt; | 28 | auto const new_end = other.end() < end_ ? other.end() : end_; |
| 28 | } | 29 | return new_start < new_end ? std::make_optional(period{new_start, new_end}) |
| 30 | : std::nullopt; | ||
| 31 | } | ||
| 29 | 32 | ||
| 30 | [[nodiscard]] auto except(period other) const -> std::pair<std::optional<period>, std::optional<period>> { | 33 | [[nodiscard]] auto except(period other) const |
| 31 | auto const before_start = start_; | 34 | -> std::pair<std::optional<period>, std::optional<period>> |
| 32 | auto const before_end = other.start(); | 35 | { |
| 33 | auto const after_start = end_; | 36 | auto const before_start = start_; |
| 34 | auto const after_end = other.end(); | 37 | auto const before_end = other.start(); |
| 35 | std::optional<period> before, after; | 38 | auto const after_start = end_; |
| 36 | if (before_start < before_end) | 39 | auto const after_end = other.end(); |
| 37 | before = period{before_start, before_end}; | 40 | std::optional<period> before, after; |
| 38 | if (after_start < after_end) | 41 | if (before_start < before_end) |
| 39 | after = period{after_end, after_start}; | 42 | before = period{before_start, before_end}; |
| 40 | return std::make_pair(before, after); | 43 | if (after_start < after_end) |
| 41 | } | 44 | after = period{after_end, after_start}; |
| 45 | return std::make_pair(before, after); | ||
| 46 | } | ||
| 42 | 47 | ||
| 43 | [[nodiscard]] auto start() const -> timestamp { return start_; } | 48 | [[nodiscard]] auto start() const -> timestamp { return start_; } |
| 44 | [[nodiscard]] auto end() const -> timestamp { return end_; } | 49 | [[nodiscard]] auto end() const -> timestamp { return end_; } |
| 45 | }; | 50 | }; |
| 46 | 51 | ||
| 47 | class period_seq { | 52 | class period_seq |
| 48 | std::vector<period> periods_; | 53 | { |
| 54 | std::vector<period> periods_; | ||
| 49 | 55 | ||
| 50 | // The way lt and ge are ordered makes a difference for how the sorting | 56 | // 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. | 57 | // (insertion based on lower_bound) works. Do not carelessly reorder this. |
| 52 | enum lt_ge : std::uint8_t { | 58 | enum lt_ge : std::uint8_t |
| 53 | ge, // >= | 59 | { |
| 54 | lt, // < | 60 | ge, // >= |
| 55 | }; | 61 | lt, // < |
| 62 | }; | ||
| 56 | 63 | ||
| 57 | // O(n log n) | 64 | // O(n log n) |
| 58 | template<std::input_iterator I, std::sentinel_for<I> S> | 65 | template <std::input_iterator I, std::sentinel_for<I> S> |
| 59 | requires std::same_as<std::iter_value_t<I>, period> | 66 | requires std::same_as<std::iter_value_t<I>, period> |
| 60 | static auto consolidate(I begin, S end) -> std::vector<period> { | 67 | static auto consolidate(I begin, S end) -> std::vector<period> |
| 61 | auto periods = std::vector<period>{}; | 68 | { |
| 62 | auto preds = std::vector<std::pair<timestamp, lt_ge>>{}; | 69 | auto periods = std::vector<period>{}; |
| 70 | auto preds = std::vector<std::pair<timestamp, lt_ge>>{}; | ||
| 63 | 71 | ||
| 64 | for (auto it = begin; it != end; it++) { | 72 | for (auto it = begin; it != end; it++) |
| 65 | auto const& period = *it; | 73 | { |
| 74 | auto const& period = *it; | ||
| 66 | 75 | ||
| 67 | auto const a = std::make_pair(period.start(), ge); | 76 | auto const a = std::make_pair(period.start(), ge); |
| 68 | auto const b = std::make_pair(period.end(), lt); | 77 | auto const b = std::make_pair(period.end(), lt); |
| 69 | preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); | 78 | preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); |
| 70 | preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); | 79 | preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); |
| 71 | } | 80 | } |
| 72 | 81 | ||
| 73 | if (preds.empty()) | 82 | if (preds.empty()) |
| 74 | return periods; | 83 | return periods; |
| 75 | 84 | ||
| 76 | if (preds.size() < 2) | 85 | if (preds.size() < 2) |
| 77 | throw std::logic_error{"period_seq::consolidate: amount of predicates should be >= 2"}; | 86 | throw std::logic_error{ |
| 78 | if (preds.front().second != ge) | 87 | "period_seq::consolidate: amount of predicates should be >= 2" |
| 79 | throw std::logic_error{"period_seq::consolidate: first element of preds should be a ge-element"}; | 88 | }; |
| 80 | if (preds.back().second != lt) | 89 | if (preds.front().second != ge) |
| 81 | throw std::logic_error{"period_seq::consolidate: last element of preds should be an lt-element"}; | 90 | throw std::logic_error{"period_seq::consolidate: first element of preds " |
| 91 | "should be a ge-element"}; | ||
| 92 | if (preds.back().second != lt) | ||
| 93 | throw std::logic_error{"period_seq::consolidate: last element of preds " | ||
| 94 | "should be an lt-element"}; | ||
| 82 | 95 | ||
| 83 | auto period_start = preds[0].first; | 96 | auto period_start = preds[0].first; |
| 84 | for (std::size_t i = 1; i < preds.size(); i++) { | 97 | 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)) { | 98 | { |
| 86 | auto const period_end = preds[i].first; | 99 | if (preds[i].second == lt |
| 87 | if (!periods.empty() && periods.back().start() == period_start) | 100 | && (i + 1 == preds.size() || preds[i + 1].second == ge)) |
| 88 | periods.back() = period{periods.back().end(), period_end}; | 101 | { |
| 89 | else | 102 | auto const period_end = preds[i].first; |
| 90 | periods.emplace_back(period_start, period_end); | 103 | if (!periods.empty() && periods.back().start() == period_start) |
| 91 | if (i + 1 != preds.size()) { | 104 | periods.back() = period{periods.back().end(), period_end}; |
| 92 | period_start = preds[i + 1].first; | 105 | else |
| 93 | i++; | 106 | periods.emplace_back(period_start, period_end); |
| 94 | } | 107 | if (i + 1 != preds.size()) |
| 108 | { | ||
| 109 | period_start = preds[i + 1].first; | ||
| 110 | i++; | ||
| 95 | } | 111 | } |
| 96 | } | 112 | } |
| 97 | |||
| 98 | return periods; | ||
| 99 | } | 113 | } |
| 100 | 114 | ||
| 101 | explicit period_seq(std::vector<period> periods) | 115 | return periods; |
| 102 | : periods_{std::move(periods)} | 116 | } |
| 117 | |||
| 118 | explicit period_seq(std::vector<period> periods) | ||
| 119 | : periods_{std::move(periods)} | ||
| 120 | { | ||
| 121 | for (auto i = 0uz; i < periods_.size(); i++) | ||
| 103 | { | 122 | { |
| 104 | for (auto i = 0uz; i < periods_.size(); i++) { | 123 | if (i + 1 < periods_.size()) |
| 105 | if (i + 1 < periods_.size()) { | 124 | { |
| 106 | if (periods_[i].end() >= periods_[i + 1].start()) { | 125 | if (periods_[i].end() >= periods_[i + 1].start()) |
| 107 | throw std::logic_error{"period_seq: vector provided to private constructor not ordered properly"}; | 126 | { |
| 108 | } | 127 | throw std::logic_error{"period_seq: vector provided to private " |
| 128 | "constructor not ordered properly"}; | ||
| 109 | } | 129 | } |
| 110 | } | 130 | } |
| 111 | } | 131 | } |
| 132 | } | ||
| 112 | 133 | ||
| 113 | public: | 134 | public: |
| 114 | template<std::input_iterator I, std::sentinel_for<I> S> | 135 | template <std::input_iterator I, std::sentinel_for<I> S> |
| 115 | requires std::same_as<std::iter_value_t<I>, period> | 136 | requires std::same_as<std::iter_value_t<I>, period> |
| 116 | explicit period_seq(I begin, S end) | 137 | explicit period_seq(I begin, S end) : periods_{consolidate(begin, end)} |
| 117 | : periods_{consolidate(begin, end)} | 138 | { |
| 118 | {} | 139 | } |
| 119 | 140 | ||
| 120 | explicit period_seq(period singleton) | 141 | explicit period_seq(period singleton) : periods_{singleton} {} |
| 121 | : periods_{singleton} | ||
| 122 | {} | ||
| 123 | 142 | ||
| 124 | [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq { | 143 | [[nodiscard]] auto intersect(period_seq const& other) const -> period_seq |
| 125 | auto it1 = periods_.begin(); auto end1 = periods_.end(); | 144 | { |
| 126 | auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); | 145 | auto it1 = periods_.begin(); |
| 146 | auto end1 = periods_.end(); | ||
| 147 | auto it2 = other.periods_.begin(); | ||
| 148 | auto end2 = other.periods_.end(); | ||
| 127 | 149 | ||
| 128 | auto res = std::vector<period>{}; | 150 | auto res = std::vector<period>{}; |
| 129 | while (it1 != end1 && it2 != end2) { | 151 | while (it1 != end1 && it2 != end2) |
| 130 | auto overlap = it1->intersect(*it2); | 152 | { |
| 131 | if (overlap) { | 153 | auto overlap = it1->intersect(*it2); |
| 132 | res.push_back(*overlap); | 154 | if (overlap) |
| 133 | if (it1->end() < it2->end()) { | 155 | { |
| 134 | it1++; | 156 | res.push_back(*overlap); |
| 135 | } else { | 157 | if (it1->end() < it2->end()) |
| 136 | it2++; | 158 | { |
| 137 | } | 159 | it1++; |
| 138 | } else { | 160 | } |
| 139 | if (it1->end() < it2->start()) { | 161 | else |
| 140 | it1++; | 162 | { |
| 141 | } else { | 163 | it2++; |
| 142 | it2++; | 164 | } |
| 143 | } | 165 | } |
| 166 | else | ||
| 167 | { | ||
| 168 | if (it1->end() < it2->start()) | ||
| 169 | { | ||
| 170 | it1++; | ||
| 171 | } | ||
| 172 | else | ||
| 173 | { | ||
| 174 | it2++; | ||
| 144 | } | 175 | } |
| 145 | } | 176 | } |
| 146 | |||
| 147 | return period_seq{res}; | ||
| 148 | } | 177 | } |
| 149 | 178 | ||
| 150 | [[nodiscard]] auto except(period_seq const& other) const -> period_seq { | 179 | return period_seq{res}; |
| 151 | // This code was pretty tricky to write, I wouldn't be surprised if it has some bugs in it. | 180 | } |
| 181 | |||
| 182 | [[nodiscard]] auto except(period_seq const& other) const -> period_seq | ||
| 183 | { | ||
| 184 | // This code was pretty tricky to write, I wouldn't be surprised if it | ||
| 185 | // has some bugs in it. | ||
| 152 | 186 | ||
| 153 | auto it1 = periods_.begin(); auto end1 = periods_.end(); | 187 | auto it1 = periods_.begin(); |
| 154 | auto it2 = other.periods_.begin(); auto end2 = other.periods_.end(); | 188 | auto end1 = periods_.end(); |
| 189 | auto it2 = other.periods_.begin(); | ||
| 190 | auto end2 = other.periods_.end(); | ||
| 155 | 191 | ||
| 156 | auto res = std::vector<period>{}; | 192 | auto res = std::vector<period>{}; |
| 157 | if (it1 == end1) | 193 | if (it1 == end1) |
| 158 | return period_seq{res}; | 194 | return period_seq{res}; |
| 159 | if (it2 == end2) | 195 | if (it2 == end2) |
| 160 | return period_seq{periods_}; | 196 | return period_seq{periods_}; |
| 161 | auto period1 = period{*it1++}; | 197 | auto period1 = period{*it1++}; |
| 162 | 198 | ||
| 163 | while (it1 != end1 && it2 != end2) { | 199 | while (it1 != end1 && it2 != end2) |
| 164 | if (period1.end() <= it2->start()) { | 200 | { |
| 165 | res.push_back(period1); | 201 | if (period1.end() <= it2->start()) |
| 202 | { | ||
| 203 | res.push_back(period1); | ||
| 204 | period1 = *it1++; | ||
| 205 | } | ||
| 206 | else if (it2->end() <= period1.start()) | ||
| 207 | { | ||
| 208 | it2++; | ||
| 209 | } | ||
| 210 | else /* period1.begin() < it2->end() && it2->begin() < | ||
| 211 | period1.end() */ | ||
| 212 | { | ||
| 213 | auto const [mbefore, mafter] = period1.except(*it2); | ||
| 214 | if (mbefore) | ||
| 215 | res.push_back(*mbefore); | ||
| 216 | if (mafter) | ||
| 217 | { | ||
| 218 | period1 = *mafter; | ||
| 219 | } | ||
| 220 | else | ||
| 221 | { | ||
| 166 | period1 = *it1++; | 222 | 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 | } | 223 | } |
| 179 | } | 224 | } |
| 180 | |||
| 181 | return period_seq{res}; | ||
| 182 | } | 225 | } |
| 183 | 226 | ||
| 184 | [[nodiscard]] auto periods() const -> std::vector<period> const& { | 227 | return period_seq{res}; |
| 185 | return periods_; | 228 | } |
| 186 | } | ||
| 187 | }; | ||
| 188 | 229 | ||
| 189 | auto operator<<(std::ostream& os, period const& p) -> std::ostream& { | 230 | [[nodiscard]] auto periods() const -> std::vector<period> const& |
| 190 | return os << "[" << p.start() << ", " << p.end() << ")"; | 231 | { |
| 232 | return periods_; | ||
| 191 | } | 233 | } |
| 234 | }; | ||
| 192 | 235 | ||
| 193 | auto operator<<(std::ostream &os, period_seq const& ps) -> std::ostream& { | 236 | auto operator<<(std::ostream& os, period const& p) -> std::ostream& |
| 194 | os << "{"; | 237 | { |
| 195 | auto it = ps.periods().begin(); | 238 | return os << "[" << p.start() << ", " << p.end() << ")"; |
| 196 | while (it != ps.periods().end()) { | 239 | } |
| 197 | os << " " << *it; | 240 | |
| 198 | if (++it != ps.periods().end()) { | 241 | auto operator<<(std::ostream& os, period_seq const& ps) -> std::ostream& |
| 199 | os << ","; | 242 | { |
| 200 | } | 243 | os << "{"; |
| 244 | auto it = ps.periods().begin(); | ||
| 245 | while (it != ps.periods().end()) | ||
| 246 | { | ||
| 247 | os << " " << *it; | ||
| 248 | if (++it != ps.periods().end()) | ||
| 249 | { | ||
| 250 | os << ","; | ||
| 201 | } | 251 | } |
| 202 | return os << " }"; | ||
| 203 | } | 252 | } |
| 253 | return os << " }"; | ||
| 254 | } | ||
| 204 | 255 | ||
| 205 | } // namespace routemon::time | 256 | } // namespace routemon::time |
diff --git a/server/src/trace.cppm b/server/src/trace.cppm index 00ecd05..35d32f3 100644 --- a/server/src/trace.cppm +++ b/server/src/trace.cppm | |||
| @@ -11,69 +11,79 @@ import :util; | |||
| 11 | 11 | ||
| 12 | namespace routemon::trace { | 12 | namespace routemon::trace { |
| 13 | 13 | ||
| 14 | class uuid7 { | 14 | class uuid7 |
| 15 | std::uint64_t high_ = 0; | 15 | { |
| 16 | std::uint64_t low_ = 0; | 16 | std::uint64_t high_ = 0; |
| 17 | std::uint64_t low_ = 0; | ||
| 17 | 18 | ||
| 18 | public: | 19 | public: |
| 19 | uuid7() { | 20 | uuid7() |
| 20 | namespace chrono = std::chrono; | 21 | { |
| 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 | namespace chrono = std::chrono; |
| 22 | if (unix_time_ms_signed < 0) | 23 | auto const unix_time_ms_signed = static_cast<std::int64_t>( |
| 23 | throw std::runtime_error{"system time before UNIX epoch"}; | 24 | chrono::duration_cast<chrono::milliseconds>( |
| 24 | auto const unix_time_ms = static_cast<std::uint64_t>(unix_time_ms_signed); | 25 | chrono::system_clock::now().time_since_epoch()) |
| 25 | if (std::countl_zero(unix_time_ms) < 16) | 26 | .count()); |
| 26 | throw std::runtime_error{"system time too great"}; | 27 | if (unix_time_ms_signed < 0) |
| 28 | throw std::runtime_error{"system time before UNIX epoch"}; | ||
| 29 | auto const unix_time_ms = static_cast<std::uint64_t>(unix_time_ms_signed); | ||
| 30 | if (std::countl_zero(unix_time_ms) < 16) | ||
| 31 | throw std::runtime_error{"system time too great"}; | ||
| 27 | 32 | ||
| 28 | auto rand = std::array<unsigned char, 10>{}; | 33 | auto rand = std::array<unsigned char, 10>{}; |
| 29 | int s = RAND_bytes(rand.data(), static_cast<int>(rand.size())); | 34 | int s = RAND_bytes(rand.data(), static_cast<int>(rand.size())); |
| 30 | if (s != 1) { | 35 | if (s != 1) |
| 31 | unsigned long e = ERR_get_error(); | 36 | { |
| 32 | throw std::runtime_error{std::format("failed to generate UUID(v7): {} ({}, code {})", ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; | 37 | unsigned long e = ERR_get_error(); |
| 33 | } | 38 | throw std::runtime_error{std::format( |
| 39 | "failed to generate UUID(v7): {} ({}, code {})", | ||
| 40 | ERR_reason_error_string(e), ERR_lib_error_string(e), e)}; | ||
| 41 | } | ||
| 34 | 42 | ||
| 35 | auto version = std::uint64_t{0b0111}; | 43 | auto version = std::uint64_t{0b0111}; |
| 36 | auto variant = std::uint64_t{0b10}; | 44 | auto variant = std::uint64_t{0b10}; |
| 37 | 45 | ||
| 38 | high_ |= unix_time_ms << 16; | 46 | high_ |= unix_time_ms << 16; |
| 39 | high_ |= version << 12; | 47 | high_ |= version << 12; |
| 40 | high_ |= std::uint64_t{rand[0]} << 4; | 48 | high_ |= std::uint64_t{rand[0]} << 4; |
| 41 | high_ |= std::uint64_t{rand[1]}; | 49 | high_ |= std::uint64_t{rand[1]}; |
| 42 | low_ |= variant << 62; | 50 | low_ |= variant << 62; |
| 43 | low_ |= std::uint64_t{rand[2]} << 54; | 51 | low_ |= std::uint64_t{rand[2]} << 54; |
| 44 | low_ |= std::uint64_t{rand[3]} << 48; | 52 | low_ |= std::uint64_t{rand[3]} << 48; |
| 45 | low_ |= std::uint64_t{rand[4]} << 40; | 53 | low_ |= std::uint64_t{rand[4]} << 40; |
| 46 | low_ |= std::uint64_t{rand[5]} << 32; | 54 | low_ |= std::uint64_t{rand[5]} << 32; |
| 47 | low_ |= std::uint64_t{rand[6]} << 24; | 55 | low_ |= std::uint64_t{rand[6]} << 24; |
| 48 | low_ |= std::uint64_t{rand[7]} << 16; | 56 | low_ |= std::uint64_t{rand[7]} << 16; |
| 49 | low_ |= std::uint64_t{rand[8]} << 8; | 57 | low_ |= std::uint64_t{rand[8]} << 8; |
| 50 | low_ |= std::uint64_t{rand[9]}; | 58 | low_ |= std::uint64_t{rand[9]}; |
| 51 | } | 59 | } |
| 52 | 60 | ||
| 53 | auto format(std::array<char, 37>& target) -> void { | 61 | auto format(std::array<char, 37>& target) -> void |
| 54 | auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; | 62 | { |
| 55 | auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; | 63 | auto high_high = (high_ & 0xffff'ffff'0000'0000) >> 32; |
| 56 | auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; | 64 | auto high_low_high = (high_ & 0x0000'0000'ffff'0000) >> 16; |
| 57 | auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; | 65 | auto low_low_high = (high_ & 0x0000'0000'0000'ffff) >> 0; |
| 58 | auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; | 66 | auto high_low = (low_ & 0xffff'0000'0000'0000) >> 48; |
| 67 | auto low_low = (low_ & 0x0000'ffff'ffff'ffff) >> 0; | ||
| 59 | 68 | ||
| 60 | std::format_to(target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", | 69 | std::format_to( |
| 61 | high_high, high_low_high, low_low_high, high_low, low_low); | 70 | target.begin(), "{:0>8x}-{:0>4x}-{:0>4x}-{:0>4x}-{:0>12x}", high_high, |
| 62 | target.back() = '\0'; | 71 | high_low_high, low_low_high, high_low, low_low); |
| 63 | } | 72 | target.back() = '\0'; |
| 64 | }; | 73 | } |
| 74 | }; | ||
| 65 | 75 | ||
| 66 | export class id { | 76 | export class id |
| 67 | std::array<char, 37> chars_; | 77 | { |
| 78 | std::array<char, 37> chars_; | ||
| 68 | 79 | ||
| 69 | public: | 80 | public: |
| 70 | id() { | 81 | id() { uuid7{}.format(chars_); } |
| 71 | uuid7{}.format(chars_); | ||
| 72 | } | ||
| 73 | 82 | ||
| 74 | auto as_string() const -> util::zstring_view { | 83 | auto as_string() const -> util::zstring_view |
| 75 | return util::zstring_view{chars_.data(), chars_.size() - 1}; | 84 | { |
| 76 | } | 85 | return util::zstring_view{chars_.data(), chars_.size() - 1}; |
| 77 | }; | 86 | } |
| 87 | }; | ||
| 78 | 88 | ||
| 79 | } // namespace routemon::trace | 89 | } // namespace routemon::trace |
diff --git a/server/src/util.cppm b/server/src/util.cppm index 65f4d67..bdccf10 100644 --- a/server/src/util.cppm +++ b/server/src/util.cppm | |||
| @@ -6,249 +6,285 @@ import std; | |||
| 6 | 6 | ||
| 7 | namespace routemon::util { | 7 | namespace routemon::util { |
| 8 | 8 | ||
| 9 | // For use with e.g. std::visit (on std::variant). | 9 | // For use with e.g. std::visit (on std::variant). |
| 10 | template<class... Ts> | 10 | template <class... Ts> |
| 11 | struct overloaded : Ts... { | 11 | struct overloaded : Ts... |
| 12 | using Ts::operator()...; | 12 | { |
| 13 | }; | 13 | using Ts::operator()...; |
| 14 | }; | ||
| 14 | 15 | ||
| 15 | export constexpr auto parse_double(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional<double> { | 16 | export constexpr auto parse_double( |
| 16 | auto x = 0.0; | 17 | std::string_view s, |
| 17 | auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); | 18 | std::chars_format fmt = std::chars_format::general) noexcept |
| 18 | if (ec == std::errc{}) { | 19 | -> std::optional<double> |
| 19 | return x; | 20 | { |
| 20 | } else { | 21 | auto x = 0.0; |
| 21 | return std::nullopt; | 22 | auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); |
| 22 | } | 23 | if (ec == std::errc{}) |
| 24 | { | ||
| 25 | return x; | ||
| 26 | } | ||
| 27 | else | ||
| 28 | { | ||
| 29 | return std::nullopt; | ||
| 23 | } | 30 | } |
| 31 | } | ||
| 24 | 32 | ||
| 25 | export constexpr auto parse_float(std::string_view s, std::chars_format fmt = std::chars_format::general) noexcept -> std::optional<float> { | 33 | export constexpr auto parse_float( |
| 26 | auto x = 0.0; | 34 | std::string_view s, |
| 27 | auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); | 35 | std::chars_format fmt = std::chars_format::general) noexcept |
| 28 | if (ec == std::errc{}) { | 36 | -> std::optional<float> |
| 29 | return x; | 37 | { |
| 30 | } else { | 38 | auto x = 0.0; |
| 31 | return std::nullopt; | 39 | auto [_, ec] = std::from_chars(s.data(), s.data() + s.size(), x, fmt); |
| 32 | } | 40 | if (ec == std::errc{}) |
| 41 | { | ||
| 42 | return x; | ||
| 33 | } | 43 | } |
| 44 | else | ||
| 45 | { | ||
| 46 | return std::nullopt; | ||
| 47 | } | ||
| 48 | } | ||
| 34 | 49 | ||
| 35 | export template<class T> | 50 | export template <class T> |
| 36 | class aolist : public std::enable_shared_from_this<aolist<T>> { | 51 | class aolist : public std::enable_shared_from_this<aolist<T>> |
| 37 | T v_; | 52 | { |
| 38 | std::shared_ptr<aolist<T> const> next_; | 53 | T v_; |
| 54 | std::shared_ptr<aolist<T> const> next_; | ||
| 39 | 55 | ||
| 40 | explicit aolist(T v, std::shared_ptr<aolist<T> const> next) | 56 | explicit aolist(T v, std::shared_ptr<aolist<T> const> next) |
| 41 | : v_{v}, next_{next} | 57 | : v_{v}, next_{next} |
| 42 | {} | 58 | { |
| 59 | } | ||
| 43 | 60 | ||
| 44 | public: | 61 | public: |
| 45 | static auto nil() -> std::shared_ptr<aolist<T>> { | 62 | static auto nil() -> std::shared_ptr<aolist<T>> { return nullptr; } |
| 46 | return nullptr; | ||
| 47 | } | ||
| 48 | 63 | ||
| 49 | static auto cons(T v, std::shared_ptr<aolist<T> const> l) -> std::shared_ptr<aolist<T>> { | 64 | static auto cons(T v, std::shared_ptr<aolist<T> const> l) |
| 50 | return std::shared_ptr<aolist<T>>{new aolist<T>{v, l}}; | 65 | -> std::shared_ptr<aolist<T>> |
| 51 | } | 66 | { |
| 67 | return std::shared_ptr<aolist<T>>{new aolist<T>{v, l}}; | ||
| 68 | } | ||
| 52 | 69 | ||
| 53 | auto next() const -> std::shared_ptr<aolist<T> const> { | 70 | auto next() const -> std::shared_ptr<aolist<T> const> { return next_; } |
| 54 | return next_; | ||
| 55 | } | ||
| 56 | 71 | ||
| 57 | auto value() const noexcept -> T const& { | 72 | auto value() const noexcept -> T const& { return v_; } |
| 58 | return v_; | 73 | }; |
| 59 | } | ||
| 60 | }; | ||
| 61 | 74 | ||
| 62 | export constexpr auto size_from_int(int x) -> std::optional<std::size_t> { | 75 | 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"); | 76 | { |
| 64 | if (x < 0) | 77 | static_assert( |
| 65 | return std::nullopt; | 78 | sizeof(int) <= sizeof(std::size_t), |
| 66 | return static_cast<std::size_t>(x); | 79 | "cannot cast int to smaller size_t type"); |
| 80 | if (x < 0) | ||
| 81 | return std::nullopt; | ||
| 82 | return static_cast<std::size_t>(x); | ||
| 83 | } | ||
| 84 | |||
| 85 | export constexpr auto int_from_size(std::size_t x) -> std::optional<int> | ||
| 86 | { | ||
| 87 | constexpr auto int_max = size_from_int(std::numeric_limits<int>::max()); | ||
| 88 | static_assert(int_max.has_value()); | ||
| 89 | if (x > *int_max) | ||
| 90 | return std::nullopt; | ||
| 91 | return static_cast<int>(x); | ||
| 92 | } | ||
| 93 | |||
| 94 | export class zstring_view | ||
| 95 | { | ||
| 96 | char const* s_; | ||
| 97 | std::size_t length_; | ||
| 98 | |||
| 99 | public: | ||
| 100 | constexpr explicit zstring_view(char const* s, std::size_t length) | ||
| 101 | : s_{s}, length_{length} | ||
| 102 | { | ||
| 67 | } | 103 | } |
| 68 | 104 | ||
| 69 | export constexpr auto int_from_size(std::size_t x) -> std::optional<int> { | 105 | constexpr zstring_view(char const* s) |
| 70 | constexpr auto int_max = size_from_int(std::numeric_limits<int>::max()); | 106 | : zstring_view{s, std::char_traits<char>::length(s)} |
| 71 | static_assert(int_max.has_value()); | 107 | { |
| 72 | if (x > *int_max) | ||
| 73 | return std::nullopt; | ||
| 74 | return static_cast<int>(x); | ||
| 75 | } | 108 | } |
| 76 | 109 | ||
| 77 | export class zstring_view { | 110 | auto length() const -> std::size_t { return length_; } |
| 78 | char const* s_; | ||
| 79 | std::size_t length_; | ||
| 80 | 111 | ||
| 81 | public: | 112 | auto c_str() const -> char const* { return s_; } |
| 82 | constexpr explicit zstring_view(char const* s, std::size_t length) : | ||
| 83 | s_{s}, length_{length} | ||
| 84 | {} | ||
| 85 | 113 | ||
| 86 | constexpr zstring_view(char const* s) : | 114 | operator std::string_view() const { return std::string_view{s_, length_}; } |
| 87 | zstring_view{s, std::char_traits<char>::length(s)} | ||
| 88 | {} | ||
| 89 | 115 | ||
| 90 | auto length() const -> std::size_t { | 116 | operator char const*() const { return s_; } |
| 91 | return length_; | 117 | }; |
| 92 | } | ||
| 93 | 118 | ||
| 94 | auto c_str() const -> char const* { | 119 | // View for null-terminated strings for which we might not |
| 95 | return s_; | 120 | // necessarily be interested in the length. String length is only |
| 96 | } | 121 | // calculated on demand, at most once on each thread (on more than |
| 122 | // one thread when racing). May be null. | ||
| 123 | // | ||
| 124 | // It is undefined behavior to assign to a lazy_zstring_view when | ||
| 125 | // it is in use by other threads. | ||
| 126 | export class lazy_zstring_view | ||
| 127 | { | ||
| 128 | static constexpr auto unset_length = std::numeric_limits<std::size_t>::max(); | ||
| 97 | 129 | ||
| 98 | operator std::string_view() const { | 130 | char const* s_; // nullable |
| 99 | return std::string_view{s_, length_}; | 131 | mutable std::atomic<std::size_t> length_; |
| 100 | } | 132 | static_assert(decltype(length_)::is_always_lock_free); |
| 101 | 133 | ||
| 102 | operator char const*() const { | 134 | public: |
| 103 | return s_; | 135 | constexpr explicit lazy_zstring_view(char const* s) |
| 104 | } | 136 | : s_{s}, length_{s ? unset_length : 0} |
| 105 | }; | 137 | { |
| 138 | } | ||
| 106 | 139 | ||
| 107 | // View for null-terminated strings for which we might not | 140 | ~lazy_zstring_view() = default; |
| 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 | 141 | ||
| 117 | char const* s_; // nullable | 142 | lazy_zstring_view(lazy_zstring_view const& sv) noexcept |
| 118 | mutable std::atomic<std::size_t> length_; | 143 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} |
| 119 | static_assert(decltype(length_)::is_always_lock_free); | 144 | { |
| 145 | } | ||
| 120 | 146 | ||
| 121 | public: | 147 | lazy_zstring_view(lazy_zstring_view&& sv) noexcept |
| 122 | constexpr explicit lazy_zstring_view(char const* s) : | 148 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} |
| 123 | s_{s}, length_{s ? unset_length : 0} | 149 | { |
| 124 | {} | 150 | } |
| 125 | ~lazy_zstring_view() = default; | ||
| 126 | 151 | ||
| 127 | lazy_zstring_view(lazy_zstring_view const& sv) noexcept | 152 | auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& |
| 128 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | 153 | { |
| 129 | {} | 154 | if (this != &rhs) |
| 130 | lazy_zstring_view(lazy_zstring_view&& sv) noexcept | 155 | { |
| 131 | : s_{sv.s_}, length_{sv.length_.load(std::memory_order_acquire)} | 156 | s_ = rhs.s_; |
| 132 | {} | 157 | length_.store( |
| 133 | auto operator=(lazy_zstring_view const& rhs) noexcept -> lazy_zstring_view& { | 158 | rhs.length_.load(std::memory_order_acquire), |
| 134 | if (this != &rhs) { | 159 | std::memory_order_release); |
| 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 | } | 160 | } |
| 161 | return *this; | ||
| 162 | } | ||
| 143 | 163 | ||
| 144 | auto length() const noexcept -> std::size_t { | 164 | auto operator=(lazy_zstring_view&& rhs) noexcept -> lazy_zstring_view& |
| 145 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | 165 | { |
| 146 | return v; | 166 | return *this = rhs; // use the copy assignment operator |
| 147 | auto l = std::char_traits<char>::length(s_); | 167 | } |
| 148 | length_.store(l, std::memory_order_release); | ||
| 149 | return l; | ||
| 150 | } | ||
| 151 | 168 | ||
| 152 | auto c_str() const noexcept -> char const* { | 169 | auto length() const noexcept -> std::size_t |
| 153 | return s_; | 170 | { |
| 154 | } | 171 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) |
| 172 | return v; | ||
| 173 | auto l = std::char_traits<char>::length(s_); | ||
| 174 | length_.store(l, std::memory_order_release); | ||
| 175 | return l; | ||
| 176 | } | ||
| 155 | 177 | ||
| 156 | operator std::string_view() const noexcept { | 178 | auto c_str() const noexcept -> char const* { return s_; } |
| 157 | return std::string_view{s_, length()}; | ||
| 158 | } | ||
| 159 | 179 | ||
| 160 | operator char const*() const noexcept { | 180 | operator std::string_view() const noexcept |
| 161 | return s_; | 181 | { |
| 162 | } | 182 | return std::string_view{s_, length()}; |
| 183 | } | ||
| 163 | 184 | ||
| 164 | auto operator==(std::string_view sv) const noexcept -> bool { | 185 | operator char const*() const noexcept { return s_; } |
| 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 | 186 | ||
| 181 | constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept -> zstring_view { | 187 | auto operator==(std::string_view sv) const noexcept -> bool |
| 182 | return zstring_view{s, length}; | 188 | { |
| 189 | if (auto v = length_.load(std::memory_order_acquire); v != unset_length) | ||
| 190 | if (sv.length() != v) | ||
| 191 | return false; | ||
| 192 | auto res = std::char_traits<char>::compare(s_, sv.data(), sv.length()); | ||
| 193 | if (res != 0) | ||
| 194 | return false; | ||
| 195 | // Strings are equal for sv.length() characters. | ||
| 196 | if (s_[sv.length()] != '\0') | ||
| 197 | return false; | ||
| 198 | // Strings are actually equal, and we have just found out the | ||
| 199 | // length of this string, so we might as well set it. | ||
| 200 | length_.store(sv.length(), std::memory_order_release); | ||
| 201 | return true; | ||
| 183 | } | 202 | } |
| 203 | }; | ||
| 184 | 204 | ||
| 185 | auto operator==(zstring_view lhs, zstring_view rhs) -> bool { | 205 | constexpr auto operator""_zsv(char const* s, std::size_t length) noexcept |
| 186 | return std::string_view{lhs} == std::string_view{rhs}; | 206 | -> zstring_view |
| 187 | } | 207 | { |
| 208 | return zstring_view{s, length}; | ||
| 209 | } | ||
| 188 | 210 | ||
| 189 | export constexpr auto split_on(std::string_view s, char c) -> std::pair<std::string_view, std::optional<std::string_view>> { | 211 | auto operator==(zstring_view lhs, zstring_view rhs) -> bool |
| 190 | if (auto i = s.find(c); i != std::string_view::npos) | 212 | { |
| 191 | return std::make_pair(s.substr(0, i), s.substr(i + 1)); | 213 | return std::string_view{lhs} == std::string_view{rhs}; |
| 192 | return std::make_pair(s, std::nullopt); | 214 | } |
| 193 | } | ||
| 194 | 215 | ||
| 195 | export template<class T> | 216 | export constexpr auto split_on(std::string_view s, char c) |
| 196 | class not_null; | 217 | -> std::pair<std::string_view, std::optional<std::string_view>> |
| 218 | { | ||
| 219 | if (auto i = s.find(c); i != std::string_view::npos) | ||
| 220 | return std::make_pair(s.substr(0, i), s.substr(i + 1)); | ||
| 221 | return std::make_pair(s, std::nullopt); | ||
| 222 | } | ||
| 197 | 223 | ||
| 198 | export template<class T> | 224 | export template <class T> |
| 199 | class not_null<T*> { | 225 | class not_null; |
| 200 | T* p_; | ||
| 201 | 226 | ||
| 202 | struct guaranteed_not_null_t {}; | 227 | export template <class T> |
| 203 | explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} | 228 | class not_null<T*> |
| 229 | { | ||
| 230 | T* p_; | ||
| 204 | 231 | ||
| 205 | public: | 232 | struct guaranteed_not_null_t |
| 206 | explicit not_null(T* p) | 233 | { |
| 207 | : p_{p} | 234 | }; |
| 208 | { if (!p_) throw std::runtime_error{"not_null constructed with null pointer"}; } | 235 | explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} |
| 209 | ~not_null() = default; | ||
| 210 | 236 | ||
| 211 | not_null(not_null const& other) = default; | 237 | public: |
| 212 | not_null(not_null&& other) noexcept = default; | 238 | explicit not_null(T* p) : p_{p} |
| 213 | auto operator=(not_null const& rhs) noexcept -> not_null& = default; | 239 | { |
| 214 | auto operator=(not_null&& rhs) noexcept -> not_null& = default; | 240 | if (!p_) |
| 241 | throw std::runtime_error{"not_null constructed with null pointer"}; | ||
| 242 | } | ||
| 243 | ~not_null() = default; | ||
| 215 | 244 | ||
| 216 | friend auto make_not_null(T* p) noexcept -> std::optional<not_null> { | 245 | not_null(not_null const& other) = default; |
| 217 | if (p) return not_null(p, guaranteed_not_null_t{}); | 246 | not_null(not_null&& other) noexcept = default; |
| 218 | else return std::nullopt; | 247 | auto operator=(not_null const& rhs) noexcept -> not_null& = default; |
| 219 | } | 248 | auto operator=(not_null&& rhs) noexcept -> not_null& = default; |
| 220 | 249 | ||
| 221 | [[nodiscard]] auto get() const noexcept -> T* { | 250 | friend auto make_not_null(T* p) noexcept -> std::optional<not_null> |
| 222 | return p_; | 251 | { |
| 223 | } | 252 | if (p) |
| 253 | return not_null(p, guaranteed_not_null_t{}); | ||
| 254 | else | ||
| 255 | return std::nullopt; | ||
| 256 | } | ||
| 224 | 257 | ||
| 225 | auto operator*() const noexcept -> std::add_lvalue_reference_t<T> { | 258 | [[nodiscard]] auto get() const noexcept -> T* { return p_; } |
| 226 | return *p_; | ||
| 227 | } | ||
| 228 | 259 | ||
| 229 | auto operator->() const noexcept -> T* { | 260 | auto operator*() const noexcept -> std::add_lvalue_reference_t<T> |
| 230 | return p_; | 261 | { |
| 231 | } | 262 | return *p_; |
| 232 | }; | 263 | } |
| 233 | export template<class T> explicit not_null(T*) -> not_null<T*>; | ||
| 234 | 264 | ||
| 235 | export template<> | 265 | auto operator->() const noexcept -> T* { return p_; } |
| 236 | class not_null<lazy_zstring_view> { | 266 | }; |
| 237 | lazy_zstring_view s_; | 267 | export template <class T> |
| 268 | explicit not_null(T*) -> not_null<T*>; | ||
| 238 | 269 | ||
| 239 | public: | 270 | export template <> |
| 240 | explicit not_null(lazy_zstring_view s) | 271 | class not_null<lazy_zstring_view> |
| 241 | : s_{std::move(s)} | 272 | { |
| 242 | { if (!s_) throw std::runtime_error{"not_null constructed with null pointer"}; } | 273 | lazy_zstring_view s_; |
| 243 | 274 | ||
| 244 | [[nodiscard]] auto get() const noexcept -> lazy_zstring_view { | 275 | public: |
| 245 | return s_; | 276 | explicit not_null(lazy_zstring_view s) : s_{std::move(s)} |
| 246 | } | 277 | { |
| 278 | if (!s_) | ||
| 279 | throw std::runtime_error{"not_null constructed with null pointer"}; | ||
| 280 | } | ||
| 247 | 281 | ||
| 248 | operator lazy_zstring_view() const noexcept { return s_; } | 282 | [[nodiscard]] auto get() const noexcept -> lazy_zstring_view { return s_; } |
| 249 | operator std::string_view() const noexcept { return s_; } | 283 | |
| 250 | operator char const*() const noexcept { return s_; } | 284 | operator lazy_zstring_view() const noexcept { return s_; } |
| 251 | }; | 285 | operator std::string_view() const noexcept { return s_; } |
| 252 | export explicit not_null(lazy_zstring_view s) -> not_null<lazy_zstring_view>; | 286 | operator char const*() const noexcept { return s_; } |
| 287 | }; | ||
| 288 | export explicit not_null(lazy_zstring_view s) -> not_null<lazy_zstring_view>; | ||
| 253 | 289 | ||
| 254 | } // namespace routemon::util | 290 | } // namespace routemon::util |
diff --git a/server/src/xml.cpp b/server/src/xml.cpp index cad42d1..c1b4358 100644 --- a/server/src/xml.cpp +++ b/server/src/xml.cpp | |||
| @@ -9,53 +9,65 @@ import :xml; | |||
| 9 | 9 | ||
| 10 | namespace routemon::xml { | 10 | namespace routemon::xml { |
| 11 | 11 | ||
| 12 | auto qname_view::operator==(qname_view const& rhs) const -> bool { | 12 | auto qname_view::operator==(qname_view const& rhs) const -> bool |
| 13 | return ns_uri == rhs.ns_uri && local == rhs.local; | 13 | { |
| 14 | } | 14 | return ns_uri == rhs.ns_uri && local == rhs.local; |
| 15 | } | ||
| 15 | 16 | ||
| 16 | executor::executor() : | 17 | executor::executor() : p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)} |
| 17 | p_{XML_ParserCreateNS("UTF-8", detail::qname_sep)} | 18 | { |
| 18 | { | 19 | XML_SetUserData(p_, this); |
| 19 | XML_SetUserData(p_, this); | 20 | XML_SetElementHandler(p_, handle_start_element, handle_end_element); |
| 20 | XML_SetElementHandler(p_, handle_start_element, handle_end_element); | 21 | XML_SetCharacterDataHandler(p_, handle_character_data); |
| 21 | XML_SetCharacterDataHandler(p_, handle_character_data); | 22 | XML_SetProcessingInstructionHandler(p_, handle_processing_instructions); |
| 22 | XML_SetProcessingInstructionHandler(p_, handle_processing_instructions); | 23 | XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref); |
| 23 | XML_SetExternalEntityRefHandler(p_, handle_external_entity_ref); | 24 | XML_SetNamespaceDeclHandler( |
| 24 | XML_SetNamespaceDeclHandler(p_, handle_start_namespace_decl, handle_end_namespace_decl); | 25 | p_, handle_start_namespace_decl, handle_end_namespace_decl); |
| 25 | XML_SetXmlDeclHandler(p_, handle_xml_decl); | 26 | XML_SetXmlDeclHandler(p_, handle_xml_decl); |
| 26 | } | 27 | } |
| 27 | 28 | ||
| 28 | executor::~executor() { | 29 | executor::~executor() { XML_ParserFree(p_); } |
| 29 | XML_ParserFree(p_); | ||
| 30 | } | ||
| 31 | 30 | ||
| 32 | auto executor::start() -> void { | 31 | auto executor::start() -> void |
| 33 | continuation_.resume(); | 32 | { |
| 34 | if (ex_) std::rethrow_exception(ex_); | 33 | continuation_.resume(); |
| 35 | } | 34 | if (ex_) |
| 35 | std::rethrow_exception(ex_); | ||
| 36 | } | ||
| 36 | 37 | ||
| 37 | auto executor::read(std::string_view xml, bool is_final) -> void { | 38 | auto executor::read(std::string_view xml, bool is_final) -> void |
| 38 | if (ex_) | 39 | { |
| 39 | throw std::runtime_error{"refusing to restart parser that was thrown in"}; | 40 | if (ex_) |
| 40 | // TODO: narrow_cast | 41 | throw std::runtime_error{"refusing to restart parser that was thrown in"}; |
| 41 | if (auto s = XML_Parse(p_, xml.data(), static_cast<int>(xml.size()), is_final); s != XML_STATUS_OK) { | 42 | // TODO: narrow_cast |
| 42 | auto errc = XML_GetErrorCode(p_); | 43 | if (auto s = |
| 43 | if (errc == XML_ERROR_ABORTED) { | 44 | XML_Parse(p_, xml.data(), static_cast<int>(xml.size()), is_final); |
| 44 | assert(ex_); | 45 | s != XML_STATUS_OK) |
| 45 | std::rethrow_exception(ex_); | 46 | { |
| 46 | } else { | 47 | auto errc = XML_GetErrorCode(p_); |
| 47 | throw std::runtime_error{std::format("failed to parse XML: {}", XML_ErrorString(errc))}; | 48 | if (errc == XML_ERROR_ABORTED) |
| 48 | } | 49 | { |
| 50 | assert(ex_); | ||
| 51 | std::rethrow_exception(ex_); | ||
| 52 | } | ||
| 53 | else | ||
| 54 | { | ||
| 55 | throw std::runtime_error{std::format( | ||
| 56 | "failed to parse XML: {}", XML_ErrorString(errc))}; | ||
| 49 | } | 57 | } |
| 50 | } | 58 | } |
| 59 | } | ||
| 51 | 60 | ||
| 52 | auto executor::end() -> void { | 61 | auto executor::end() -> void |
| 53 | if (ex_) | 62 | { |
| 54 | throw std::runtime_error{"refusing to restart parser that was thrown in"}; | 63 | if (ex_) |
| 55 | ev_ = eof_event{}; | 64 | throw std::runtime_error{"refusing to restart parser that was thrown in"}; |
| 56 | advance_ = false; | 65 | ev_ = eof_event{}; |
| 57 | while (continuation_) continuation_.resume(); | 66 | advance_ = false; |
| 58 | if (ex_) std::rethrow_exception(ex_); | 67 | while (continuation_) |
| 59 | } | 68 | continuation_.resume(); |
| 69 | if (ex_) | ||
| 70 | std::rethrow_exception(ex_); | ||
| 71 | } | ||
| 60 | 72 | ||
| 61 | } // namespace routemon::xml | 73 | } // namespace routemon::xml |
diff --git a/server/src/xml.cppm b/server/src/xml.cppm index 957f149..ff27c5c 100644 --- a/server/src/xml.cppm +++ b/server/src/xml.cppm | |||
| @@ -23,610 +23,808 @@ static_assert(std::is_same_v<XML_Char, char>); | |||
| 23 | 23 | ||
| 24 | namespace routemon::xml { | 24 | namespace routemon::xml { |
| 25 | 25 | ||
| 26 | struct qname_view { | 26 | struct qname_view |
| 27 | std::string_view ns_uri; | 27 | { |
| 28 | std::string_view local; | 28 | std::string_view ns_uri; |
| 29 | std::string_view local; | ||
| 29 | 30 | ||
| 30 | auto operator==(qname_view const& rhs) const -> bool; | 31 | auto operator==(qname_view const& rhs) const -> bool; |
| 31 | }; | 32 | }; |
| 32 | 33 | ||
| 33 | namespace detail { | 34 | namespace detail { |
| 34 | 35 | ||
| 35 | constexpr auto qname_sep = '\xFF'; | 36 | constexpr auto qname_sep = '\xFF'; |
| 36 | auto split_name(char const* name) noexcept -> qname_view { | 37 | auto split_name(char const* name) noexcept -> qname_view |
| 37 | auto [l, r] = util::split_on(name, qname_sep); | 38 | { |
| 38 | if (r) return { .ns_uri = l, .local = *r }; | 39 | auto [l, r] = util::split_on(name, qname_sep); |
| 39 | else return { .ns_uri = std::string_view{}, .local = l }; | 40 | if (r) |
| 40 | } | 41 | return {.ns_uri = l, .local = *r}; |
| 42 | else | ||
| 43 | return {.ns_uri = std::string_view{}, .local = l}; | ||
| 44 | } | ||
| 41 | 45 | ||
| 42 | } // namespace detail | 46 | } // namespace detail |
| 43 | 47 | ||
| 44 | class attribute_view_iterator { | 48 | class attribute_view_iterator |
| 45 | std::string_view default_ns_uri_; | 49 | { |
| 46 | char const* const* attrs_; | 50 | std::string_view default_ns_uri_; |
| 51 | char const* const* attrs_; | ||
| 47 | 52 | ||
| 48 | auto advance() -> void { | 53 | auto advance() -> void { attrs_ += 2; } |
| 49 | attrs_ += 2; | ||
| 50 | } | ||
| 51 | 54 | ||
| 52 | public: | 55 | public: |
| 53 | using difference_type = std::ptrdiff_t; | 56 | using difference_type = std::ptrdiff_t; |
| 54 | using value_type = std::pair<qname_view, char const*>; | 57 | using value_type = std::pair<qname_view, char const*>; |
| 55 | 58 | ||
| 56 | struct sentinel { | 59 | struct sentinel |
| 57 | friend constexpr auto operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool { | 60 | { |
| 58 | return !*it.attrs_; | 61 | friend constexpr auto |
| 59 | } | 62 | operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool |
| 60 | }; | 63 | { |
| 64 | return !*it.attrs_; | ||
| 65 | } | ||
| 66 | }; | ||
| 61 | 67 | ||
| 62 | inline explicit attribute_view_iterator(std::string_view default_ns_uri, char const* const* attrs) | 68 | inline explicit attribute_view_iterator( |
| 63 | : default_ns_uri_{default_ns_uri}, attrs_{attrs} | 69 | std::string_view default_ns_uri, char const* const* attrs) |
| 64 | {} | 70 | : default_ns_uri_{default_ns_uri}, attrs_{attrs} |
| 71 | { | ||
| 72 | } | ||
| 65 | 73 | ||
| 66 | inline auto operator*() const -> std::pair<qname_view, char const*> { | 74 | inline auto operator*() const -> std::pair<qname_view, char const*> |
| 67 | if (!*attrs_) | 75 | { |
| 68 | throw std::runtime_error{"end of attribute list"}; | 76 | if (!*attrs_) |
| 69 | auto qname = detail::split_name(attrs_[0]); | 77 | throw std::runtime_error{"end of attribute list"}; |
| 70 | if (qname.ns_uri.empty()) | 78 | auto qname = detail::split_name(attrs_[0]); |
| 71 | qname.ns_uri = default_ns_uri_; | 79 | if (qname.ns_uri.empty()) |
| 72 | return std::make_pair(qname, attrs_[1]); | 80 | qname.ns_uri = default_ns_uri_; |
| 73 | } | 81 | return std::make_pair(qname, attrs_[1]); |
| 82 | } | ||
| 74 | 83 | ||
| 75 | // Pre-increment | 84 | // Pre-increment |
| 76 | inline auto operator++() -> attribute_view_iterator& { | 85 | inline auto operator++() -> attribute_view_iterator& |
| 77 | advance(); | 86 | { |
| 78 | return *this; | 87 | advance(); |
| 79 | } | 88 | return *this; |
| 89 | } | ||
| 80 | 90 | ||
| 81 | // Post-increment | 91 | // Post-increment |
| 82 | inline auto operator++(int) -> attribute_view_iterator { | 92 | inline auto operator++(int) -> attribute_view_iterator |
| 83 | auto pre = *this; | 93 | { |
| 84 | advance(); | 94 | auto pre = *this; |
| 85 | return pre; | 95 | advance(); |
| 86 | } | 96 | return pre; |
| 87 | }; | 97 | } |
| 88 | static_assert(std::input_iterator<attribute_view_iterator>); | 98 | }; |
| 99 | static_assert(std::input_iterator<attribute_view_iterator>); | ||
| 89 | 100 | ||
| 90 | class attribute_view : std::ranges::view_base { | 101 | class attribute_view : std::ranges::view_base |
| 91 | std::string_view default_ns_uri_; | 102 | { |
| 92 | char const* const* attrs_; | 103 | std::string_view default_ns_uri_; |
| 104 | char const* const* attrs_; | ||
| 93 | 105 | ||
| 94 | public: | 106 | public: |
| 95 | inline explicit attribute_view(std::string_view default_ns_uri, char const** attrs) | 107 | inline explicit attribute_view( |
| 96 | : default_ns_uri_{default_ns_uri}, attrs_{const_cast<char const* const*>(attrs)} | 108 | std::string_view default_ns_uri, char const** attrs) |
| 97 | {} | 109 | : default_ns_uri_{default_ns_uri}, |
| 110 | attrs_{const_cast<char const* const*>(attrs)} | ||
| 111 | { | ||
| 112 | } | ||
| 98 | 113 | ||
| 99 | [[nodiscard]] inline auto begin() const -> attribute_view_iterator { | 114 | [[nodiscard]] inline auto begin() const -> attribute_view_iterator |
| 100 | return attribute_view_iterator{default_ns_uri_, attrs_}; | 115 | { |
| 101 | } | 116 | return attribute_view_iterator{default_ns_uri_, attrs_}; |
| 117 | } | ||
| 102 | 118 | ||
| 103 | [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel { | 119 | [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel |
| 104 | return {}; | 120 | { |
| 105 | } | 121 | return {}; |
| 122 | } | ||
| 106 | 123 | ||
| 107 | inline auto lookup(qname_view want) -> std::optional<util::not_null<util::lazy_zstring_view>> { | 124 | inline auto lookup(qname_view want) |
| 108 | for (auto const& [name, v] : *this) { | 125 | -> std::optional<util::not_null<util::lazy_zstring_view>> |
| 109 | if (name == want) { | 126 | { |
| 110 | return util::not_null{util::lazy_zstring_view{v}}; | 127 | for (auto const& [name, v] : *this) |
| 111 | } | 128 | { |
| 129 | if (name == want) | ||
| 130 | { | ||
| 131 | return util::not_null{util::lazy_zstring_view{v}}; | ||
| 112 | } | 132 | } |
| 113 | return std::nullopt; | ||
| 114 | } | 133 | } |
| 115 | }; | 134 | return std::nullopt; |
| 116 | static_assert(std::ranges::input_range<attribute_view>); | 135 | } |
| 136 | }; | ||
| 137 | static_assert(std::ranges::input_range<attribute_view>); | ||
| 117 | 138 | ||
| 118 | template<class T> class promise; | 139 | template <class T> |
| 140 | class promise; | ||
| 119 | 141 | ||
| 120 | template<class T> | 142 | template <class T> |
| 121 | struct [[clang::coro_await_elidable, clang::coro_return_type]] parser { | 143 | struct [[clang::coro_await_elidable, clang::coro_return_type]] parser |
| 122 | using promise_type = promise<T>; | 144 | { |
| 123 | using result_type = promise_type::result_type; | 145 | using promise_type = promise<T>; |
| 124 | using handle_type = std::coroutine_handle<promise_type>; | 146 | using result_type = promise_type::result_type; |
| 147 | using handle_type = std::coroutine_handle<promise_type>; | ||
| 125 | 148 | ||
| 126 | private: | 149 | private: |
| 127 | handle_type h_; | 150 | handle_type h_; |
| 128 | 151 | ||
| 129 | public: | 152 | public: |
| 130 | explicit parser(handle_type h) | 153 | explicit parser(handle_type h) : h_{h} { assert(h); } |
| 131 | : h_{h} | ||
| 132 | { assert(h); } | ||
| 133 | 154 | ||
| 134 | parser(const parser&) = delete; | 155 | parser(const parser&) = delete; |
| 135 | parser(parser&& c) noexcept | 156 | parser(parser&& c) noexcept : h_{std::exchange(c.h_, nullptr)} {} |
| 136 | : h_{std::exchange(c.h_, nullptr)} | 157 | auto operator=(const parser&) -> parser& = delete; |
| 137 | {} | 158 | auto operator=(parser&&) -> parser& = delete; |
| 138 | auto operator=(const parser&) -> parser& = delete; | ||
| 139 | auto operator=(parser&&) -> parser& = delete; | ||
| 140 | 159 | ||
| 141 | [[nodiscard]] auto promise() const -> promise_type& { | 160 | [[nodiscard]] auto promise() const -> promise_type& { return h_.promise(); } |
| 142 | return h_.promise(); | ||
| 143 | } | ||
| 144 | 161 | ||
| 145 | ~parser() { | 162 | ~parser() |
| 146 | if (h_) h_.destroy(); | 163 | { |
| 147 | } | 164 | if (h_) |
| 148 | }; | 165 | h_.destroy(); |
| 166 | } | ||
| 167 | }; | ||
| 149 | 168 | ||
| 150 | struct start_element_event { | 169 | struct start_element_event |
| 151 | qname_view name; | 170 | { |
| 152 | attribute_view attrs; | 171 | qname_view name; |
| 153 | }; | 172 | attribute_view attrs; |
| 154 | struct end_element_event { | 173 | }; |
| 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 | 174 | ||
| 179 | class executor; | 175 | struct end_element_event |
| 180 | using executor_ref = util::not_null<executor*>; | 176 | { |
| 177 | qname_view name; | ||
| 178 | }; | ||
| 181 | 179 | ||
| 182 | struct current_event_t { | 180 | struct character_data_event |
| 183 | executor_ref executor; | 181 | { |
| 184 | }; | 182 | std::string_view data; |
| 185 | auto current_event(executor_ref executor) -> current_event_t { | 183 | }; |
| 186 | return current_event_t{executor}; | ||
| 187 | } | ||
| 188 | 184 | ||
| 189 | class promise_base { | 185 | struct processing_instructions_event |
| 190 | executor_ref executor_; | 186 | { |
| 191 | std::coroutine_handle<promise_base> continuation_ = nullptr; | 187 | util::lazy_zstring_view target; |
| 188 | util::lazy_zstring_view data; | ||
| 189 | }; | ||
| 192 | 190 | ||
| 193 | public: | 191 | struct xml_decl_event |
| 194 | // Not having this constructor marked inline messes with coroutine | 192 | { |
| 195 | // HALO. (Hours 'wasted': many) | 193 | util::lazy_zstring_view version; |
| 196 | inline explicit promise_base(executor_ref executor) | 194 | util::lazy_zstring_view encoding; |
| 197 | : executor_{executor} | 195 | std::optional<bool> standalone; |
| 198 | {} | 196 | }; |
| 199 | 197 | ||
| 200 | [[nodiscard]] inline auto executor() const -> executor& { | 198 | struct eof_event |
| 201 | return *executor_; | 199 | { |
| 202 | } | 200 | }; |
| 203 | 201 | ||
| 204 | inline auto base_handle() -> std::coroutine_handle<promise_base> { | 202 | using event = std::variant< |
| 205 | return std::coroutine_handle<promise_base>::from_promise(*this); | 203 | start_element_event, end_element_event, character_data_event, |
| 206 | } | 204 | processing_instructions_event, xml_decl_event, eof_event>; |
| 207 | 205 | ||
| 208 | [[nodiscard]] inline auto continuation() const -> std::coroutine_handle<promise_base> { | 206 | template <class T> |
| 209 | return continuation_; | 207 | concept event_type = requires(event ev) { std::get<T>(ev); }; |
| 210 | } | ||
| 211 | inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void { | ||
| 212 | continuation_ = c; | ||
| 213 | } | ||
| 214 | }; | ||
| 215 | 208 | ||
| 216 | struct position { | 209 | class executor; |
| 217 | std::size_t line; | 210 | using executor_ref = util::not_null<executor*>; |
| 218 | std::size_t col; | ||
| 219 | }; | ||
| 220 | 211 | ||
| 221 | class executor { | 212 | struct current_event_t |
| 222 | XML_Parser p_; | 213 | { |
| 223 | std::exception_ptr ex_ = nullptr; | 214 | executor_ref executor; |
| 224 | std::coroutine_handle<promise_base> continuation_ = nullptr; | 215 | }; |
| 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 | 216 | ||
| 230 | inline auto try_handle_event(event ev) noexcept -> void { | 217 | auto current_event(executor_ref executor) -> current_event_t |
| 231 | assert(!ex_); | 218 | { |
| 219 | return current_event_t{executor}; | ||
| 220 | } | ||
| 232 | 221 | ||
| 233 | try { | 222 | class promise_base |
| 234 | ev_ = std::move(ev); | 223 | { |
| 235 | } catch (...) { | 224 | executor_ref executor_; |
| 236 | ex_ = std::current_exception(); | 225 | std::coroutine_handle<promise_base> continuation_ = nullptr; |
| 237 | return; | 226 | |
| 238 | } | 227 | public: |
| 239 | advance_ = false; | 228 | // Not having this constructor marked inline messes with coroutine |
| 240 | if (!continuation_) { | 229 | // HALO. (Hours 'wasted': many) |
| 241 | // Parser returned (all subparsers are done) and has set the continuation to nullptr. | 230 | inline explicit promise_base(executor_ref executor) : executor_{executor} {} |
| 242 | if (auto s = XML_StopParser(p_, /* resumable */ false); s != XML_STATUS_OK) { | 231 | |
| 243 | ex_ = std::make_exception_ptr(std::runtime_error{"unexpected error when stopping XML parser"}); | 232 | [[nodiscard]] inline auto executor() const -> executor& { return *executor_; } |
| 244 | return; | 233 | |
| 245 | } | 234 | inline auto base_handle() -> std::coroutine_handle<promise_base> |
| 246 | ex_ = std::make_exception_ptr(std::runtime_error{"parser did not consume entire XML document"}); | 235 | { |
| 236 | return std::coroutine_handle<promise_base>::from_promise(*this); | ||
| 237 | } | ||
| 238 | |||
| 239 | [[nodiscard]] inline auto continuation() const | ||
| 240 | -> std::coroutine_handle<promise_base> | ||
| 241 | { | ||
| 242 | return continuation_; | ||
| 243 | } | ||
| 244 | inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void | ||
| 245 | { | ||
| 246 | continuation_ = c; | ||
| 247 | } | ||
| 248 | }; | ||
| 249 | |||
| 250 | struct position | ||
| 251 | { | ||
| 252 | std::size_t line; | ||
| 253 | std::size_t col; | ||
| 254 | }; | ||
| 255 | |||
| 256 | class executor | ||
| 257 | { | ||
| 258 | XML_Parser p_; | ||
| 259 | std::exception_ptr ex_ = nullptr; | ||
| 260 | std::coroutine_handle<promise_base> continuation_ = nullptr; | ||
| 261 | std::vector<std::optional<std::string>> default_namespace_; | ||
| 262 | std::unordered_map<std::string_view, std::vector<std::string>> namespaces_; | ||
| 263 | std::optional<event> ev_; | ||
| 264 | bool advance_ = true; | ||
| 265 | |||
| 266 | inline auto try_handle_event(event ev) noexcept -> void | ||
| 267 | { | ||
| 268 | assert(!ex_); | ||
| 269 | |||
| 270 | try | ||
| 271 | { | ||
| 272 | ev_ = std::move(ev); | ||
| 273 | } | ||
| 274 | catch (...) | ||
| 275 | { | ||
| 276 | ex_ = std::current_exception(); | ||
| 277 | return; | ||
| 278 | } | ||
| 279 | advance_ = false; | ||
| 280 | if (!continuation_) | ||
| 281 | { | ||
| 282 | // Parser returned (all subparsers are done) and has set the | ||
| 283 | // continuation to nullptr. | ||
| 284 | if (auto s = XML_StopParser(p_, /* resumable */ false); | ||
| 285 | s != XML_STATUS_OK) | ||
| 286 | { | ||
| 287 | ex_ = std::make_exception_ptr( | ||
| 288 | std::runtime_error{"unexpected error when stopping XML parser"}); | ||
| 247 | return; | 289 | return; |
| 248 | } | 290 | } |
| 249 | continuation_.resume(); | 291 | ex_ = std::make_exception_ptr( |
| 250 | if (ex_) { | 292 | std::runtime_error{"parser did not consume entire XML document"}); |
| 251 | // Not sure if it's useful to report this error. | 293 | return; |
| 252 | std::ignore = XML_StopParser(p_, /* resumable */ false); | 294 | } |
| 253 | } | 295 | continuation_.resume(); |
| 296 | if (ex_) | ||
| 297 | { | ||
| 298 | // Not sure if it's useful to report this error. | ||
| 299 | std::ignore = XML_StopParser(p_, /* resumable */ false); | ||
| 254 | } | 300 | } |
| 301 | } | ||
| 255 | 302 | ||
| 256 | static auto handle_start_element(void* ctx, char const* name, char const** attrs) noexcept -> void { | 303 | static auto |
| 257 | auto qname = detail::split_name(name); | 304 | handle_start_element(void* ctx, char const* name, char const** attrs) noexcept |
| 258 | static_cast<executor*>(ctx)->try_handle_event(start_element_event{ | 305 | -> void |
| 259 | .name = qname, | 306 | { |
| 260 | .attrs = attribute_view{qname.ns_uri, attrs}, | 307 | auto qname = detail::split_name(name); |
| 308 | static_cast<executor*>(ctx)->try_handle_event( | ||
| 309 | start_element_event{ | ||
| 310 | .name = qname, | ||
| 311 | .attrs = attribute_view{qname.ns_uri, attrs}, | ||
| 261 | }); | 312 | }); |
| 262 | } | 313 | } |
| 263 | static auto handle_end_element(void* ctx, char const* name) noexcept -> void { | 314 | |
| 264 | static_cast<executor*>(ctx)->try_handle_event(end_element_event{ | 315 | static auto handle_end_element(void* ctx, char const* name) noexcept -> void |
| 265 | .name = detail::split_name(name), | 316 | { |
| 317 | static_cast<executor*>(ctx)->try_handle_event( | ||
| 318 | end_element_event{ | ||
| 319 | .name = detail::split_name(name), | ||
| 266 | }); | 320 | }); |
| 267 | } | 321 | } |
| 268 | static auto handle_character_data(void* ctx, char const* s, int len) noexcept -> void { | 322 | |
| 269 | static_cast<executor*>(ctx)->try_handle_event(character_data_event{ | 323 | static auto handle_character_data(void* ctx, char const* s, int len) noexcept |
| 270 | .data = std::string_view{s, static_cast<std::size_t>(len)}, | 324 | -> void |
| 325 | { | ||
| 326 | static_cast<executor*>(ctx)->try_handle_event( | ||
| 327 | character_data_event{ | ||
| 328 | .data = std::string_view{s, static_cast<std::size_t>(len)}, | ||
| 271 | }); | 329 | }); |
| 272 | } | 330 | } |
| 273 | static auto handle_processing_instructions(void* ctx, char const* target, char const* data) noexcept -> void { | 331 | |
| 274 | static_cast<executor*>(ctx)->try_handle_event(processing_instructions_event{ | 332 | static auto handle_processing_instructions( |
| 275 | .target = util::lazy_zstring_view{target}, | 333 | void* ctx, char const* target, char const* data) noexcept -> void |
| 276 | .data = util::lazy_zstring_view{data}, | 334 | { |
| 335 | static_cast<executor*>(ctx)->try_handle_event( | ||
| 336 | processing_instructions_event{ | ||
| 337 | .target = util::lazy_zstring_view{target}, | ||
| 338 | .data = util::lazy_zstring_view{data}, | ||
| 277 | }); | 339 | }); |
| 340 | } | ||
| 341 | |||
| 342 | static auto handle_external_entity_ref( | ||
| 343 | XML_Parser, char const* /* context */, char const* /* base */, | ||
| 344 | char const* /* system_id */, char const* /* public_id */) noexcept -> int | ||
| 345 | { | ||
| 346 | return XML_STATUS_ERROR; | ||
| 347 | } | ||
| 348 | |||
| 349 | static auto handle_start_namespace_decl( | ||
| 350 | void* ctx, char const* prefix, char const* uri) noexcept -> void | ||
| 351 | { | ||
| 352 | if (prefix) | ||
| 353 | { | ||
| 354 | static_cast<executor*>(ctx) | ||
| 355 | ->namespaces_[std::string_view{prefix}] | ||
| 356 | .emplace_back(uri); | ||
| 278 | } | 357 | } |
| 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 { | 358 | else |
| 280 | return XML_STATUS_ERROR; | 359 | { |
| 360 | static_cast<executor*>(ctx)->default_namespace_.push_back( | ||
| 361 | uri ? std::make_optional<std::string>(uri) : std::nullopt); | ||
| 281 | } | 362 | } |
| 282 | static auto handle_start_namespace_decl(void* ctx, char const* prefix, char const* uri) noexcept -> void { | 363 | } |
| 283 | if (prefix) { | 364 | |
| 284 | static_cast<executor*>(ctx)->namespaces_[std::string_view{prefix}].emplace_back(uri); | 365 | static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept |
| 285 | } else { | 366 | -> void |
| 286 | static_cast<executor*>(ctx)->default_namespace_.push_back(uri ? std::make_optional<std::string>(uri) : std::nullopt); | 367 | { |
| 287 | } | 368 | if (prefix) |
| 369 | { | ||
| 370 | static_cast<executor*>(ctx) | ||
| 371 | ->namespaces_[std::string_view{prefix}] | ||
| 372 | .pop_back(); | ||
| 288 | } | 373 | } |
| 289 | static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept -> void { | 374 | else |
| 290 | if (prefix) { | 375 | { |
| 291 | static_cast<executor*>(ctx)->namespaces_[std::string_view{prefix}].pop_back(); | 376 | static_cast<executor*>(ctx)->default_namespace_.pop_back(); |
| 292 | } else { | ||
| 293 | static_cast<executor*>(ctx)->default_namespace_.pop_back(); | ||
| 294 | } | ||
| 295 | } | 377 | } |
| 296 | static auto handle_xml_decl(void * ctx, char const* version, char const* encoding, int standalone) noexcept -> void { | 378 | } |
| 297 | static_cast<executor*>(ctx)->try_handle_event(xml_decl_event{ | 379 | |
| 298 | .version = util::lazy_zstring_view{version}, | 380 | static auto handle_xml_decl( |
| 299 | .encoding = util::lazy_zstring_view{encoding}, | 381 | void* ctx, char const* version, char const* encoding, |
| 300 | .standalone = standalone < 0 ? std::nullopt : std::make_optional(standalone > 0), | 382 | int standalone) noexcept -> void |
| 383 | { | ||
| 384 | static_cast<executor*>(ctx)->try_handle_event( | ||
| 385 | xml_decl_event{ | ||
| 386 | .version = util::lazy_zstring_view{version}, | ||
| 387 | .encoding = util::lazy_zstring_view{encoding}, | ||
| 388 | .standalone = standalone < 0 ? std::nullopt | ||
| 389 | : std::make_optional(standalone > 0), | ||
| 301 | }); | 390 | }); |
| 302 | } | 391 | } |
| 303 | 392 | ||
| 304 | inline auto advance_flag() -> bool { | 393 | inline auto advance_flag() -> bool { return advance_; } |
| 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 | 394 | ||
| 314 | template<class T> friend class promise; | 395 | inline auto set_exception(std::exception_ptr ex) -> void |
| 396 | { | ||
| 397 | ex_ = std::move(ex); | ||
| 398 | } | ||
| 315 | 399 | ||
| 316 | public: | 400 | [[nodiscard]] inline auto take_exception() -> std::exception_ptr |
| 317 | executor(); | 401 | { |
| 402 | return std::exchange(ex_, nullptr); | ||
| 403 | } | ||
| 318 | 404 | ||
| 319 | executor(executor const&) = delete; | 405 | template <class T> |
| 320 | executor(executor&&) = delete; | 406 | friend class promise; |
| 321 | auto operator=(executor const&) -> executor& = delete; | ||
| 322 | auto operator=(executor&&) -> executor& = delete; | ||
| 323 | 407 | ||
| 324 | ~executor(); | 408 | public: |
| 409 | executor(); | ||
| 325 | 410 | ||
| 326 | inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void { | 411 | executor(executor const&) = delete; |
| 327 | continuation_ = c; | 412 | executor(executor&&) = delete; |
| 328 | } | 413 | auto operator=(executor const&) -> executor& = delete; |
| 329 | inline auto set_advance_flag() -> void { | 414 | auto operator=(executor&&) -> executor& = delete; |
| 330 | if (!ev_ || !std::holds_alternative<eof_event>(ev_.value())) { | 415 | |
| 331 | advance_ = true; | 416 | ~executor(); |
| 332 | } | 417 | |
| 333 | } | 418 | inline auto set_continuation(std::coroutine_handle<promise_base> c) -> void |
| 334 | inline auto event() const -> std::optional<event> const& { | 419 | { |
| 335 | return ev_; | 420 | continuation_ = c; |
| 336 | } | 421 | } |
| 337 | inline auto resolve_namespace(std::string_view prefix) -> std::optional<std::string_view> { | 422 | |
| 338 | if (auto it = namespaces_.find(prefix); it != namespaces_.end() && !it->second.empty()) | 423 | inline auto set_advance_flag() -> void |
| 339 | return it->second.back(); | 424 | { |
| 340 | return std::nullopt; | 425 | if (!ev_ || !std::holds_alternative<eof_event>(ev_.value())) |
| 426 | { | ||
| 427 | advance_ = true; | ||
| 341 | } | 428 | } |
| 342 | [[nodiscard]] inline auto position() -> position { | 429 | } |
| 343 | return { | 430 | |
| 431 | inline auto event() const -> std::optional<event> const& { return ev_; } | ||
| 432 | |||
| 433 | inline auto resolve_namespace(std::string_view prefix) | ||
| 434 | -> std::optional<std::string_view> | ||
| 435 | { | ||
| 436 | if (auto it = namespaces_.find(prefix); | ||
| 437 | it != namespaces_.end() && !it->second.empty()) | ||
| 438 | return it->second.back(); | ||
| 439 | return std::nullopt; | ||
| 440 | } | ||
| 441 | |||
| 442 | [[nodiscard]] inline auto position() -> position | ||
| 443 | { | ||
| 444 | return { | ||
| 344 | .line = XML_GetCurrentLineNumber(p_), | 445 | .line = XML_GetCurrentLineNumber(p_), |
| 345 | .col = XML_GetCurrentColumnNumber(p_), | 446 | .col = XML_GetCurrentColumnNumber(p_), |
| 346 | }; | 447 | }; |
| 347 | } | 448 | } |
| 348 | 449 | ||
| 349 | auto start() -> void; | 450 | auto start() -> void; |
| 350 | auto read(std::string_view xml, bool is_final) -> void; | 451 | auto read(std::string_view xml, bool is_final) -> void; |
| 351 | auto end() -> void; | 452 | auto end() -> void; |
| 352 | }; | 453 | }; |
| 353 | 454 | ||
| 354 | template<class T> | 455 | template <class T> |
| 355 | class promise_returnable : public promise_base { | 456 | class promise_returnable : public promise_base |
| 356 | std::optional<T> returned_value_; | 457 | { |
| 458 | std::optional<T> returned_value_; | ||
| 357 | 459 | ||
| 358 | public: | 460 | public: |
| 359 | using result_type = T; | 461 | using result_type = T; |
| 360 | using promise_base::promise_base; | 462 | using promise_base::promise_base; |
| 361 | 463 | ||
| 362 | template<class U> | 464 | template <class U> |
| 363 | auto return_value(U&& v) -> void { | 465 | auto return_value(U&& v) -> void |
| 364 | returned_value_.emplace(std::forward<U>(v)); | 466 | { |
| 365 | } | 467 | returned_value_.emplace(std::forward<U>(v)); |
| 366 | auto returned_value() -> T&& { | 468 | } |
| 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 | 469 | ||
| 373 | template<> | 470 | auto returned_value() -> T&& |
| 374 | class promise_returnable<void> : public promise_base { | 471 | { |
| 375 | public: | 472 | if (!returned_value_) |
| 376 | using result_type = void; | 473 | throw std::runtime_error{"XML coroutine did not return"}; |
| 377 | using promise_base::promise_base; | 474 | return std::forward<T>(returned_value_.value()); |
| 475 | } | ||
| 476 | }; | ||
| 378 | 477 | ||
| 379 | auto return_void() -> void {} | 478 | template <> |
| 380 | }; | 479 | class promise_returnable<void> : public promise_base |
| 480 | { | ||
| 481 | public: | ||
| 482 | using result_type = void; | ||
| 483 | using promise_base::promise_base; | ||
| 381 | 484 | ||
| 382 | template<class T> | 485 | auto return_void() -> void {} |
| 383 | class promise : public promise_returnable<T> { | 486 | }; |
| 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 | 487 | ||
| 392 | auto handle() -> std::coroutine_handle<promise<T>> { | 488 | template <class T> |
| 393 | return {parser<T>::handle_type::from_promise(*this)}; | 489 | class promise : public promise_returnable<T> |
| 394 | } | 490 | { |
| 491 | public: | ||
| 492 | // Called with all the coroutine's arguments. | ||
| 493 | // Ignoring all but the first argument, which should be the executor. | ||
| 494 | template <class... Args> | ||
| 495 | explicit promise(executor_ref executor, Args&&...) | ||
| 496 | : promise_returnable<T>{executor} | ||
| 497 | { | ||
| 498 | } | ||
| 395 | 499 | ||
| 396 | auto get_return_object() -> parser<T> { | 500 | auto handle() -> std::coroutine_handle<promise<T>> |
| 397 | return parser<T>{handle()}; | 501 | { |
| 398 | } | 502 | return {parser<T>::handle_type::from_promise(*this)}; |
| 503 | } | ||
| 399 | 504 | ||
| 400 | auto initial_suspend() { | 505 | auto get_return_object() -> parser<T> { return parser<T>{handle()}; } |
| 401 | return std::suspend_always{}; | 506 | |
| 402 | } | 507 | auto initial_suspend() { return std::suspend_always{}; } |
| 403 | auto final_suspend() noexcept { | ||
| 404 | struct awaiter { | ||
| 405 | std::coroutine_handle<> h_; | ||
| 406 | 508 | ||
| 407 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } | 509 | auto final_suspend() noexcept |
| 408 | auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> { return h_; } | 510 | { |
| 409 | constexpr auto await_resume() const noexcept -> void { return; } | 511 | struct awaiter |
| 410 | }; | 512 | { |
| 411 | if (this->continuation()) { | 513 | std::coroutine_handle<> h_; |
| 412 | return awaiter{this->continuation()}; | 514 | |
| 413 | } else { | 515 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool |
| 414 | this->executor().set_continuation(nullptr); | 516 | { |
| 415 | return awaiter{std::noop_coroutine()}; | 517 | return false; |
| 416 | } | 518 | } |
| 519 | auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> | ||
| 520 | { | ||
| 521 | return h_; | ||
| 522 | } | ||
| 523 | constexpr auto await_resume() const noexcept -> void { return; } | ||
| 524 | }; | ||
| 525 | if (this->continuation()) | ||
| 526 | { | ||
| 527 | return awaiter{this->continuation()}; | ||
| 417 | } | 528 | } |
| 418 | 529 | else | |
| 419 | auto unhandled_exception() -> void { | 530 | { |
| 420 | this->executor().set_exception(std::current_exception()); | 531 | this->executor().set_continuation(nullptr); |
| 532 | return awaiter{std::noop_coroutine()}; | ||
| 421 | } | 533 | } |
| 534 | } | ||
| 422 | 535 | ||
| 423 | auto await_transform(current_event_t const& req) { | 536 | auto unhandled_exception() -> void |
| 424 | struct awaiter { | 537 | { |
| 425 | executor_ref executor_; | 538 | this->executor().set_exception(std::current_exception()); |
| 539 | } | ||
| 426 | 540 | ||
| 427 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { | 541 | auto await_transform(current_event_t const& req) |
| 428 | return !executor_->advance_flag(); | 542 | { |
| 429 | } | 543 | struct awaiter |
| 430 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> void { | 544 | { |
| 431 | executor_->set_continuation(h.promise().base_handle()); | 545 | executor_ref executor_; |
| 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 | 546 | ||
| 441 | template<class U> | 547 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool |
| 442 | auto await_transform(parser<U> const& coro) { | 548 | { |
| 443 | struct [[clang::coro_await_elidable]] awaiter { | 549 | return !executor_->advance_flag(); |
| 444 | util::not_null<promise<U>*> next_; | 550 | } |
| 551 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> void | ||
| 552 | { | ||
| 553 | executor_->set_continuation(h.promise().base_handle()); | ||
| 554 | } | ||
| 555 | [[nodiscard]] auto await_resume() const -> event | ||
| 556 | { | ||
| 557 | assert(executor_->event()); | ||
| 558 | return executor_->event().value(); | ||
| 559 | } | ||
| 560 | }; | ||
| 561 | return awaiter{req.executor}; | ||
| 562 | } | ||
| 563 | |||
| 564 | template <class U> | ||
| 565 | auto await_transform(parser<U> const& coro) | ||
| 566 | { | ||
| 567 | struct [[clang::coro_await_elidable]] awaiter | ||
| 568 | { | ||
| 569 | util::not_null<promise<U>*> next_; | ||
| 445 | 570 | ||
| 446 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } | 571 | [[nodiscard]] constexpr auto await_ready() const noexcept -> bool |
| 447 | auto await_suspend(std::coroutine_handle<promise<T>> h) -> std::coroutine_handle<> { | 572 | { |
| 448 | // Passed coroutine handle will be the same as parser<T>::handle_type::from_promise(*this) | 573 | return false; |
| 449 | next_->set_continuation(h.promise().base_handle()); | 574 | } |
| 450 | return next_->handle(); | 575 | auto await_suspend(std::coroutine_handle<promise<T>> h) |
| 576 | -> std::coroutine_handle<> | ||
| 577 | { | ||
| 578 | // Passed coroutine handle will be the same as | ||
| 579 | // parser<T>::handle_type::from_promise(*this) | ||
| 580 | next_->set_continuation(h.promise().base_handle()); | ||
| 581 | return next_->handle(); | ||
| 582 | } | ||
| 583 | auto await_resume() -> U | ||
| 584 | { | ||
| 585 | // Promise is still valid since coroutine frame is still alive | ||
| 586 | // (and suspended): control was transferred back to this | ||
| 587 | // coroutine via symmetric transfer in final_suspend(). Assuming | ||
| 588 | // that the destructor for coro still needs to run. | ||
| 589 | if (auto ex = next_->executor().take_exception()) | ||
| 590 | { | ||
| 591 | std::rethrow_exception(ex); | ||
| 451 | } | 592 | } |
| 452 | auto await_resume() -> U { | 593 | else |
| 453 | // Promise is still valid since coroutine frame is still alive (and suspended): | 594 | { |
| 454 | // control was transferred back to this coroutine via symmetric transfer in | 595 | if constexpr (!std::is_void_v<U>) |
| 455 | // final_suspend(). Assuming that the destructor for coro still needs to run. | 596 | { |
| 456 | if (auto ex = next_->executor().take_exception()) { | 597 | return std::move(next_->returned_value()); |
| 457 | std::rethrow_exception(ex); | ||
| 458 | } else { | ||
| 459 | if constexpr (!std::is_void_v<U>) { | ||
| 460 | return std::move(next_->returned_value()); | ||
| 461 | } | ||
| 462 | } | 598 | } |
| 463 | } | 599 | } |
| 464 | }; | 600 | } |
| 465 | return awaiter{util::not_null{&coro.promise()}}; | 601 | }; |
| 466 | } | 602 | return awaiter{util::not_null{&coro.promise()}}; |
| 467 | }; | 603 | } |
| 604 | }; | ||
| 468 | 605 | ||
| 469 | // Helpers for handling XML documents. Non-polymorphic functions | 606 | // Helpers for handling XML documents. Non-polymorphic functions |
| 470 | // should be marked inline to allow HALO across TU boundaries. | 607 | // should be marked inline to allow HALO across TU boundaries. |
| 471 | 608 | ||
| 472 | template<class T> | 609 | template <class T> |
| 473 | concept unconstrained = true; | 610 | concept unconstrained = true; |
| 474 | 611 | ||
| 475 | template<class T, template<class U> concept C> | 612 | template <class T, template <class U> concept C> |
| 476 | concept parser_of = requires { | 613 | concept parser_of = requires { |
| 477 | typename T::result_type; | 614 | typename T::result_type; |
| 478 | requires std::same_as<parser<typename T::result_type>, T>; | 615 | requires std::same_as<parser<typename T::result_type>, T>; |
| 479 | requires C<typename T::result_type>; | 616 | requires C<typename T::result_type>; |
| 480 | }; | 617 | }; |
| 481 | 618 | ||
| 482 | template<parser_of<unconstrained> T> | 619 | template <parser_of<unconstrained> T> |
| 483 | using parser_result_t = T::result_type; | 620 | using parser_result_t = T::result_type; |
| 484 | 621 | ||
| 485 | template<class T, class... Args> | 622 | template <class T, class... Args> |
| 486 | concept parser_invocable = std::invocable<T, Args...> && parser_of<std::invoke_result_t<T, Args...>, unconstrained>; | 623 | concept parser_invocable = |
| 624 | std::invocable<T, Args...> | ||
| 625 | && parser_of<std::invoke_result_t<T, Args...>, unconstrained>; | ||
| 487 | 626 | ||
| 488 | template<class Fn, class... Args> | 627 | template <class Fn, class... Args> |
| 489 | requires parser_invocable<Fn, Args...> | 628 | requires parser_invocable<Fn, Args...> |
| 490 | using parser_invoke_result_t = parser_result_t<std::invoke_result_t<Fn, Args...>>; | 629 | using parser_invoke_result_t = |
| 630 | parser_result_t<std::invoke_result_t<Fn, Args...>>; | ||
| 491 | 631 | ||
| 492 | template<event_type T> auto expect_event(executor_ref e) -> parser<T> { | 632 | template <event_type T> |
| 493 | auto ev = co_await current_event(e); | 633 | auto expect_event(executor_ref e) -> parser<T> |
| 494 | if (!std::holds_alternative<T>(ev)) { | 634 | { |
| 495 | auto pos = e->position(); | 635 | auto ev = co_await current_event(e); |
| 496 | throw std::runtime_error{std::format("at {}:{}: unexpected event type, have {}", pos.line, pos.col, ev.index())}; | 636 | if (!std::holds_alternative<T>(ev)) |
| 497 | } | 637 | { |
| 498 | e->set_advance_flag(); | 638 | auto pos = e->position(); |
| 499 | co_return std::get<T>(ev); | 639 | throw std::runtime_error{std::format( |
| 640 | "at {}:{}: unexpected event type, have {}", pos.line, pos.col, | ||
| 641 | ev.index())}; | ||
| 500 | } | 642 | } |
| 643 | e->set_advance_flag(); | ||
| 644 | co_return std::get<T>(ev); | ||
| 645 | } | ||
| 501 | 646 | ||
| 502 | inline auto expect_start_element(executor_ref e, qname_view want) -> parser<attribute_view> { | 647 | inline auto expect_start_element(executor_ref e, qname_view want) |
| 503 | auto ev = co_await expect_event<start_element_event>(e); | 648 | -> parser<attribute_view> |
| 504 | if (ev.name != want) { | 649 | { |
| 505 | auto pos = e->position(); | 650 | auto ev = co_await expect_event<start_element_event>(e); |
| 506 | throw std::runtime_error{std::format("at {}:{}: unexpected element started", pos.line, pos.col)}; | 651 | if (ev.name != want) |
| 507 | } | 652 | { |
| 508 | co_return ev.attrs; | 653 | auto pos = e->position(); |
| 654 | throw std::runtime_error{std::format( | ||
| 655 | "at {}:{}: unexpected element started", pos.line, pos.col)}; | ||
| 509 | } | 656 | } |
| 657 | co_return ev.attrs; | ||
| 658 | } | ||
| 510 | 659 | ||
| 511 | inline auto allow_start_element(executor_ref e, qname_view want) -> parser<std::optional<attribute_view>> { | 660 | inline auto allow_start_element(executor_ref e, qname_view want) |
| 512 | auto ev = co_await current_event(e); | 661 | -> parser<std::optional<attribute_view>> |
| 513 | if (auto const* pev = std::get_if<start_element_event>(&ev)) { | 662 | { |
| 514 | if (pev->name == want) { | 663 | auto ev = co_await current_event(e); |
| 515 | e->set_advance_flag(); | 664 | if (auto const* pev = std::get_if<start_element_event>(&ev)) |
| 516 | co_return pev->attrs; | 665 | { |
| 517 | } | 666 | if (pev->name == want) |
| 667 | { | ||
| 668 | e->set_advance_flag(); | ||
| 669 | co_return pev->attrs; | ||
| 518 | } | 670 | } |
| 519 | co_return std::nullopt; | ||
| 520 | } | 671 | } |
| 672 | co_return std::nullopt; | ||
| 673 | } | ||
| 521 | 674 | ||
| 522 | inline auto expect_end_element(executor_ref e, qname_view want) -> parser<void> { | 675 | inline auto expect_end_element(executor_ref e, qname_view want) -> parser<void> |
| 523 | auto ev = co_await expect_event<end_element_event>(e); | 676 | { |
| 524 | if (ev.name != want) { | 677 | auto ev = co_await expect_event<end_element_event>(e); |
| 525 | throw std::runtime_error{"unexpected element ended"}; | 678 | if (ev.name != want) |
| 526 | } | 679 | { |
| 680 | throw std::runtime_error{"unexpected element ended"}; | ||
| 527 | } | 681 | } |
| 682 | } | ||
| 528 | 683 | ||
| 529 | inline auto ignore_whitespace(executor_ref e) -> parser<void> { | 684 | inline auto ignore_whitespace(executor_ref e) -> parser<void> |
| 530 | auto all_whitespace = [](std::string_view s) -> bool { | 685 | { |
| 531 | for (auto c : s) | 686 | auto all_whitespace = [](std::string_view s) -> bool |
| 532 | if (c != ' ' && c != '\r' && c != '\n' && c != '\t') | 687 | { |
| 533 | return false; | 688 | for (auto c : s) |
| 534 | return true; | 689 | if (c != ' ' && c != '\r' && c != '\n' && c != '\t') |
| 535 | }; | 690 | return false; |
| 691 | return true; | ||
| 692 | }; | ||
| 536 | 693 | ||
| 537 | while (true) { | 694 | while (true) |
| 538 | auto ev = co_await current_event(e); | 695 | { |
| 539 | if (auto const* pev = std::get_if<character_data_event>(&ev); pev && all_whitespace(pev->data)) { | 696 | auto ev = co_await current_event(e); |
| 540 | e->set_advance_flag(); | 697 | if (auto const* pev = std::get_if<character_data_event>(&ev); |
| 541 | } else { | 698 | pev && all_whitespace(pev->data)) |
| 542 | co_return; | 699 | { |
| 543 | } | 700 | e->set_advance_flag(); |
| 701 | } | ||
| 702 | else | ||
| 703 | { | ||
| 704 | co_return; | ||
| 544 | } | 705 | } |
| 545 | } | 706 | } |
| 707 | } | ||
| 546 | 708 | ||
| 547 | auto expect_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p) | 709 | auto expect_element( |
| 710 | executor_ref e, qname_view want, | ||
| 711 | parser_invocable<executor_ref, attribute_view> auto p) | ||
| 548 | -> parser<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> | 712 | -> parser<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> |
| 713 | { | ||
| 714 | co_await ignore_whitespace(e); | ||
| 715 | auto attrs = co_await expect_start_element(e, want); | ||
| 716 | auto&& res = co_await p(e, attrs); | ||
| 717 | co_await expect_end_element(e, want); | ||
| 718 | co_await ignore_whitespace(e); | ||
| 719 | co_return std::forward< | ||
| 720 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res); | ||
| 721 | } | ||
| 722 | |||
| 723 | auto allow_element( | ||
| 724 | executor_ref e, qname_view want, | ||
| 725 | parser_invocable<executor_ref, attribute_view> auto p) | ||
| 726 | -> parser<std::optional< | ||
| 727 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>> | ||
| 728 | { | ||
| 729 | co_await ignore_whitespace(e); | ||
| 730 | if (auto mattrs = co_await allow_start_element(e, want)) | ||
| 549 | { | 731 | { |
| 550 | co_await ignore_whitespace(e); | 732 | auto&& res = co_await p(e, *mattrs); |
| 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); | 733 | co_await expect_end_element(e, want); |
| 554 | co_await ignore_whitespace(e); | 734 | co_await ignore_whitespace(e); |
| 555 | co_return std::forward<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>(res); | 735 | co_return std::make_optional( |
| 736 | std::forward< | ||
| 737 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>( | ||
| 738 | res)); | ||
| 556 | } | 739 | } |
| 740 | co_return std::nullopt; | ||
| 741 | } | ||
| 557 | 742 | ||
| 558 | auto allow_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p) | 743 | auto allow_element( |
| 559 | -> parser<std::optional<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>>> | 744 | executor_ref e, qname_view want, |
| 745 | parser_invocable<executor_ref, attribute_view> auto p) -> parser<bool> | ||
| 746 | requires std::is_void_v< | ||
| 747 | parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> | ||
| 748 | { | ||
| 749 | co_await ignore_whitespace(e); | ||
| 750 | if (auto mattrs = co_await allow_start_element(e, want)) | ||
| 560 | { | 751 | { |
| 752 | co_await p(e, *mattrs); | ||
| 753 | co_await expect_end_element(e, want); | ||
| 561 | co_await ignore_whitespace(e); | 754 | co_await ignore_whitespace(e); |
| 562 | if (auto mattrs = co_await allow_start_element(e, want)) { | 755 | co_return true; |
| 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 | } | 756 | } |
| 757 | co_return false; | ||
| 758 | } | ||
| 570 | 759 | ||
| 571 | auto allow_element(executor_ref e, qname_view want, parser_invocable<executor_ref, attribute_view> auto p) | 760 | inline auto |
| 572 | -> parser<bool> | 761 | ignore_contents(executor_ref e, std::optional<qname_view> muntil = std::nullopt) |
| 573 | requires std::is_void_v<parser_invoke_result_t<decltype(p), executor_ref, attribute_view>> | 762 | -> parser<void> |
| 763 | { | ||
| 764 | std::size_t depth = 0; | ||
| 765 | while (true) | ||
| 574 | { | 766 | { |
| 575 | co_await ignore_whitespace(e); | 767 | auto ev = co_await current_event(e); |
| 576 | if (auto mattrs = co_await allow_start_element(e, want)) { | 768 | if (auto* pev = std::get_if<start_element_event>(&ev)) |
| 577 | co_await p(e, *mattrs); | 769 | { |
| 578 | co_await expect_end_element(e, want); | 770 | if (depth == 0 && muntil && pev->name == *muntil) |
| 579 | co_await ignore_whitespace(e); | 771 | { |
| 580 | co_return true; | 772 | co_return; |
| 773 | } | ||
| 774 | else | ||
| 775 | { | ||
| 776 | depth++; | ||
| 777 | } | ||
| 581 | } | 778 | } |
| 582 | co_return false; | 779 | else if (std::holds_alternative<end_element_event>(ev)) |
| 583 | } | 780 | { |
| 584 | 781 | if (depth == 0) | |
| 585 | inline auto ignore_contents(executor_ref e, std::optional<qname_view> muntil = std::nullopt) -> parser<void> { | 782 | { |
| 586 | std::size_t depth = 0; | 783 | co_return; |
| 587 | while (true) { | 784 | } |
| 588 | auto ev = co_await current_event(e); | 785 | else |
| 589 | if (auto* pev = std::get_if<start_element_event>(&ev)) { | 786 | { |
| 590 | if (depth == 0 && muntil && pev->name == *muntil) { | 787 | depth--; |
| 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 | } | 788 | } |
| 602 | e->set_advance_flag(); | ||
| 603 | } | 789 | } |
| 790 | e->set_advance_flag(); | ||
| 604 | } | 791 | } |
| 605 | inline auto ignore_element_contents(executor_ref e, attribute_view) -> parser<void> { | 792 | } |
| 606 | co_await ignore_contents(e); | 793 | inline auto ignore_element_contents(executor_ref e, attribute_view) |
| 607 | } | 794 | -> parser<void> |
| 795 | { | ||
| 796 | co_await ignore_contents(e); | ||
| 797 | } | ||
| 608 | 798 | ||
| 609 | inline auto read_string(executor_ref e) -> parser<std::string> { | 799 | inline auto read_string(executor_ref e) -> parser<std::string> |
| 610 | std::string s; | 800 | { |
| 611 | while (true) { | 801 | std::string s; |
| 612 | auto ev = co_await current_event(e); | 802 | while (true) |
| 613 | if (auto* pev = std::get_if<character_data_event>(&ev)) { | 803 | { |
| 614 | e->set_advance_flag(); | 804 | auto ev = co_await current_event(e); |
| 615 | s += pev->data; | 805 | if (auto* pev = std::get_if<character_data_event>(&ev)) |
| 616 | } else { | 806 | { |
| 617 | co_return s; | 807 | e->set_advance_flag(); |
| 618 | } | 808 | s += pev->data; |
| 809 | } | ||
| 810 | else | ||
| 811 | { | ||
| 812 | co_return s; | ||
| 619 | } | 813 | } |
| 620 | } | 814 | } |
| 621 | inline auto read_string_contents(executor_ref e, attribute_view) -> parser<std::string> { | 815 | } |
| 622 | co_return co_await read_string(e); | 816 | inline auto read_string_contents(executor_ref e, attribute_view) |
| 623 | } | 817 | -> parser<std::string> |
| 818 | { | ||
| 819 | co_return co_await read_string(e); | ||
| 820 | } | ||
| 624 | 821 | ||
| 625 | template<auto f> | 822 | template <auto f> |
| 626 | auto hohalo() { | 823 | auto hohalo() |
| 627 | return []<class... Args>(Args&&... args) -> std::invoke_result_t<decltype(f), Args...> { | 824 | { |
| 628 | co_return co_await f(std::forward<Args>(args)...); | 825 | return []<class... Args>( |
| 629 | }; | 826 | Args&&... args) -> std::invoke_result_t<decltype(f), Args...> |
| 630 | } | 827 | { co_return co_await f(std::forward<Args>(args)...); }; |
| 828 | } | ||
| 631 | 829 | ||
| 632 | } // namespace routemon::xml | 830 | } // namespace routemon::xml |