diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-26 20:47:45 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-26 20:47:45 +0100 |
| commit | 90aa957ca9b7c217af7569009d1675e0f3ff8e9b (patch) | |
| tree | e6a61ca2b6928e6414372b9b1484ce80fa2fb0b3 | |
| parent | 1db86c401df11423c945634d8b2a483e97afa878 (diff) | |
| download | nihil-90aa957ca9b7c217af7569009d1675e0f3ff8e9b.tar.gz nihil-90aa957ca9b7c217af7569009d1675e0f3ff8e9b.tar.bz2 | |
ucl, config: use monadic error handling more
| -rw-r--r-- | nihil.config/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | nihil.config/error.ccm | 23 | ||||
| -rw-r--r-- | nihil.config/nihil.config.ccm | 1 | ||||
| -rw-r--r-- | nihil.config/option.cc | 70 | ||||
| -rw-r--r-- | nihil.config/option.ccm | 61 | ||||
| -rw-r--r-- | nihil.config/read.cc | 35 | ||||
| -rw-r--r-- | nihil.config/read.ccm | 6 | ||||
| -rw-r--r-- | nihil.config/store.cc | 92 | ||||
| -rw-r--r-- | nihil.config/store.ccm | 41 | ||||
| -rw-r--r-- | nihil.config/string.cc | 42 | ||||
| -rw-r--r-- | nihil.config/string.ccm | 34 | ||||
| -rw-r--r-- | nihil.config/tests/string.cc | 22 | ||||
| -rw-r--r-- | nihil.config/write.cc | 27 | ||||
| -rw-r--r-- | nihil.config/write.ccm | 6 | ||||
| -rw-r--r-- | nihil.ucl/object_cast.ccm | 29 | ||||
| -rw-r--r-- | nihil.ucl/parser.cc | 71 | ||||
| -rw-r--r-- | nihil.ucl/parser.ccm | 101 | ||||
| -rw-r--r-- | nihil.ucl/tests/array.cc | 53 | ||||
| -rw-r--r-- | nihil.ucl/tests/boolean.cc | 18 | ||||
| -rw-r--r-- | nihil.ucl/tests/emit.cc | 24 | ||||
| -rw-r--r-- | nihil.ucl/tests/integer.cc | 16 | ||||
| -rw-r--r-- | nihil.ucl/tests/object.cc | 15 | ||||
| -rw-r--r-- | nihil.ucl/tests/parse.cc | 14 | ||||
| -rw-r--r-- | nihil.ucl/tests/real.cc | 17 | ||||
| -rw-r--r-- | nihil.ucl/tests/string.cc | 15 |
25 files changed, 508 insertions, 326 deletions
diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt index 2aa0dae..fb2c1db 100644 --- a/nihil.config/CMakeLists.txt +++ b/nihil.config/CMakeLists.txt @@ -5,7 +5,6 @@ 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 diff --git a/nihil.config/error.ccm b/nihil.config/error.ccm deleted file mode 100644 index 4e7131a..0000000 --- a/nihil.config/error.ccm +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { - 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 d20c3fe..8957305 100644 --- a/nihil.config/nihil.config.ccm +++ b/nihil.config/nihil.config.ccm @@ -6,7 +6,6 @@ module; export module nihil.config; -export import :error; export import :option; export import :read; export import :store; diff --git a/nihil.config/option.cc b/nihil.config/option.cc index 9bf77c9..588a48f 100644 --- a/nihil.config/option.cc +++ b/nihil.config/option.cc @@ -4,58 +4,86 @@ module; +#include <coroutine> +#include <expected> #include <iostream> #include <string> module nihil.config; +import nihil; +import nihil.ucl; + namespace nihil::config { +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 + -> std::string_view { - return self._name; + return self.m_name; } -// Human-readable description of this option. auto option::description(this option const &self) noexcept --> std::string_view + -> std::string_view { - return self._description; + return self.m_description; } -// If true, this option is set to its default value. auto option::is_default(this option const &self) noexcept --> bool + -> bool +{ + return self.m_is_default; +} + +auto option::is_default(this option &self, bool b) -> void { - return self._is_default; + self.m_is_default = b; } -// Get or set this option as a string. -auto option::string(this option const &self) --> std::string +auto option::string(this option const &self) -> std::string { return self.get_string(); } auto option::string(this option &self, std::string_view value) --> void + -> std::expected<void, error> { - self.set_string(value); - self._is_default = false; + co_await self.set_string(value); + self.is_default(false); + co_return {}; } -option::option(std::string_view name, - std::string_view description) - : _name(name) - , _description(description) +auto option::ucl(this option const &self) + -> std::expected<nihil::ucl::object, error> { + return self.get_ucl(); } -auto option::is_default(bool b) --> void +auto option::ucl(this option &self, nihil::ucl::object const &value) + -> std::expected<void, error> { - _is_default = b; + co_await self.set_ucl(value); + self.is_default(false); + co_return {}; } auto operator<<(std::ostream &strm, option const &opt) diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm index c6a8329..c095d34 100644 --- a/nihil.config/option.ccm +++ b/nihil.config/option.ccm @@ -4,46 +4,67 @@ module; +#include <expected> #include <iosfwd> #include <string> export module nihil.config:option; +import nihil; import nihil.ucl; -import :error; 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. - auto name(this option const &self) noexcept -> std::string_view; + [[nodiscard]] auto name(this option const &) noexcept + -> std::string_view; // Human-readable description of this option. - auto description(this option const &self) noexcept -> std::string_view; + [[nodiscard]] auto description(this option const &) noexcept + -> std::string_view; // If true, this option is set to its default value. - auto is_default(this option const &self) noexcept -> bool; + [[nodiscard]] auto is_default(this option const &) noexcept + -> bool; - // Get or set this option as a string. - auto string(this option const &self) -> std::string; - auto string(this option &self, std::string_view value) -> void; + /* + * 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. */ - virtual auto to_ucl() const -> ucl::object = 0; + [[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. */ - virtual auto from_ucl(ucl::object const &) -> void = 0; + [[nodiscard]] auto ucl(this option &, ucl::object const &) + -> std::expected<void, error>; // Not copyable or movable. option(option const &) = delete; @@ -52,18 +73,28 @@ export struct option protected: option(std::string_view name, std::string_view description); - auto is_default(bool b) -> void; + auto is_default(this option &, bool) -> void; /* * 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; + virtual auto get_string() const + -> std::string = 0; + virtual auto set_string(std::string_view) + -> std::expected<void, error> = 0; + + /* + * Get or set this option as a UCL object. + */ + virtual auto get_ucl() const + -> std::expected<ucl::object, error> = 0; + virtual auto set_ucl(ucl::object const &) + -> std::expected<void, error> = 0; private: - std::string _name; - std::string _description; - bool _is_default = true; + std::string m_name; + std::string m_description; + bool m_is_default = true; }; /* diff --git a/nihil.config/read.cc b/nihil.config/read.cc index 0a5fcad..e7def91 100644 --- a/nihil.config/read.cc +++ b/nihil.config/read.cc @@ -11,15 +11,15 @@ module; #include <iterator> #include <string> +module nihil.config; + import nihil; import nihil.ucl; -module nihil.config; - namespace nihil::config { auto read_from(std::filesystem::path const &filename) - -> std::expected<void, nihil::error> + -> std::expected<void, error> { // TODO: nihil.ucl should have a way to load UCL from a filename. @@ -29,33 +29,20 @@ auto read_from(std::filesystem::path const &filename) // 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) - return {}; + co_return {}; auto errstr = std::format("cannot read {}", filename.string()); - return std::unexpected(nihil::error(errstr, err.error())); + co_return std::unexpected(error(errstr, err.error())); } // 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())); + 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); } - return {}; + co_return {}; } } // namespace nihil::config diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm index 18f7213..fca78eb 100644 --- a/nihil.config/read.ccm +++ b/nihil.config/read.ccm @@ -7,16 +7,16 @@ module; #include <expected> #include <filesystem> -import nihil; - export module nihil.config:read; +import nihil; + namespace nihil::config { /* * Load the configuration from a file. */ export [[nodiscard]] auto read_from(std::filesystem::path const &filename) - -> std::expected<void, nihil::error>; + -> std::expected<void, error>; } // namespace nihil::config diff --git a/nihil.config/store.cc b/nihil.config/store.cc index 2ec8ade..6f93677 100644 --- a/nihil.config/store.cc +++ b/nihil.config/store.cc @@ -5,6 +5,7 @@ module; #include <coroutine> +#include <expected> #include <filesystem> #include <format> #include <map> @@ -15,75 +16,78 @@ import nihil; namespace nihil::config { -unknown_option::unknown_option(std::string_view option_name) - : error(std::format("unknown configuration variable '{}'", - option_name)) - , _option_name(option_name) -{ -} +store::store() = default; -auto unknown_option::option_name(this unknown_option const &self) --> std::string_view +auto store::get() -> store & { - return self._option_name; -} - -auto store::get() --> store& -{ - if (instance == nullptr) - instance = new store; - - return *instance; + static auto instance = store(); + return instance; } auto store::register_option(this store &self, option *object) --> void + -> std::expected<void, error> { - auto [it, okay] = self.options.insert( + auto [it, okay] = self.m_options.insert( std::pair{object->name(), object}); - if (!okay) - throw error(std::format( - "INTERNAL ERROR: attempt to register " - "duplicate config value '{0}'", - object->name())); + 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) --> void + -> std::expected<void, error> { - 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); + 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) --> option & + -> std::expected<option const *, error> { - if (auto it = self.options.find(name); it != self.options.end()) - return *it->second; + if (auto it = self.m_options.find(name); it != self.m_options.end()) + return it->second; - throw unknown_option(name); + 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 const &self) --> nihil::generator<option const &> +auto store::all(this store &self) -> nihil::generator<option *> { - for (auto &&it : self.options) - co_yield *it.second; + for (auto &&it : self.m_options) + co_yield it.second; } auto get_option(std::string_view option_name) --> option & + -> std::expected<option *, error> { - return store::get().fetch(option_name); + co_return co_await store::get().fetch(option_name); } } // namespace nihil::config diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm index 77b44b5..03d09d3 100644 --- a/nihil.config/store.ccm +++ b/nihil.config/store.ccm @@ -9,51 +9,50 @@ module; */ #include <coroutine> +#include <expected> #include <string> #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 { - unknown_option(std::string_view option_name); - auto option_name(this unknown_option const &self) -> std::string_view; - -private: - std::string _option_name; -}; - struct store final { /* * Get the global config store. */ - static auto get() -> store &; + [[nodiscard]] static auto get() -> store &; /* * Register a new value with the config store. */ - auto register_option(this store &self, option *object) -> void; + [[nodiscard]] auto register_option(this store &, option *object) + -> std::expected<void, error>; /* * Remove a value from the config store. */ - auto unregister_option(this store &self, option *object) -> void; + [[nodiscard]] auto unregister_option(this store &, option *object) + -> std::expected<void, error>; /* * Fetch an existing value in the config store. */ - auto fetch(this store const &self, std::string_view name) -> option &; + [[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. */ - auto all(this store const &self) -> nihil::generator<option const &>; + [[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; @@ -62,19 +61,15 @@ struct store final { store& operator=(store &&) = delete; private: - /* - * The global configuration store, created by init() and accessed via - * get(). - */ - inline static store *instance = nullptr; + store(); - std::map<std::string_view, option *> options; - store() = default; + std::map<std::string_view, option *> m_options; }; /* * The public API. */ -export auto get_option(std::string_view option_name) -> option &; +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 index 6b201ae..7d0c038 100644 --- a/nihil.config/string.cc +++ b/nihil.config/string.cc @@ -4,11 +4,14 @@ module; +#include <coroutine> +#include <expected> #include <format> #include <string> module nihil.config; +import nihil; import nihil.ucl; namespace nihil::config { @@ -18,40 +21,41 @@ string::string( std::string_view name, std::string_view description) noexcept : option(name, description) - , _storage(storage) + , m_storage(storage) { - store::get().register_option(this); } -string::~string() -{ - store::get().unregister_option(this); -} +string::~string() = default; auto string::get_string() const -> std::string { - return _storage; + return m_storage; } -auto string::set_string(std::string_view new_value) -> void +auto string::set_string(std::string_view new_value) + -> std::expected<void, error> { - _storage = new_value; + m_storage = new_value; + return {}; } -auto string::to_ucl() const -> ucl::object +auto string::get_ucl() const -> std::expected<ucl::object, error> { - return ucl::string(_storage); + return ucl::string(m_storage); } -auto string::from_ucl(ucl::object const &uclobj) -> void +auto string::set_ucl(ucl::object const &uclobj) -> std::expected<void, error> { - 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()))); - } + 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 index ae5efb9..b6a7e05 100644 --- a/nihil.config/string.ccm +++ b/nihil.config/string.ccm @@ -4,16 +4,21 @@ module; +#include <expected> #include <format> #include <string> export module nihil.config:string; +import nihil; 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, @@ -22,14 +27,31 @@ export struct string final : option ~string(); - auto get_string() const -> std::string override; - auto set_string(std::string_view new_value) -> void override; - - auto to_ucl() const -> ucl::object override; - auto from_ucl(ucl::object const &uclobj) -> void override; + /* + * 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 &_storage; + std::string &m_storage; }; } // namespace nihil::config diff --git a/nihil.config/tests/string.cc b/nihil.config/tests/string.cc index 7e95190..aeb1ef8 100644 --- a/nihil.config/tests/string.cc +++ b/nihil.config/tests/string.cc @@ -12,23 +12,25 @@ 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 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.name() == "test_option"); - REQUIRE(opt.description() == "This is a test option"); - REQUIRE(opt.is_default() == true); - REQUIRE(opt.string() == ""); + auto opt = nihil::config::get_option("test_option"); + REQUIRE(opt); - opt.string("testing"); + 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"); } - REQUIRE_THROWS_AS(nihil::config::get_option("test_option"), - nihil::config::unknown_option); + opt = nihil::config::get_option("test_option"); + REQUIRE(!opt); } diff --git a/nihil.config/write.cc b/nihil.config/write.cc index 2b451bd..8c02c43 100644 --- a/nihil.config/write.cc +++ b/nihil.config/write.cc @@ -4,43 +4,36 @@ module; +#include <coroutine> #include <expected> #include <filesystem> #include <format> #include <utility> +module nihil.config; + import nihil; import nihil.ucl; -module nihil.config; - namespace nihil::config { auto write_to(std::filesystem::path const &filename) - -> std::expected<void, nihil::error> -try { + -> 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()) + if (option->is_default()) continue; - uclconfig.insert({option.name(), option.to_ucl()}); + auto uobj = co_await option->ucl(); + uclconfig.insert({option->name(), uobj}); } auto ucl_text = std::format("{:c}", uclconfig); - auto ret = safe_write_file(filename, ucl_text); - if (!ret) - return std::unexpected(nihil::error( - std::format("cannot write {}", filename.string()), - ret.error())); - - return {}; -} catch (ucl::error const &exc) { - return std::unexpected(nihil::error( - "failed to serialize configuration", - nihil::error(exc.what()))); + co_await safe_write_file(filename, ucl_text); + co_return {}; } }; diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm index 1a07dd7..cb35d76 100644 --- a/nihil.config/write.ccm +++ b/nihil.config/write.ccm @@ -7,16 +7,16 @@ module; #include <expected> #include <filesystem> -import nihil; - export module nihil.config:write; +import nihil; + export namespace nihil::config { /* * Write all config values (except defaults) to disk. */ auto write_to(std::filesystem::path const &filename) -> - std::expected<void, nihil::error>; + std::expected<void, error>; }; diff --git a/nihil.ucl/object_cast.ccm b/nihil.ucl/object_cast.ccm index b10ffbc..07588a1 100644 --- a/nihil.ucl/object_cast.ccm +++ b/nihil.ucl/object_cast.ccm @@ -4,12 +4,15 @@ module; +#include <coroutine> #include <cstdlib> +#include <expected> #include <ucl.h> export module nihil.ucl:object_cast; +import nihil; import :type; import :object; import :array; @@ -25,21 +28,22 @@ namespace nihil::ucl { template<datatype To> struct convert_check { - auto check(::ucl_object_t const *from) -> void + [[nodiscard]] auto check(::ucl_object_t const *from) + -> std::expected<void, type_mismatch> { auto from_type = static_cast<object_type>(::ucl_object_type(from)); auto to_type = To::ucl_type; // Converting from anything to object is permitted. if (to_type == object_type::object) - return; + return {}; // Converting between two equal types is permitted. if (from_type == to_type) - return; + return {}; // Otherwise, this is an error. - throw type_mismatch(to_type, from_type); + return std::unexpected(type_mismatch(to_type, from_type)); } }; @@ -47,7 +51,8 @@ struct convert_check template<typename T> struct convert_check<array<T>> { - auto check(::ucl_object_t const *from) -> void + [[nodiscard]] auto check(::ucl_object_t const *from) + -> std::expected<void, type_mismatch> { using To = array<T>; auto from_type = static_cast<object_type>(::ucl_object_type(from)); @@ -55,13 +60,17 @@ struct convert_check<array<T>> // If the source type is not an array, this is an error. if (from_type != object_type::array) - throw type_mismatch(to_type, from_type); + co_return std::unexpected( + type_mismatch(to_type, from_type)); for (std::size_t i = 0, size = ::ucl_array_size(from); i < size; ++i) { auto const *arr_obj = ::ucl_array_find_index(from, i); - convert_check<typename To::value_type>{}.check(arr_obj); + co_await convert_check<typename To::value_type>{} + .check(arr_obj); } + + co_return {}; } }; @@ -69,12 +78,12 @@ struct convert_check<array<T>> * Convert a UCL object to another type. */ export template<datatype To> -auto object_cast(object const &from) -> To +auto object_cast(object const &from) -> std::expected<To, type_mismatch> { auto uobj = from.get_ucl_object(); - convert_check<To>{}.check(uobj); - return To(nihil::ucl::ref, uobj); + co_await convert_check<To>{}.check(uobj); + co_return To(nihil::ucl::ref, uobj); } } // namespace nihil::ucl diff --git a/nihil.ucl/parser.cc b/nihil.ucl/parser.cc index 816116d..611fe50 100644 --- a/nihil.ucl/parser.cc +++ b/nihil.ucl/parser.cc @@ -4,6 +4,7 @@ module; +#include <expected> #include <functional> #include <string> @@ -11,15 +12,23 @@ module; module nihil.ucl; +import nihil; + namespace nihil::ucl { -parse_error::parse_error(std::string what) - : error(std::move(what)) +auto make_parser(int flags) -> std::expected<parser, nihil::error> { + auto *p = ::ucl_parser_new(flags); + if (p != nullptr) + return p; + + // TODO: Is there a way to get the actual error here? + return std::unexpected(nihil::error("failed to create parser")); } -auto macro_handler::handle(unsigned char const *data, std::size_t len, void *ud) --> bool +auto macro_handler::handle(unsigned char const *data, + std::size_t len, void *ud) + -> bool { auto handler = static_cast<macro_handler *>(ud); auto string = std::string_view( @@ -28,48 +37,66 @@ auto macro_handler::handle(unsigned char const *data, std::size_t len, void *ud) return handler->callback(string); } -parser::parser(int flags) +parser::parser(::ucl_parser *uclp) + : m_parser(uclp) { - if ((_parser = ::ucl_parser_new(flags)) != nullptr) - return; +} - throw error("failed to create UCL parser"); +parser::~parser() +{ + if (m_parser) + ::ucl_parser_free(m_parser); } -parser::parser() - : parser(0) +parser::parser(parser &&other) noexcept + : m_parser(std::exchange(other.m_parser, nullptr)) + , m_macros(std::move(other.m_macros)) { } -parser::~parser() +auto parser::operator=(this parser &self, parser &&other) noexcept + -> parser & { - if (_parser) - ::ucl_parser_free(_parser); + if (&self != &other) { + if (self.m_parser) + ::ucl_parser_free(self.m_parser); + + self.m_parser = std::exchange(other.m_parser, nullptr); + self.m_macros = std::move(other.m_macros); + } + + return self; } auto parser::register_value( this parser &self, std::string_view variable, std::string_view value) --> void + -> void { ::ucl_parser_register_variable( - self._parser, + self.get_parser(), std::string(variable).c_str(), std::string(value).c_str()); } auto parser::top(this parser &self) -> map<object> { - if (self._parser == nullptr) - throw error("attempt to call top() on an empty parser"); + auto obj = ::ucl_parser_get_object(self.get_parser()); + if (obj != nullptr) + // ucl_parser_get_object() refs the object for us. + return {noref, obj}; - auto obj = ::ucl_parser_get_object(self._parser); - if (obj == nullptr) - throw error("attempt to call top() on an empty parser"); + throw std::logic_error( + "attempt to call top() on an invalid ucl::parser"); +} - // ucl_parser_get_objects() refs the object for us. - return {noref, obj}; +auto parser::get_parser(this parser &self) -> ::ucl_parser * +{ + if (self.m_parser == nullptr) + throw std::logic_error("attempt to fetch a null ucl::parser"); + + return self.m_parser; } } // namespace nihil::ucl diff --git a/nihil.ucl/parser.ccm b/nihil.ucl/parser.ccm index f817b76..efddd5f 100644 --- a/nihil.ucl/parser.ccm +++ b/nihil.ucl/parser.ccm @@ -4,6 +4,8 @@ module; +#include <coroutine> +#include <expected> #include <format> #include <functional> #include <memory> @@ -21,13 +23,6 @@ import :map; namespace nihil::ucl { -/* - * Exception thrown when an issue occurs parsing UCL. - */ -export struct parse_error : error { - parse_error(std::string what); -}; - // UCL parser flags. export inline constexpr int parser_key_lower = UCL_PARSER_KEY_LOWERCASE; export inline constexpr int parser_zerocopy = UCL_PARSER_ZEROCOPY; @@ -49,88 +44,118 @@ struct macro_handler { /* * A UCL parser. This wraps the C ucl_parser API. + * + * parser itself is not exported; use make_parser() to create one. */ -export struct parser { - // Create a new parser with the given flags. - parser(int flags); - - // Create a new parser with the default flags. - parser(); +struct parser { + // Create a parser from a UCL parser. + parser(::ucl_parser *); // Destroy our parser when we're destroyed. ~parser(); + // Not copyable. + parser(parser const &) = delete; + auto operator=(this parser &, parser const &) -> parser & = delete; + + // Movable. + parser(parser &&) noexcept; + auto operator=(this parser &, parser &&) noexcept -> parser &; + // Add a parser macro. Unlike ucl_parser_register_macro, this doesn't // take a userdata parameter; it's assumed the user will use lambda // capture or similar if needed. template<std::invocable<std::string_view> F> auto register_macro(this parser &self, std::string_view name, - F &&func) -> void + F &&func) + -> void requires (std::same_as<bool, std::invoke_result<F>>) { auto handler = std::make_unique<macro_handler>( std::move(func)); auto cname = std::string(name); - ::ucl_parser_register_macro(self._parser, cname.c_str(), - ¯o_handler::handle, - handler.get()); + ::ucl_parser_register_macro( + self.get_parser(), cname.c_str(), + ¯o_handler::handle, handler.get()); - self._macros.emplace_back(std::move(handler)); + self.m_macros.emplace_back(std::move(handler)); } // Add a parser variable. auto register_value(this parser &self, std::string_view variable, - std::string_view value) -> void; + std::string_view value) + -> void; // Add data to the parser. - auto add(this parser &self, - std::ranges::contiguous_range auto &&data) - -> void + [[nodiscard]] auto add(this parser &self, + std::ranges::contiguous_range auto &&data) + -> std::expected<void, nihil::error> // Only bytes (chars) are permitted. requires(sizeof(std::ranges::range_value_t<decltype(data)>) == 1) { - // UCL accepts unsigned chars, but this is quite unhelpful - // when reading from files or strings. + auto *p = self.get_parser(); auto dptr = reinterpret_cast<unsigned char const *>( std::ranges::data(data)); - auto ret = ::ucl_parser_add_chunk(self._parser, dptr, - std::ranges::size(data)); - if (ret == false) - throw parse_error(::ucl_parser_get_error(self._parser)); + auto ret = ::ucl_parser_add_chunk( + p, dptr, std::ranges::size(data)); + + if (ret == true) + return {}; + + return std::unexpected(nihil::error(::ucl_parser_get_error(p))); } - auto add(this parser &self, std::ranges::range auto &&data) - -> void + [[nodiscard]] auto add(this parser &self, + std::ranges::range auto &&data) + -> std::expected<void, nihil::error> requires (!std::ranges::contiguous_range<decltype(data)>) { auto cdata = std::vector<char>( std::from_range, std::forward<decltype(data)>(data)); - return self.add(std::move(cdata)); + co_await self.add(std::move(cdata)); + co_return {}; } // Return the top object of this parser. - auto top(this parser &self) -> map<object>; + [[nodiscard]] auto top(this parser &self) -> map<object>; + + // Return the stored parser object. + [[nodiscard]] auto get_parser(this parser &self) -> ::ucl_parser *; private: // The parser object. Should never be null, unless we've been // moved-from. - ucl_parser *_parser = nullptr; + ucl_parser *m_parser; // Functions added by register_macro. We have to store these as // pointers because we pass the address to libucl. - std::vector<std::unique_ptr<macro_handler>> _macros; + std::vector<std::unique_ptr<macro_handler>> m_macros; }; +// Create a parser with the given flags. +export [[nodiscard]] auto +make_parser(int flags = 0) -> std::expected<parser, nihil::error>; + // Utility function to parse something and return the top-level object. -export auto parse(std::ranges::range auto &&data) -> map<object> { - auto p = parser(); - p.add(std::forward<decltype(data)>(data)); - return p.top(); +export [[nodiscard]] auto +parse(int flags, std::ranges::range auto &&data) + -> std::expected<map<object>, nihil::error> +{ + auto p = co_await make_parser(flags); + co_await p.add(std::forward<decltype(data)>(data)); + co_return p.top(); +} + +export [[nodiscard]] auto +parse(std::ranges::range auto &&data) + -> std::expected<map<object>, nihil::error> +{ + co_return co_await parse(0, std::forward<decltype(data)>(data)); } } // namespace nihil::ucl diff --git a/nihil.ucl/tests/array.cc b/nihil.ucl/tests/array.cc index ce0976f..fb23178 100644 --- a/nihil.ucl/tests/array.cc +++ b/nihil.ucl/tests/array.cc @@ -4,12 +4,14 @@ #include <algorithm> #include <concepts> +#include <expected> #include <ranges> #include <string> #include <catch2/catch_test_macros.hpp> #include <ucl.h> +import nihil; import nihil.ucl; TEST_CASE("ucl: array: invariants", "[ucl]") @@ -154,8 +156,14 @@ TEST_CASE("ucl: array: parse", "[ucl]") using namespace std::literals; using namespace nihil::ucl; - auto obj = parse("value = [1, 42, 666]"sv); - auto arr = object_cast<array<integer>>(obj["value"]); + auto obj_err = parse("value = [1, 42, 666]"sv); + REQUIRE(obj_err); + auto obj = *obj_err; + + auto err = object_cast<array<integer>>(obj["value"]); + REQUIRE(err); + + auto arr = *err; REQUIRE(arr.size() == 3); REQUIRE(arr[0] == 1); REQUIRE(arr[1] == 42); @@ -167,7 +175,9 @@ TEST_CASE("ucl: array: emit", "[ucl]") using namespace nihil::ucl; auto ucl = parse("array = [1, 42, 666];"); - auto output = std::format("{:c}", ucl); + REQUIRE(ucl); + + auto output = std::format("{:c}", *ucl); REQUIRE(output == "array [\n" " 1,\n" @@ -211,7 +221,8 @@ TEST_CASE("ucl: array: bad object_cast", "[ucl]") auto arr = array<integer>(); - REQUIRE_THROWS_AS(object_cast<integer>(arr), type_mismatch); + auto cast_ok = object_cast<integer>(arr); + REQUIRE(!cast_ok); } TEST_CASE("ucl: array: heterogeneous elements", "[ucl]") @@ -219,19 +230,27 @@ TEST_CASE("ucl: array: heterogeneous elements", "[ucl]") using namespace std::literals; using namespace nihil::ucl; - auto obj = parse("array [ 42, true, \"test\" ];"); - auto arr = object_cast<array<>>(obj["array"]); + auto obj_err = parse("array [ 42, true, \"test\" ];"); + REQUIRE(obj_err); + auto obj = *obj_err; + auto err = object_cast<array<>>(obj["array"]); + REQUIRE(err); + + auto arr = *err; REQUIRE(arr.size() == 3); auto int_obj = object_cast<integer>(arr[0]); - REQUIRE(int_obj == 42); + REQUIRE(int_obj); + REQUIRE(*int_obj == 42); auto bool_obj = object_cast<boolean>(arr[1]); - REQUIRE(bool_obj == true); + REQUIRE(bool_obj); + REQUIRE(*bool_obj == true); auto string_obj = object_cast<string>(arr[2]); - REQUIRE(string_obj == "test"); + REQUIRE(string_obj); + REQUIRE(*string_obj == "test"); } TEST_CASE("ucl: array: heterogenous cast", "[ucl]") @@ -243,10 +262,14 @@ TEST_CASE("ucl: array: heterogenous cast", "[ucl]") arr.push_back(boolean(true)); // Converting to an array<integer> should fail. - REQUIRE_THROWS_AS(object_cast<array<integer>>(arr), type_mismatch); + auto cast_ok = object_cast<array<integer>>(arr); + REQUIRE(!cast_ok); // Converting to array<object> should succeed. - auto obj_arr = object_cast<array<object>>(arr); + auto err = object_cast<array<object>>(arr); + REQUIRE(err); + + auto obj_arr = *err; REQUIRE(obj_arr[0] == integer(42)); } @@ -261,10 +284,14 @@ TEST_CASE("ucl: array: homogeneous cast", "[ucl]") auto obj = object(ref, arr.get_ucl_object()); // Converting to array<string> should fail. - REQUIRE_THROWS_AS(object_cast<array<string>>(obj), type_mismatch); + auto cast_ok = object_cast<array<string>>(obj); + REQUIRE(!cast_ok); // Converting to an array<integer> should succeed. - auto obj_arr = object_cast<array<integer>>(obj); + auto err = object_cast<array<integer>>(obj); + REQUIRE(err); + + auto obj_arr = *err; REQUIRE(obj_arr[0] == 1); REQUIRE(obj_arr[1] == 42); } diff --git a/nihil.ucl/tests/boolean.cc b/nihil.ucl/tests/boolean.cc index 49dc408..495071d 100644 --- a/nihil.ucl/tests/boolean.cc +++ b/nihil.ucl/tests/boolean.cc @@ -62,8 +62,11 @@ TEST_CASE("ucl: boolean: key()", "[ucl]") { using namespace nihil::ucl; - auto obj = parse("a_bool = true"); - REQUIRE(object_cast<boolean>(obj["a_bool"]).key() == "a_bool"); + auto err = parse("a_bool = true"); + REQUIRE(err); + + auto obj = *err; + REQUIRE(object_cast<boolean>(obj["a_bool"])->key() == "a_bool"); auto b = nihil::ucl::boolean(true); REQUIRE(b.key() == ""); @@ -95,10 +98,14 @@ TEST_CASE("ucl: boolean: parse", "[ucl]") { using namespace std::literals; - auto obj = nihil::ucl::parse("value = true"sv); + auto err = nihil::ucl::parse("value = true"sv); + REQUIRE(err); + + auto obj = *err; + auto v = obj["value"]; REQUIRE(v.key() == "value"); - REQUIRE(object_cast<nihil::ucl::boolean>(v) == true); + REQUIRE(*object_cast<nihil::ucl::boolean>(v) == true); } TEST_CASE("ucl: boolean: emit", "[ucl]") @@ -111,9 +118,10 @@ TEST_CASE("ucl: boolean: emit", "[ucl]") TEST_CASE("ucl: boolean: parse and emit", "[ucl]") { auto ucl = nihil::ucl::parse("bool = true;"); + REQUIRE(ucl); auto output = std::string(); - emit(ucl, nihil::ucl::emitter::configuration, + emit(*ucl, nihil::ucl::emitter::configuration, std::back_inserter(output)); REQUIRE(output == "bool = true;\n"); diff --git a/nihil.ucl/tests/emit.cc b/nihil.ucl/tests/emit.cc index a8487c6..d75255b 100644 --- a/nihil.ucl/tests/emit.cc +++ b/nihil.ucl/tests/emit.cc @@ -15,11 +15,13 @@ TEST_CASE("ucl: emit to std::ostream", "[ucl]") using namespace std::literals; auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); + REQUIRE(obj); + auto strm = std::ostringstream(); - strm << obj; + strm << *obj; // The ostream emitter produces JSON. - REQUIRE(strm.str() == std::format("{:j}", obj)); + REQUIRE(strm.str() == std::format("{:j}", *obj)); } TEST_CASE("ucl: emit JSON with std::format", "[ucl]") @@ -27,7 +29,9 @@ TEST_CASE("ucl: emit JSON with std::format", "[ucl]") using namespace std::literals; auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); - auto str = std::format("{:j}", obj); + REQUIRE(obj); + + auto str = std::format("{:j}", *obj); REQUIRE(str == "{\n" @@ -39,7 +43,7 @@ TEST_CASE("ucl: emit JSON with std::format", "[ucl]") "}"); // Make sure JSON is the default format. - auto str2 = std::format("{}", obj); + auto str2 = std::format("{}", *obj); REQUIRE(str == str2); } @@ -48,7 +52,9 @@ TEST_CASE("ucl: emit compact JSON with std::format", "[ucl]") using namespace std::literals; auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); - auto str = std::format("{:J}", obj); + REQUIRE(obj); + + auto str = std::format("{:J}", *obj); REQUIRE(str == "{\"int\":[1,42,666]}"); } @@ -58,7 +64,9 @@ TEST_CASE("ucl: emit configuration with std::format", "[ucl]") using namespace std::literals; auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); - auto str = std::format("{:c}", obj); + REQUIRE(obj); + + auto str = std::format("{:c}", *obj); REQUIRE(str == "int [\n" @@ -73,7 +81,9 @@ TEST_CASE("ucl: emit YAML with std::format", "[ucl]") using namespace std::literals; auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); - auto str = std::format("{:y}", obj); + REQUIRE(obj); + + auto str = std::format("{:y}", *obj); REQUIRE(str == "int: [\n" diff --git a/nihil.ucl/tests/integer.cc b/nihil.ucl/tests/integer.cc index 811a864..05647fe 100644 --- a/nihil.ucl/tests/integer.cc +++ b/nihil.ucl/tests/integer.cc @@ -63,8 +63,11 @@ TEST_CASE("ucl: integer: key()", "[ucl]") { using namespace nihil::ucl; - auto obj = parse("an_int = 42"); - REQUIRE(object_cast<integer>(obj["an_int"]).key() == "an_int"); + auto err = parse("an_int = 42"); + REQUIRE(err); + + auto obj = *err; + REQUIRE(object_cast<integer>(obj["an_int"])->key() == "an_int"); auto i = nihil::ucl::integer(42); REQUIRE(i.key() == ""); @@ -96,7 +99,11 @@ TEST_CASE("ucl: integer: parse", "[ucl]") { using namespace std::literals; - auto obj = nihil::ucl::parse("value = 42"sv); + auto err = nihil::ucl::parse("value = 42"sv); + REQUIRE(err); + + auto obj = *err; + auto v = obj["value"]; REQUIRE(v.key() == "value"); REQUIRE(object_cast<nihil::ucl::integer>(v) == 42); @@ -112,9 +119,10 @@ TEST_CASE("ucl: integer: emit", "[ucl]") TEST_CASE("ucl: integer: parse and emit", "[ucl]") { auto ucl = nihil::ucl::parse("int = 42;"); + REQUIRE(ucl); auto output = std::string(); - emit(ucl, nihil::ucl::emitter::configuration, + emit(*ucl, nihil::ucl::emitter::configuration, std::back_inserter(output)); REQUIRE(output == "int = 42;\n"); diff --git a/nihil.ucl/tests/object.cc b/nihil.ucl/tests/object.cc index 1bbcf4f..3ad180e 100644 --- a/nihil.ucl/tests/object.cc +++ b/nihil.ucl/tests/object.cc @@ -26,12 +26,19 @@ TEST_CASE("ucl object: compare", "[ucl]") using namespace std::literals; auto obj_41 = nihil::ucl::parse("int = 41;"sv); + REQUIRE(obj_41); + auto obj_42 = nihil::ucl::parse("int = 42;"sv); + REQUIRE(obj_42); + auto obj_42_2 = nihil::ucl::parse("int = 42;"sv); + REQUIRE(obj_42_2); + auto obj_43 = nihil::ucl::parse("int = 43;"sv); + REQUIRE(obj_43); - REQUIRE(obj_42 == obj_42_2); - REQUIRE(obj_42 != obj_43); - REQUIRE(obj_42 < obj_43); - REQUIRE(obj_42 > obj_41); + REQUIRE(*obj_42 == *obj_42_2); + REQUIRE(*obj_42 != *obj_43); + REQUIRE(*obj_42 < *obj_43); + REQUIRE(*obj_42 > *obj_41); } diff --git a/nihil.ucl/tests/parse.cc b/nihil.ucl/tests/parse.cc index 3cf5742..c56974e 100644 --- a/nihil.ucl/tests/parse.cc +++ b/nihil.ucl/tests/parse.cc @@ -15,13 +15,18 @@ TEST_CASE("ucl parse: iterate array", "[ucl]") using namespace std::literals; using namespace nihil::ucl; - auto obj = parse("value = [1, 42, 666];"sv); + auto err = parse("value = [1, 42, 666];"sv); + REQUIRE(err); + + auto obj = *err; auto arr = obj["value"]; REQUIRE(arr.key() == "value"); - auto vec = std::vector(std::from_range, - object_cast<array<integer>>(arr)); + auto ints = object_cast<array<integer>>(arr); + REQUIRE(ints); + + auto vec = std::vector(std::from_range, *ints); REQUIRE(vec.size() == 3); REQUIRE(vec[0] == 1); @@ -36,8 +41,9 @@ TEST_CASE("ucl parse: iterate hash", "[ucl]") auto input = "int = 42; bool = true; str = \"test\";"sv; auto obj = parse(input); + REQUIRE(obj); - for (auto &&[key, value] : obj) { + for (auto &&[key, value] : *obj) { REQUIRE(key == value.key()); if (key == "int") diff --git a/nihil.ucl/tests/real.cc b/nihil.ucl/tests/real.cc index b11c113..be4e213 100644 --- a/nihil.ucl/tests/real.cc +++ b/nihil.ucl/tests/real.cc @@ -31,7 +31,7 @@ TEST_CASE("ucl: real: invariants", "[ucl]") TEST_CASE("ucl: real: construct", "[ucl]") { auto obj = nihil::ucl::real(42.1); - REQUIRE_THAT(object_cast<nihil::ucl::real>(obj).value(), + REQUIRE_THAT(object_cast<nihil::ucl::real>(obj)->value(), Catch::Matchers::WithinRel(42.1)); } @@ -80,19 +80,26 @@ TEST_CASE("ucl: real: parse", "[ucl]") { using namespace std::literals; - auto obj = nihil::ucl::parse("value = 42.1"sv); + auto err = nihil::ucl::parse("value = 42.1"sv); + REQUIRE(err); + + auto obj = *err; + auto v = obj["value"]; REQUIRE(v.key() == "value"); - REQUIRE_THAT(object_cast<nihil::ucl::real>(v).value(), + REQUIRE_THAT(object_cast<nihil::ucl::real>(v)->value(), Catch::Matchers::WithinRel(42.1)); } TEST_CASE("ucl: real: emit", "[ucl]") { - auto ucl = nihil::ucl::parse("real = 42.2"); + auto err = nihil::ucl::parse("real = 42.2"); + REQUIRE(err); + + auto obj = *err; auto output = std::string(); - emit(ucl, nihil::ucl::emitter::configuration, + emit(obj, nihil::ucl::emitter::configuration, std::back_inserter(output)); REQUIRE(output == "real = 42.2;\n"); diff --git a/nihil.ucl/tests/string.cc b/nihil.ucl/tests/string.cc index e7eb0ad..995e95a 100644 --- a/nihil.ucl/tests/string.cc +++ b/nihil.ucl/tests/string.cc @@ -109,8 +109,11 @@ TEST_CASE("ucl: string: key()", "[ucl]") { using namespace nihil::ucl; - auto obj = parse("a_string = \"test\""); - REQUIRE(object_cast<string>(obj["a_string"]).key() == "a_string"); + auto err = parse("a_string = \"test\""); + REQUIRE(err); + + auto obj = *err; + REQUIRE(object_cast<string>(obj["a_string"])->key() == "a_string"); auto s = nihil::ucl::string("test"); REQUIRE(s.key() == ""); @@ -188,7 +191,10 @@ TEST_CASE("ucl: string: parse", "[ucl]") { using namespace std::literals; - auto obj = nihil::ucl::parse("value = \"te\\\"st\""sv); + auto err = nihil::ucl::parse("value = \"te\\\"st\""sv); + REQUIRE(err); + + auto obj = *err; auto v = obj["value"]; REQUIRE(v.key() == "value"); REQUIRE(object_cast<nihil::ucl::string>(v) == "te\"st"); @@ -204,9 +210,10 @@ TEST_CASE("ucl: string: emit", "[ucl]") TEST_CASE("ucl: string: parse and emit", "[ucl]") { auto ucl = nihil::ucl::parse("str = \"te\\\"st\";"); + REQUIRE(ucl); auto output = std::string(); - emit(ucl, nihil::ucl::emitter::configuration, + emit(*ucl, nihil::ucl::emitter::configuration, std::back_inserter(output)); REQUIRE(output == "str = \"te\\\"st\";\n"); |
