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