aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-21 17:18:57 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-21 17:18:57 +0100
commitd27d1302d1fa1b96bf8f53f17fce947f19d21330 (patch)
treef56ef29d7816c3b574b3359c746355a44e3b0819
parent75c6b5fee029ec95e7e45e18525e3e78b9616f48 (diff)
downloadnihil-d27d1302d1fa1b96bf8f53f17fce947f19d21330.tar.gz
nihil-d27d1302d1fa1b96bf8f53f17fce947f19d21330.tar.bz2
add nihil.config (incomplete)
-rw-r--r--CMakeLists.txt19
-rw-r--r--nihil.config/CMakeLists.txt11
-rw-r--r--nihil.config/error.ccm26
-rw-r--r--nihil.config/nihil.config.ccm12
-rw-r--r--nihil.config/option.ccm84
-rw-r--r--nihil.config/store.ccm183
-rw-r--r--nihil.config/string.ccm54
-rw-r--r--nihil.ucl/CMakeLists.txt13
-rw-r--r--nihil.ucl/nihil.ucl.ccm9
-rw-r--r--nihil/CMakeLists.txt5
-rw-r--r--nihil/tests/CMakeLists.txt (renamed from tests/CMakeLists.txt)0
-rw-r--r--nihil/tests/command_map.cc (renamed from tests/command_map.cc)0
-rw-r--r--nihil/tests/ctype.cc (renamed from tests/ctype.cc)0
-rw-r--r--nihil/tests/fd.cc (renamed from tests/fd.cc)0
-rw-r--r--nihil/tests/generator.cc (renamed from tests/generator.cc)0
-rw-r--r--nihil/tests/generic_error.cc (renamed from tests/generic_error.cc)0
-rw-r--r--nihil/tests/getenv.cc (renamed from tests/getenv.cc)0
-rw-r--r--nihil/tests/guard.cc (renamed from tests/guard.cc)0
-rw-r--r--nihil/tests/next_word.cc (renamed from tests/next_word.cc)0
-rw-r--r--nihil/tests/skipws.cc (renamed from tests/skipws.cc)0
-rw-r--r--nihil/tests/spawn.cc (renamed from tests/spawn.cc)0
-rw-r--r--nihil/tests/tabulate.cc (renamed from tests/tabulate.cc)0
22 files changed, 414 insertions, 2 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9df3b46..457f603 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,8 +4,14 @@ cmake_minimum_required(VERSION 3.28)
project(libexi)
+option(NIHIL_CONFIG "Build the nihil.config library" ON)
+option(NIHIL_UCL "Build the nihil.ucl library" ON)
+option(NIHIL_TESTS "Build nihil's unit tests" ON)
+
set(CMAKE_CXX_STANDARD 26)
+find_package(PkgConfig REQUIRED)
+
add_compile_options(-W)
add_compile_options(-Wall)
add_compile_options(-Wextra)
@@ -13,6 +19,15 @@ add_compile_options(-Werror)
add_compile_options(-Wpedantic)
add_subdirectory(nihil)
-add_subdirectory(tests)
-enable_testing()
+if(NIHIL_UCL)
+ add_subdirectory(nihil.ucl)
+endif()
+
+if(NIHIL_CONFIG)
+ add_subdirectory(nihil.config)
+endif()
+
+if(NIHIL_TESTS)
+ enable_testing()
+endif()
diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt
new file mode 100644
index 0000000..d96b116
--- /dev/null
+++ b/nihil.config/CMakeLists.txt
@@ -0,0 +1,11 @@
+# This source code is released into the public domain.
+
+add_library(nihil.config STATIC)
+target_sources(nihil.config PUBLIC
+ FILE_SET modules TYPE CXX_MODULES FILES
+ nihil.config.ccm
+ error.ccm
+ store.ccm
+ option.ccm
+ string.ccm)
+target_link_libraries(nihil.config PUBLIC nihil nihil.ucl)
diff --git a/nihil.config/error.ccm b/nihil.config/error.ccm
new file mode 100644
index 0000000..0da91cb
--- /dev/null
+++ b/nihil.config/error.ccm
@@ -0,0 +1,26 @@
+/*
+ * 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 {
+ template<typename... Args>
+ error(std::format_string<Args...> fmt, Args &&...args)
+ : generic_error(fmt, std::forward<Args>(args)...)
+ {}
+};
+
+} // namespace nihil::config
diff --git a/nihil.config/nihil.config.ccm b/nihil.config/nihil.config.ccm
new file mode 100644
index 0000000..0b12885
--- /dev/null
+++ b/nihil.config/nihil.config.ccm
@@ -0,0 +1,12 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+export module nihil.config;
+
+export import :error;
+export import :option;
+export import :store;
+export import :string;
diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm
new file mode 100644
index 0000000..207eb65
--- /dev/null
+++ b/nihil.config/option.ccm
@@ -0,0 +1,84 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <string>
+
+#include <ucl++.h>
+
+export module nihil.config:option;
+
+import nihil;
+import :error;
+
+namespace nihil::config {
+
+/*
+ * Base class for options; this is what config_store interacts with.
+ */
+
+export struct option
+{
+ // Short name of this option.
+ auto name(this option const &self) noexcept -> std::string_view
+ {
+ return self._name;
+ }
+
+ // Human-readable description of this option.
+ auto description(this option const &self) noexcept -> std::string_view
+ {
+ return self._description;
+ }
+
+ // If true, this option is set to its default value.
+ auto is_default(this option const &self) noexcept -> bool
+ {
+ return self._is_default;
+ }
+
+ // Get or set this option as a string.
+ auto string(this option const &self) -> std::string
+ {
+ return self.get_string();
+ }
+
+ void string(this option &self, std::string_view value)
+ {
+ self.set_string(value);
+ self._is_default = false;
+ }
+
+ /*
+ * Add this option to a UCL object. This is used when writing the
+ * configuration file.
+ */
+ virtual void add_to_ucl(ucl_object_t *) const = 0;
+
+ // Not copyable or movable.
+ option(option const &) = delete;
+ auto operator=(option const &) -> option& = delete;
+
+protected:
+ option(std::string_view name,
+ std::string_view description)
+ : _name(name)
+ , _description(description)
+ {
+ }
+
+ /*
+ * 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;
+
+private:
+ std::string _name;
+ std::string _description;
+ bool _is_default = true;
+};
+
+} // namespace nihil
diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm
new file mode 100644
index 0000000..7ed4ccb
--- /dev/null
+++ b/nihil.config/store.ccm
@@ -0,0 +1,183 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+/*
+ * The configuration store. There should only be one of these.
+ */
+
+#include <coroutine>
+#include <format>
+#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 {
+ std::string varname;
+
+ unknown_option(std::string_view varname_)
+ : error("unknown configuration variable '{}'", varname_)
+ , varname(varname_)
+ {}
+};
+
+export struct store final {
+ /*
+ * Get the global config store.
+ */
+ static auto get() -> store& {
+ if (instance == nullptr)
+ instance = new store;
+
+ 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.
+ */
+ auto register_option(this store &self, option *object) -> void
+ {
+ auto [it, okay] = self.options.insert(
+ std::pair{object->name(), object});
+
+ if (okay)
+ return;
+
+ throw error("INTERNAL ERROR: attempt to register "
+ "duplicate config value '{0}'",
+ object->name());
+ }
+
+ /*
+ * Fetch an existing value in the config store.
+ */
+ auto fetch(this store const &self, std::string_view name)
+ -> option &
+ {
+ if (auto it = self.options.find(name); it != self.options.end())
+ return *it->second;
+
+ throw unknown_option(name);
+ }
+
+ /*
+ * Fetch all values in the configuration store.
+ */
+ auto all(this auto &&self) -> nihil::generator<option const &>
+ {
+ for (auto &&it : self.options)
+ 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;
+ store& operator=(store const &) = delete;
+ store& operator=(store &&) = delete;
+
+private:
+ /*
+ * The global configuration store, created by init() and accessed via
+ * get().
+ */
+ inline static store *instance = nullptr;
+
+ std::map<std::string_view, option *> options;
+ store() = default;
+};
+
+} // namespace nihil::config
diff --git a/nihil.config/string.ccm b/nihil.config/string.ccm
new file mode 100644
index 0000000..f3273c3
--- /dev/null
+++ b/nihil.config/string.ccm
@@ -0,0 +1,54 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <stdexcept>
+#include <string>
+
+#include <ucl++.h>
+
+export module nihil.config:string;
+
+import nihil;
+import :option;
+import :store;
+
+namespace nihil::config {
+
+struct string final : option
+{
+ string(std::string &storage,
+ std::string_view name,
+ std::string_view description) noexcept
+ : option(name, description)
+ , _storage(storage)
+ {
+ store::get().register_option(this);
+ }
+
+ auto get_string() const -> std::string override
+ {
+ return _storage;
+ };
+
+ auto set_string(std::string_view new_value) -> void override
+ {
+ _storage = new_value;
+ }
+
+ auto add_to_ucl(ucl_object_t *ucl) const -> 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);
+ }
+
+private:
+ std::string &_storage;
+};
+
+} // namespace nihil::config
diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt
new file mode 100644
index 0000000..59021aa
--- /dev/null
+++ b/nihil.ucl/CMakeLists.txt
@@ -0,0 +1,13 @@
+# This source code is released into the public domain.
+
+pkg_check_modules(LIBUCL REQUIRED libucl)
+
+add_library(nihil.ucl STATIC)
+target_sources(nihil.ucl PUBLIC
+ FILE_SET modules TYPE CXX_MODULES FILES
+ nihil.ucl.ccm)
+target_compile_options(nihil.ucl PUBLIC ${LIBUCL_CFLAGS_OTHER})
+target_include_directories(nihil.ucl PUBLIC ${LIBUCL_INCLUDE_DIRS})
+target_link_libraries(nihil.ucl PUBLIC ${LIBUCL_LIBRARIES})
+target_link_directories(nihil.ucl PUBLIC ${LIBUCL_LIBRARY_DIRS})
+
diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm
new file mode 100644
index 0000000..23151e0
--- /dev/null
+++ b/nihil.ucl/nihil.ucl.ccm
@@ -0,0 +1,9 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+export module nihil.ucl;
+
+// TODO: Implement.
diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt
index 81f11c6..72f7e22 100644
--- a/nihil/CMakeLists.txt
+++ b/nihil/CMakeLists.txt
@@ -20,3 +20,8 @@ target_sources(nihil PUBLIC
spawn.ccm
tabulate.ccm
usage_error.ccm)
+
+if(NIHIL_TESTS)
+ add_subdirectory(tests)
+ enable_testing()
+endif()
diff --git a/tests/CMakeLists.txt b/nihil/tests/CMakeLists.txt
index abeab88..abeab88 100644
--- a/tests/CMakeLists.txt
+++ b/nihil/tests/CMakeLists.txt
diff --git a/tests/command_map.cc b/nihil/tests/command_map.cc
index 75b6f0d..75b6f0d 100644
--- a/tests/command_map.cc
+++ b/nihil/tests/command_map.cc
diff --git a/tests/ctype.cc b/nihil/tests/ctype.cc
index 87f5103..87f5103 100644
--- a/tests/ctype.cc
+++ b/nihil/tests/ctype.cc
diff --git a/tests/fd.cc b/nihil/tests/fd.cc
index fbf353e..fbf353e 100644
--- a/tests/fd.cc
+++ b/nihil/tests/fd.cc
diff --git a/tests/generator.cc b/nihil/tests/generator.cc
index 8657756..8657756 100644
--- a/tests/generator.cc
+++ b/nihil/tests/generator.cc
diff --git a/tests/generic_error.cc b/nihil/tests/generic_error.cc
index b213af9..b213af9 100644
--- a/tests/generic_error.cc
+++ b/nihil/tests/generic_error.cc
diff --git a/tests/getenv.cc b/nihil/tests/getenv.cc
index adfa84f..adfa84f 100644
--- a/tests/getenv.cc
+++ b/nihil/tests/getenv.cc
diff --git a/tests/guard.cc b/nihil/tests/guard.cc
index f88aa9b..f88aa9b 100644
--- a/tests/guard.cc
+++ b/nihil/tests/guard.cc
diff --git a/tests/next_word.cc b/nihil/tests/next_word.cc
index 4055485..4055485 100644
--- a/tests/next_word.cc
+++ b/nihil/tests/next_word.cc
diff --git a/tests/skipws.cc b/nihil/tests/skipws.cc
index 2159e2e..2159e2e 100644
--- a/tests/skipws.cc
+++ b/nihil/tests/skipws.cc
diff --git a/tests/spawn.cc b/nihil/tests/spawn.cc
index 455223e..455223e 100644
--- a/tests/spawn.cc
+++ b/nihil/tests/spawn.cc
diff --git a/tests/tabulate.cc b/nihil/tests/tabulate.cc
index 84f8b33..84f8b33 100644
--- a/tests/tabulate.cc
+++ b/nihil/tests/tabulate.cc