summaryrefslogtreecommitdiffstats
path: root/server/src/config.cpp
diff options
context:
space:
mode:
authorRutger Broekhoff2026-08-28 18:03:05 +0200
committerRutger Broekhoff2026-08-28 18:03:05 +0200
commit973aec43ea54bbf95b64fbcb636403401d1ca60e (patch)
tree41b7911c420766a9b463245b9296f44c5bf35258 /server/src/config.cpp
downloadroutemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.tar.gz
routemon-973aec43ea54bbf95b64fbcb636403401d1ca60e.zip
Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14
Diffstat (limited to 'server/src/config.cpp')
-rw-r--r--server/src/config.cpp167
1 files changed, 167 insertions, 0 deletions
diff --git a/server/src/config.cpp b/server/src/config.cpp
new file mode 100644
index 0000000..3b17691
--- /dev/null
+++ b/server/src/config.cpp
@@ -0,0 +1,167 @@
1module;
2
3#include <boost/json.hpp>
4#include <boost/system/system_error.hpp>
5
6module routemon:config$impl;
7
8import std;
9import :config;
10import :log;
11import :util;
12
13namespace json = boost::json;
14
15namespace routemon::config {
16
17 class location {
18 std::optional<std::pair<std::string_view, util::not_null<location const*>>> next_;
19
20 auto append_to(std::string& s) const -> void {
21 if (next_) {
22 s += next_->first;
23 s += ".";
24 next_->second->append_to(s);
25 }
26 }
27
28 public:
29 location() = default;
30
31 explicit location(location const& next, std::string_view entry)
32 : next_{std::make_pair(entry, util::not_null{&next})}
33 {}
34
35 [[nodiscard]] auto to_string() const -> std::string {
36 if (!next_)
37 return "";
38 auto s = std::string{next_->first};
39 next_->second->append_to(s);
40 return s;
41 }
42
43 auto sub(std::string_view entry) const& -> location {
44 return location{*this, entry};
45 }
46 };
47
48 class object_reader {
49 location loc_;
50 json::object const& obj_;
51 std::unordered_set<std::string> visited_;
52
53 public:
54 object_reader(json::value const& jv, location loc)
55 try : loc_{std::move(loc)}, obj_{jv.as_object()}
56 {} catch (boost::system::system_error const&) {
57 throw std::runtime_error{std::format("expected an object at {}", loc.to_string())};
58 }
59
60 auto check_unused() const -> void {
61 for (auto const& kv : obj_) {
62 if (!visited_.contains(std::string{kv.key()})) {
63 throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())};
64 }
65 }
66 }
67
68 template<class T>
69 auto expect_at(std::string_view key) -> T {
70 visited_.emplace(key);
71 auto const loc = loc_.sub(key);
72 if (auto const jv = obj_.try_at(key)) {
73 try {
74 return json::value_to<T>(*jv, loc);
75 } catch (boost::system::system_error const& e) {
76 throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())};
77 }
78 } else {
79 throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())};
80 }
81 }
82 };
83
84 auto as_checked_object(json::value const& jv, location const& loc, std::invocable<object_reader&> auto const& f) -> decltype(f(std::declval<object_reader&>())) {
85 auto r = object_reader{jv, loc};
86 auto&& v = f(r);
87 r.check_unused();
88 return std::forward<decltype(f(r))>(v);
89 }
90
91 auto tag_invoke(json::value_to_tag<rwgps> const&, json::value const& jv, location const& loc) -> rwgps {
92 return as_checked_object(jv, loc, [](object_reader& r) -> rwgps {
93 return {
94 .api_key = r.expect_at<std::string>("api_key"),
95 .auth_token = r.expect_at<std::string>("auth_token"),
96 };
97 });
98 }
99
100 auto tag_invoke(json::value_to_tag<situations> const&, json::value const& jv, location const& loc) -> situations {
101 return as_checked_object(jv, loc, [](object_reader& r) -> situations {
102 return {
103 .datex2_filename = r.expect_at<std::string>("datex2_filename"),
104 };
105 });
106 }
107
108 auto tag_invoke(json::value_to_tag<database> const&, json::value const& jv, location const& loc) -> database {
109 return as_checked_object(jv, loc, [](object_reader& r) -> database {
110 return {
111 .sqlite3_filename = r.expect_at<std::string>("sqlite3_filename"),
112 };
113 });
114 }
115
116 auto tag_invoke(json::value_to_tag<http_server> const&, json::value const& jv, location const& loc) -> http_server {
117 return as_checked_object(jv, loc, [](object_reader& r) -> http_server {
118 return {
119 .lax_cors = r.expect_at<bool>("lax_cors"),
120 };
121 });
122 }
123
124 auto tag_invoke(json::value_to_tag<logger> const&, json::value const& jv, location const& loc) -> logger {
125 return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger {
126 auto const level_str = r.expect_at<std::string>("level");
127 auto level = log::level{};
128 if (level_str == "debug")
129 level = log::level::debug;
130 else if (level_str == "info")
131 level = log::level::info;
132 else if (level_str == "warn")
133 level = log::level::warn;
134 else if (level_str == "error")
135 level = log::level::error;
136 else
137 throw std::runtime_error{std::format("unable to parse log level {:?} at {}: expected one of {{debug, info, warn, error}}", level_str, obj_loc.sub("level").to_string())};
138 return logger{level};
139 });
140 }
141
142 auto json_value_to_app(json::value const& jv) -> app {
143 return as_checked_object(jv, location{}, [](object_reader& r) -> app {
144 return {
145 .rwgps = r.expect_at<rwgps>("rwgps"),
146 .situations = r.expect_at<situations>("situations"),
147 .database = r.expect_at<database>("database"),
148 .http_server = r.expect_at<http_server>("http_server"),
149 .logger = r.expect_at<logger>("logger"),
150 };
151 });
152 }
153
154 auto load_file(std::string const& filename) -> app {
155 auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode?
156 if (!f.is_open())
157 throw std::runtime_error{std::format("failed to open {}", filename)};
158 auto jv = json::value{};
159 try {
160 jv = json::parse(f);
161 } catch (boost::system::system_error const& e) {
162 throw std::runtime_error{std::format("failed to parse: {}", e.code().message())};
163 }
164 return json_value_to_app(jv);
165 }
166
167} // namespace routemon::config