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_) { next_->second->append_to(s); s += next_->first; 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_->second->append_to(s); s += next_->first; 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 allow_at(std::string_view key) -> std::optional { visited_.emplace(key); auto const loc = loc_.sub(key); if (auto const jv = obj_.try_at(key); jv && !jv->is_null()) { 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 { return std::nullopt; } } template auto expect_at(std::string_view key) -> T { if (auto&& mv = allow_at(key); mv) { return std::forward(*mv); } else { throw std::runtime_error{std::format( "did not find expected key {}", loc_.sub(key).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_filename = r.expect_at("api_key_filename"), .auth_token_filename = r.expect_at("auth_token_filename"), }; }); } 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.allow_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 { // TODO: migrate away from json::value_to w/ tag_invoke. // Boost.JSON does not properly propagate custom exceptions accross // json::value_to calls when certain containers are involved // (std::{optional,variant,vector}). auto f = std::ifstream{filename, std::ios::binary}; 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