From 973aec43ea54bbf95b64fbcb636403401d1ca60e Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 18:03:05 +0200 Subject: Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14 --- server/src/config.cpp | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 server/src/config.cpp (limited to 'server/src/config.cpp') 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 @@ +module; + +#include +#include + +module routemon:config$impl; + +import std; +import :config; +import :log; +import :util; + +namespace json = boost::json; + +namespace routemon::config { + + class location { + std::optional>> next_; + + auto append_to(std::string& s) const -> void { + if (next_) { + s += next_->first; + s += "."; + next_->second->append_to(s); + } + } + + public: + location() = default; + + explicit location(location const& next, std::string_view entry) + : next_{std::make_pair(entry, util::not_null{&next})} + {} + + [[nodiscard]] auto to_string() const -> std::string { + if (!next_) + return ""; + auto s = std::string{next_->first}; + next_->second->append_to(s); + return s; + } + + auto sub(std::string_view entry) const& -> location { + return location{*this, entry}; + } + }; + + class object_reader { + location loc_; + json::object const& obj_; + std::unordered_set visited_; + + public: + object_reader(json::value const& jv, location loc) + try : loc_{std::move(loc)}, obj_{jv.as_object()} + {} catch (boost::system::system_error const&) { + throw std::runtime_error{std::format("expected an object at {}", loc.to_string())}; + } + + auto check_unused() const -> void { + for (auto const& kv : obj_) { + if (!visited_.contains(std::string{kv.key()})) { + throw std::runtime_error{std::format("unexpected key {}", loc_.sub(kv.key()).to_string())}; + } + } + } + + template + auto expect_at(std::string_view key) -> T { + visited_.emplace(key); + auto const loc = loc_.sub(key); + if (auto const jv = obj_.try_at(key)) { + try { + return json::value_to(*jv, loc); + } catch (boost::system::system_error const& e) { + throw std::runtime_error{std::format("failed to read {}: {}", loc.to_string(), e.code().message())}; + } + } else { + throw std::runtime_error{std::format("did not find expected key {}", loc.to_string())}; + } + } + }; + + auto as_checked_object(json::value const& jv, location const& loc, std::invocable auto const& f) -> decltype(f(std::declval())) { + auto r = object_reader{jv, loc}; + auto&& v = f(r); + r.check_unused(); + return std::forward(v); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> rwgps { + return as_checked_object(jv, loc, [](object_reader& r) -> rwgps { + return { + .api_key = r.expect_at("api_key"), + .auth_token = r.expect_at("auth_token"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> situations { + return as_checked_object(jv, loc, [](object_reader& r) -> situations { + return { + .datex2_filename = r.expect_at("datex2_filename"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> database { + return as_checked_object(jv, loc, [](object_reader& r) -> database { + return { + .sqlite3_filename = r.expect_at("sqlite3_filename"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> http_server { + return as_checked_object(jv, loc, [](object_reader& r) -> http_server { + return { + .lax_cors = r.expect_at("lax_cors"), + }; + }); + } + + auto tag_invoke(json::value_to_tag const&, json::value const& jv, location const& loc) -> logger { + return as_checked_object(jv, loc, [obj_loc = loc](object_reader& r) -> logger { + auto const level_str = r.expect_at("level"); + auto level = log::level{}; + if (level_str == "debug") + level = log::level::debug; + else if (level_str == "info") + level = log::level::info; + else if (level_str == "warn") + level = log::level::warn; + else if (level_str == "error") + level = log::level::error; + else + 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())}; + return logger{level}; + }); + } + + auto json_value_to_app(json::value const& jv) -> app { + return as_checked_object(jv, location{}, [](object_reader& r) -> app { + return { + .rwgps = r.expect_at("rwgps"), + .situations = r.expect_at("situations"), + .database = r.expect_at("database"), + .http_server = r.expect_at("http_server"), + .logger = r.expect_at("logger"), + }; + }); + } + + auto load_file(std::string const& filename) -> app { + auto f = std::ifstream{filename}; // TODO: ensure that we are opening in binary mode? + if (!f.is_open()) + throw std::runtime_error{std::format("failed to open {}", filename)}; + auto jv = json::value{}; + try { + jv = json::parse(f); + } catch (boost::system::system_error const& e) { + throw std::runtime_error{std::format("failed to parse: {}", e.code().message())}; + } + return json_value_to_app(jv); + } + +} // namespace routemon::config -- cgit v1.3