aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.config
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.config')
-rw-r--r--nihil.config/CMakeLists.txt31
-rw-r--r--nihil.config/nihil.config.ccm13
-rw-r--r--nihil.config/option.cc97
-rw-r--r--nihil.config/option.ccm105
-rw-r--r--nihil.config/read.cc50
-rw-r--r--nihil.config/read.ccm22
-rw-r--r--nihil.config/store.cc95
-rw-r--r--nihil.config/store.ccm75
-rw-r--r--nihil.config/string.cc62
-rw-r--r--nihil.config/string.ccm56
-rw-r--r--nihil.config/tests/CMakeLists.txt15
-rw-r--r--nihil.config/tests/string.cc36
-rw-r--r--nihil.config/write.cc41
-rw-r--r--nihil.config/write.ccm22
14 files changed, 720 insertions, 0 deletions
diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt
new file mode 100644
index 0000000..8a52d3c
--- /dev/null
+++ b/nihil.config/CMakeLists.txt
@@ -0,0 +1,31 @@
+# This source code is released into the public domain.
+
+add_library(nihil.config STATIC)
+target_link_libraries(nihil.config PRIVATE
+ nihil.error
+ nihil.generator
+ nihil.posix
+ nihil.ucl
+)
+target_sources(nihil.config
+ PUBLIC FILE_SET modules TYPE CXX_MODULES FILES
+ nihil.config.ccm
+ read.ccm
+ store.ccm
+ write.ccm
+
+ option.ccm
+ string.ccm
+
+ PRIVATE
+ option.cc
+ read.cc
+ store.cc
+ string.cc
+ write.cc
+)
+
+if(NIHIL_TESTS)
+ add_subdirectory(tests)
+ enable_testing()
+endif()
diff --git a/nihil.config/nihil.config.ccm b/nihil.config/nihil.config.ccm
new file mode 100644
index 0000000..8957305
--- /dev/null
+++ b/nihil.config/nihil.config.ccm
@@ -0,0 +1,13 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+export module nihil.config;
+
+export import :option;
+export import :read;
+export import :store;
+export import :string;
+export import :write;
diff --git a/nihil.config/option.cc b/nihil.config/option.cc
new file mode 100644
index 0000000..886f4b6
--- /dev/null
+++ b/nihil.config/option.cc
@@ -0,0 +1,97 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <iostream>
+#include <string>
+
+module nihil.config;
+
+import nihil.error;
+import nihil.monad;
+import nihil.ucl;
+
+namespace nihil::config {
+
+//NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
+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
+{
+ return self.m_name;
+}
+
+auto option::description(this option const &self) noexcept
+ -> std::string_view
+{
+ return self.m_description;
+}
+
+auto option::is_default(this option const &self) noexcept
+ -> bool
+{
+ return self.m_is_default;
+}
+
+auto option::is_default(this option &self, bool b) -> void
+{
+ self.m_is_default = b;
+}
+
+auto option::string(this option const &self) -> std::string
+{
+ return self.get_string();
+}
+
+auto option::string(this option &self, std::string_view value)
+ -> std::expected<void, error>
+{
+ co_await self.set_string(value);
+ self.is_default(false);
+ co_return {};
+}
+
+auto option::ucl(this option const &self)
+ -> std::expected<nihil::ucl::object, error>
+{
+ return self.get_ucl();
+}
+
+auto option::ucl(this option &self, nihil::ucl::object const &value)
+ -> std::expected<void, error>
+{
+ co_await self.set_ucl(value);
+ self.is_default(false);
+ co_return {};
+}
+
+auto operator<<(std::ostream &strm, option const &opt)
+-> std::ostream &
+{
+ return strm << "<" << opt.name() << "=" << opt.string() << ">";
+}
+
+} // namespace nihil
diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm
new file mode 100644
index 0000000..4b95793
--- /dev/null
+++ b/nihil.config/option.ccm
@@ -0,0 +1,105 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <iosfwd>
+#include <string>
+
+export module nihil.config:option;
+
+import nihil.error;
+import nihil.ucl;
+
+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.
+ [[nodiscard]] auto name(this option const &) noexcept
+ -> std::string_view;
+
+ // Human-readable description of this option.
+ [[nodiscard]] auto description(this option const &) noexcept
+ -> std::string_view;
+
+ // If true, this option is set to its default value.
+ [[nodiscard]] auto is_default(this option const &) noexcept
+ -> bool;
+
+ /*
+ * 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.
+ */
+ [[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.
+ */
+ [[nodiscard]] auto ucl(this option &, ucl::object const &)
+ -> std::expected<void, error>;
+
+ // Not copyable or movable.
+ option(option const &) = delete;
+ auto operator=(option const &) -> option& = delete;
+
+protected:
+ option(std::string_view name, std::string_view description);
+
+ auto is_default(this option &, bool) -> void;
+
+ /*
+ * Get or set this option as a string.
+ */
+ [[nodiscard]] virtual auto get_string() const
+ -> std::string = 0;
+ [[nodiscard]] virtual auto set_string(std::string_view)
+ -> std::expected<void, error> = 0;
+
+ /*
+ * Get or set this option as a UCL object.
+ */
+ [[nodiscard]] virtual auto get_ucl() const
+ -> std::expected<ucl::object, error> = 0;
+ [[nodiscard]] virtual auto set_ucl(ucl::object const &)
+ -> std::expected<void, error> = 0;
+
+private:
+ std::string m_name;
+ std::string m_description;
+ bool m_is_default = true;
+};
+
+/*
+ * Make options printable. This is mostly useful for testing.
+ */
+export auto operator<<(std::ostream &strm, option const &opt) -> std::ostream &;
+
+} // namespace nihil
diff --git a/nihil.config/read.cc b/nihil.config/read.cc
new file mode 100644
index 0000000..48484fb
--- /dev/null
+++ b/nihil.config/read.cc
@@ -0,0 +1,50 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <iterator>
+#include <string>
+
+module nihil.config;
+
+import nihil.error;
+import nihil.monad;
+import nihil.posix;
+import nihil.ucl;
+
+namespace nihil::config {
+
+auto read_from(std::filesystem::path const &filename)
+ -> std::expected<void, error>
+{
+ // TODO: nihil.ucl should have a way to load UCL from a filename.
+
+ std::string config_text;
+ auto err = read_file(filename, std::back_inserter(config_text));
+ if (!err) {
+ // 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)
+ co_return {};
+ auto errstr = std::format("cannot read {}", filename.string());
+ co_return std::unexpected(error(errstr, err.error()));
+ }
+
+ // Parse the UCL.
+ 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);
+ }
+
+ co_return {};
+}
+
+} // namespace nihil::config
diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm
new file mode 100644
index 0000000..9cf28c9
--- /dev/null
+++ b/nihil.config/read.ccm
@@ -0,0 +1,22 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+
+export module nihil.config:read;
+
+import nihil.error;
+
+namespace nihil::config {
+
+/*
+ * Load the configuration from a file.
+ */
+export [[nodiscard]] auto read_from(std::filesystem::path const &filename)
+ -> std::expected<void, error>;
+
+} // namespace nihil::config
diff --git a/nihil.config/store.cc b/nihil.config/store.cc
new file mode 100644
index 0000000..0fb8cc0
--- /dev/null
+++ b/nihil.config/store.cc
@@ -0,0 +1,95 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <map>
+
+module nihil.config;
+
+import nihil.error;
+import nihil.generator;
+import nihil.monad;
+
+namespace nihil::config {
+
+store::store() = default;
+
+auto store::get() -> store &
+{
+ static auto instance = store();
+ return instance;
+}
+
+
+auto store::register_option(this store &self, option *object)
+ -> std::expected<void, error>
+{
+ auto [it, okay] = self.m_options.insert(
+ std::pair{object->name(), object});
+
+ 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)
+ -> std::expected<void, error>
+{
+ 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)
+ -> std::expected<option const *, error>
+{
+ if (auto it = self.m_options.find(name); it != self.m_options.end())
+ return it->second;
+
+ 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 &self) -> nihil::generator<option *>
+{
+ for (auto &&it : self.m_options)
+ co_yield it.second;
+}
+
+auto get_option(std::string_view option_name)
+ -> std::expected<option *, error>
+{
+ co_return co_await store::get().fetch(option_name);
+}
+
+} // namespace nihil::config
diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm
new file mode 100644
index 0000000..4d37ce0
--- /dev/null
+++ b/nihil.config/store.ccm
@@ -0,0 +1,75 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+/*
+ * The configuration store. There should only be one of these.
+ */
+
+#include <coroutine>
+#include <expected>
+#include <string>
+#include <map>
+
+export module nihil.config:store;
+
+import nihil.generator;
+import :option;
+
+namespace nihil::config {
+
+struct store final {
+ /*
+ * Get the global config store.
+ */
+ [[nodiscard]] static auto get() -> store &;
+
+ /*
+ * Register a new value with the config store.
+ */
+ [[nodiscard]] auto register_option(this store &, option *object)
+ -> std::expected<void, error>;
+
+ /*
+ * Remove a value from the config store.
+ */
+ [[nodiscard]] auto unregister_option(this store &, option *object)
+ -> std::expected<void, error>;
+
+ /*
+ * Fetch an existing value in the config store.
+ */
+ [[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.
+ */
+ [[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;
+ store(store &&) = delete;
+ store& operator=(store const &) = delete;
+ store& operator=(store &&) = delete;
+
+private:
+ store();
+
+ std::map<std::string_view, option *> m_options;
+};
+
+/*
+ * The public API.
+ */
+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
new file mode 100644
index 0000000..0ca4605
--- /dev/null
+++ b/nihil.config/string.cc
@@ -0,0 +1,62 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <format>
+#include <string>
+
+module nihil.config;
+
+import nihil.error;
+import nihil.monad;
+import nihil.ucl;
+
+namespace nihil::config {
+
+string::string(
+ std::string &storage,
+ std::string_view name,
+ std::string_view description) noexcept
+ : option(name, description)
+ , m_storage(storage)
+{
+}
+
+string::~string() = default;
+
+auto string::get_string() const -> std::string
+{
+ return m_storage;
+}
+
+auto string::set_string(std::string_view new_value)
+ -> std::expected<void, error>
+{
+ m_storage = new_value;
+ return {};
+}
+
+auto string::get_ucl() const -> std::expected<ucl::object, error>
+{
+ return ucl::string(m_storage);
+}
+
+auto string::set_ucl(ucl::object const &uclobj) -> std::expected<void, error>
+{
+ 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
new file mode 100644
index 0000000..668bbc0
--- /dev/null
+++ b/nihil.config/string.ccm
@@ -0,0 +1,56 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <format>
+#include <string>
+
+export module nihil.config:string;
+
+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,
+ std::string_view name,
+ std::string_view description) noexcept;
+
+ ~string();
+
+ /*
+ * 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 &m_storage;
+};
+
+} // namespace nihil::config
diff --git a/nihil.config/tests/CMakeLists.txt b/nihil.config/tests/CMakeLists.txt
new file mode 100644
index 0000000..1805f7f
--- /dev/null
+++ b/nihil.config/tests/CMakeLists.txt
@@ -0,0 +1,15 @@
+# This source code is released into the public domain.
+
+add_executable(nihil.config.test
+ string.cc
+)
+
+target_link_libraries(nihil.config.test PRIVATE
+ nihil.config
+ Catch2::Catch2WithMain)
+
+find_package(Catch2 REQUIRED)
+
+include(CTest)
+include(Catch)
+catch_discover_tests(nihil.config.test)
diff --git a/nihil.config/tests/string.cc b/nihil.config/tests/string.cc
new file mode 100644
index 0000000..aeb1ef8
--- /dev/null
+++ b/nihil.config/tests/string.cc
@@ -0,0 +1,36 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.config;
+
+TEST_CASE("nihil.config: string option", "[nihil][nihil.config]")
+{
+ std::string storage;
+
+ 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);
+
+ 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");
+ }
+
+ opt = nihil::config::get_option("test_option");
+ REQUIRE(!opt);
+}
diff --git a/nihil.config/write.cc b/nihil.config/write.cc
new file mode 100644
index 0000000..80125a8
--- /dev/null
+++ b/nihil.config/write.cc
@@ -0,0 +1,41 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <coroutine>
+#include <expected>
+#include <filesystem>
+#include <format>
+#include <utility>
+
+module nihil.config;
+
+import nihil.error;
+import nihil.monad;
+import nihil.posix;
+import nihil.ucl;
+
+namespace nihil::config {
+
+auto write_to(std::filesystem::path const &filename)
+ -> 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())
+ continue;
+
+ auto uobj = co_await option->ucl();
+ uclconfig.insert({option->name(), uobj});
+ }
+
+ auto ucl_text = std::format("{:c}", uclconfig);
+ co_await safe_write_file(filename, ucl_text);
+ co_return {};
+}
+
+};
diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm
new file mode 100644
index 0000000..564bb20
--- /dev/null
+++ b/nihil.config/write.ccm
@@ -0,0 +1,22 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <expected>
+#include <filesystem>
+
+export module nihil.config:write;
+
+import nihil.error;
+
+namespace nihil::config {
+
+/*
+ * Write all config values (except defaults) to disk.
+ */
+export [[nodiscard]] auto write_to(std::filesystem::path const &filename) ->
+ std::expected<void, error>;
+
+};