diff options
Diffstat (limited to 'nihil.config')
| -rw-r--r-- | nihil.config/CMakeLists.txt | 31 | ||||
| -rw-r--r-- | nihil.config/nihil.config.ccm | 13 | ||||
| -rw-r--r-- | nihil.config/option.cc | 97 | ||||
| -rw-r--r-- | nihil.config/option.ccm | 105 | ||||
| -rw-r--r-- | nihil.config/read.cc | 50 | ||||
| -rw-r--r-- | nihil.config/read.ccm | 22 | ||||
| -rw-r--r-- | nihil.config/store.cc | 95 | ||||
| -rw-r--r-- | nihil.config/store.ccm | 75 | ||||
| -rw-r--r-- | nihil.config/string.cc | 62 | ||||
| -rw-r--r-- | nihil.config/string.ccm | 56 | ||||
| -rw-r--r-- | nihil.config/tests/CMakeLists.txt | 15 | ||||
| -rw-r--r-- | nihil.config/tests/string.cc | 36 | ||||
| -rw-r--r-- | nihil.config/write.cc | 41 | ||||
| -rw-r--r-- | nihil.config/write.ccm | 22 |
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>; + +}; |
