summaryrefslogtreecommitdiffstats
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/api.cpp383
-rw-r--r--server/src/api.cppm116
-rw-r--r--server/src/config.cpp323
-rw-r--r--server/src/config.cppm54
-rw-r--r--server/src/database.cppm52
-rw-r--r--server/src/datex2.cppm540
-rw-r--r--server/src/geo.cppm59
-rw-r--r--server/src/gpx.cpp341
-rw-r--r--server/src/gpx.cppm57
-rw-r--r--server/src/http_client.cppm74
-rw-r--r--server/src/http_common.cppm228
-rw-r--r--server/src/http_server.cppm1249
-rw-r--r--server/src/locale.cppm424
-rw-r--r--server/src/log.cppm235
-rw-r--r--server/src/main.cpp101
-rw-r--r--server/src/problem.cppm85
-rw-r--r--server/src/req_ctx.cppm65
-rw-r--r--server/src/rwgps.cppm211
-rw-r--r--server/src/sqlite3.cppm454
-rw-r--r--server/src/srv.cppm370
-rw-r--r--server/src/time.cppm353
-rw-r--r--server/src/trace.cppm120
-rw-r--r--server/src/util.cppm428
-rw-r--r--server/src/xml.cpp94
-rw-r--r--server/src/xml.cppm1166
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
18namespace { 18namespace {
19 19
20 namespace chrono = std::chrono; 20namespace chrono = std::chrono;
21 namespace json = boost::json; 21namespace json = boost::json;
22 namespace views = std::views; 22namespace views = std::views;
23 23
24} // namespace <anonymous> 24} // namespace
25 25
26namespace routemon::api { 26namespace routemon::api {
27 27
28 auto json_value_from_point(geo::point const& p) -> json::value { 28auto 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 { 33auto 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
41auto 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
50auto 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{ 59auto 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{ 72auto 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{ 80auto 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{ 88auto 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) 98auto 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
107handler::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> { 136auto 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 { 270auto 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
16namespace { 16namespace {
17 17
18 namespace chrono = std::chrono; 18namespace chrono = std::chrono;
19 namespace json = boost::json; 19namespace json = boost::json;
20 namespace views = std::views; 20namespace views = std::views;
21 21
22} // namespace <anonymous> 22} // namespace
23 23
24export 24export namespace routemon::api {
25namespace routemon::api {
26 25
27 struct relevant_road_closure { 26struct 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 { 31struct 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 { 39struct 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 { 44struct 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 { 49struct 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 { 55struct 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 { 60auto 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>>; 63auto 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;
66auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg)
67 -> void;
68auto tag_invoke(json::value_from_tag, json::value& jv, track const& track)
69 -> void;
70auto tag_invoke(
71 json::value_from_tag, json::value& jv, process_gpx_result const& res)
72 -> void;
73auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info)
74 -> void;
66 75
67 log::logger l_; 76class 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>; 92public:
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
15namespace routemon::config { 15namespace routemon::config {
16 16
17 class location { 17class 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: 32public:
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 { 55class 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: 61public:
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 { 107auto 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 { 118auto 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 { 133auto 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 { 147auto 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 { 161auto 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 { 175auto 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 { 202auto 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
218auto 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
6namespace routemon::config { 6namespace routemon::config {
7 7
8 export struct rwgps { 8export 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 { 14export struct situations
14 std::string datex2_filename; 15{
15 }; 16 std::string datex2_filename;
17};
16 18
17 export struct database { 19export struct database
18 std::string sqlite3_filename; 20{
19 }; 21 std::string sqlite3_filename;
22};
20 23
21 export struct http_server { 24export struct http_server
22 bool lax_cors; 25{
23 }; 26 bool lax_cors;
27};
24 28
25 export struct logger { 29export struct logger
26 log::level level; 30{
27 }; 31 log::level level;
32};
28 33
29 export struct app { 34export 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; 43export 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
6namespace routemon::database { 6namespace routemon::database {
7 7
8 static constexpr std::int64_t expected_database_version = 1; 8static constexpr std::int64_t expected_database_version = 1;
9 9
10 export class connection { 10export 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: 18public:
18 // Nothing here yet 19 // Nothing here yet
19 }; 20};
20 21
21 export auto open(std::string const& filename) -> std::shared_ptr<connection> { 22export 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 @@
1module; 1module;
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
18namespace routemon::datex2 { 18namespace routemon::datex2 {
19 19
20 export struct situation; 20export 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 { 22export 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> { 30export 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 { 39export 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_; 45auto 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 { 53export 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: 273public:
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
9export namespace routemon::geo { 9export namespace routemon::geo {
10 10
11 using point = bgeo::model::point<double, 2, bgeo::cs::spherical_equatorial<bgeo::degree>>; 11using 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>; 13using linestring = bgeo::model::linestring<point>;
14 using stype = bgeo::srs::spheroid<double>; 14using box = bgeo::model::box<point>;
15 using vincenty_strategy = bgeo::strategy::distance::vincenty<stype>; 15using stype = bgeo::srs::spheroid<double>;
16using 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 { 18auto 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
14namespace routemon::gpx { 14namespace routemon::gpx {
15 15
16 namespace v10 { 16namespace v10 {
17 17
18 constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv; 18constexpr auto xmlns = "http://www.topografix.com/GPX/1/0"sv;
19 auto qname(std::string_view local) -> xml::qname_view { 19auto 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> { 24auto 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> { 50auto 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> { 60auto 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>()); 76auto 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; 103namespace 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> { 105constexpr auto xmlns = "http://www.topografix.com/GPX/1/1"sv;
90 auto meta = metadata{}; 106auto 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> { 111auto 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> { 123auto 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> { 149auto 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>())) 161auto 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); 177auto 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 { 203auto 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 { 220reader::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 { 225auto reader::init() -> void { e_.start(); }
182 e_.read(std::string_view{}, true); 226
183 e_.end(); 227auto 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(). 229auto 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
8namespace routemon::gpx { 8namespace routemon::gpx {
9 9
10 struct metadata { 10struct 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 { 16struct track_segment
16 geo::linestring waypoints; 17{
17 }; 18 geo::linestring waypoints;
19};
18 20
19 struct track { 21struct 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 { 28struct 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 { 35class reader
32 xml::executor e_; 36{
33 xml::parser<file> p_; 37 xml::executor e_;
38 xml::parser<file> p_;
34 39
35 public: 40public:
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
17namespace routemon::http { 17namespace routemon::http {
18 18
19 export class client { 19export 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: 25public:
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
11namespace boost::beast { 11namespace boost::beast {
12 12
13 namespace concepts { 13namespace concepts {
14 14
15 template<class T> 15template <class T>
16 concept buffers_generator = beast::is_buffers_generator<T>::value; 16concept buffers_generator = beast::is_buffers_generator<T>::value;
17 17
18 template<class T> 18template <class T>
19 concept const_buffer_sequence = beast::is_const_buffer_sequence<T>::value; 19concept const_buffer_sequence = beast::is_const_buffer_sequence<T>::value;
20 20
21 } // namespace concepts 21} // namespace concepts
22 22
23 namespace http::concepts { 23namespace http::concepts {
24 24
25 template<class T> 25template <class T>
26 concept fields = is_fields<T>::value; 26concept fields = is_fields<T>::value;
27 27
28 template<class T> 28template <class T>
29 concept body = is_body<T>::value; 29concept body = is_body<T>::value;
30 30
31 template<class T> 31template <class T>
32 concept body_reader = is_body_reader<T>::value; 32concept 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
38namespace routemon::http { 38namespace routemon::http {
39 39
40 struct supported_verb { 40struct 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 { 97export 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 @@
1module; 1module;
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
24namespace routemon::http { 24namespace routemon::http {
25 25
26 struct readable_request { 26struct 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 { 33class presponse
33 public: 34{
34 using const_buffers_type = beast::span<net::const_buffer>; 35public:
36 using const_buffers_type = beast::span<net::const_buffer>;
35 37
36 private: 38private:
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> 133public:
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};
160static_assert(beast::concepts::buffers_generator<presponse>);
176 161
177 struct keep_alive { 162template <class Ctx>
178 bool value; 163using next_handler_t = std::function<auto(Ctx)->net::awaitable<presponse>>;
179 164
180 explicit keep_alive(bool value) : value{value} {} 165template <class OuterCtx, class InnerCtx>
181 }; 166using 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> 172template <class Ctx>
184 auto make_rsp(bhttp::status status, keep_alive ka) -> bhttp::response<Body> { 173auto 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 { 183template <class InnerCtx>
192 auto rsp = make_rsp<bhttp::string_body>(problem.status, ka); 184struct 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 { 189template <class OuterCtx>
200 verb_set allow_methods; 190auto 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 204struct 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; 209template <class Ctx>
221 auto p = bhttp::request_parser<Body>{std::move(*r.p)}; 210using 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
214struct 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) { 221template <bhttp::concepts::body Body>
230 auto tpl = problem::tpl{ 222auto 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
230auto 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
241struct preflight_response
242{
243 verb_set allow_methods;
244 std::vector<bhttp::field> allow_headers;
245};
246auto 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).
267template <bhttp::concepts::body_reader Body>
268auto 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
277template <>
278auto 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> 301template <class InnerCtx>
243 struct routed_ctx : InnerCtx { 302struct routed_ctx : InnerCtx
244 verb_set route_methods; 303{
245 }; 304 verb_set route_methods;
305};
246 306
247 template<class Ctx> 307template <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> { 312auto 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> 342auto 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> 357template <class Ctx>
290 auto middleware_compose(middleware_t<A, B> ab, middleware_t<B, C> bc) -> middleware_t<A, C> { 358auto 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> 365template <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> { 366auto 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
379template <class A, class B>
380auto 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> 395template <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 { 399struct 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> 464template <class Ctx>
368 struct route_tree { 465struct 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> 479template <class OuterCtx, class InnerCtx>
382 auto middleware_wrap_tree(middleware_t<OuterCtx, InnerCtx> mw, route_tree<InnerCtx> const& tree) -> route_tree<OuterCtx> { 480auto 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> 507template <class T>
399 concept match_arg = std::constructible_from<T, std::string const&>; 508concept match_arg = std::constructible_from<T, std::string const&>;
400 509
401 template<class Ctx, match_arg... MatchArgs> 510template <class Ctx, match_arg... MatchArgs>
402 using route_handler_fn_t = std::function<auto(Ctx, readable_request, MatchArgs...) -> net::awaitable<presponse>>; 511using route_handler_fn_t = std::function<
512 auto(Ctx, readable_request, MatchArgs...)->net::awaitable<presponse>>;
403 513
404 template<class Ctx, match_arg... MatchArgs> 514template <class Ctx, match_arg... MatchArgs>
405 auto degen_route_handler(route_handler_fn_t<Ctx, MatchArgs...> fn) -> basic_route_handler_fn_t<Ctx> { 515auto 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> 532template <class Ctx, match_arg... MatchArgs>
423 struct dtree : handler_map<route_handler_fn_t<Ctx, MatchArgs...>> { 533struct 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...> { 543template <class Ctx, match_arg... MatchArgs>
432 auto sub = typename route_tree<Ctx>::named_subtrees{ 544struct 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: 591template <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) 592class 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 { 599public:
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;
11export namespace blocale = boost::locale; 11export namespace blocale = boost::locale;
12 12
13export namespace routemon { 13export namespace routemon {
14 using lformat = blocale::format; 14using lformat = blocale::format;
15 using blocale::translate; 15using blocale::gettext;
16 using blocale::gettext; 16using blocale::translate;
17} // namespace routemon 17} // namespace routemon
18 18
19namespace routemon::locale { 19namespace routemon::locale {
20 20
21 struct locale_priority { 21struct 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 { 27auto 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 { 34struct 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>; 52using 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>;
54using 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 { 56auto 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 { 62class 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: 67public:
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> 81export template <class T>
72 concept locale_input_range = 82concept 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 { 89export 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: 195public:
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
266export 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)}; 280constexpr 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"}; 284constexpr 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> { 287export 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
13namespace routemon::log { 13namespace routemon::log {
14 14
15 export enum class level : std::uint8_t { 15export 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 { 22namespace {
23 23
24 auto operator<<(std::ostream& os, level lvl) -> std::ostream& { 24auto 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 { 46export 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: 78public:
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> { 84export 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 { 89export 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: 109public:
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
4import std; 4import std;
5import routemon; 5import routemon;
@@ -7,17 +7,23 @@ import routemon;
7namespace chrono = std::chrono; 7namespace chrono = std::chrono;
8namespace net = boost::asio; 8namespace net = boost::asio;
9 9
10enum class exit_status { 10enum class exit_status
11{
11 failure, 12 failure,
12 bad_usage, 13 bad_usage,
13}; 14};
14 15
15auto real_main(std::span<char const*> args) -> exit_status { 16auto 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
97auto main(int argc, char* argv[]) -> int { 124auto 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
15namespace routemon::problem { 15namespace routemon::problem {
16 16
17 export struct details { 17export 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 { 43export 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 { 59export 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
11namespace routemon { 11namespace routemon {
12 12
13 export class req_ctx { 13export 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: 21public:
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
18namespace routemon::rwgps { 18namespace routemon::rwgps {
19 19
20 export struct route_summary { 20export 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 { 29struct 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 { 37struct get_routes_meta
36 pagination pagination; 38{
37 }; 39 pagination pagination;
40};
38 41
39 struct get_routes_response { 42struct 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 { 48auto 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 { 60auto 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 { 72auto 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 { 81auto 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 { 91auto 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 { 97export 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: 128public:
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
10namespace routemon::sqlite3 { 10namespace routemon::sqlite3 {
11 11
12 class mutex_guard { 12class 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: 23public:
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: 27private:
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&>())) { 31auto 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&>())) { 37auto 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 { 43class error : public std::exception
38 int code_; 44{
39 std::string message_; 45 int code_;
46 std::string message_;
40 47
41 public: 48public:
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> 64template <class T, template <class U> concept C>
60 concept optional_of = requires { 65concept 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> 71template <class T>
67 concept scannable_prim = 72concept 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> 75template <class T>
73 concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>; 76concept scannable = scannable_prim<T> || optional_of<T, scannable_prim>;
74 77
75 class statement { 78class statement
76 ::sqlite3_stmt* stmt_; 79{
80 ::sqlite3_stmt* stmt_;
77 81
78 public: 82public:
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 { 94class 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 { 141public:
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 { 183class 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: 191public:
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 { 213export 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: 224public:
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 { 288export 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 @@
1module; 1module;
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
33namespace routemon::srv { 33namespace routemon::srv {
34 34
35 class gpx_parse_error_category_impl : public std::error_category { 35class gpx_parse_error_category_impl : public std::error_category
36 public: 36{
37 char const* name() const noexcept override { return "gpx_parse"; } 37public:
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};
45auto 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}
50auto 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 { 55class 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: 59public:
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 { 80struct 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>); 158static_assert(bhttp::concepts::body<readable_gpx_body>);
159static_assert(bhttp::concepts::body_reader<readable_gpx_body>);
130 160
131 class handler { 161class handler
132 api::handler inner_; 162{
163 api::handler inner_;
133 164
134 public: 165public:
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: 169private:
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>> { 230public:
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> { 254export 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 { 269public:
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
5export namespace routemon::time { 5export namespace routemon::time {
6 6
7 using timestamp = std::chrono::time_point<std::chrono::utc_clock>; 7using timestamp = std::chrono::time_point<std::chrono::utc_clock>;
8 8
9 class period { 9class 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: 16public:
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 { 52class 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: 134public:
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& { 236auto 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()) { 241auto 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
12namespace routemon::trace { 12namespace routemon::trace {
13 13
14 class uuid7 { 14class 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: 19public:
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 { 76export class id
67 std::array<char, 37> chars_; 77{
78 std::array<char, 37> chars_;
68 79
69 public: 80public:
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
7namespace routemon::util { 7namespace 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> 10template <class... Ts>
11 struct overloaded : Ts... { 11struct 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> { 16export 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> { 33export 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> 50export template <class T>
36 class aolist : public std::enable_shared_from_this<aolist<T>> { 51class 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: 61public:
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> { 75export 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
85export 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
94export class zstring_view
95{
96 char const* s_;
97 std::size_t length_;
98
99public:
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.
126export 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 { 134public:
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 { 205constexpr 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>> { 211auto 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> 216export 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> 224export template <class T>
199 class not_null<T*> { 225class not_null;
200 T* p_;
201 226
202 struct guaranteed_not_null_t {}; 227export template <class T>
203 explicit not_null(T* p, guaranteed_not_null_t) noexcept : p_{p} {} 228class 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; 237public:
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_; 267export template <class T>
268explicit not_null(T*) -> not_null<T*>;
238 269
239 public: 270export template <>
240 explicit not_null(lazy_zstring_view s) 271class 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 { 275public:
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};
288export 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
10namespace routemon::xml { 10namespace routemon::xml {
11 11
12 auto qname_view::operator==(qname_view const& rhs) const -> bool { 12auto 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() : 17executor::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() { 29executor::~executor() { XML_ParserFree(p_); }
29 XML_ParserFree(p_);
30 }
31 30
32 auto executor::start() -> void { 31auto 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 { 38auto 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 { 61auto 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
24namespace routemon::xml { 24namespace routemon::xml {
25 25
26 struct qname_view { 26struct 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 { 34namespace detail {
34 35
35 constexpr auto qname_sep = '\xFF'; 36constexpr auto qname_sep = '\xFF';
36 auto split_name(char const* name) noexcept -> qname_view { 37auto 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 { 48class 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: 55public:
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};
99static_assert(std::input_iterator<attribute_view_iterator>);
89 100
90 class attribute_view : std::ranges::view_base { 101class 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: 106public:
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};
137static_assert(std::ranges::input_range<attribute_view>);
117 138
118 template<class T> class promise; 139template <class T>
140class promise;
119 141
120 template<class T> 142template <class T>
121 struct [[clang::coro_await_elidable, clang::coro_return_type]] parser { 143struct [[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: 149private:
127 handle_type h_; 150 handle_type h_;
128 151
129 public: 152public:
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 { 169struct 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; 175struct end_element_event
180 using executor_ref = util::not_null<executor*>; 176{
177 qname_view name;
178};
181 179
182 struct current_event_t { 180struct 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 { 185struct 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: 191struct 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& { 198struct eof_event
201 return *executor_; 199{
202 } 200};
203 201
204 inline auto base_handle() -> std::coroutine_handle<promise_base> { 202using 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> { 206template <class T>
209 return continuation_; 207concept 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 { 209class executor;
217 std::size_t line; 210using executor_ref = util::not_null<executor*>;
218 std::size_t col;
219 };
220 211
221 class executor { 212struct 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 { 217auto current_event(executor_ref executor) -> current_event_t
231 assert(!ex_); 218{
219 return current_event_t{executor};
220}
232 221
233 try { 222class 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 } 227public:
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
250struct position
251{
252 std::size_t line;
253 std::size_t col;
254};
255
256class 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(); 408public:
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> 455template <class T>
355 class promise_returnable : public promise_base { 456class promise_returnable : public promise_base
356 std::optional<T> returned_value_; 457{
458 std::optional<T> returned_value_;
357 459
358 public: 460public:
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 {} 478template <>
380 }; 479class promise_returnable<void> : public promise_base
480{
481public:
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>> { 488template <class T>
393 return {parser<T>::handle_type::from_promise(*this)}; 489class promise : public promise_returnable<T>
394 } 490{
491public:
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> 609template <class T>
473 concept unconstrained = true; 610concept unconstrained = true;
474 611
475 template<class T, template<class U> concept C> 612template <class T, template <class U> concept C>
476 concept parser_of = requires { 613concept 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> 619template <parser_of<unconstrained> T>
483 using parser_result_t = T::result_type; 620using parser_result_t = T::result_type;
484 621
485 template<class T, class... Args> 622template <class T, class... Args>
486 concept parser_invocable = std::invocable<T, Args...> && parser_of<std::invoke_result_t<T, Args...>, unconstrained>; 623concept 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> 627template <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...>>; 629using 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> { 632template <event_type T>
493 auto ev = co_await current_event(e); 633auto 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> { 647inline 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>> { 660inline 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> { 675inline 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> { 684inline 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) 709auto 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
723auto 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) 743auto 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) 760inline auto
572 -> parser<bool> 761ignore_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); 793inline 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> { 799inline 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); 816inline 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> 822template <auto f>
626 auto hohalo() { 823auto 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