aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.config
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-26 20:47:45 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-26 20:47:45 +0100
commit90aa957ca9b7c217af7569009d1675e0f3ff8e9b (patch)
treee6a61ca2b6928e6414372b9b1484ce80fa2fb0b3 /nihil.config
parent1db86c401df11423c945634d8b2a483e97afa878 (diff)
downloadnihil-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.txt1
-rw-r--r--nihil.config/error.ccm23
-rw-r--r--nihil.config/nihil.config.ccm1
-rw-r--r--nihil.config/option.cc70
-rw-r--r--nihil.config/option.ccm61
-rw-r--r--nihil.config/read.cc35
-rw-r--r--nihil.config/read.ccm6
-rw-r--r--nihil.config/store.cc92
-rw-r--r--nihil.config/store.ccm41
-rw-r--r--nihil.config/string.cc42
-rw-r--r--nihil.config/string.ccm34
-rw-r--r--nihil.config/tests/string.cc22
-rw-r--r--nihil.config/write.cc27
-rw-r--r--nihil.config/write.ccm6
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>;
};