diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-23 16:28:11 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-23 16:28:11 +0100 |
| commit | d5963532328ce5f1c9f266bf7e760b7d18a60c15 (patch) | |
| tree | 28e8d4b98f2f3adbd2f02bcc656ad74e626677c9 /nihil.config | |
| parent | 0fa623093366351ad47583f47add6e51f56a56d8 (diff) | |
| download | nihil-d5963532328ce5f1c9f266bf7e760b7d18a60c15.tar.gz nihil-d5963532328ce5f1c9f266bf7e760b7d18a60c15.tar.bz2 | |
various updates
Diffstat (limited to 'nihil.config')
| -rw-r--r-- | nihil.config/CMakeLists.txt | 14 | ||||
| -rw-r--r-- | nihil.config/error.ccm | 5 | ||||
| -rw-r--r-- | nihil.config/nihil.config.ccm | 1 | ||||
| -rw-r--r-- | nihil.config/option.ccm | 26 | ||||
| -rw-r--r-- | nihil.config/read.ccm | 63 | ||||
| -rw-r--r-- | nihil.config/store.ccm | 137 | ||||
| -rw-r--r-- | nihil.config/string.ccm | 31 | ||||
| -rw-r--r-- | nihil.config/tests/CMakeLists.txt | 15 | ||||
| -rw-r--r-- | nihil.config/tests/string.cc | 34 | ||||
| -rw-r--r-- | nihil.config/write.ccm | 42 |
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())); +} + +}; |
