From 90aa957ca9b7c217af7569009d1675e0f3ff8e9b Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Thu, 26 Jun 2025 20:47:45 +0100 Subject: ucl, config: use monadic error handling more --- nihil.config/CMakeLists.txt | 1 - nihil.config/error.ccm | 23 ----------- nihil.config/nihil.config.ccm | 1 - nihil.config/option.cc | 70 ++++++++++++++++++++++---------- nihil.config/option.ccm | 61 +++++++++++++++++++++------- nihil.config/read.cc | 35 ++++++---------- nihil.config/read.ccm | 6 +-- nihil.config/store.cc | 92 ++++++++++++++++++++++--------------------- nihil.config/store.ccm | 41 +++++++++---------- nihil.config/string.cc | 42 +++++++++++--------- nihil.config/string.ccm | 34 +++++++++++++--- nihil.config/tests/string.cc | 22 ++++++----- nihil.config/write.cc | 27 +++++-------- nihil.config/write.ccm | 6 +-- 14 files changed, 251 insertions(+), 210 deletions(-) delete mode 100644 nihil.config/error.ccm (limited to 'nihil.config') diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt index 2aa0dae..fb2c1db 100644 --- a/nihil.config/CMakeLists.txt +++ b/nihil.config/CMakeLists.txt @@ -5,7 +5,6 @@ target_link_libraries(nihil.config PUBLIC nihil nihil.ucl) target_sources(nihil.config PUBLIC FILE_SET modules TYPE CXX_MODULES FILES nihil.config.ccm - error.ccm read.ccm store.ccm write.ccm diff --git a/nihil.config/error.ccm b/nihil.config/error.ccm deleted file mode 100644 index 4e7131a..0000000 --- a/nihil.config/error.ccm +++ /dev/null @@ -1,23 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil.config:error; - -import nihil; - -namespace nihil::config { - -/* - * Exception thrown when an issue occurs with the configuration. - */ -export struct error : generic_error { - error(std::string what) : generic_error(std::move(what)) {} -}; - -} // namespace nihil::config diff --git a/nihil.config/nihil.config.ccm b/nihil.config/nihil.config.ccm index d20c3fe..8957305 100644 --- a/nihil.config/nihil.config.ccm +++ b/nihil.config/nihil.config.ccm @@ -6,7 +6,6 @@ module; export module nihil.config; -export import :error; export import :option; export import :read; export import :store; diff --git a/nihil.config/option.cc b/nihil.config/option.cc index 9bf77c9..588a48f 100644 --- a/nihil.config/option.cc +++ b/nihil.config/option.cc @@ -4,58 +4,86 @@ module; +#include +#include #include #include module nihil.config; +import nihil; +import nihil.ucl; + namespace nihil::config { +option::option(std::string_view name, std::string_view description) + : m_name(name) + , m_description(description) +{ + auto okay = store::get().register_option(this); + if (okay) + return; + + std::print(std::cerr, + "INTERNAL ERROR: failed to register " + "configuration option '{}': {}", + m_name, okay.error()); + std::exit(1); +} + +option::~option() +{ + std::ignore = store::get().unregister_option(this); +} + auto option::name(this option const &self) noexcept --> std::string_view + -> std::string_view { - return self._name; + return self.m_name; } -// Human-readable description of this option. auto option::description(this option const &self) noexcept --> std::string_view + -> std::string_view { - return self._description; + return self.m_description; } -// If true, this option is set to its default value. auto option::is_default(this option const &self) noexcept --> bool + -> bool +{ + return self.m_is_default; +} + +auto option::is_default(this option &self, bool b) -> void { - return self._is_default; + self.m_is_default = b; } -// Get or set this option as a string. -auto option::string(this option const &self) --> std::string +auto option::string(this option const &self) -> std::string { return self.get_string(); } auto option::string(this option &self, std::string_view value) --> void + -> std::expected { - self.set_string(value); - self._is_default = false; + co_await self.set_string(value); + self.is_default(false); + co_return {}; } -option::option(std::string_view name, - std::string_view description) - : _name(name) - , _description(description) +auto option::ucl(this option const &self) + -> std::expected { + return self.get_ucl(); } -auto option::is_default(bool b) --> void +auto option::ucl(this option &self, nihil::ucl::object const &value) + -> std::expected { - _is_default = b; + co_await self.set_ucl(value); + self.is_default(false); + co_return {}; } auto operator<<(std::ostream &strm, option const &opt) diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm index c6a8329..c095d34 100644 --- a/nihil.config/option.ccm +++ b/nihil.config/option.ccm @@ -4,46 +4,67 @@ module; +#include #include #include export module nihil.config:option; +import nihil; import nihil.ucl; -import :error; namespace nihil::config { /* * Base class for options; this is what config_store interacts with. + * + * Base classes should override the four protected functions: + * + * get_string() + * set_string() + * get_ucl() + * set_ucl() + * + * Overriding any other members is not permitted. */ export struct option { + virtual ~option(); + // Short name of this option. - auto name(this option const &self) noexcept -> std::string_view; + [[nodiscard]] auto name(this option const &) noexcept + -> std::string_view; // Human-readable description of this option. - auto description(this option const &self) noexcept -> std::string_view; + [[nodiscard]] auto description(this option const &) noexcept + -> std::string_view; // If true, this option is set to its default value. - auto is_default(this option const &self) noexcept -> bool; + [[nodiscard]] auto is_default(this option const &) noexcept + -> bool; - // Get or set this option as a string. - auto string(this option const &self) -> std::string; - auto string(this option &self, std::string_view value) -> void; + /* + * Get or set this option as a string. The specific conversion + * method depends on the derived option type. + */ + [[nodiscard]] auto string(this option const &) -> std::string; + [[nodiscard]] auto string(this option &, std::string_view value) + -> std::expected; /* * Return this object as a UCL object. This is used when writing the * configuration file. */ - virtual auto to_ucl() const -> ucl::object = 0; + [[nodiscard]] auto ucl(this option const &) + -> std::expected; /* * Set this object from a UCL object. This is used when reading the * configuration file. */ - virtual auto from_ucl(ucl::object const &) -> void = 0; + [[nodiscard]] auto ucl(this option &, ucl::object const &) + -> std::expected; // Not copyable or movable. option(option const &) = delete; @@ -52,18 +73,28 @@ export struct option protected: option(std::string_view name, std::string_view description); - auto is_default(bool b) -> void; + auto is_default(this option &, bool) -> void; /* * Get or set this option as a string. */ - virtual auto get_string() const -> std::string = 0; - virtual auto set_string(std::string_view) -> void = 0; + virtual auto get_string() const + -> std::string = 0; + virtual auto set_string(std::string_view) + -> std::expected = 0; + + /* + * Get or set this option as a UCL object. + */ + virtual auto get_ucl() const + -> std::expected = 0; + virtual auto set_ucl(ucl::object const &) + -> std::expected = 0; private: - std::string _name; - std::string _description; - bool _is_default = true; + std::string m_name; + std::string m_description; + bool m_is_default = true; }; /* diff --git a/nihil.config/read.cc b/nihil.config/read.cc index 0a5fcad..e7def91 100644 --- a/nihil.config/read.cc +++ b/nihil.config/read.cc @@ -11,15 +11,15 @@ module; #include #include +module nihil.config; + import nihil; import nihil.ucl; -module nihil.config; - namespace nihil::config { auto read_from(std::filesystem::path const &filename) - -> std::expected + -> std::expected { // TODO: nihil.ucl should have a way to load UCL from a filename. @@ -29,33 +29,20 @@ auto read_from(std::filesystem::path const &filename) // Ignore ENOENT, it simply means we haven't created the // config file yet, so default values will be used. if (err.error().root_cause() == std::errc::no_such_file_or_directory) - return {}; + co_return {}; auto errstr = std::format("cannot read {}", filename.string()); - return std::unexpected(nihil::error(errstr, err.error())); + co_return std::unexpected(error(errstr, err.error())); } // Parse the UCL. - try { - auto uclconfig = ucl::parse(config_text); - - for (auto &&[key, value] : uclconfig) { - auto &opt = store::get().fetch(key); - opt.from_ucl(value); - } - } catch (unknown_option const &) { - // This is probably an old option which has been removed; - // ignore it, and we'll remove the bad option next time - // we write the config. - } catch (error const &err) { - // Include the filename in any other config errors. - throw error(std::format("{}: {}", filename.string(), - err.what())); - } catch (ucl::error const &err) { - throw error(std::format("{}: {}", filename.string(), - err.what())); + auto uclconfig = co_await ucl::parse(config_text); + + for (auto &&[key, value] : uclconfig) { + auto opt = co_await store::get().fetch(key); + co_await opt->ucl(value); } - return {}; + co_return {}; } } // namespace nihil::config diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm index 18f7213..fca78eb 100644 --- a/nihil.config/read.ccm +++ b/nihil.config/read.ccm @@ -7,16 +7,16 @@ module; #include #include -import nihil; - export module nihil.config:read; +import nihil; + namespace nihil::config { /* * Load the configuration from a file. */ export [[nodiscard]] auto read_from(std::filesystem::path const &filename) - -> std::expected; + -> std::expected; } // namespace nihil::config diff --git a/nihil.config/store.cc b/nihil.config/store.cc index 2ec8ade..6f93677 100644 --- a/nihil.config/store.cc +++ b/nihil.config/store.cc @@ -5,6 +5,7 @@ module; #include +#include #include #include #include @@ -15,75 +16,78 @@ import nihil; namespace nihil::config { -unknown_option::unknown_option(std::string_view option_name) - : error(std::format("unknown configuration variable '{}'", - option_name)) - , _option_name(option_name) -{ -} +store::store() = default; -auto unknown_option::option_name(this unknown_option const &self) --> std::string_view +auto store::get() -> store & { - return self._option_name; -} - -auto store::get() --> store& -{ - if (instance == nullptr) - instance = new store; - - return *instance; + static auto instance = store(); + return instance; } auto store::register_option(this store &self, option *object) --> void + -> std::expected { - auto [it, okay] = self.options.insert( + auto [it, okay] = self.m_options.insert( std::pair{object->name(), object}); - if (!okay) - throw error(std::format( - "INTERNAL ERROR: attempt to register " - "duplicate config value '{0}'", - object->name())); + if (okay) + return {}; + + return std::unexpected(error(std::format( + "attempt to register duplicate " + "configuration option '{0}'", + object->name()))); } auto store::unregister_option(this store &self, option *object) --> void + -> std::expected { - auto it = self.options.find(object->name()); - if (it == self.options.end()) - throw error(std::format( - "INTERNAL ERROR: attempt to unregister " - "non-existent config value '{}'", - object->name())); - - self.options.erase(it); + auto it = self.m_options.find(object->name()); + if (it == self.m_options.end()) + return std::unexpected(error(std::format( + "attempt to unregister non-existent " + "configuration option '{}'", + object->name()))); + + self.m_options.erase(it); + return {}; } auto store::fetch(this store const &self, std::string_view name) --> option & + -> std::expected