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