diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-26 20:47:45 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-26 20:47:45 +0100 |
| commit | 90aa957ca9b7c217af7569009d1675e0f3ff8e9b (patch) | |
| tree | e6a61ca2b6928e6414372b9b1484ce80fa2fb0b3 /nihil.config | |
| parent | 1db86c401df11423c945634d8b2a483e97afa878 (diff) | |
| download | nihil-90aa957ca9b7c217af7569009d1675e0f3ff8e9b.tar.gz nihil-90aa957ca9b7c217af7569009d1675e0f3ff8e9b.tar.bz2 | |
ucl, config: use monadic error handling more
Diffstat (limited to 'nihil.config')
| -rw-r--r-- | nihil.config/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | nihil.config/error.ccm | 23 | ||||
| -rw-r--r-- | nihil.config/nihil.config.ccm | 1 | ||||
| -rw-r--r-- | nihil.config/option.cc | 70 | ||||
| -rw-r--r-- | nihil.config/option.ccm | 61 | ||||
| -rw-r--r-- | nihil.config/read.cc | 35 | ||||
| -rw-r--r-- | nihil.config/read.ccm | 6 | ||||
| -rw-r--r-- | nihil.config/store.cc | 92 | ||||
| -rw-r--r-- | nihil.config/store.ccm | 41 | ||||
| -rw-r--r-- | nihil.config/string.cc | 42 | ||||
| -rw-r--r-- | nihil.config/string.ccm | 34 | ||||
| -rw-r--r-- | nihil.config/tests/string.cc | 22 | ||||
| -rw-r--r-- | nihil.config/write.cc | 27 | ||||
| -rw-r--r-- | nihil.config/write.ccm | 6 |
14 files changed, 251 insertions, 210 deletions
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 <format> -#include <utility> - -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 <coroutine> +#include <expected> #include <iostream> #include <string> 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<void, error> { - 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<nihil::ucl::object, error> { + return self.get_ucl(); } -auto option::is_default(bool b) --> void +auto option::ucl(this option &self, nihil::ucl::object const &value) + -> std::expected<void, error> { - _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 <expected> #include <iosfwd> #include <string> 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<void, error>; /* * 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<ucl::object, error>; /* * 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<void, error>; // 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<void, error> = 0; + + /* + * Get or set this option as a UCL object. + */ + virtual auto get_ucl() const + -> std::expected<ucl::object, error> = 0; + virtual auto set_ucl(ucl::object const &) + -> std::expected<void, error> = 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 <iterator> #include <string> +module nihil.config; + import nihil; import nihil.ucl; -module nihil.config; - namespace nihil::config { auto read_from(std::filesystem::path const &filename) - -> std::expected<void, nihil::error> + -> std::expected<void, error> { // 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 <expected> #include <filesystem> -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<void, nihil::error>; + -> std::expected<void, error>; } // 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 <coroutine> +#include <expected> #include <filesystem> #include <format> #include <map> @@ -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<void, error> { - 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<void, error> { - 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<option const *, error> { - if (auto it = self.options.find(name); it != self.options.end()) - return *it->second; + if (auto it = self.m_options.find(name); it != self.m_options.end()) + return it->second; - throw unknown_option(name); + return std::unexpected(error(std::format( + "unknown configuration option '{}'", + name))); +} + +auto store::fetch(this store &self, std::string_view name) + -> std::expected<option *, error> +{ + auto opt = co_await static_cast<store const &>(self).fetch(name); + co_return const_cast<option *>(opt); +} + +auto store::all(this store const &self) -> nihil::generator<option const *> +{ + for (auto &&it : self.m_options) + co_yield it.second; } -auto store::all(this store const &self) --> nihil::generator<option const &> +auto store::all(this store &self) -> nihil::generator<option *> { - for (auto &&it : self.options) - co_yield *it.second; + for (auto &&it : self.m_options) + co_yield it.second; } auto get_option(std::string_view option_name) --> option & + -> std::expected<option *, error> { - return store::get().fetch(option_name); + co_return co_await store::get().fetch(option_name); } } // namespace nihil::config diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm index 77b44b5..03d09d3 100644 --- a/nihil.config/store.ccm +++ b/nihil.config/store.ccm @@ -9,51 +9,50 @@ module; */ #include <coroutine> +#include <expected> #include <string> #include <map> export module nihil.config:store; import nihil; -import :error; import :option; namespace nihil::config { -// Exception thrown on an attempt to fetch an undefined option. -export struct unknown_option final : error { - unknown_option(std::string_view option_name); - auto option_name(this unknown_option const &self) -> std::string_view; - -private: - std::string _option_name; -}; - struct store final { /* * Get the global config store. */ - static auto get() -> store &; + [[nodiscard]] static auto get() -> store &; /* * Register a new value with the config store. */ - auto register_option(this store &self, option *object) -> void; + [[nodiscard]] auto register_option(this store &, option *object) + -> std::expected<void, error>; /* * Remove a value from the config store. */ - auto unregister_option(this store &self, option *object) -> void; + [[nodiscard]] auto unregister_option(this store &, option *object) + -> std::expected<void, error>; /* * Fetch an existing value in the config store. */ - auto fetch(this store const &self, std::string_view name) -> option &; + [[nodiscard]] auto fetch(this store const &, std::string_view name) + -> std::expected<option const *, error>; + [[nodiscard]] auto fetch(this store &, std::string_view name) + -> std::expected<option *, error>; /* * Fetch all values in the configuration store. */ - auto all(this store const &self) -> nihil::generator<option const &>; + [[nodiscard]] auto all(this store const &self) + -> nihil::generator<option const *>; + [[nodiscard]] auto all(this store &self) + -> nihil::generator<option *>; // Not movable or copyable. store(store const &) = delete; @@ -62,19 +61,15 @@ struct store final { store& operator=(store &&) = delete; private: - /* - * The global configuration store, created by init() and accessed via - * get(). - */ - inline static store *instance = nullptr; + store(); - std::map<std::string_view, option *> options; - store() = default; + std::map<std::string_view, option *> m_options; }; /* * The public API. */ -export auto get_option(std::string_view option_name) -> option &; +export auto get_option(std::string_view option_name) + -> std::expected<option *, error>; } // namespace nihil::config diff --git a/nihil.config/string.cc b/nihil.config/string.cc index 6b201ae..7d0c038 100644 --- a/nihil.config/string.cc +++ b/nihil.config/string.cc @@ -4,11 +4,14 @@ module; +#include <coroutine> +#include <expected> #include <format> #include <string> module nihil.config; +import nihil; import nihil.ucl; namespace nihil::config { @@ -18,40 +21,41 @@ string::string( std::string_view name, std::string_view description) noexcept : option(name, description) - , _storage(storage) + , m_storage(storage) { - store::get().register_option(this); } -string::~string() -{ - store::get().unregister_option(this); -} +string::~string() = default; auto string::get_string() const -> std::string { - return _storage; + return m_storage; } -auto string::set_string(std::string_view new_value) -> void +auto string::set_string(std::string_view new_value) + -> std::expected<void, error> { - _storage = new_value; + m_storage = new_value; + return {}; } -auto string::to_ucl() const -> ucl::object +auto string::get_ucl() const -> std::expected<ucl::object, error> { - return ucl::string(_storage); + return ucl::string(m_storage); } -auto string::from_ucl(ucl::object const &uclobj) -> void +auto string::set_ucl(ucl::object const &uclobj) -> std::expected<void, error> { - try { - _storage = object_cast<ucl::string>(uclobj).value(); - is_default(false); - } catch (ucl::type_mismatch const &exc) { - throw error(std::format("'{}': expected string, not {}", - name(), str(exc.actual_type()))); - } + auto obj = co_await object_cast<ucl::string>(uclobj) + .transform_error([&] (ucl::type_mismatch const &m) { + return error(std::format( + "'{}': expected string, not {}", + name(), str(m.actual_type()))); + }); + + m_storage = obj.value(); + is_default(false); + co_return {}; } } // namespace nihil::config diff --git a/nihil.config/string.ccm b/nihil.config/string.ccm index ae5efb9..b6a7e05 100644 --- a/nihil.config/string.ccm +++ b/nihil.config/string.ccm @@ -4,16 +4,21 @@ module; +#include <expected> #include <format> #include <string> export module nihil.config:string; +import nihil; import nihil.ucl; import :option; namespace nihil::config { +/* + * A string option. The backing type is std::string. + */ export struct string final : option { string(std::string &storage, @@ -22,14 +27,31 @@ export struct string final : option ~string(); - auto get_string() const -> std::string override; - auto set_string(std::string_view new_value) -> void override; - - auto to_ucl() const -> ucl::object override; - auto from_ucl(ucl::object const &uclobj) -> void override; + /* + * Get this option as a string; simply returns the storage. + */ + [[nodiscard]] auto get_string() const -> std::string override; + + /* + * Set this option to a string value; assigns to the storage. + */ + [[nodiscard]] auto set_string(std::string_view new_value) + -> std::expected<void, error> override; + + /* + * Convert this option to a UCL object. + */ + [[nodiscard]] auto get_ucl() const + -> std::expected<ucl::object, error> override; + + /* + * Set this option from a UCL object. + */ + [[nodiscard]] auto set_ucl(ucl::object const &uclobj) + -> std::expected<void, error> override; private: - std::string &_storage; + std::string &m_storage; }; } // namespace nihil::config diff --git a/nihil.config/tests/string.cc b/nihil.config/tests/string.cc index 7e95190..aeb1ef8 100644 --- a/nihil.config/tests/string.cc +++ b/nihil.config/tests/string.cc @@ -12,23 +12,25 @@ TEST_CASE("nihil.config: string option", "[nihil][nihil.config]") { std::string storage; - REQUIRE_THROWS_AS(nihil::config::get_option("test_option"), - nihil::config::unknown_option); + auto opt = nihil::config::get_option("test_option"); + REQUIRE(!opt); { auto string_option = nihil::config::string( storage, "test_option", "This is a test option"); - auto &opt = nihil::config::get_option("test_option"); - REQUIRE(opt.name() == "test_option"); - REQUIRE(opt.description() == "This is a test option"); - REQUIRE(opt.is_default() == true); - REQUIRE(opt.string() == ""); + auto opt = nihil::config::get_option("test_option"); + REQUIRE(opt); - opt.string("testing"); + REQUIRE((*opt)->name() == "test_option"); + REQUIRE((*opt)->description() == "This is a test option"); + REQUIRE((*opt)->is_default() == true); + REQUIRE((*opt)->string() == ""); + + REQUIRE((*opt)->string("testing")); REQUIRE(storage == "testing"); } - REQUIRE_THROWS_AS(nihil::config::get_option("test_option"), - nihil::config::unknown_option); + opt = nihil::config::get_option("test_option"); + REQUIRE(!opt); } diff --git a/nihil.config/write.cc b/nihil.config/write.cc index 2b451bd..8c02c43 100644 --- a/nihil.config/write.cc +++ b/nihil.config/write.cc @@ -4,43 +4,36 @@ module; +#include <coroutine> #include <expected> #include <filesystem> #include <format> #include <utility> +module nihil.config; + import nihil; import nihil.ucl; -module nihil.config; - namespace nihil::config { auto write_to(std::filesystem::path const &filename) - -> std::expected<void, nihil::error> -try { + -> std::expected<void, error> +{ auto uclconfig = ucl::map<ucl::object>(); // Add all the options to the UCL object. for (auto const &option : store::get().all()) { - if (option.is_default()) + if (option->is_default()) continue; - uclconfig.insert({option.name(), option.to_ucl()}); + auto uobj = co_await option->ucl(); + uclconfig.insert({option->name(), uobj}); } auto ucl_text = std::format("{:c}", uclconfig); - auto ret = safe_write_file(filename, ucl_text); - if (!ret) - return std::unexpected(nihil::error( - std::format("cannot write {}", filename.string()), - ret.error())); - - return {}; -} catch (ucl::error const &exc) { - return std::unexpected(nihil::error( - "failed to serialize configuration", - nihil::error(exc.what()))); + co_await safe_write_file(filename, ucl_text); + co_return {}; } }; diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm index 1a07dd7..cb35d76 100644 --- a/nihil.config/write.ccm +++ b/nihil.config/write.ccm @@ -7,16 +7,16 @@ module; #include <expected> #include <filesystem> -import nihil; - export module nihil.config:write; +import nihil; + export namespace nihil::config { /* * Write all config values (except defaults) to disk. */ auto write_to(std::filesystem::path const &filename) -> - std::expected<void, nihil::error>; + std::expected<void, error>; }; |
