aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.config
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-23 16:28:11 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-23 16:28:11 +0100
commitd5963532328ce5f1c9f266bf7e760b7d18a60c15 (patch)
tree28e8d4b98f2f3adbd2f02bcc656ad74e626677c9 /nihil.config
parent0fa623093366351ad47583f47add6e51f56a56d8 (diff)
downloadnihil-d5963532328ce5f1c9f266bf7e760b7d18a60c15.tar.gz
nihil-d5963532328ce5f1c9f266bf7e760b7d18a60c15.tar.bz2
various updates
Diffstat (limited to 'nihil.config')
-rw-r--r--nihil.config/CMakeLists.txt14
-rw-r--r--nihil.config/error.ccm5
-rw-r--r--nihil.config/nihil.config.ccm1
-rw-r--r--nihil.config/option.ccm26
-rw-r--r--nihil.config/read.ccm63
-rw-r--r--nihil.config/store.ccm137
-rw-r--r--nihil.config/string.ccm31
-rw-r--r--nihil.config/tests/CMakeLists.txt15
-rw-r--r--nihil.config/tests/string.cc34
-rw-r--r--nihil.config/write.ccm42
10 files changed, 253 insertions, 115 deletions
diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt
index d96b116..ed2bba3 100644
--- a/nihil.config/CMakeLists.txt
+++ b/nihil.config/CMakeLists.txt
@@ -1,11 +1,21 @@
# This source code is released into the public domain.
add_library(nihil.config STATIC)
+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
+
option.ccm
- string.ccm)
-target_link_libraries(nihil.config PUBLIC nihil nihil.ucl)
+ string.ccm
+)
+
+if(NIHIL_TESTS)
+ add_subdirectory(tests)
+ enable_testing()
+endif()
diff --git a/nihil.config/error.ccm b/nihil.config/error.ccm
index 0da91cb..4e7131a 100644
--- a/nihil.config/error.ccm
+++ b/nihil.config/error.ccm
@@ -17,10 +17,7 @@ namespace nihil::config {
* Exception thrown when an issue occurs with the configuration.
*/
export struct error : generic_error {
- template<typename... Args>
- error(std::format_string<Args...> fmt, Args &&...args)
- : generic_error(fmt, std::forward<Args>(args)...)
- {}
+ 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 0b12885..4ada81c 100644
--- a/nihil.config/nihil.config.ccm
+++ b/nihil.config/nihil.config.ccm
@@ -10,3 +10,4 @@ export import :error;
export import :option;
export import :store;
export import :string;
+export import :write;
diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm
index 207eb65..1be542e 100644
--- a/nihil.config/option.ccm
+++ b/nihil.config/option.ccm
@@ -4,13 +4,14 @@
module;
+#include <iostream>
#include <string>
#include <ucl++.h>
export module nihil.config:option;
-import nihil;
+import nihil.ucl;
import :error;
namespace nihil::config {
@@ -52,10 +53,16 @@ export struct option
}
/*
- * Add this option to a UCL object. This is used when writing the
+ * Return this object as a UCL object. This is used when writing the
* configuration file.
*/
- virtual void add_to_ucl(ucl_object_t *) const = 0;
+ virtual auto to_ucl() const -> ucl::object = 0;
+
+ /*
+ * Set this object from a UCL object. This is used when reading the
+ * configuration file.
+ */
+ virtual auto from_ucl(ucl::object const &) -> void = 0;
// Not copyable or movable.
option(option const &) = delete;
@@ -69,6 +76,11 @@ protected:
{
}
+ auto is_default(bool b) -> void
+ {
+ _is_default = b;
+ }
+
/*
* Get or set this option as a string.
*/
@@ -81,4 +93,12 @@ private:
bool _is_default = true;
};
+/*
+ * Make options printable. This is mostly useful for testing.
+ */
+export auto operator<<(std::ostream &strm, option const &opt) -> std::ostream &
+{
+ return strm << "<" << opt.name() << "=" << opt.string() << ">";
+}
+
} // namespace nihil
diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm
new file mode 100644
index 0000000..8d6c202
--- /dev/null
+++ b/nihil.config/read.ccm
@@ -0,0 +1,63 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <filesystem>
+#include <format>
+#include <iterator>
+#include <string>
+
+export module nihil.config:read;
+
+import nihil;
+import nihil.ucl;
+
+import :error;
+import :store;
+
+namespace nihil::config {
+
+/*
+ * Load the configuration from a file. Throws config::error on failure.
+ */
+export auto read_from(std::filesystem::path const &filename) -> void
+{
+ // 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() == std::errc::no_such_file_or_directory)
+ return;
+ throw error(std::format("{}: {}",
+ filename.string(),
+ err.error().message()));
+ }
+
+ // 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()));
+ }
+}
+
+} // namespace nihil::config
diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm
index 7ed4ccb..e0eebc0 100644
--- a/nihil.config/store.ccm
+++ b/nihil.config/store.ccm
@@ -9,12 +9,12 @@ module;
*/
#include <coroutine>
+#include <filesystem>
#include <format>
#include <map>
export module nihil.config:store;
-import nihil;
import :error;
import :option;
@@ -22,15 +22,22 @@ namespace nihil::config {
// Exception thrown on an attempt to fetch an undefined option.
export struct unknown_option final : error {
- std::string varname;
-
- unknown_option(std::string_view varname_)
- : error("unknown configuration variable '{}'", varname_)
- , varname(varname_)
+ unknown_option(std::string_view option_name)
+ : error(std::format("unknown configuration variable '{}'",
+ option_name))
+ , _option_name(option_name)
{}
+
+ auto option_name(this unknown_option const &self) -> std::string_view
+ {
+ return self._option_name;
+ }
+
+private:
+ std::string _option_name;
};
-export struct store final {
+struct store final {
/*
* Get the global config store.
*/
@@ -41,52 +48,6 @@ export struct store final {
return *instance;
}
- /*
- * Initialise the global config store.
- */
-#if 0
- void init(context const &ctx) {
- std::string config_text;
-
- // Load the configuration text.
- auto config_path = ctx.dbdir / "config.ucl";
- try {
- read_file(config_path, std::back_inserter(config_text));
- } catch (io_error const &exc) {
- // Ignore ENOENT, it simply means we haven't created the
- // config file yet, so default values will be used.
- if (exc.error == std::errc::no_such_file_or_directory)
- return;
- throw;
- }
-
- // Parse the UCL.
-
- std::string err;
- auto uclconfig = ucl::Ucl::parse(config_text, err);
-
- if (!uclconfig)
- throw error("{0}: {1}", config_path, err);
-
- auto const &cfg = get();
- for (auto const &uclvalue : uclconfig) {
- auto &value = cfg.fetch(uclvalue.key());
-
- switch (uclvalue.type()) {
- case UCL_INT:
- value.integer(uclvalue.int_value());
- break;
- case UCL_STRING:
- value.string(uclvalue.string_value());
- break;
- default:
- throw error(
- "INTERNAL ERROR: unknown value type {0}",
- static_cast<int>(uclvalue.type()));
- }
- }
- }
-#endif
/*
* Register a new value with the config store.
@@ -96,12 +57,26 @@ export struct store final {
auto [it, okay] = self.options.insert(
std::pair{object->name(), object});
- if (okay)
- return;
+ if (!okay)
+ throw error(std::format(
+ "INTERNAL ERROR: attempt to register "
+ "duplicate config value '{0}'",
+ object->name()));
+ }
- throw error("INTERNAL ERROR: attempt to register "
- "duplicate config value '{0}'",
- object->name());
+ /*
+ * Remove a value from the config store.
+ */
+ auto unregister_option(this store &self, option *object) -> void
+ {
+ 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);
}
/*
@@ -125,44 +100,6 @@ export struct store final {
co_yield *it.second;
}
- /*
- * Write all config values (except defaults) to disk.
- */
-#if 0
- void store::write_all(this store const &self, context const &ctx) {
- // The UCL C++ API doesn't seem to support creating new objects
- // from scratch, so we use the C API here. We should probably
- // provider a better wrapper for this.
-
- auto ucl = ::ucl_object_typed_new(UCL_OBJECT);
- auto ucl_guard = guard([ucl] { ::ucl_object_unref(ucl); });
-
- // Add all the options to the UCL object.
- for (auto const &option : self.fetch_all()) {
- if (option.is_default)
- continue;
-
- option.add_to_ucl(ucl);
- }
-
- // Dump the UCL object to a string.
- auto *ucl_c_text = reinterpret_cast<char *>(
- ::ucl_object_emit(ucl, UCL_EMIT_CONFIG));
- //NOLINTNEXTLINE(cppcoreguidelines-no-malloc)
- auto ucl_text_guard = guard([ucl_c_text] { ::free(ucl_c_text); });
- std::string ucl_text(ucl_c_text);
-
- // Write the object to a file.
- auto config_path = ctx.dbdir / "config.ucl";
-
- try {
- safe_write_file(config_path, ucl_text);
- } catch (io_error const &exc) {
- throw error("{}", exc.what());
- }
- }
-#endif
-
// Not movable or copyable.
store(store const &) = delete;
store(store &&) = delete;
@@ -180,4 +117,12 @@ private:
store() = default;
};
+/*
+ * The public API.
+ */
+export auto get_option(std::string_view option_name) -> option &
+{
+ return store::get().fetch(option_name);
+}
+
} // namespace nihil::config
diff --git a/nihil.config/string.ccm b/nihil.config/string.ccm
index f3273c3..57770ae 100644
--- a/nihil.config/string.ccm
+++ b/nihil.config/string.ccm
@@ -4,20 +4,19 @@
module;
-#include <stdexcept>
+#include <format>
#include <string>
-#include <ucl++.h>
-
export module nihil.config:string;
import nihil;
+import nihil.ucl;
import :option;
import :store;
namespace nihil::config {
-struct string final : option
+export struct string final : option
{
string(std::string &storage,
std::string_view name,
@@ -28,6 +27,11 @@ struct string final : option
store::get().register_option(this);
}
+ ~string()
+ {
+ store::get().unregister_option(this);
+ }
+
auto get_string() const -> std::string override
{
return _storage;
@@ -38,13 +42,20 @@ struct string final : option
_storage = new_value;
}
- auto add_to_ucl(ucl_object_t *ucl) const -> void override
+ auto to_ucl() const -> ucl::object override
+ {
+ return ucl::string(_storage);
+ }
+
+ auto from_ucl(ucl::object const &uclobj) -> void override
{
- auto ucl_value = ucl_object_fromstring_common(
- _storage.data(), _storage.size(),
- UCL_STRING_RAW);
- ucl_object_insert_key(ucl, ucl_value,
- name().data(), name().size(), true);
+ 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())));
+ }
}
private:
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..7e95190
--- /dev/null
+++ b/nihil.config/tests/string.cc
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+ REQUIRE_THROWS_AS(nihil::config::get_option("test_option"),
+ nihil::config::unknown_option);
+
+ {
+ 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() == "");
+
+ opt.string("testing");
+ REQUIRE(storage == "testing");
+ }
+
+ REQUIRE_THROWS_AS(nihil::config::get_option("test_option"),
+ nihil::config::unknown_option);
+}
diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm
new file mode 100644
index 0000000..947c7ee
--- /dev/null
+++ b/nihil.config/write.ccm
@@ -0,0 +1,42 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <filesystem>
+#include <format>
+#include <utility>
+
+export module nihil.config:write;
+
+import nihil.ucl;
+import :store;
+
+namespace nihil::config {
+
+/*
+ * Write all config values (except defaults) to disk.
+ */
+auto write_to(std::filesystem::path const &filename) -> void
+try {
+ 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;
+
+ uclconfig.insert({option.name(), option.to_ucl()});
+ }
+
+ auto ucl_text = std::format("{:c}", uclconfig);
+ auto ret = safe_write_file(filename, ucl_text);
+ if (!ret)
+ throw error(std::format("{}: {}", filename.string(),
+ ret.error().message()));
+} catch (ucl::error const &exc) {
+ throw error(std::format("{}: {}", filename.string(), exc.what()));
+}
+
+};