diff options
Diffstat (limited to 'nihil.ucl')
33 files changed, 4877 insertions, 0 deletions
diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt new file mode 100644 index 0000000..9d8ab3a --- /dev/null +++ b/nihil.ucl/CMakeLists.txt @@ -0,0 +1,46 @@ +# This source code is released into the public domain. + +pkg_check_modules(LIBUCL REQUIRED libucl) + +add_library(nihil.ucl STATIC) +target_link_libraries(nihil.ucl PRIVATE nihil.error nihil.monad) + +target_sources(nihil.ucl + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.ucl.ccm + emit.ccm + errc.ccm + object.ccm + object_cast.ccm + parser.ccm + type.ccm + + array.ccm + boolean.ccm + integer.ccm + map.ccm + real.ccm + string.ccm + + PRIVATE + emit.cc + errc.cc + parser.cc + type.cc + + object.cc + boolean.cc + integer.cc + real.cc + string.cc +) + +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}) + +if(NIHIL_TESTS) + add_subdirectory(tests) + enable_testing() +endif() diff --git a/nihil.ucl/array.ccm b/nihil.ucl/array.ccm new file mode 100644 index 0000000..e3730ab --- /dev/null +++ b/nihil.ucl/array.ccm @@ -0,0 +1,468 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cassert> +#include <cerrno> +#include <cstdint> +#include <cstdlib> +#include <format> +#include <iostream> +#include <string> +#include <system_error> +#include <utility> + +#include <ucl.h> + +export module nihil.ucl:array; + +import :object; + +namespace nihil::ucl { + +export template<datatype T> +struct array; + +export template<datatype T> +struct array_iterator { + using difference_type = std::ptrdiff_t; + using value_type = T; + using reference = T&; + using pointer = T*; + + array_iterator() = default; + + [[nodiscard]] auto operator* (this array_iterator const &self) -> T + { + auto arr = self.get_array(); + if (self.m_idx >= ::ucl_array_size(arr)) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "access past end of array"); + + auto uobj = ::ucl_array_find_index(arr, self.m_idx); + if (uobj == nullptr) + throw std::runtime_error( + "nihil::ucl::array_iterator: " + "failed to fetch UCL array index"); + + return T(nihil::ucl::ref, uobj); + } + + [[nodiscard]] auto operator[] (this array_iterator const &self, + difference_type idx) + -> T + { + return *(self + idx); + } + + auto operator++ (this array_iterator &self) -> array_iterator & + { + auto arr = self.get_array(); + if (self.m_idx == ::ucl_array_size(arr)) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "iterating past end of array"); + + ++self.m_idx; + return self; + } + + auto operator++ (this array_iterator &self, int) -> array_iterator + { + auto copy = self; + ++self; + return copy; + } + + auto operator-- (this array_iterator &self) -> array_iterator& + { + if (self.m_idx == 0) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "iterating before start of array"); + --self.m_idx; + return self; + } + + auto operator-- (this array_iterator &self, int) -> array_iterator + { + auto copy = self; + --self; + return copy; + } + + [[nodiscard]] auto operator== (this array_iterator const &lhs, + array_iterator const &rhs) + -> bool + { + // Empty iterators should compare equal. + if (lhs.m_array == nullptr && rhs.m_array == nullptr) + return true; + + if (lhs.get_array() != rhs.get_array()) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "comparing iterators of different arrays"); + + return lhs.m_idx == rhs.m_idx; + } + + [[nodiscard]] auto operator<=> (this array_iterator const &lhs, + array_iterator const &rhs) + { + // Empty iterators should compare equal. + if (lhs.m_array == nullptr && rhs.m_array == nullptr) + return std::strong_ordering::equal; + + if (lhs.get_array() != rhs.get_array()) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "comparing iterators of different arrays"); + + return lhs.m_idx <=> rhs.m_idx; + } + + auto operator+= (this array_iterator &lhs, difference_type rhs) + -> array_iterator & + { + auto arr = lhs.get_array(); + // m_idx cannot be greater than the array size + auto max_inc = ::ucl_array_size(arr) - lhs.m_idx; + + if (std::cmp_greater(rhs, max_inc)) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "iterating past end of array"); + + lhs.m_idx += rhs; + return lhs; + } + + auto operator-= (this array_iterator &lhs, difference_type rhs) + -> array_iterator & + { + if (std::cmp_greater(rhs, lhs.m_idx)) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "iterating before start of array"); + lhs.m_idx -= rhs; + return lhs; + } + + [[nodiscard]] auto operator- (this array_iterator const &lhs, + array_iterator const &rhs) + -> difference_type + { + if (lhs.get_array() != rhs.get_array()) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "comparing iterators of different arrays"); + + return lhs.m_idx - rhs.m_idx; + } + +private: + friend struct array<T>; + + ::ucl_object_t const * m_array{}; + std::size_t m_idx{}; + + [[nodiscard]] auto get_array(this array_iterator const &self) + -> ::ucl_object_t const * + { + if (self.m_array == nullptr) + throw std::logic_error( + "nihil::ucl::array_iterator: " + "attempt to access an empty iterator"); + + return self.m_array; + } + + array_iterator(::ucl_object_t const *array, std::size_t idx) + : m_array(array) + , m_idx(idx) + {} +}; + +export template<datatype T> [[nodiscard]] +auto operator+(array_iterator<T> const &lhs, + typename array_iterator<T>::difference_type rhs) +-> array_iterator<T> +{ + auto copy = lhs; + copy += rhs; + return copy; +} + +export template<datatype T> [[nodiscard]] +auto operator+(typename array_iterator<T>::difference_type lhs, + array_iterator<T> const &rhs) + -> array_iterator<T> +{ + return rhs - lhs; +} + +export template<datatype T> [[nodiscard]] +auto operator-(array_iterator<T> const &lhs, + typename array_iterator<T>::difference_type rhs) + -> array_iterator<T> +{ + auto copy = lhs; + copy -= rhs; + return copy; +} + +export template<datatype T = object> +struct array final : object { + inline static constexpr object_type ucl_type = object_type::array; + + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using iterator = array_iterator<T>; + + /* + * Create an empty array. Throws std::system_error on failure. + */ + array() : object(noref, [] { + auto *uobj = ::ucl_object_typed_new(UCL_ARRAY); + if (uobj == nullptr) + throw std::system_error( + std::make_error_code(std::errc(errno))); + return uobj; + }()) + { + } + + /* + * Create an array from a UCL object. Throws type_mismatch + * on failure. + * + * Unlike object_cast<>, this does not check the type of the contained + * elements, which means object access can throw type_mismatch. + */ + array(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != array::ucl_type) + throw type_mismatch(array::ucl_type, + actual_type); + return uobj; + }()) + { + } + + array(noref_t, ::ucl_object_t *uobj) + : object(nihil::ucl::noref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != array::ucl_type) + throw type_mismatch(array::ucl_type, + actual_type); + return uobj; + }()) + { + } + + /* + * Create an array from an iterator pair. + */ + template<std::input_iterator Iterator> + requires(std::convertible_to<std::iter_value_t<Iterator>, T>) + array(Iterator first, Iterator last) + : array() + { + // This is exception safe, because if we throw here the + // base class destructor will free the array. + while (first != last) { + push_back(*first); + ++first; + } + } + + /* + * Create an array from a range. + */ + template<std::ranges::range Range> + requires(std::convertible_to<std::ranges::range_value_t<Range>, T>) + array(std::from_range_t, Range &&range) + : array(std::ranges::begin(range), + std::ranges::end(range)) + { + } + + /* + * Create an array from an initializer_list. + */ + array(std::initializer_list<T> const &list) + : array(std::ranges::begin(list), + std::ranges::end(list)) + { + } + + /* + * Array iterator access. + */ + + [[nodiscard]] auto begin(this array const &self) -> iterator + { + return {self.get_ucl_object(), 0}; + } + + [[nodiscard]] auto end(this array const &self) -> iterator + { + return {self.get_ucl_object(), self.size()}; + } + + /* + * Return the size of this array. + */ + [[nodiscard]] auto size(this array const &self) -> size_type + { + return ::ucl_array_size(self.get_ucl_object()); + } + + /* + * Test if this array is empty. + */ + [[nodiscard]] auto empty(this array const &self) -> bool + { + return self.size() == 0; + } + + /* + * Reserve space for future insertions. + */ + auto reserve(this array &self, size_type nelems) -> void + { + ::ucl_object_reserve(self.get_ucl_object(), nelems); + } + + /* + * Append an element to the array. + */ + auto push_back(this array &self, value_type const &v) -> void + { + auto uobj = ::ucl_object_ref(v.get_ucl_object()); + ::ucl_array_append(self.get_ucl_object(), uobj); + } + + /* + * Prepend an element to the array. + */ + auto push_front(this array &self, value_type const &v) -> void + { + auto uobj = ::ucl_object_ref(v.get_ucl_object()); + ::ucl_array_prepend(self.get_ucl_object(), uobj); + } + + /* + * Access an array element by index. + */ + [[nodiscard]] auto at(this array const &self, size_type idx) -> T + { + if (idx >= self.size()) + throw std::out_of_range("UCL array index out of range"); + + auto uobj = ::ucl_array_find_index(self.get_ucl_object(), idx); + if (uobj == nullptr) + throw std::runtime_error( + "failed to fetch UCL array index"); + + return T(nihil::ucl::ref, uobj); + } + + [[nodiscard]] auto operator[] (this array const &self, size_type idx) -> T + { + return self.at(idx); + } + + /* + * Return the first element. + */ + [[nodiscard]] auto front(this array const &self) -> T + { + return self.at(0); + } + + /* + * Return the last element. + */ + [[nodiscard]] auto back(this array const &self) -> T + { + if (self.empty()) + throw std::out_of_range("attempt to access back() on " + "empty UCL array"); + return self.at(self.size() - 1); + } +}; + +/* + * Comparison operators. + */ + +export template<datatype T> [[nodiscard]] +auto operator==(array<T> const &a, array<T> const &b) -> bool +{ + if (a.size() != b.size()) + return false; + + for (typename array<T>::size_type i = 0; i < a.size(); ++i) + if (a.at(i) != b.at(i)) + return false; + + return true; +} + +/* + * Print an array to an ostream; uses the same format as std::format(). + */ +export template<datatype T> +auto operator<<(std::ostream &strm, array<T> const &a) -> std::ostream & +{ + return strm << std::format("{}", a); +} + +} // namespace nihil::ucl + +/* + * std::formatter for an array. The output format is a list of values + * on a single line: [1, 2, 3]. + */ +export template<typename T> +struct std::formatter<nihil::ucl::array<T>, char> +{ + template<class ParseContext> + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + return ctx.begin(); + } + + template<class FmtContext> + FmtContext::iterator format(nihil::ucl::array<T> const &o, + FmtContext& ctx) const + { + auto it = ctx.out(); + bool first = true; + + *it++ = '['; + + for (auto &&elm : o) { + if (first) + first = false; + else { + *it++ = ','; + *it++ = ' '; + } + + it = std::format_to(it, "{}", elm); + } + + *it++ = ']'; + return it; + } +}; diff --git a/nihil.ucl/boolean.cc b/nihil.ucl/boolean.cc new file mode 100644 index 0000000..91f2b17 --- /dev/null +++ b/nihil.ucl/boolean.cc @@ -0,0 +1,106 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <compare> +#include <cstdlib> +#include <expected> +#include <system_error> + +#include <ucl.h> + +module nihil.ucl; + +import nihil.error; + +namespace nihil::ucl { + +auto make_boolean(boolean::contained_type value) + -> std::expected<boolean, error> +{ + auto *uobj = ::ucl_object_frombool(value); + if (uobj == nullptr) + return std::unexpected(error( + errc::failed_to_create_object, + error(std::errc(errno)))); + + return boolean(noref, uobj); +} + +boolean::boolean() + : boolean(false) +{ +} + +boolean::boolean(contained_type value) + : object(noref, [&] { + auto *uobj = ::ucl_object_frombool(value); + if (uobj == nullptr) + throw std::system_error( + std::make_error_code(std::errc(errno))); + return uobj; + }()) +{ +} + +boolean::boolean(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != boolean::ucl_type) + throw type_mismatch(boolean::ucl_type, actual_type); + return uobj; + }()) +{ +} + +boolean::boolean(noref_t, ::ucl_object_t *uobj) + : object(nihil::ucl::noref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != boolean::ucl_type) + throw type_mismatch(boolean::ucl_type, actual_type); + return uobj; + }()) +{ +} + +auto boolean::value(this boolean const &self) + -> contained_type +{ + auto v = contained_type{}; + auto const *uobj = self.get_ucl_object(); + + if (::ucl_object_toboolean_safe(uobj, &v)) + return v; + + std::abort(); +} + +auto operator== (boolean const &a, boolean const &b) + -> bool +{ + return a.value() == b.value(); +} + +auto operator<=> (boolean const &a, boolean const &b) + -> std::strong_ordering +{ + return a.value() <=> b.value(); +} + +auto operator== (boolean const &a, boolean::contained_type b) + -> bool +{ + return a.value() == b; +} + +auto operator<=> (boolean const &a, boolean::contained_type b) + -> std::strong_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/boolean.ccm b/nihil.ucl/boolean.ccm new file mode 100644 index 0000000..068dfdd --- /dev/null +++ b/nihil.ucl/boolean.ccm @@ -0,0 +1,91 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cassert> +#include <cstdint> +#include <cstdlib> +#include <expected> +#include <format> +#include <string> + +#include <ucl.h> + +export module nihil.ucl:boolean; + +import :object; + +namespace nihil::ucl { + +export struct boolean final : object { + using contained_type = bool; + + inline static constexpr object_type ucl_type = object_type::boolean; + + /* + * Create a boolean holding the value false. Throws std::system_error + * on failure. + */ + boolean(); + + /* + * Create a boolean holding a specific value. Throws std::system_error + * on failure. + */ + explicit boolean(bool); + + /* + * Create a new boolean from a UCL object. Throws type_mismatch + * on failure. + */ + boolean(ref_t, ::ucl_object_t const *uobj); + boolean(noref_t, ::ucl_object_t *uobj); + + // Return this object's value. + auto value(this boolean const &self) -> contained_type; +}; + +/* + * Boolean constructors. These return an error instead of throwing. + */ + +export [[nodiscard]] auto +make_boolean(boolean::contained_type = false) -> std::expected<boolean, error>; + +/* + * Comparison operators. + */ + +export auto operator== (boolean const &a, boolean const &b) -> bool; +export auto operator== (boolean const &a, boolean::contained_type b) -> bool; +export auto operator<=> (boolean const &a, boolean const &b) + -> std::strong_ordering; +export auto operator<=> (boolean const &a, boolean::contained_type b) + -> std::strong_ordering; + +} // namespace nihil::ucl + +/* + * std::formatter for a boolean. This provides the same format operations + * as std::formatter<bool>. + */ +export template<> +struct std::formatter<nihil::ucl::boolean, char> +{ + std::formatter<bool> base_formatter; + + template<class ParseContext> + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + return base_formatter.parse(ctx); + } + + template<class FmtContext> + FmtContext::iterator format(nihil::ucl::boolean const &o, + FmtContext& ctx) const + { + return base_formatter.format(o.value(), ctx); + } +}; diff --git a/nihil.ucl/emit.cc b/nihil.ucl/emit.cc new file mode 100644 index 0000000..480ddd8 --- /dev/null +++ b/nihil.ucl/emit.cc @@ -0,0 +1,21 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <iostream> +#include <iterator> + +module nihil.ucl; + +namespace nihil::ucl { + +auto operator<<(std::ostream &stream, object const &o) +-> std::ostream & +{ + emit(o, emitter::json, std::ostream_iterator<char>(stream)); + return stream; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/emit.ccm b/nihil.ucl/emit.ccm new file mode 100644 index 0000000..b88f8e7 --- /dev/null +++ b/nihil.ucl/emit.ccm @@ -0,0 +1,209 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <array> +#include <charconv> +#include <cstdlib> +#include <format> +#include <iterator> +#include <iosfwd> +#include <span> +#include <string> +#include <utility> + +#include <ucl.h> + +export module nihil.ucl:emit; + +import :object; + +namespace nihil::ucl { + +export enum struct emitter { + configuration = UCL_EMIT_CONFIG, + compact_json = UCL_EMIT_JSON_COMPACT, + json = UCL_EMIT_JSON, + yaml = UCL_EMIT_YAML, +}; + +/* + * Wrap ucl_emitter_functions for a particular output iterator type. + * + * We can't throw exceptions here since we're called from C code. The emit + * functions return an integer value, but it's not really clear what this is + * for and the C API seems to mostly ignore it. So, we just eat errors and + * keep going. + */ +template<std::output_iterator<char> Iterator> +struct emit_wrapper { + emit_wrapper(Iterator iterator_) + : iterator(std::move(iterator_)) + {} + + static auto append_character(unsigned char c, std::size_t nchars, + void *ud) + noexcept -> int + try { + auto *self = static_cast<emit_wrapper *>(ud); + + while (nchars--) + *self->iterator++ = static_cast<char>(c); + + return 0; + } catch (...) { + return 0; + } + + static auto append_len(unsigned char const *str, std::size_t len, + void *ud) + noexcept -> int + try { + auto *self = static_cast<emit_wrapper *>(ud); + + for (auto c : std::span(str, len)) + *self->iterator++ = static_cast<char>(c); + + return 0; + } catch (...) { + return 0; + } + + static auto append_int(std::int64_t value, void *ud) + noexcept -> int + try { + auto constexpr bufsize = + std::numeric_limits<std::int64_t>::digits10; + auto buf = std::array<char, bufsize>(); + + auto *self = static_cast<emit_wrapper *>(ud); + auto result = std::to_chars(buf.data(), buf.data() + buf.size(), + value, 10); + + if (result.ec == std::errc()) + for (auto c : std::span(buf.data(), result.ptr)) + *self->iterator++ = c; + + return 0; + } catch (...) { + return 0; + } + + static auto append_double(double value, void *ud) + noexcept -> int + try { + auto constexpr bufsize = + std::numeric_limits<double>::digits10; + auto buf = std::array<char, bufsize>(); + + auto *self = static_cast<emit_wrapper *>(ud); + auto result = std::to_chars(buf.data(), buf.data() + buf.size(), + value); + + if (result.ec == std::errc()) + for (auto c : std::span(buf.data(), result.ptr)) + *self->iterator++ = c; + + return 0; + } catch (...) { + return 0; + } + + auto get_functions(this emit_wrapper &self) -> ucl_emitter_functions + { + auto ret = ucl_emitter_functions{}; + + ret.ucl_emitter_append_character = &emit_wrapper::append_character; + ret.ucl_emitter_append_len = &emit_wrapper::append_len; + ret.ucl_emitter_append_int = &emit_wrapper::append_int; + ret.ucl_emitter_append_double = &emit_wrapper::append_double; + ret.ud = &self; + + return ret; + } + +private: + Iterator iterator{}; +}; + +export auto emit(object const &object, emitter format, + std::output_iterator<char> auto &&it) + -> void +{ + auto ucl_format = static_cast<ucl_emitter>(format); + auto wrapper = emit_wrapper(it); + auto functions = wrapper.get_functions(); + + ::ucl_object_emit_full(object.get_ucl_object(), ucl_format, + &functions, nullptr); +} + +/* + * Basic ostream printer for UCL; default to JSON since it's probably what + * most people expect. + */ +export auto operator<<(std::ostream &, object const &) -> std::ostream &; + +} // namespace nihil::ucl + +/* + * Specialisation of std::formatter<> for object. + */ +template<std::derived_from<nihil::ucl::object> T> +struct std::formatter<T, char> +{ + nihil::ucl::emitter emitter = nihil::ucl::emitter::json; + + template<class ParseContext> + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + auto it = ctx.begin(); + auto end = ctx.end(); + + while (it != end) { + switch (*it) { + case 'j': + emitter = nihil::ucl::emitter::json; + break; + case 'J': + emitter = nihil::ucl::emitter::compact_json; + break; + case 'c': + emitter = nihil::ucl::emitter::configuration; + break; + case 'y': + emitter = nihil::ucl::emitter::yaml; + break; + case '}': + return it; + default: + throw std::format_error("Invalid format string " + "for UCL object"); + } + + ++it; + } + + return it; + } + + template<class FmtContext> + FmtContext::iterator format(nihil::ucl::object const &o, + FmtContext& ctx) const + { + // We can't use emit() here since the context iterator is not + // an std::output_iterator. + + auto out = ctx.out(); + + auto ucl_format = static_cast<::ucl_emitter>(emitter); + auto wrapper = nihil::ucl::emit_wrapper(out); + auto functions = wrapper.get_functions(); + + ::ucl_object_emit_full(o.get_ucl_object(), ucl_format, + &functions, nullptr); + return out; + } +}; diff --git a/nihil.ucl/errc.cc b/nihil.ucl/errc.cc new file mode 100644 index 0000000..0b65b86 --- /dev/null +++ b/nihil.ucl/errc.cc @@ -0,0 +1,49 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <string> +#include <system_error> + +module nihil.ucl; + +namespace nihil::ucl { + +struct ucl_error_category final : std::error_category { + auto name() const noexcept -> char const * override; + auto message(int err) const -> std::string override; +}; + +auto ucl_category() noexcept -> std::error_category & +{ + static auto category = ucl_error_category(); + return category; +} + +auto make_error_condition(errc ec) -> std::error_condition +{ + return {static_cast<int>(ec), ucl_category()}; +} + +auto ucl_error_category::name() const noexcept -> char const * +{ + return "nihil.ucl"; +} + +auto ucl_error_category::message(int err) const -> std::string +{ + switch (static_cast<errc>(err)) { + case errc::no_error: + return "No error"; + case errc::failed_to_create_object: + return "Failed to create UCL object"; + case errc::type_mismatch: + return "UCL type does not match expected type"; + default: + return "Undefined error"; + } +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/errc.ccm b/nihil.ucl/errc.ccm new file mode 100644 index 0000000..8f0444d --- /dev/null +++ b/nihil.ucl/errc.ccm @@ -0,0 +1,33 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <string> +#include <system_error> + +export module nihil.ucl:errc; + +namespace nihil::ucl { + +export enum struct errc { + no_error = 0, + + // ucl_object_new() or similar failed, e.g. out of memory + failed_to_create_object, + // Trying to create an object from a UCL object of the wrong type + type_mismatch, +}; + +export auto ucl_category() noexcept -> std::error_category &; +export auto make_error_condition(errc ec) -> std::error_condition; + +} // namespace nihil::ucl + +namespace std { + +export template<> +struct is_error_condition_enum<nihil::ucl::errc> : true_type {}; + +} // namespace std diff --git a/nihil.ucl/integer.cc b/nihil.ucl/integer.cc new file mode 100644 index 0000000..825d8f6 --- /dev/null +++ b/nihil.ucl/integer.cc @@ -0,0 +1,102 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <compare> +#include <cstdlib> +#include <expected> +#include <system_error> + +#include <ucl.h> + +module nihil.ucl; + +import nihil.error; + +namespace nihil::ucl { + +integer::integer() + : integer(0) +{ +} + +integer::integer(contained_type value) + : integer(noref, [&] { + auto *uobj = ::ucl_object_fromint(value); + if (uobj == nullptr) + throw std::system_error( + std::make_error_code(std::errc(errno))); + return uobj; + }()) +{ +} + +integer::integer(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != integer::ucl_type) + throw type_mismatch(integer::ucl_type, actual_type); + return uobj; + }()) +{ +} + +integer::integer(noref_t, ::ucl_object_t *uobj) + : object(nihil::ucl::noref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != integer::ucl_type) + throw type_mismatch(integer::ucl_type, actual_type); + return uobj; + }()) +{ +} + +auto make_integer(integer::contained_type value) + -> std::expected<integer, error> +{ + auto *uobj = ::ucl_object_fromint(value); + if (uobj == nullptr) + return std::unexpected(error( + errc::failed_to_create_object, + error(std::errc(errno)))); + + return integer(noref, uobj); +} + +auto integer::value(this integer const &self) -> contained_type +{ + auto v = contained_type{}; + auto const *uobj = self.get_ucl_object(); + + if (::ucl_object_toint_safe(uobj, &v)) + return v; + + std::abort(); +} + +auto operator== (integer const &a, integer const &b) -> bool +{ + return a.value() == b.value(); +} + +auto operator<=> (integer const &a, integer const &b) -> std::strong_ordering +{ + return a.value() <=> b.value(); +} + +auto operator== (integer const &a, integer::contained_type b) -> bool +{ + return a.value() == b; +} + +auto operator<=> (integer const &a, integer::contained_type b) + -> std::strong_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/integer.ccm b/nihil.ucl/integer.ccm new file mode 100644 index 0000000..e35a471 --- /dev/null +++ b/nihil.ucl/integer.ccm @@ -0,0 +1,115 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <compare> +#include <cstdint> +#include <cstdlib> +#include <expected> +#include <format> +#include <utility> + +#include <ucl.h> + +export module nihil.ucl:integer; + +import :object; +import :type; + +namespace nihil::ucl { + +export struct integer final : object { + using contained_type = std::int64_t; + inline static constexpr object_type ucl_type = object_type::integer; + + /* + * Create an integer holding the value 0. Throws std::system_error + * on failure. + */ + integer(); + + /* + * Create an integer holding a specific value. Throws std::system_error + * on failure. + */ + explicit integer(contained_type value); + + /* + * Create a new integer from a UCL object. Throws type_mismatch + * on failure. + */ + integer(ref_t, ::ucl_object_t const *uobj); + integer(noref_t, ::ucl_object_t *uobj); + + // Return the value of this object. + [[nodiscard]] auto value(this integer const &self) -> contained_type; +}; + +/* + * Integer constructors. These return an error instead of throwing. + */ + +export [[nodiscard]] auto +make_integer(integer::contained_type = 0) -> std::expected<integer, error>; + +/* + * Comparison operators. + */ + +export [[nodiscard]] auto operator== (integer const &a, + integer const &b) -> bool; + +export [[nodiscard]] auto operator== (integer const &a, + integer::contained_type b) -> bool; + +export [[nodiscard]] auto operator<=> (integer const &a, + integer const &b) + -> std::strong_ordering; + +export [[nodiscard]] auto operator<=> (integer const &a, + integer::contained_type b) + -> std::strong_ordering; + +/* + * Literal operator. + */ +inline namespace literals { +export constexpr auto operator""_ucl (unsigned long long i) -> integer +{ + if (std::cmp_greater(i, std::numeric_limits<std::int64_t>::max())) + throw std::out_of_range("literal out of range"); + + return integer(static_cast<std::int64_t>(i)); +} +} // namespace nihil::ucl::literals + +} // namespace nihil::ucl + +namespace nihil { inline namespace literals { + export using namespace ::nihil::ucl::literals; +}} // namespace nihil::literals + +/* + * std::formatter for an integer. This provides the same format operations + * as std::formatter<std::int64_t>. + */ +export template<> +struct std::formatter<nihil::ucl::integer, char> +{ + std::formatter<std::int64_t> base_formatter; + + template<class ParseContext> + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + return base_formatter.parse(ctx); + } + + template<class FmtContext> + FmtContext::iterator format(nihil::ucl::integer const &o, + FmtContext& ctx) const + { + return base_formatter.format(o.value(), ctx); + } +}; diff --git a/nihil.ucl/map.ccm b/nihil.ucl/map.ccm new file mode 100644 index 0000000..fa77601 --- /dev/null +++ b/nihil.ucl/map.ccm @@ -0,0 +1,293 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cassert> +#include <cstdint> +#include <cstdlib> +#include <format> +#include <memory> +#include <optional> +#include <string> +#include <system_error> + +#include <ucl.h> + +export module nihil.ucl:map; + +import :object; + +namespace nihil::ucl { + +// Exception thrown when map::operator[] does not find the key. +export struct key_not_found : error { + key_not_found(std::string_view key) + : error(std::format("key '{}' not found in map", key)) + , m_key(key) + {} + + auto key(this key_not_found const &self) -> std::string_view + { + return self.m_key; + } + +private: + std::string m_key; +}; + +export template<datatype T> +struct map; + +template<datatype T> +struct map_iterator { + using difference_type = std::ptrdiff_t; + using value_type = std::pair<std::string_view, T>; + using reference = value_type &; + using const_reference = value_type const &; + using pointer = value_type *; + using const_pointer = value_type const *; + + struct sentinel{}; + + [[nodiscard]] auto operator==(this map_iterator const &self, sentinel) + -> bool + { + return (self.m_state->cur == nullptr); + } + + auto operator++(this map_iterator &self) -> map_iterator & + { + self.m_state->next(); + return self; + } + + auto operator++(this map_iterator &self, int) -> map_iterator & + { + self.m_state->next(); + return self; + } + + [[nodiscard]] auto operator*(this map_iterator const &self) + -> value_type + { + auto obj = T(ref, self.m_state->cur); + return {obj.key(), std::move(obj)}; + } + +private: + friend struct map<T>; + + map_iterator(::ucl_object_t const *obj) + : m_state(std::make_shared<state>(obj)) + { + ++(*this); + } + + struct state { + state(::ucl_object_t const *obj) + { + iter = ::ucl_object_iterate_new(obj); + if (iter == nullptr) + throw std::system_error(make_error_code( + std::errc(errno))); + } + + state(state const &) = delete; + auto operator=(this state &, state const &) -> state& = delete; + + ~state() + { + if (iter != nullptr) + ::ucl_object_iterate_free(iter); + } + + auto next() -> void + { + cur = ::ucl_object_iterate_safe(iter, true); + } + + ucl_object_iter_t iter = nullptr; + ucl_object_t const *cur = nullptr; + }; + + std::shared_ptr<state> m_state; +}; + +export template<datatype T = object> +struct map final : object { + inline static constexpr object_type ucl_type = object_type::object; + + using value_type = std::pair<std::string_view, T>; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using iterator = map_iterator<T>; + + /* + * Create an empty map. Throws std::system_error on failure. + */ + map() : object(noref, [] { + auto *uobj = ::ucl_object_typed_new(UCL_OBJECT); + if (uobj == nullptr) + throw std::system_error( + std::make_error_code(std::errc(errno))); + return uobj; + }()) + { + } + + /* + * Create a map from a UCL object. Throws type_mismatch on failure. + * + * Unlike object_cast<>, this does not check the type of the contained + * elements, which means object access can throw type_mismatch. + */ + map(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != map::ucl_type) + throw type_mismatch(map::ucl_type, + actual_type); + return uobj; + }()) + { + if (type() != ucl_type) + throw type_mismatch(ucl_type, type()); + } + + map(noref_t, ::ucl_object_t *uobj) + : object(nihil::ucl::noref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != map::ucl_type) + throw type_mismatch(map::ucl_type, + actual_type); + return uobj; + }()) + { + } + + /* + * Create a map from an iterator pair. + */ + template<std::input_iterator Iterator> + requires(std::convertible_to<std::iter_value_t<Iterator>, value_type>) + map(Iterator first, Iterator last) + : map() + { + // This is exception safe, because if we throw here the + // base class destructor will free the map. + while (first != last) { + insert(*first); + ++first; + } + } + + /* + * Create a map from a range. + */ + template<std::ranges::range Range> + requires(std::convertible_to<std::ranges::range_value_t<Range>, + value_type>) + map(std::from_range_t, Range &&range) + : map(std::ranges::begin(range), + std::ranges::end(range)) + { + } + + /* + * Create a map from an initializer_list. + */ + map(std::initializer_list<value_type> const &list) + : map(std::ranges::begin(list), std::ranges::end(list)) + { + } + + /* + * Map iterator access. + */ + + [[nodiscard]] auto begin(this map const &self) -> iterator + { + return {self.get_ucl_object()}; + } + + [[nodiscard]] auto end(this map const &) -> iterator::sentinel + { + return {}; + } + + /* + * Reserve space for future insertions. + */ + auto reserve(this map &self, size_type nelems) -> void + { + ::ucl_object_reserve(self.get_ucl_object(), nelems); + } + + /* + * Add an element to the map. + */ + auto insert(this map &self, value_type const &v) -> void + { + auto uobj = ::ucl_object_ref(v.second.get_ucl_object()); + + ::ucl_object_insert_key(self.get_ucl_object(), uobj, + v.first.data(), v.first.size(), true); + } + + /* + * Access a map element by key. + */ + [[nodiscard]] auto find(this map const &self, std::string_view key) + -> std::optional<T> + { + auto const *obj = ::ucl_object_lookup_len( + self.get_ucl_object(), + key.data(), key.size()); + if (obj == nullptr) + return {}; + + return {T(nihil::ucl::ref, obj)}; + } + + /* + * Remove an object from the map. + */ + auto remove(this map &self, std::string_view key) -> bool + { + return ::ucl_object_delete_keyl(self.get_ucl_object(), + key.data(), key.size()); + } + + /* + * Remove an object from the map and return it. + */ + auto pop(this map &self, std::string_view key) + -> std::optional<T> + { + auto *uobj = ::ucl_object_pop_keyl(self.get_ucl_object(), + key.data(), key.size()); + if (uobj) + return T(noref, uobj); + return {}; + } + + /* + * Equivalent to find(), except it throws key_not_found if the key + * doesn't exist in the map. + */ + [[nodiscard]] auto operator[] (this map const &self, + std::string_view key) + -> T + { + auto obj = self.find(key); + if (obj) + return *obj; + throw key_not_found(key); + } +}; + +} // namespace nihil::ucl diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm new file mode 100644 index 0000000..b16eb3d --- /dev/null +++ b/nihil.ucl/nihil.ucl.ccm @@ -0,0 +1,21 @@ +/* + * This source code is released into the public domain. + */ + +module; + +export module nihil.ucl; + +export import :emit; +export import :errc; +export import :object; +export import :object_cast; +export import :parser; +export import :type; + +export import :array; +export import :boolean; +export import :integer; +export import :map; +export import :real; +export import :string; diff --git a/nihil.ucl/object.cc b/nihil.ucl/object.cc new file mode 100644 index 0000000..53fc4c7 --- /dev/null +++ b/nihil.ucl/object.cc @@ -0,0 +1,114 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdlib> +#include <string> +#include <utility> + +#include <ucl.h> + +module nihil.ucl; + +namespace nihil::ucl { + +object::object(ref_t, ::ucl_object_t const *object) + : m_object(::ucl_object_ref(object)) +{ +} + +object::object(noref_t, ::ucl_object_t *object) + : m_object(object) +{ +} + +object::~object() { + if (m_object != nullptr) + ::ucl_object_unref(m_object); +} + +object::object(object &&other) noexcept + : m_object(std::exchange(other.m_object, nullptr)) +{} + +object::object(object const &other) + : m_object(nullptr) +{ + m_object = ::ucl_object_copy(other.get_ucl_object()); + if (m_object == nullptr) + throw std::runtime_error("failed to copy UCL object"); +} + +auto object::operator=(this object &self, object &&other) noexcept + -> object & +{ + if (&self != &other) + self.m_object = std::exchange(other.m_object, nullptr); + return self; +} + +auto object::operator=(this object &self, object const &other) -> object & +{ + return self = object(other); +} + +auto object::ref(this object const &self) -> object +{ + return object(nihil::ucl::ref, self.get_ucl_object()); +} + +auto object::type(this object const &self) -> object_type +{ + auto utype = ::ucl_object_type(self.get_ucl_object()); + return static_cast<object_type>(utype); +} + +auto object::get_ucl_object(this object &self) -> ::ucl_object_t * +{ + if (self.m_object == nullptr) + throw std::logic_error("attempt to access empty UCL object"); + return self.m_object; +} + +auto object::get_ucl_object(this object const &self) -> ::ucl_object_t const * +{ + if (self.m_object == nullptr) + throw std::logic_error("attempt to access empty UCL object"); + return self.m_object; +} + +// Return the key of this object. +auto object::key(this object const &self) -> std::string_view +{ + auto dlen = std::size_t{}; + auto const *dptr = ::ucl_object_keyl(self.get_ucl_object(), + &dlen); + return {dptr, dlen}; +} + +auto swap(object &a, object &b) -> void +{ + std::swap(a.m_object, b.m_object); +} + +auto operator<=>(object const &lhs, object const &rhs) -> std::strong_ordering +{ + auto cmp = ::ucl_object_compare(lhs.get_ucl_object(), + rhs.get_ucl_object()); + + if (cmp < 0) + return std::strong_ordering::less; + else if (cmp > 0) + return std::strong_ordering::greater; + else + return std::strong_ordering::equal; +} + +auto operator==(object const &lhs, object const &rhs) -> bool +{ + return (lhs <=> rhs) == std::strong_ordering::equal; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm new file mode 100644 index 0000000..9a7eaf7 --- /dev/null +++ b/nihil.ucl/object.ccm @@ -0,0 +1,88 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * A UCL object. The object is immutable and internally refcounted, so it + * may be copied as needed. + * + */ + +#include <compare> +#include <cstddef> +#include <string> + +#include <ucl.h> + +export module nihil.ucl:object; + +import :type; + +namespace nihil::ucl { + +/*********************************************************************** + * The basic object type. + */ + +// Ref the UCL object when creating an object. +export inline constexpr struct ref_t {} ref; +// Don't ref the UCL object. +export inline constexpr struct noref_t {} noref; + +export struct object { + inline static constexpr object_type ucl_type = object_type::object; + + // Create an object from an existing ucl_object_t. The first argument + // determines whether we ref the object or not. + + object(ref_t, ::ucl_object_t const *object); + object(noref_t, ::ucl_object_t *object); + + // Free our object on destruction. + virtual ~object(); + + // Movable. + object(object &&other) noexcept; + auto operator=(this object &self, object &&other) noexcept -> object&; + + // Copyable. + // Note that this copies the entire UCL object. + object(object const &other); + auto operator=(this object &self, object const &other) -> object &; + + // Increase the refcount of this object. + [[nodiscard]] auto ref(this object const &self) -> object; + + // Return the type of this object. + [[nodiscard]] auto type(this object const &self) -> object_type; + + // Return the underlying object. + [[nodiscard]] auto get_ucl_object(this object &self) + -> ::ucl_object_t *; + + [[nodiscard]] auto get_ucl_object(this object const &self) + -> ::ucl_object_t const *; + + // Return the key of this object. + [[nodiscard]] auto key(this object const &self) -> std::string_view; + +protected: + // The object we're wrapping. + ::ucl_object_t *m_object = nullptr; + + friend auto swap(object &a, object &b) -> void; +}; + +/*********************************************************************** + * Object comparison. + */ + +export [[nodiscard]] auto operator==(object const &lhs, object const &rhs) + -> bool; + +export [[nodiscard]] auto operator<=>(object const &lhs, object const &rhs) + -> std::strong_ordering; + +} // namespace nihil::ucl diff --git a/nihil.ucl/object_cast.ccm b/nihil.ucl/object_cast.ccm new file mode 100644 index 0000000..3fa9eba --- /dev/null +++ b/nihil.ucl/object_cast.ccm @@ -0,0 +1,89 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <coroutine> +#include <cstdlib> +#include <expected> + +#include <ucl.h> + +export module nihil.ucl:object_cast; + +import nihil.monad; +import :type; +import :object; +import :array; + +namespace nihil::ucl { + +/* + * Ensure a UCL object is convertible to another type. Throws type_mismatch + * if not. + */ + +// Implementation for basic types. +template<datatype To> +struct convert_check +{ + [[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 {}; + + // Converting between two equal types is permitted. + if (from_type == to_type) + return {}; + + // Otherwise, this is an error. + return std::unexpected(type_mismatch(to_type, from_type)); + } +}; + +// Implementation for array. +template<typename T> +struct convert_check<array<T>> +{ + [[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)); + auto to_type = To::ucl_type; + + // If the source type is not an array, this is an error. + if (from_type != object_type::array) + 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); + co_await convert_check<typename To::value_type>{} + .check(arr_obj); + } + + co_return {}; + } +}; + +/* + * Convert a UCL object to another type. + */ +export template<datatype To> +auto object_cast(object const &from) -> std::expected<To, type_mismatch> +{ + auto uobj = from.get_ucl_object(); + + 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 new file mode 100644 index 0000000..0a08670 --- /dev/null +++ b/nihil.ucl/parser.cc @@ -0,0 +1,102 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <functional> +#include <string> + +#include <ucl.h> + +module nihil.ucl; + +import nihil.error; + +namespace nihil::ucl { + +auto make_parser(int flags) -> std::expected<parser, 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(error("failed to create parser")); +} + +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( + reinterpret_cast<char const *>(data), + len); + return handler->callback(string); +} + +parser::parser(::ucl_parser *uclp) + : m_parser(uclp) +{ +} + +parser::~parser() +{ + if (m_parser) + ::ucl_parser_free(m_parser); +} + +parser::parser(parser &&other) noexcept + : m_parser(std::exchange(other.m_parser, nullptr)) + , m_macros(std::move(other.m_macros)) +{ +} + +auto parser::operator=(this parser &self, parser &&other) noexcept + -> 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 +{ + ::ucl_parser_register_variable( + self.get_parser(), + std::string(variable).c_str(), + std::string(value).c_str()); +} + +auto parser::top(this parser &self) -> map<object> +{ + auto obj = ::ucl_parser_get_object(self.get_parser()); + if (obj != nullptr) + // ucl_parser_get_object() refs the object for us. + return {noref, obj}; + + throw std::logic_error( + "attempt to call top() on an invalid ucl::parser"); +} + +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 new file mode 100644 index 0000000..5fa3495 --- /dev/null +++ b/nihil.ucl/parser.ccm @@ -0,0 +1,160 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <coroutine> +#include <expected> +#include <format> +#include <functional> +#include <memory> +#include <string> +#include <vector> + +#include <ucl.h> + +export module nihil.ucl:parser; + +import nihil.monad; +import :object; +import :map; + +namespace nihil::ucl { + +// UCL parser flags. +export inline constexpr int parser_key_lower = UCL_PARSER_KEY_LOWERCASE; +export inline constexpr int parser_zerocopy = UCL_PARSER_ZEROCOPY; +export inline constexpr int parser_no_time = UCL_PARSER_NO_TIME; + +// A macro handler. This proxies the C API callback to the C++ API. +using macro_callback_t = bool (std::string_view); + +struct macro_handler { + std::function<macro_callback_t> callback; + + // Handle a callback from the C API. + static auto handle( + unsigned char const *data, + std::size_t len, void + *ud) + -> bool; +}; + +/* + * A UCL parser. This wraps the C ucl_parser API. + * + * parser itself is not exported; use make_parser() to create one. + */ +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 + requires (std::same_as<bool, std::invoke_result<F>>) + { + auto handler = std::make_unique<macro_handler>( + std::forward<F>(func)); + + auto cname = std::string(name); + ::ucl_parser_register_macro( + self.get_parser(), cname.c_str(), + ¯o_handler::handle, handler.get()); + + 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; + + // Add data to the parser. + [[nodiscard]] auto add(this parser &self, + std::ranges::contiguous_range auto &&data) + -> std::expected<void, error> + // Only bytes (chars) are permitted. + requires(sizeof(std::ranges::range_value_t<decltype(data)>) == 1) + { + auto *p = self.get_parser(); + auto dptr = reinterpret_cast<unsigned char const *>( + std::ranges::data(data)); + + auto ret = ::ucl_parser_add_chunk( + p, dptr, std::ranges::size(data)); + + if (ret == true) + return {}; + + return std::unexpected(error(::ucl_parser_get_error(p))); + } + + [[nodiscard]] auto add(this parser &self, + std::ranges::range auto &&data) + -> std::expected<void, error> + requires (!std::ranges::contiguous_range<decltype(data)>) + { + auto cdata = std::vector<char>( + std::from_range, + std::forward<decltype(data)>(data)); + co_await self.add(std::move(cdata)); + co_return {}; + } + + // Return the top object of this parser. + [[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 *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>> m_macros; +}; + +// Create a parser with the given flags. +export [[nodiscard]] auto +make_parser(int flags = 0) -> std::expected<parser, error>; + +// Utility function to parse something and return the top-level object. +export [[nodiscard]] auto +parse(int flags, std::ranges::range auto &&data) + -> std::expected<map<object>, 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>, error> +{ + co_return co_await parse(0, std::forward<decltype(data)>(data)); +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/real.cc b/nihil.ucl/real.cc new file mode 100644 index 0000000..6d9e082 --- /dev/null +++ b/nihil.ucl/real.cc @@ -0,0 +1,104 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cassert> +#include <compare> +#include <cstdlib> +#include <expected> +#include <string> +#include <system_error> + +#include <ucl.h> + +module nihil.ucl; + +import nihil.error; + +namespace nihil::ucl { + +auto make_real(real::contained_type value) + -> std::expected<real, error> +{ + auto *uobj = ::ucl_object_fromdouble(value); + if (uobj == nullptr) + return std::unexpected(error( + errc::failed_to_create_object, + error(std::errc(errno)))); + + return real(noref, uobj); +} + +real::real() + : real(0) +{ +} + +real::real(contained_type value) + : real(noref, [&] { + auto *uobj = ::ucl_object_fromdouble(value); + if (uobj == nullptr) + throw std::system_error( + std::make_error_code(std::errc(errno))); + return uobj; + }()) +{ +} + +real::real(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != real::ucl_type) + throw type_mismatch(real::ucl_type, actual_type); + return uobj; + }()) +{ +} + +real::real(noref_t, ::ucl_object_t *uobj) + : object(nihil::ucl::noref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != real::ucl_type) + throw type_mismatch(real::ucl_type, actual_type); + return uobj; + }()) +{ +} + +auto real::value(this real const &self) -> contained_type +{ + auto v = contained_type{}; + auto const *uobj = self.get_ucl_object(); + + if (::ucl_object_todouble_safe(uobj, &v)) + return v; + + std::abort(); +} + +auto operator== (real const &a, real const &b) -> bool +{ + return a.value() == b.value(); +} + +auto operator<=> (real const &a, real const &b) -> std::partial_ordering +{ + return a.value() <=> b.value(); +} + +auto operator== (real const &a, real::contained_type b) -> bool +{ + return a.value() == b; +} + +auto operator<=> (real const &a, real::contained_type b) + -> std::partial_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/real.ccm b/nihil.ucl/real.ccm new file mode 100644 index 0000000..f425a9a --- /dev/null +++ b/nihil.ucl/real.ccm @@ -0,0 +1,112 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <compare> +#include <expected> +#include <format> +#include <utility> + +#include <ucl.h> + +export module nihil.ucl:real; + +import :object; +import :type; + +namespace nihil::ucl { + +export struct real final : object { + using contained_type = double; + + inline static constexpr object_type ucl_type = object_type::real; + + /* + * Create a real holding the value 0. Throws std::system_error + * on failure. + */ + real(); + + /* + * Create a real holding a specific value. Throws std::system_error + * on failure. + */ + explicit real(contained_type value); + + /* + * Create a new real from a UCL object. Throws type_mismatch + * on failure. + */ + real(ref_t, ::ucl_object_t const *uobj); + real(noref_t, ::ucl_object_t *uobj); + + // Return the value of this real. + [[nodiscard]] auto value(this real const &self) -> contained_type; +}; + +/* + * Real constructors. These return an error instead of throwing. + */ + +export [[nodiscard]] auto +make_real(real::contained_type = 0) -> std::expected<real, error>; + +/* + * Comparison operators. + */ + +export [[nodiscard]] auto operator== (real const &a, real const &b) -> bool; + +export [[nodiscard]] auto operator== (real const &a, + real::contained_type b) -> bool; + +export [[nodiscard]] auto operator<=> (real const &a, real const &b) + -> std::partial_ordering; + +export [[nodiscard]] auto operator<=> (real const &a, real::contained_type b) + -> std::partial_ordering; + +/* + * Literal operator. + */ +inline namespace literals { +export constexpr auto operator""_ucl (long double d) -> real +{ + if (d > static_cast<long double>(std::numeric_limits<double>::max()) || + d < static_cast<long double>(std::numeric_limits<double>::min())) + throw std::out_of_range("literal out of range"); + + return real(static_cast<double>(d)); +} +} // namespace nihil::ucl::literals + +} // namespace nihil::ucl + +namespace nihil { inline namespace literals { + export using namespace ::nihil::ucl::literals; +}} // namespace nihil::literals + +/* + * std::formatter for a real. This provides the same format operations + * as std::formatter<double>; + */ +export template<> +struct std::formatter<nihil::ucl::real, char> +{ + std::formatter<double> base_formatter; + + template<class ParseContext> + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + return base_formatter.parse(ctx); + } + + template<class FmtContext> + FmtContext::iterator format(nihil::ucl::real const &o, + FmtContext& ctx) const + { + return base_formatter.format(o.value(), ctx); + } +}; diff --git a/nihil.ucl/string.cc b/nihil.ucl/string.cc new file mode 100644 index 0000000..67e97f4 --- /dev/null +++ b/nihil.ucl/string.cc @@ -0,0 +1,187 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdlib> +#include <expected> +#include <iosfwd> +#include <string> +#include <system_error> + +#include <ucl.h> + +module nihil.ucl; + +import nihil.error; + +namespace nihil::ucl { + +auto make_string() -> std::expected<string, error> +{ + return make_string(std::string_view("")); +} + +auto make_string(char const *s) -> std::expected<string, error> +{ + return make_string(std::string_view(s)); +} + +auto make_string(std::string_view s) -> std::expected<string, error> +{ + auto *uobj = ::ucl_object_fromstring_common( + s.data(), s.size(), UCL_STRING_RAW); + + if (uobj == nullptr) + return std::unexpected(error( + errc::failed_to_create_object, + error(std::errc(errno)))); + + return string(noref, uobj); +} + +string::string(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != string::ucl_type) + throw type_mismatch(string::ucl_type, actual_type); + return uobj; + }()) +{ +} + +string::string(noref_t, ::ucl_object_t *uobj) + : object(nihil::ucl::noref, [&] { + auto actual_type = static_cast<object_type>( + ::ucl_object_type(uobj)); + if (actual_type != string::ucl_type) + throw type_mismatch(string::ucl_type, actual_type); + return uobj; + }()) +{ +} + +string::string() + : string(std::string_view("")) +{} + +string::string(std::string_view value) + : string(noref, [&] { + auto *uobj = ::ucl_object_fromstring_common( + value.data(), value.size(), UCL_STRING_RAW); + if (uobj == nullptr) + throw std::system_error( + std::make_error_code(std::errc(errno))); + return uobj; + }()) +{ +} + +string::string(char const *value) + : string(std::string_view(value)) +{ +} + +auto string::value(this string const &self) -> contained_type +{ + char const *dptr{}; + std::size_t dlen; + + auto const *uobj = self.get_ucl_object(); + if (::ucl_object_tolstring_safe(uobj, &dptr, &dlen)) + return {dptr, dlen}; + + // This should never fail. + std::abort(); +} + +auto string::size(this string const &self) -> size_type +{ + return self.value().size(); +} + +auto string::empty(this string const &self) -> bool +{ + return self.size() == 0; +} + +auto string::data(this string const &self) -> pointer +{ + char const *dptr{}; + + auto const *uobj = self.get_ucl_object(); + if (::ucl_object_tostring_safe(uobj, &dptr)) + return dptr; + + // This should never fail. + std::abort(); +} + +auto string::begin(this string const &self) -> iterator +{ + return self.data(); +} + +auto string::end(this string const &self) -> iterator +{ + return self.data() + self.size(); +} + +auto operator== (string const &a, string const &b) + -> bool +{ + return a.value() == b.value(); +} + +auto operator<=> (string const &a, string const &b) + -> std::strong_ordering +{ + return a.value() <=> b.value(); +} + +/* + * For convenience, allow comparison with C++ strings without having to + * construct a temporary UCL object. + */ + +auto operator==(string const &lhs, std::string_view rhs) -> bool +{ + return lhs.value() == rhs; +} + +auto operator<=>(string const &lhs, std::string_view rhs) + -> std::strong_ordering +{ + return lhs.value() <=> rhs; +} + +auto operator==(string const &lhs, std::string const &rhs) -> bool +{ + return lhs == std::string_view(rhs); +} + +auto operator<=>(string const &lhs, std::string const &rhs) + -> std::strong_ordering +{ + return lhs <=> std::string_view(rhs); +} + +auto operator==(string const &lhs, char const *rhs) -> bool +{ + return lhs == std::string_view(rhs); +} + +auto operator<=>(string const &lhs, char const *rhs) + -> std::strong_ordering +{ + return lhs <=> std::string_view(rhs); +} + +auto operator<<(std::ostream &strm, string const &s) -> std::ostream & +{ + return strm << s.value(); +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/string.ccm b/nihil.ucl/string.ccm new file mode 100644 index 0000000..c757bf1 --- /dev/null +++ b/nihil.ucl/string.ccm @@ -0,0 +1,229 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdlib> +#include <expected> +#include <format> +#include <iosfwd> +#include <string> + +#include <ucl.h> + +export module nihil.ucl:string; + +import :object; +import :type; + +namespace nihil::ucl { + +export struct string final : object { + using contained_type = std::string_view; + inline static constexpr object_type ucl_type = object_type::string; + + // string is a container of char + using value_type = char const; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = value_type &; + using pointer = value_type *; + using iterator = pointer; + + /* + * Create a new empty string. Throws std::system_error on failure. + */ + string(); + + /* + * Create a string from a value. Throws std::system_error on failure. + */ + explicit string(std::string_view); + + /* + * Create a string from a C literal. Throws std::system_error + * on failure. + */ + explicit string(char const *); + + /* + * Create a string from a contiguous range. The range's value type + * must be char. Throws std::system_error on failure. + */ + template<std::ranges::contiguous_range Range> + requires (!std::same_as<std::string_view, Range> && + std::same_as<char, std::ranges::range_value_t<Range>>) + explicit string(Range &&range) + : string(std::string_view(std::ranges::begin(range), + std::ranges::end(range))) + {} + + /* + * Create a string from a non-contiguous range. This requires a + * temporary value due to limitations of the UCL C API. + */ + template<std::ranges::range Range> + requires (!std::ranges::contiguous_range<Range> && + std::same_as<char, std::ranges::range_value_t<Range>>) + explicit string(Range &&range) + : string(std::string(std::from_range, range)) + {} + + /* + * Create a string from an iterator pair. The iterator's value type + * must be char. If the iterator pair is not contiguous, the value + * will be copied to a temporary first. + * + * Throws std::system_error on failure. + */ + template<std::input_iterator Iterator> + requires (std::same_as<char, std::iter_value_t<Iterator>>) + string(Iterator first, Iterator last) + : string(std::ranges::subrange(first, last)) + {} + + /* + * Create a new string from a UCL object. Throws type_mismatch + * on failure. + */ + string(ref_t, ::ucl_object_t const *uobj); + string(noref_t, ::ucl_object_t *uobj); + + // Return the value of this string. + [[nodiscard]] auto value(this string const &self) -> contained_type; + + // Return the size of this string. + [[nodiscard]] auto size(this string const &self) -> size_type; + + // Test if this string is empty. + [[nodiscard]] auto empty(this string const &self) -> bool; + + // Access this string's data + [[nodiscard]] auto data(this string const &self) -> pointer; + + // Iterator access + [[nodiscard]] auto begin(this string const &self) -> iterator; + [[nodiscard]] auto end(this string const &self) -> iterator; +}; + +/* + * String constructors. These return an error instead of throwing. + */ + +// Empty string +export [[nodiscard]] auto +make_string() -> std::expected<string, error>; + +// From string_view +export [[nodiscard]] auto +make_string(std::string_view) -> std::expected<string, error>; + +// From C literal +export [[nodiscard]] auto +make_string(char const *) -> std::expected<string, error>; + +// From contiguous range +export template<std::ranges::contiguous_range Range> +requires (!std::same_as<std::string_view, Range> && + std::same_as<char, std::ranges::range_value_t<Range>>) +[[nodiscard]] auto make_string(Range &&range) +{ + return make_string(std::string_view(range)); +} + +// From non-contiguous range +export template<std::ranges::range Range> +requires (!std::ranges::contiguous_range<Range> && + std::same_as<char, std::ranges::range_value_t<Range>>) +[[nodiscard]] auto make_string(Range &&range) +{ + return make_string(std::string(std::from_range, range)); +} + +// From iterator pair +export template<std::input_iterator Iterator> +requires (std::same_as<char, std::iter_value_t<Iterator>>) +[[nodiscard]] auto make_string(Iterator first, Iterator last) +{ + return make_string(std::ranges::subrange(first, last)); +} + +/* + * Comparison operators. + */ + +export [[nodiscard]] auto operator== (string const &a, string const &b) -> bool; +export [[nodiscard]] auto operator<=> (string const &a, string const &b) + -> std::strong_ordering; + +/* + * For convenience, allow comparison with C++ strings without having to + * construct a temporary UCL object. + */ + +export [[nodiscard]] auto operator==(string const &lhs, + std::string_view rhs) -> bool; + +export [[nodiscard]] auto operator==(string const &lhs, + std::string const &rhs) -> bool; + +export [[nodiscard]] auto operator==(string const &lhs, + char const *rhs) -> bool; + +export [[nodiscard]] auto operator<=>(string const &lhs, + std::string_view rhs) + -> std::strong_ordering; + +export [[nodiscard]] auto operator<=>(string const &lhs, + std::string const &rhs) + -> std::strong_ordering; + +export [[nodiscard]] auto operator<=>(string const &lhs, + char const *rhs) + -> std::strong_ordering; + +/* + * Print a string to a stream. + */ +export auto operator<<(std::ostream &, string const &) -> std::ostream &; + +/* + * Literal operator. + */ +inline namespace literals { + export constexpr auto operator""_ucl (char const *s, std::size_t n) + -> string + { + return string(std::string_view(s, n)); + } +} // namespace nihil::ucl::literals + +} // namespace nihil::ucl + +namespace nihil { inline namespace literals { + export using namespace ::nihil::ucl::literals; +}} // namespace nihil::literals + +/* + * std::formatter for a string. This provides the same format operations + * as std::formatter<std::string_view>. + */ +export template<> +struct std::formatter<nihil::ucl::string, char> +{ + std::formatter<std::string_view> base_formatter; + + template<class ParseContext> + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + return base_formatter.parse(ctx); + } + + template<class FmtContext> + FmtContext::iterator format(nihil::ucl::string const &o, + FmtContext& ctx) const + { + return base_formatter.format(o.value(), ctx); + } +}; diff --git a/nihil.ucl/tests/CMakeLists.txt b/nihil.ucl/tests/CMakeLists.txt new file mode 100644 index 0000000..0257b4f --- /dev/null +++ b/nihil.ucl/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +# This source code is released into the public domain. + +add_executable(nihil.ucl.test + emit.cc + parse.cc + + object.cc + array.cc + boolean.cc + integer.cc + map.cc + real.cc + string.cc +) + +target_link_libraries(nihil.ucl.test PRIVATE nihil.ucl Catch2::Catch2WithMain) + +find_package(Catch2 REQUIRED) + +include(CTest) +include(Catch) +catch_discover_tests(nihil.ucl.test) diff --git a/nihil.ucl/tests/array.cc b/nihil.ucl/tests/array.cc new file mode 100644 index 0000000..866fa45 --- /dev/null +++ b/nihil.ucl/tests/array.cc @@ -0,0 +1,478 @@ +/* + * This source code is released into the public domain. + */ + +#include <algorithm> +#include <concepts> +#include <expected> +#include <ranges> +#include <string> + +#include <catch2/catch_test_macros.hpp> +#include <ucl.h> + +import nihil.ucl; + +TEST_CASE("ucl: array: invariants", "[ucl]") +{ + using namespace nihil::ucl; + + REQUIRE(array<>::ucl_type == object_type::array); + REQUIRE(static_cast<::ucl_type>(array<>::ucl_type) == UCL_ARRAY); + + static_assert(std::destructible<array<>>); + static_assert(std::default_initializable<array<>>); + static_assert(std::move_constructible<array<>>); + static_assert(std::copy_constructible<array<>>); + static_assert(std::equality_comparable<array<>>); + static_assert(std::totally_ordered<array<>>); + static_assert(std::swappable<array<>>); + + static_assert(std::ranges::sized_range<array<integer>>); + static_assert(std::same_as<std::ranges::range_value_t<array<integer>>, + integer>); +} + +TEST_CASE("ucl: array: constructor", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default") { + auto arr = array<integer>(); + REQUIRE(arr.size() == 0); + REQUIRE(str(arr.type()) == "array"); + } + + SECTION("from range") { + auto vec = std::vector{integer(1), integer(42)}; + auto arr = array<integer>(std::from_range, vec); + + REQUIRE(arr.size() == 2); + REQUIRE(arr[0] == 1); + REQUIRE(arr[1] == 42); + } + + SECTION("from iterator pair") { + auto vec = std::vector{integer(1), integer(42)}; + auto arr = array<integer>(std::ranges::begin(vec), + std::ranges::end(vec)); + + REQUIRE(arr.size() == 2); + REQUIRE(arr[0] == 1); + REQUIRE(arr[1] == 42); + } + + SECTION("from initializer_list") { + auto arr = array<integer>{integer(1), integer(42)}; + + REQUIRE(arr.size() == 2); + REQUIRE(arr[0] == 1); + REQUIRE(arr[1] == 42); + } +} + +TEST_CASE("ucl: array: construct from UCL object", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("ref, correct type") { + auto uarr = ::ucl_object_typed_new(UCL_ARRAY); + auto uint = ::ucl_object_fromint(42); + ::ucl_array_append(uarr, uint); + + auto arr = array<integer>(ref, uarr); + REQUIRE(arr[0] == 42); + + ::ucl_object_unref(uarr); + } + + SECTION("noref, correct type") { + auto uarr = ::ucl_object_typed_new(UCL_ARRAY); + auto uint = ::ucl_object_fromint(42); + ::ucl_array_append(uarr, uint); + + auto arr = array<integer>(noref, uarr); + REQUIRE(arr[0] == 42); + } + + SECTION("ref, wrong element type") { + auto uarr = ::ucl_object_typed_new(UCL_ARRAY); + auto uint = ::ucl_object_frombool(true); + ::ucl_array_append(uarr, uint); + + auto arr = array<integer>(noref, uarr); + REQUIRE_THROWS_AS(arr[0], type_mismatch); + } + + SECTION("ref, wrong type") { + auto uobj = ::ucl_object_frombool(true); + + REQUIRE_THROWS_AS(array(ref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, wrong type") { + auto uobj = ::ucl_object_frombool(true); + + REQUIRE_THROWS_AS(array(noref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } +} + +TEST_CASE("ucl: array: swap", "[ucl]") +{ + // do not add using namespace nihil::ucl + + auto arr1 = nihil::ucl::array<nihil::ucl::integer>{ + nihil::ucl::integer(1), + nihil::ucl::integer(2) + }; + + auto arr2 = nihil::ucl::array<nihil::ucl::integer>{ + nihil::ucl::integer(3), + }; + + swap(arr1, arr2); + + REQUIRE(arr1.size() == 1); + REQUIRE(arr1[0] == 3); + + REQUIRE(arr2.size() == 2); + REQUIRE(arr2[0] == 1); +} + +TEST_CASE("ucl: array: push_back", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<integer>(); + REQUIRE(arr.size() == 0); + + arr.push_back(integer(1)); + arr.push_back(integer(42)); + arr.push_back(integer(666)); + + REQUIRE(arr.size() == 3); + REQUIRE(arr[0] == 1); + REQUIRE(arr[1] == 42); + REQUIRE(arr[2] == 666); + + REQUIRE_THROWS_AS(arr[3], std::out_of_range); + + REQUIRE(arr.front() == 1); + REQUIRE(arr.back() == 666); +} + +TEST_CASE("ucl: array: compare", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<integer>{ + integer(1), integer(42), integer(666) + }; + + auto arr2 = array<integer>(); + REQUIRE(arr != arr2); + + arr2.push_back(integer(1)); + arr2.push_back(integer(42)); + arr2.push_back(integer(666)); + REQUIRE(arr == arr2); + + auto arr3 = array<integer>{ + integer(1), integer(1), integer(1) + }; + + REQUIRE(arr != arr3); +} + +TEST_CASE("ucl: array: iterator", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<integer>{integer(1), integer(42), integer(666)}; + + auto it = arr.begin(); + REQUIRE(*it == 1); + auto end = arr.end(); + REQUIRE(it != end); + REQUIRE(it < end); + + ++it; + REQUIRE(*it == 42); + + ++it; + REQUIRE(*it == 666); + + --it; + REQUIRE(*it == 42); + + ++it; + REQUIRE(it != end); + ++it; + REQUIRE(it == end); +} + +TEST_CASE("ucl: array: parse", "[ucl]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto obj = parse("value = [1, 42, 666]"sv).value(); + + auto arr = object_cast<array<integer>>(obj["value"]).value(); + + REQUIRE(arr.size() == 3); + REQUIRE(arr[0] == 1); + REQUIRE(arr[1] == 42); + REQUIRE(arr[2] == 666); +} + +TEST_CASE("ucl: array: emit", "[ucl]") +{ + using namespace nihil::ucl; + + auto ucl = parse("array = [1, 42, 666];").value(); + + auto output = std::format("{:c}", ucl); + REQUIRE(output == +"array [\n" +" 1,\n" +" 42,\n" +" 666,\n" +"]\n"); +} + +TEST_CASE("ucl: array: format", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("empty array") { + auto arr = array<integer>(); + REQUIRE(std::format("{}", arr) == "[]"); + } + + SECTION("bare array") { + auto arr = array<integer>{ + integer(1), integer(42), integer(666) + }; + + auto output = std::format("{}", arr); + REQUIRE(output == "[1, 42, 666]"); + } + + SECTION("parsed array") { + auto ucl = parse("array = [1, 42, 666];").value(); + auto arr = object_cast<array<integer>>(ucl["array"]).value(); + + auto output = std::format("{}", arr); + REQUIRE(output == "[1, 42, 666]"); + } +} + +TEST_CASE("ucl: array: print to ostream", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("empty array") { + auto arr = array<integer>(); + auto strm = std::ostringstream(); + strm << arr; + + REQUIRE(strm.str() == "[]"); + } + + SECTION("bare array") { + auto arr = array<integer>{ + integer(1), integer(42), integer(666) + }; + auto strm = std::ostringstream(); + strm << arr; + + REQUIRE(strm.str() == "[1, 42, 666]"); + } + + SECTION("parsed array") { + auto ucl = parse("array = [1, 42, 666];").value(); + auto arr = object_cast<array<integer>>(ucl["array"]).value(); + auto strm = std::ostringstream(); + strm << arr; + + REQUIRE(strm.str() == "[1, 42, 666]"); + } +} + +TEST_CASE("ucl: array is a sized_range", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<integer>{integer(1), integer(42), integer(666)}; + + auto size = std::ranges::size(arr); + REQUIRE(size == 3); + + auto begin = std::ranges::begin(arr); + static_assert(std::random_access_iterator<decltype(begin)>); + + auto end = std::ranges::end(arr); + static_assert(std::sentinel_for<decltype(end), decltype(begin)>); + + REQUIRE(std::distance(begin, end) == 3); + + auto vec = std::vector<integer>(); + std::ranges::copy(arr, std::back_inserter(vec)); + REQUIRE(std::ranges::equal(arr, vec)); + + auto arr_as_ints = + arr | std::views::transform(&integer::value); + auto int_vec = std::vector<integer::contained_type>(); + std::ranges::copy(arr_as_ints, std::back_inserter(int_vec)); + REQUIRE(int_vec == std::vector<std::int64_t>{1, 42, 666}); + +} + +TEST_CASE("ucl: array: bad object_cast", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<integer>(); + + auto cast_ok = object_cast<integer>(arr); + REQUIRE(!cast_ok); +} + +TEST_CASE("ucl: array: heterogeneous elements", "[ucl]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + 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); + REQUIRE(*int_obj == 42); + + auto bool_obj = object_cast<boolean>(arr[1]); + REQUIRE(bool_obj); + REQUIRE(*bool_obj == true); + + auto string_obj = object_cast<string>(arr[2]); + REQUIRE(string_obj); + REQUIRE(*string_obj == "test"); +} + +TEST_CASE("ucl: array: heterogenous cast", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<>(); + arr.push_back(integer(42)); + arr.push_back(boolean(true)); + + // Converting to an array<integer> should fail. + auto cast_ok = object_cast<array<integer>>(arr); + REQUIRE(!cast_ok); + + // Converting to array<object> should succeed. + auto err = object_cast<array<object>>(arr); + REQUIRE(err); + + auto obj_arr = *err; + REQUIRE(obj_arr[0] == integer(42)); +} + +TEST_CASE("ucl: array: homogeneous cast", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<>(); + arr.push_back(integer(1)); + arr.push_back(integer(42)); + + auto obj = object(ref, arr.get_ucl_object()); + + // Converting to array<string> should fail. + auto cast_ok = object_cast<array<string>>(obj); + REQUIRE(!cast_ok); + + // Converting to an array<integer> should succeed. + auto err = object_cast<array<integer>>(obj); + REQUIRE(err); + + auto obj_arr = *err; + REQUIRE(obj_arr[0] == 1); + REQUIRE(obj_arr[1] == 42); +} + +TEST_CASE("array iterator: empty iterator", "[ucl]") +{ + using namespace nihil::ucl; + + auto it = array_iterator<integer>(); + + REQUIRE_THROWS_AS(*it, std::logic_error); + REQUIRE_THROWS_AS(it[0], std::logic_error); + REQUIRE_THROWS_AS(it++, std::logic_error); + REQUIRE_THROWS_AS(++it, std::logic_error); + + auto it2 = array_iterator<integer>(); + REQUIRE(it == it2); + REQUIRE((it < it2) == false); + REQUIRE((it > it2) == false); +} + +TEST_CASE("array iterator: invalid operations", "[ucl]") +{ + using namespace nihil::ucl; + + auto arr = array<integer>{ integer(42) }; + auto it = arr.begin(); + + SECTION("decrement before start") { + REQUIRE_THROWS_AS(--it, std::logic_error); + REQUIRE_THROWS_AS(it--, std::logic_error); + REQUIRE_THROWS_AS(it - 1, std::logic_error); + } + + SECTION("increment past end") { + ++it; + REQUIRE(it == arr.end()); + + REQUIRE_THROWS_AS(++it, std::logic_error); + REQUIRE_THROWS_AS(it++, std::logic_error); + REQUIRE_THROWS_AS(it + 1, std::logic_error); + } + + SECTION("dereference iterator at end") { + REQUIRE_THROWS_AS(it[1], std::logic_error); + + ++it; + REQUIRE(it == arr.end()); + + REQUIRE_THROWS_AS(*it, std::logic_error); + } + + SECTION("compare with different array") { + auto arr2 = array<integer>{ integer(42) }; + REQUIRE_THROWS_AS(it == arr2.begin(), std::logic_error); + REQUIRE_THROWS_AS(it > arr2.begin(), std::logic_error); + REQUIRE_THROWS_AS(it - arr2.begin(), std::logic_error); + } + + SECTION("compare with empty iterator") { + auto it2 = array_iterator<integer>(); + REQUIRE_THROWS_AS(it == it2, std::logic_error); + REQUIRE_THROWS_AS(it > it2, std::logic_error); + REQUIRE_THROWS_AS(it - it2, std::logic_error); + } +} diff --git a/nihil.ucl/tests/boolean.cc b/nihil.ucl/tests/boolean.cc new file mode 100644 index 0000000..f7ef95e --- /dev/null +++ b/nihil.ucl/tests/boolean.cc @@ -0,0 +1,224 @@ +/* + * This source code is released into the public domain. + */ + +#include <concepts> +#include <string> + +#include <catch2/catch_test_macros.hpp> +#include <ucl.h> + +import nihil.ucl; + +TEST_CASE("ucl: boolean: invariants", "[ucl]") +{ + using namespace nihil::ucl; + + static_assert(std::same_as<bool, boolean::contained_type>); + REQUIRE(boolean::ucl_type == object_type::boolean); + REQUIRE(static_cast<::ucl_type>(boolean::ucl_type) == UCL_BOOLEAN); + + static_assert(std::destructible<boolean>); + static_assert(std::default_initializable<boolean>); + static_assert(std::move_constructible<boolean>); + static_assert(std::copy_constructible<boolean>); + static_assert(std::equality_comparable<boolean>); + static_assert(std::totally_ordered<boolean>); + static_assert(std::swappable<boolean>); +} + +TEST_CASE("ucl: boolean: constructor", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default") { + auto b = boolean(); + REQUIRE(b == false); + } + + SECTION("with value") { + auto b = boolean(true); + REQUIRE(b == true); + } +} + +TEST_CASE("ucl: boolean: construct from UCL object", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("ref, correct type") { + auto uobj = ::ucl_object_frombool(true); + + auto i = boolean(ref, uobj); + REQUIRE(i == true); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, correct type") { + auto uobj = ::ucl_object_frombool(true); + + auto i = boolean(noref, uobj); + REQUIRE(i == true); + } + + SECTION("ref, wrong type") { + auto uobj = ::ucl_object_fromint(1); + + REQUIRE_THROWS_AS(boolean(ref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, wrong type") { + auto uobj = ::ucl_object_fromint(1); + + REQUIRE_THROWS_AS(boolean(noref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } +} + +TEST_CASE("ucl: boolean: make_boolean", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default value") { + auto b = make_boolean().value(); + REQUIRE(b == false); + } + + SECTION("explicit value") { + auto b = make_boolean(true).value(); + REQUIRE(b == true); + } +} + +TEST_CASE("ucl: boolean: swap", "[ucl]") +{ + // do not add using namespace nihil::ucl + + auto b1 = nihil::ucl::boolean(true); + auto b2 = nihil::ucl::boolean(false); + + swap(b1, b2); + + REQUIRE(b1 == false); + REQUIRE(b2 == true); +} + +TEST_CASE("ucl: boolean: value()", "[ucl]") +{ + auto b = nihil::ucl::boolean(true); + REQUIRE(b.value() == true); +} + +TEST_CASE("ucl: boolean: key()", "[ucl]") +{ + using namespace nihil::ucl; + + 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() == ""); +} + +TEST_CASE("ucl: boolean: comparison", "[ucl]") +{ + using namespace nihil::ucl; + + auto b = boolean(true); + + SECTION("operator==") { + REQUIRE(b == true); + REQUIRE(b == boolean(true)); + } + + SECTION("operator!=") { + REQUIRE(b != false); + REQUIRE(b != boolean(false)); + } + + SECTION("operator<") { + REQUIRE(b <= true); + REQUIRE(b <= nihil::ucl::boolean(true)); + } + + SECTION("operator>") { + REQUIRE(b > false); + REQUIRE(b > nihil::ucl::boolean(false)); + } +} + +TEST_CASE("ucl: boolean: parse", "[ucl]") +{ + using namespace nihil::ucl; + + auto obj = parse("value = true").value(); + + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE(object_cast<boolean>(v).value() == true); +} + +TEST_CASE("ucl: boolean: parse and emit", "[ucl]") +{ + using namespace nihil::ucl; + + auto ucl = parse("bool = true;").value(); + + auto output = std::string(); + emit(ucl, nihil::ucl::emitter::configuration, + std::back_inserter(output)); + + REQUIRE(output == "bool = true;\n"); +} + +TEST_CASE("ucl: boolean: format", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("bare boolean") { + auto str = std::format("{}", boolean(true)); + REQUIRE(str == "true"); + } + + SECTION("parsed boolean") { + auto obj = parse("bool = true;").value(); + auto b = object_cast<boolean>(obj["bool"]).value(); + + auto str = std::format("{}", b); + REQUIRE(str == "true"); + } + + SECTION("with format string") { + auto str = std::format("{: >5}", boolean(true)); + REQUIRE(str == " true"); + } +} + +TEST_CASE("ucl: boolean: print to ostream", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("bare boolean") { + auto strm = std::ostringstream(); + strm << boolean(true); + + REQUIRE(strm.str() == "true"); + } + + SECTION("parsed boolean") { + auto obj = parse("bool = true;").value(); + auto i = object_cast<boolean>(obj["bool"]).value(); + + auto strm = std::ostringstream(); + strm << i; + + REQUIRE(strm.str() == "true"); + } +} diff --git a/nihil.ucl/tests/emit.cc b/nihil.ucl/tests/emit.cc new file mode 100644 index 0000000..a7dcd71 --- /dev/null +++ b/nihil.ucl/tests/emit.cc @@ -0,0 +1,93 @@ +/* + * This source code is released into the public domain. + */ + +#include <format> +#include <sstream> + +#include <catch2/catch_test_macros.hpp> + +import nihil.ucl; + +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; + + // The ostream emitter produces JSON. + REQUIRE(strm.str() == std::format("{:j}", *obj)); +} + +TEST_CASE("ucl: emit JSON with std::format", "[ucl]") +{ + using namespace std::literals; + + auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); + REQUIRE(obj); + + auto str = std::format("{:j}", *obj); + + REQUIRE(str == +"{\n" +" \"int\": [\n" +" 1,\n" +" 42,\n" +" 666\n" +" ]\n" +"}"); + + // Make sure JSON is the default format. + auto str2 = std::format("{}", *obj); + REQUIRE(str == str2); +} + +TEST_CASE("ucl: emit compact JSON with std::format", "[ucl]") +{ + using namespace std::literals; + + auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); + REQUIRE(obj); + + auto str = std::format("{:J}", *obj); + + REQUIRE(str == "{\"int\":[1,42,666]}"); +} + +TEST_CASE("ucl: emit configuration with std::format", "[ucl]") +{ + using namespace std::literals; + + auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); + REQUIRE(obj); + + auto str = std::format("{:c}", *obj); + + REQUIRE(str == +"int [\n" +" 1,\n" +" 42,\n" +" 666,\n" +"]\n"); +} + +TEST_CASE("ucl: emit YAML with std::format", "[ucl]") +{ + using namespace std::literals; + + auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv); + REQUIRE(obj); + + auto str = std::format("{:y}", *obj); + + REQUIRE(str == +"int: [\n" +" 1,\n" +" 42,\n" +" 666\n" +"]"); +} diff --git a/nihil.ucl/tests/integer.cc b/nihil.ucl/tests/integer.cc new file mode 100644 index 0000000..6584764 --- /dev/null +++ b/nihil.ucl/tests/integer.cc @@ -0,0 +1,247 @@ +/* + * This source code is released into the public domain. + */ + +#include <concepts> +#include <cstdint> +#include <string> + +#include <catch2/catch_test_macros.hpp> +#include <ucl.h> + +import nihil.ucl; + +TEST_CASE("ucl: integer: invariants", "[ucl]") +{ + using namespace nihil::ucl; + + static_assert(std::same_as<std::int64_t, integer::contained_type>); + REQUIRE(integer::ucl_type == object_type::integer); + REQUIRE(static_cast<::ucl_type>(integer::ucl_type) == UCL_INT); + + static_assert(std::destructible<integer>); + static_assert(std::default_initializable<integer>); + static_assert(std::move_constructible<integer>); + static_assert(std::copy_constructible<integer>); + static_assert(std::equality_comparable<integer>); + static_assert(std::totally_ordered<integer>); + static_assert(std::swappable<integer>); +} + +TEST_CASE("ucl: integer: constructor", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default") { + auto i = integer(); + REQUIRE(i == 0); + } + + SECTION("with value") { + auto i = integer(42); + REQUIRE(i == 42); + } +} + +TEST_CASE("ucl: integer: literal", "[ucl]") +{ + SECTION("with namespace nihil::ucl::literals") { + using namespace nihil::ucl::literals; + + auto i = 42_ucl; + REQUIRE(i.type() == nihil::ucl::object_type::integer); + REQUIRE(i == 42); + } + + SECTION("with namespace nihil::literals") { + using namespace nihil::literals; + + auto i = 42_ucl; + REQUIRE(i.type() == nihil::ucl::object_type::integer); + REQUIRE(i == 42); + } +} + +TEST_CASE("ucl: integer: construct from UCL object", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("ref, correct type") { + auto uobj = ::ucl_object_fromint(42); + + auto i = integer(ref, uobj); + REQUIRE(i == 42); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, correct type") { + auto uobj = ::ucl_object_fromint(42); + + auto i = integer(noref, uobj); + REQUIRE(i == 42); + } + + SECTION("ref, wrong type") { + auto uobj = ::ucl_object_frombool(true); + + REQUIRE_THROWS_AS(integer(ref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, wrong type") { + auto uobj = ::ucl_object_frombool(true); + + REQUIRE_THROWS_AS(integer(noref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } +} + +TEST_CASE("ucl: integer: make_integer", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default value") { + auto i = make_integer().value(); + REQUIRE(i == 0); + } + + SECTION("explicit value") { + auto i = make_integer(42).value(); + REQUIRE(i == 42); + } +} + +TEST_CASE("ucl: integer: swap", "[ucl]") +{ + // do not add using namespace nihil::ucl + + auto i1 = nihil::ucl::integer(1); + auto i2 = nihil::ucl::integer(2); + + swap(i1, i2); + + REQUIRE(i1 == 2); + REQUIRE(i2 == 1); +} + +TEST_CASE("ucl: integer: value()", "[ucl]") +{ + using namespace nihil::ucl; + + auto i = 42_ucl; + REQUIRE(i.value() == 42); +} + +TEST_CASE("ucl: integer: key()", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("parsed with key") { + auto obj = parse("an_int = 42").value(); + auto i = object_cast<integer>(obj["an_int"]).value(); + REQUIRE(i.key() == "an_int"); + } + + SECTION("bare integer, no key") { + auto i = 42_ucl; + REQUIRE(i.key() == ""); + } +} + +TEST_CASE("ucl: integer: comparison", "[ucl]") +{ + using namespace nihil::ucl; + + auto i = 42_ucl; + + SECTION("operator==") { + REQUIRE(i == 42); + REQUIRE(i == 42_ucl); + } + + SECTION("operator!=") { + REQUIRE(i != 1); + REQUIRE(i != 1_ucl); + } + + SECTION("operator<") { + REQUIRE(i < 43); + REQUIRE(i < 43_ucl); + } + + SECTION("operator>") { + REQUIRE(i > 1); + REQUIRE(i > 1_ucl); + } +} + +TEST_CASE("ucl: integer: parse", "[ucl]") +{ + using namespace nihil::ucl; + + auto obj = parse("value = 42").value(); + + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE(object_cast<integer>(v) == 42); +} + +TEST_CASE("ucl: integer: parse and emit", "[ucl]") +{ + using namespace nihil::ucl; + + auto ucl = parse("int = 42;").value(); + + auto output = std::string(); + emit(ucl, emitter::configuration, std::back_inserter(output)); + + REQUIRE(output == "int = 42;\n"); +} + +TEST_CASE("ucl: integer: format", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("bare integer") { + auto str = std::format("{}", 42_ucl); + REQUIRE(str == "42"); + } + + SECTION("parsed integer") { + auto obj = parse("int = 42;").value(); + auto i = object_cast<integer>(obj["int"]).value(); + + auto str = std::format("{}", i); + REQUIRE(str == "42"); + } + + SECTION("with format string") { + auto str = std::format("{:-05}", 42_ucl); + REQUIRE(str == "00042"); + } +} + +TEST_CASE("ucl: integer: print to ostream", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("bare integer") { + auto strm = std::ostringstream(); + strm << 42_ucl; + + REQUIRE(strm.str() == "42"); + } + + SECTION("parsed integer") { + auto obj = parse("int = 42;").value(); + auto i = object_cast<integer>(obj["int"]).value(); + + auto strm = std::ostringstream(); + strm << i; + + REQUIRE(strm.str() == "42"); + } +} diff --git a/nihil.ucl/tests/map.cc b/nihil.ucl/tests/map.cc new file mode 100644 index 0000000..7240cb3 --- /dev/null +++ b/nihil.ucl/tests/map.cc @@ -0,0 +1,192 @@ +/* + * This source code is released into the public domain. + */ + +#include <concepts> + +#include <catch2/catch_test_macros.hpp> +#include <ucl.h> + +import nihil.ucl; + +//NOLINTBEGIN(bugprone-unchecked-optional-access) + +TEST_CASE("ucl: map: invariants", "[ucl]") +{ + using namespace nihil::ucl; + + REQUIRE(map<>::ucl_type == object_type::object); + REQUIRE(static_cast<::ucl_type>(map<>::ucl_type) == UCL_OBJECT); + + static_assert(std::destructible<map<>>); + static_assert(std::default_initializable<map<>>); + static_assert(std::move_constructible<map<>>); + static_assert(std::copy_constructible<map<>>); + static_assert(std::equality_comparable<map<>>); + static_assert(std::totally_ordered<map<>>); + static_assert(std::swappable<map<>>); + + static_assert(std::ranges::range<map<integer>>); + static_assert(std::same_as<std::pair<std::string_view, integer>, + std::ranges::range_value_t<map<integer>>>); +} + +TEST_CASE("ucl: map: default construct", "[ucl]") +{ + auto map = nihil::ucl::map<>(); + REQUIRE(str(map.type()) == "object"); +} + +TEST_CASE("ucl: map: construct from initializer_list", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + REQUIRE(str(map.type()) == "object"); + REQUIRE(map["1"] == 1); + REQUIRE(map["42"] == 42); +} + +TEST_CASE("ucl: map: construct from range", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto vec = std::vector<std::pair<std::string_view, integer>>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + auto map = nihil::ucl::map<integer>(std::from_range, vec); + + REQUIRE(str(map.type()) == "object"); + REQUIRE(map["1"] == 1); + REQUIRE(map["42"] == 42); +} + +TEST_CASE("ucl: map: construct from iterator pair", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto vec = std::vector<std::pair<std::string_view, integer>>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + auto map = nihil::ucl::map<integer>(std::ranges::begin(vec), + std::ranges::end(vec)); + + REQUIRE(str(map.type()) == "object"); + REQUIRE(map["1"] == 1); + REQUIRE(map["42"] == 42); +} + +TEST_CASE("ucl: map: insert", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto m = map<integer>(); + + m.insert({"test1"sv, integer(42)}); + m.insert({"test2"sv, integer(666)}); + + REQUIRE(m["test1"] == 42); + REQUIRE(m["test2"] == 666); +} + +TEST_CASE("ucl: map: find", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + auto obj = map.find("42"); + REQUIRE(obj.value() == 42); + + obj = map.find("43"); + REQUIRE(!obj.has_value()); +} + +TEST_CASE("ucl: map: iterate", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + auto i = 0u; + + for (auto [key, value] : map) { + if (key == "1") + REQUIRE(value == 1); + else if (key == "42") + REQUIRE(value == 42); + else + REQUIRE(false); + ++i; + } + + REQUIRE(i == 2); +} + +TEST_CASE("ucl: map: operator[] throws key_not_found", "[ucl]") +{ + auto map = nihil::ucl::map<nihil::ucl::integer>(); + REQUIRE_THROWS_AS(map["nonesuch"], nihil::ucl::key_not_found); +} + +TEST_CASE("ucl: map: remove", "[uc]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + REQUIRE(map.find("42") != std::nullopt); + REQUIRE(map.remove("42") == true); + REQUIRE(map.find("42") == std::nullopt); + REQUIRE(map["1"] == 1); + + REQUIRE(map.remove("42") == false); +} + +TEST_CASE("ucl: map: pop", "[uc]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + REQUIRE(map.find("42") != std::nullopt); + + auto obj = map.pop("42"); + REQUIRE(obj.value() == 42); + + REQUIRE(!map.find("42")); + REQUIRE(map["1"] == 1); + + obj = map.pop("42"); + REQUIRE(!obj); +} + +//NOLINTEND(bugprone-unchecked-optional-access) diff --git a/nihil.ucl/tests/object.cc b/nihil.ucl/tests/object.cc new file mode 100644 index 0000000..3ad180e --- /dev/null +++ b/nihil.ucl/tests/object.cc @@ -0,0 +1,44 @@ +/* + * This source code is released into the public domain. + */ + +#include <catch2/catch_test_macros.hpp> + +#include <ucl.h> + +import nihil.ucl; + +TEST_CASE("ucl object: get_ucl_object", "[ucl]") +{ + auto obj = nihil::ucl::integer(42); + + REQUIRE(obj.get_ucl_object() != nullptr); + static_assert(std::same_as<::ucl_object_t *, + decltype(obj.get_ucl_object())>); + + auto const cobj = obj; + static_assert(std::same_as<::ucl_object_t const *, + decltype(cobj.get_ucl_object())>); +} + +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); +} diff --git a/nihil.ucl/tests/parse.cc b/nihil.ucl/tests/parse.cc new file mode 100644 index 0000000..43ce219 --- /dev/null +++ b/nihil.ucl/tests/parse.cc @@ -0,0 +1,55 @@ +/* + * This source code is released into the public domain. + */ + +#include <string> + +#include <catch2/catch_test_macros.hpp> +#include <catch2/matchers/catch_matchers_floating_point.hpp> + +import nihil.ucl; + +TEST_CASE("ucl parse: iterate array", "[ucl]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto err = parse("value = [1, 42, 666];"sv); + REQUIRE(err); + + auto obj = *err; + + auto arr = obj["value"]; + REQUIRE(arr.key() == "value"); + + 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); + REQUIRE(vec[1] == 42); + REQUIRE(vec[2] == 666); +} + +TEST_CASE("ucl parse: iterate hash", "[ucl]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto input = "int = 42; bool = true; str = \"test\";"sv; + auto obj = parse(input); + REQUIRE(obj); + + for (auto &&[key, value] : *obj) { + REQUIRE(key == value.key()); + + if (key == "int") + REQUIRE(object_cast<integer>(value) == 42); + else if (key == "bool") + REQUIRE(object_cast<boolean>(value) == true); + else if (key == "str") + REQUIRE(object_cast<string>(value) == "test"); + } +} diff --git a/nihil.ucl/tests/real.cc b/nihil.ucl/tests/real.cc new file mode 100644 index 0000000..421917e --- /dev/null +++ b/nihil.ucl/tests/real.cc @@ -0,0 +1,248 @@ +/* + * This source code is released into the public domain. + */ + +#include <concepts> +#include <string> + +#include <catch2/catch_test_macros.hpp> +#include <catch2/matchers/catch_matchers_floating_point.hpp> +#include <ucl.h> + +import nihil.ucl; + +TEST_CASE("ucl: real: invariants", "[ucl]") +{ + using namespace nihil::ucl; + + static_assert(std::same_as<double, real::contained_type>); + REQUIRE(real::ucl_type == object_type::real); + REQUIRE(static_cast<::ucl_type>(real::ucl_type) == UCL_FLOAT); + + static_assert(std::destructible<real>); + static_assert(std::default_initializable<real>); + static_assert(std::move_constructible<real>); + static_assert(std::copy_constructible<real>); + static_assert(std::equality_comparable<real>); + static_assert(std::totally_ordered<real>); + static_assert(std::swappable<real>); +} + +TEST_CASE("ucl: real: constructor", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default") { + auto r = real(); + REQUIRE(r == 0); + } + + SECTION("with value") { + auto r = real(42.1); + REQUIRE_THAT(r.value(), Catch::Matchers::WithinRel(42.1)); + } +} + +TEST_CASE("ucl: real: literal", "[ucl]") +{ + SECTION("with namespace nihil::ucl::literals") { + using namespace nihil::ucl::literals; + + auto r = 42.5_ucl; + REQUIRE(r.type() == nihil::ucl::object_type::real); + REQUIRE_THAT(r.value(), Catch::Matchers::WithinRel(42.5)); + } + + SECTION("with namespace nihil::literals") { + using namespace nihil::literals; + + auto r = 42.5_ucl; + REQUIRE(r.type() == nihil::ucl::object_type::real); + REQUIRE_THAT(r.value(), Catch::Matchers::WithinRel(42.5)); + } +} + +TEST_CASE("ucl: real: construct from UCL object", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("ref, correct type") { + auto uobj = ::ucl_object_fromdouble(42); + + auto r = real(ref, uobj); + REQUIRE(r == 42); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, correct type") { + auto uobj = ::ucl_object_fromdouble(42); + + auto r = real(noref, uobj); + REQUIRE(r == 42); + } + + SECTION("ref, wrong type") { + auto uobj = ::ucl_object_fromint(42); + + REQUIRE_THROWS_AS(real(ref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, wrong type") { + auto uobj = ::ucl_object_fromint(42); + + REQUIRE_THROWS_AS(real(noref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } +} + +TEST_CASE("ucl: real: make_real", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("default value") { + auto i = make_real().value(); + REQUIRE(i == 0); + } + + SECTION("explicit value") { + auto i = make_real(42).value(); + REQUIRE(i == 42); + } +} + +TEST_CASE("ucl: real: swap", "[ucl]") +{ + // do not add using namespace nihil::ucl + + auto r1 = nihil::ucl::real(1); + auto r2 = nihil::ucl::real(2); + + swap(r1, r2); + + REQUIRE(r1 == 2.); + REQUIRE(r2 == 1.); +} + +TEST_CASE("ucl: real: value()", "[ucl]") +{ + using namespace nihil::ucl; + + auto r = 42.5_ucl; + REQUIRE_THAT(r.value(), Catch::Matchers::WithinRel(42.5)); +} + +TEST_CASE("ucl: real: key()", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("parsed with key") { + auto obj = parse("a_real = 42.5").value(); + auto r = object_cast<real>(obj["a_real"]).value(); + REQUIRE(r.key() == "a_real"); + } + + SECTION("bare real, no key") { + auto i = 42.5_ucl; + REQUIRE(i.key() == ""); + } +} + +TEST_CASE("ucl: real: comparison", "[ucl]") +{ + using namespace nihil::ucl; + + auto i = nihil::ucl::real(42.5); + + SECTION("operator==") { + REQUIRE(i == 42.5); + REQUIRE(i == 42.5_ucl); + } + + SECTION("operator!=") { + REQUIRE(i != 1); + REQUIRE(i != 1._ucl); + } + + SECTION("operator<") { + REQUIRE(i < 43); + REQUIRE(i < 43._ucl); + } + + SECTION("operator>") { + REQUIRE(i > 1); + REQUIRE(i > 1._ucl); + } +} + +TEST_CASE("ucl: real: parse", "[ucl]") +{ + using namespace nihil::ucl; + + auto obj = parse("value = 42.1").value(); + + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE_THAT(object_cast<real>(v).value().value(), + Catch::Matchers::WithinRel(42.1)); +} + +TEST_CASE("ucl: real: parse and emit", "[ucl]") +{ + using namespace nihil::ucl; + + auto ucl = parse("real = 42.2").value(); + + auto output = std::string(); + emit(ucl, emitter::configuration, std::back_inserter(output)); + + REQUIRE(output == "real = 42.2;\n"); +} + +TEST_CASE("ucl: real: format", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("bare real") { + auto str = std::format("{}", 42.5_ucl); + REQUIRE(str == "42.5"); + } + + SECTION("parsed real") { + auto obj = parse("real = 42.5;").value(); + auto r = object_cast<real>(obj["real"]).value(); + + auto str = std::format("{}", r); + REQUIRE(str == "42.5"); + } + + SECTION("with format string") { + auto str = std::format("{:10.5f}", 42.5_ucl); + REQUIRE(str == " 42.50000"); + } +} + +TEST_CASE("ucl: real: print to ostream", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("bare real") { + auto strm = std::ostringstream(); + strm << 42.5_ucl; + + REQUIRE(strm.str() == "42.5"); + } + + SECTION("parsed real") { + auto obj = parse("real = 42.5;").value(); + auto i = object_cast<real>(obj["real"]).value(); + + auto strm = std::ostringstream(); + strm << i; + + REQUIRE(strm.str() == "42.5"); + } +} diff --git a/nihil.ucl/tests/string.cc b/nihil.ucl/tests/string.cc new file mode 100644 index 0000000..6409b8d --- /dev/null +++ b/nihil.ucl/tests/string.cc @@ -0,0 +1,415 @@ +/* + * This source code is released into the public domain. + */ + +#include <concepts> +#include <list> +#include <sstream> +#include <string> +#include <vector> + +#include <catch2/catch_test_macros.hpp> +#include <ucl.h> + +import nihil.ucl; + +TEST_CASE("ucl: string: invariants", "[ucl]") +{ + using namespace nihil::ucl; + + static_assert(std::same_as<std::string_view, string::contained_type>); + REQUIRE(string::ucl_type == object_type::string); + REQUIRE(static_cast<::ucl_type>(string::ucl_type) == UCL_STRING); + + static_assert(std::destructible<string>); + static_assert(std::default_initializable<string>); + static_assert(std::move_constructible<string>); + static_assert(std::copy_constructible<string>); + static_assert(std::equality_comparable<string>); + static_assert(std::totally_ordered<string>); + static_assert(std::swappable<string>); + + static_assert(std::ranges::contiguous_range<string>); + static_assert(std::same_as<char, std::ranges::range_value_t<string>>); +} + +TEST_CASE("ucl: string: literal", "[ucl]") +{ + SECTION("with namespace nihil::ucl::literals") { + using namespace nihil::ucl::literals; + + auto s = "testing"_ucl; + REQUIRE(s.type() == nihil::ucl::object_type::string); + REQUIRE(s == "testing"); + } + + SECTION("with namespace nihil::literals") { + using namespace nihil::literals; + + auto s = "testing"_ucl; + REQUIRE(s.type() == nihil::ucl::object_type::string); + REQUIRE(s == "testing"); + } +} + +TEST_CASE("ucl: string: construct", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + SECTION("empty string") { + auto str = string(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == ""); + } + + SECTION("with integer-like value") { + auto str = "42"_ucl; + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "42"); + } + + SECTION("with boolean-like value") { + auto str = "true"_ucl; + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "true"); + } + + SECTION("from string literal") { + auto str = string("testing"); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from std::string") { + auto str = string("testing"s); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from std::string_view") { + auto str = string("testing"sv); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from contiguous range") { + auto s = std::vector{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = string(s); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from non-contiguous range") { + auto s = std::list{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = string(s); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from contiguous iterator pair") { + auto s = std::vector{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = string(s.begin(), s.end()); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from non-contiguous iterator pair") { + auto s = std::list{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = string(s.begin(), s.end()); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } +} + +TEST_CASE("ucl: string: construct from UCL object", "[ucl]") +{ + using namespace nihil::ucl; + + SECTION("ref, correct type") { + auto uobj = ::ucl_object_fromstring("testing"); + + auto s = string(ref, uobj); + REQUIRE(s == "testing"); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, correct type") { + auto uobj = ::ucl_object_fromstring("testing"); + + auto s = string(noref, uobj); + REQUIRE(s == "testing"); + } + + SECTION("ref, wrong type") { + auto uobj = ::ucl_object_frombool(true); + + REQUIRE_THROWS_AS(string(ref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } + + SECTION("noref, wrong type") { + auto uobj = ::ucl_object_frombool(true); + + REQUIRE_THROWS_AS(string(noref, uobj), type_mismatch); + + ::ucl_object_unref(uobj); + } +} + +TEST_CASE("ucl: string: make_string", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + SECTION("empty string") { + auto str = make_string().value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == ""); + } + + SECTION("from string literal") { + auto str = make_string("testing").value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from std::string") { + auto str = make_string("testing"s).value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from std::string_view") { + auto str = make_string("testing"sv).value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from contiguous range") { + auto s = std::vector{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = make_string(s).value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from non-contiguous range") { + auto s = std::list{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = make_string(s).value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from contiguous iterator pair") { + auto s = std::vector{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = make_string(s.begin(), s.end()).value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } + + SECTION("from non-contiguous iterator pair") { + auto s = std::list{'t', 'e', 's', 't', 'i', 'n', 'g'}; + auto str = make_string(s.begin(), s.end()).value(); + REQUIRE(str.type() == object_type::string); + REQUIRE(str == "testing"); + } +} + +TEST_CASE("ucl: string: swap", "[ucl]") +{ + // do not add using namespace nihil::ucl + + auto s1 = nihil::ucl::string("one"); + auto s2 = nihil::ucl::string("two"); + + swap(s1, s2); + + REQUIRE(s1 == "two"); + REQUIRE(s2 == "one"); +} + +TEST_CASE("ucl: string: value()", "[ucl]") +{ + using namespace nihil::ucl; + + auto s = string("te\"st"); + REQUIRE(s.value() == "te\"st"); +} + +TEST_CASE("ucl: string: key()", "[ucl]") +{ + using namespace nihil::ucl; + + auto err = parse("a_string = \"test\""); + REQUIRE(err); + + auto obj = *err; + REQUIRE(object_cast<string>(obj["a_string"])->key() == "a_string"); + + auto s = string("test"); + REQUIRE(s.key() == ""); +} + +TEST_CASE("ucl: string: size", "[ucl]") +{ + using namespace nihil::ucl; + + REQUIRE(string().size() == 0); + REQUIRE(string("test").size() == 4); +} + +TEST_CASE("ucl: string: empty", "[ucl]") +{ + using namespace nihil::ucl; + + REQUIRE(string().empty() == true); + REQUIRE(string("test").empty() == false); +} + +TEST_CASE("ucl: string: iterate", "[ucl]") +{ + using namespace nihil::ucl; + + auto str = "test"_ucl; + + SECTION("as iterator pair") { + auto begin = str.begin(); + static_assert(std::contiguous_iterator<decltype(begin)>); + + auto end = str.end(); + static_assert(std::sentinel_for<decltype(end), + decltype(begin)>); + + REQUIRE(*begin == 't'); + ++begin; + REQUIRE(*begin == 'e'); + ++begin; + REQUIRE(*begin == 's'); + ++begin; + REQUIRE(*begin == 't'); + ++begin; + + REQUIRE(begin == end); + } + + SECTION("as range") { + auto s = std::string(std::from_range, str); + REQUIRE(s == "test"); + } +} + +TEST_CASE("ucl: string: comparison", "[ucl]") +{ + using namespace nihil::ucl; + + auto str = "testing"_ucl; + + SECTION("operator==") { + REQUIRE(str == "testing"_ucl); + REQUIRE(str == std::string_view("testing")); + REQUIRE(str == std::string("testing")); + REQUIRE(str == "testing"); + } + + SECTION("operator!=") { + REQUIRE(str != "test"_ucl); + REQUIRE(str != std::string_view("test")); + REQUIRE(str != std::string("test")); + REQUIRE(str != "test"); + } + + SECTION("operator<") { + REQUIRE(str < "zzz"_ucl); + REQUIRE(str < std::string_view("zzz")); + REQUIRE(str < std::string("zzz")); + REQUIRE(str < "zzz"); + } + + SECTION("operator>") { + REQUIRE(str > "aaa"_ucl); + REQUIRE(str > std::string_view("aaa")); + REQUIRE(str > std::string("aaa")); + REQUIRE(str > "aaa"); + } +} + +TEST_CASE("ucl: string: parse", "[ucl]") +{ + using namespace nihil::ucl; + + auto obj = parse("value = \"te\\\"st\"").value(); + + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE(object_cast<nihil::ucl::string>(v).value() == "te\"st"); +} + +TEST_CASE("ucl: string: emit", "[ucl]") +{ + using namespace nihil::ucl; + + auto ucl = parse("str = \"te\\\"st\";").value(); + + auto output = std::string(); + emit(ucl, emitter::configuration, std::back_inserter(output)); + + REQUIRE(output == "str = \"te\\\"st\";\n"); +} + +TEST_CASE("ucl: string: format", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto constexpr test_string = "te\"st"sv; + + SECTION("bare string") { + auto str = std::format("{}", string(test_string)); + REQUIRE(str == test_string); + } + + SECTION("parsed string") { + auto obj = parse("string = \"te\\\"st\";").value(); + auto s = object_cast<string>(obj["string"]).value(); + + auto str = std::format("{}", s); + REQUIRE(str == test_string); + } + + SECTION("with format string") { + auto str = std::format("{: >10}", string(test_string)); + REQUIRE(str == " te\"st"); + } +} + +TEST_CASE("ucl: string: print to ostream", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto constexpr test_string = "te\"st"sv; + + SECTION("bare string") { + auto strm = std::ostringstream(); + strm << string(test_string); + + REQUIRE(strm.str() == test_string); + } + + SECTION("parsed string") { + auto obj = parse("string = \"te\\\"st\";").value(); + auto s = object_cast<string>(obj["string"]).value(); + + auto strm = std::ostringstream(); + strm << s; + + REQUIRE(strm.str() == test_string); + } + + SECTION("with format string") { + auto str = std::format("{: >10}", string(test_string)); + REQUIRE(str == " te\"st"); + } +} diff --git a/nihil.ucl/type.cc b/nihil.ucl/type.cc new file mode 100644 index 0000000..7d9cad7 --- /dev/null +++ b/nihil.ucl/type.cc @@ -0,0 +1,62 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <format> + +module nihil.ucl; + +namespace nihil::ucl { + +auto str(object_type type) -> std::string_view { + using namespace std::literals; + + switch (type) { + case object_type::object: + return "object"sv; + case object_type::array: + return "array"sv; + case object_type::integer: + return "integer"sv; + case object_type::real: + return "real"sv; + case object_type::string: + return "string"sv; + case object_type::boolean: + return "boolean"sv; + case object_type::time: + return "time"sv; + case object_type::userdata: + return "userdata"sv; + case object_type::null: + return "null"sv; + default: + // Don't fail here, since UCL might add more types that we + // don't know about. + return "unknown"sv; + } +} + +type_mismatch::type_mismatch(object_type expected_type, + object_type actual_type) + : error(std::format( + "expected type '{}' != actual type '{}'", + ucl::str(expected_type), ucl::str(actual_type))) + , m_expected_type(expected_type) + , m_actual_type(actual_type) +{ +} + +auto type_mismatch::expected_type(this type_mismatch const &self) -> object_type +{ + return self.m_expected_type; +} + +auto type_mismatch::actual_type(this type_mismatch const &self) -> object_type +{ + return self.m_actual_type; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/type.ccm b/nihil.ucl/type.ccm new file mode 100644 index 0000000..f3b3aef --- /dev/null +++ b/nihil.ucl/type.ccm @@ -0,0 +1,58 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <concepts> +#include <format> +#include <stdexcept> +#include <string> + +#include <ucl.h> + +export module nihil.ucl:type; + +import nihil.error; + +namespace nihil::ucl { + +// Our strongly-typed version of ::ucl_type. +export enum struct object_type { + object = UCL_OBJECT, + array = UCL_ARRAY, + integer = UCL_INT, + real = UCL_FLOAT, + string = UCL_STRING, + boolean = UCL_BOOLEAN, + time = UCL_TIME, + userdata = UCL_USERDATA, + null = UCL_NULL, +}; + +// Get the name of a type. +export auto str(object_type type) -> std::string_view; + +// Concept of a UCL data type. +export template<typename T> +concept datatype = requires(T o) { + { o.get_ucl_object() } -> std::convertible_to<::ucl_object_t const *>; + { o.type() } -> std::same_as<object_type>; + { T::ucl_type } -> std::convertible_to<object_type>; +}; + +// Exception thrown when a type assertion fails. +export struct type_mismatch : error { + type_mismatch(object_type expected_type, object_type actual_type); + + // The type we expected. + auto expected_type(this type_mismatch const &self) -> object_type; + // The type we got. + auto actual_type(this type_mismatch const &self) -> object_type; + +private: + object_type m_expected_type; + object_type m_actual_type; +}; + +} // namespace nihil::ucl |
