aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.ucl
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.ucl')
-rw-r--r--nihil.ucl/CMakeLists.txt46
-rw-r--r--nihil.ucl/array.ccm468
-rw-r--r--nihil.ucl/boolean.cc106
-rw-r--r--nihil.ucl/boolean.ccm91
-rw-r--r--nihil.ucl/emit.cc21
-rw-r--r--nihil.ucl/emit.ccm209
-rw-r--r--nihil.ucl/errc.cc49
-rw-r--r--nihil.ucl/errc.ccm33
-rw-r--r--nihil.ucl/integer.cc102
-rw-r--r--nihil.ucl/integer.ccm115
-rw-r--r--nihil.ucl/map.ccm293
-rw-r--r--nihil.ucl/nihil.ucl.ccm21
-rw-r--r--nihil.ucl/object.cc114
-rw-r--r--nihil.ucl/object.ccm88
-rw-r--r--nihil.ucl/object_cast.ccm89
-rw-r--r--nihil.ucl/parser.cc102
-rw-r--r--nihil.ucl/parser.ccm160
-rw-r--r--nihil.ucl/real.cc104
-rw-r--r--nihil.ucl/real.ccm112
-rw-r--r--nihil.ucl/string.cc187
-rw-r--r--nihil.ucl/string.ccm229
-rw-r--r--nihil.ucl/tests/CMakeLists.txt22
-rw-r--r--nihil.ucl/tests/array.cc478
-rw-r--r--nihil.ucl/tests/boolean.cc224
-rw-r--r--nihil.ucl/tests/emit.cc93
-rw-r--r--nihil.ucl/tests/integer.cc247
-rw-r--r--nihil.ucl/tests/map.cc192
-rw-r--r--nihil.ucl/tests/object.cc44
-rw-r--r--nihil.ucl/tests/parse.cc55
-rw-r--r--nihil.ucl/tests/real.cc248
-rw-r--r--nihil.ucl/tests/string.cc415
-rw-r--r--nihil.ucl/type.cc62
-rw-r--r--nihil.ucl/type.ccm58
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(),
+ &macro_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