aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.ucl
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-22 14:46:53 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-22 14:46:53 +0100
commitf41970666675f873d7c1075efd192f22df8d17fe (patch)
tree09b8c4da91a7efeb37a92d322d3e729e4dbde659 /nihil.ucl
parentd27d1302d1fa1b96bf8f53f17fce947f19d21330 (diff)
downloadnihil-f41970666675f873d7c1075efd192f22df8d17fe.tar.gz
nihil-f41970666675f873d7c1075efd192f22df8d17fe.tar.bz2
add nihil.ucl (incomplete)
Diffstat (limited to 'nihil.ucl')
-rw-r--r--nihil.ucl/CMakeLists.txt19
-rw-r--r--nihil.ucl/array.ccm342
-rw-r--r--nihil.ucl/boolean.ccm77
-rw-r--r--nihil.ucl/emit.ccm141
-rw-r--r--nihil.ucl/error.ccm26
-rw-r--r--nihil.ucl/integer.ccm77
-rw-r--r--nihil.ucl/nihil.ucl.ccm9
-rw-r--r--nihil.ucl/object.ccm279
-rw-r--r--nihil.ucl/parser.ccm170
-rw-r--r--nihil.ucl/real.ccm76
-rw-r--r--nihil.ucl/string.ccm78
-rw-r--r--nihil.ucl/tests/CMakeLists.txt23
-rw-r--r--nihil.ucl/tests/array.cc109
-rw-r--r--nihil.ucl/tests/boolean.cc50
-rw-r--r--nihil.ucl/tests/emit.cc8
-rw-r--r--nihil.ucl/tests/integer.cc50
-rw-r--r--nihil.ucl/tests/object.cc22
-rw-r--r--nihil.ucl/tests/parse.cc50
-rw-r--r--nihil.ucl/tests/real.cc53
-rw-r--r--nihil.ucl/tests/string.cc38
20 files changed, 1695 insertions, 2 deletions
diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt
index 59021aa..f01fb25 100644
--- a/nihil.ucl/CMakeLists.txt
+++ b/nihil.ucl/CMakeLists.txt
@@ -3,11 +3,28 @@
pkg_check_modules(LIBUCL REQUIRED libucl)
add_library(nihil.ucl STATIC)
+target_link_libraries(nihil.ucl PUBLIC nihil)
target_sources(nihil.ucl PUBLIC
FILE_SET modules TYPE CXX_MODULES FILES
- nihil.ucl.ccm)
+ nihil.ucl.ccm
+ emit.ccm
+ error.ccm
+ object.ccm
+ parser.ccm
+
+ array.ccm
+ boolean.ccm
+ integer.ccm
+ real.ccm
+ string.ccm
+)
+
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..26cd9b9
--- /dev/null
+++ b/nihil.ucl/array.ccm
@@ -0,0 +1,342 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+#include <string>
+
+#include <ucl.h>
+
+export module nihil.ucl:array;
+
+import :object;
+
+namespace nihil::ucl {
+
+export template<datatype T>
+struct array;
+
+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;
+
+ auto operator* (this array_iterator const &self) -> T
+ {
+ auto uobj = ::ucl_array_find_index(self._array, self._idx);
+ if (uobj == nullptr)
+ throw error("failed to fetch UCL array index");
+
+ return T(::ucl_object_ref(uobj));
+ }
+
+ auto operator[] (this array_iterator const &self,
+ difference_type idx)
+ -> T
+ {
+ return *(self + idx);
+ }
+
+ auto operator++ (this array_iterator &self) -> array_iterator&
+ {
+ ++self._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._idx == 0)
+ throw std::out_of_range("attempt to iterate before "
+ "start of UCL array");
+ --self._idx;
+ return self;
+ }
+
+ auto operator-- (this array_iterator &self, int) -> array_iterator
+ {
+ auto copy = self;
+ --self;
+ return copy;
+ }
+
+ auto operator== (this array_iterator const &lhs,
+ array_iterator const &rhs)
+ -> bool
+ {
+ return lhs._idx == rhs._idx;
+ }
+
+ auto operator<=> (this array_iterator const &lhs,
+ array_iterator const &rhs)
+ {
+ return lhs._idx <=> rhs._idx;
+ }
+
+ auto operator+= (this array_iterator &lhs,
+ difference_type rhs)
+ -> array_iterator &
+ {
+ lhs._idx += rhs;
+ return lhs;
+ }
+
+ auto operator-= (this array_iterator &lhs,
+ difference_type rhs)
+ -> array_iterator &
+ {
+ lhs._idx -= rhs;
+ return lhs;
+ }
+
+ auto operator- (this array_iterator const &lhs,
+ array_iterator const &rhs)
+ -> difference_type
+ {
+ return lhs._idx - rhs._idx;
+ }
+
+private:
+ friend struct array<T>;
+
+ ::ucl_object_t const *_array{};
+ std::size_t _idx{};
+
+ array_iterator(::ucl_object_t const *array, std::size_t idx)
+ : _array(array)
+ , _idx(idx)
+ {}
+};
+
+export template<datatype T>
+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>
+auto operator+(typename array_iterator<T>::difference_type lhs,
+ array_iterator<T> const &rhs)
+ -> array_iterator<T>
+{
+ return rhs - lhs;
+}
+
+export template<datatype T>
+auto operator-(array_iterator<T> const &lhs,
+ typename array_iterator<T>::difference_type rhs)
+ -> array_iterator<T>
+{
+ auto copy = lhs;
+ copy -= rhs;
+ return copy;
+}
+
+static_assert(std::random_access_iterator<array_iterator<object>>);
+
+export template<datatype T>
+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;
+
+ array() : object(::ucl_object_typed_new(UCL_ARRAY))
+ {
+ if (_object == nullptr)
+ throw error("failed to create UCL object");
+ }
+
+ explicit array(::ucl_object_t *uobj) : object(uobj)
+ {
+ assert(type() == object_type::array);
+ }
+
+ /*
+ * 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)
+ : object(::ucl_object_typed_new(UCL_ARRAY))
+ {
+ if (_object == nullptr)
+ throw error("failed to create UCL object");
+
+ // 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.
+ */
+
+ auto begin(this array const &self) -> array_iterator<T>
+ {
+ return {self.get_ucl_object(), 0};
+ }
+
+ auto end(this array const &self) -> array_iterator<T>
+ {
+ return {self.get_ucl_object(), self.size()};
+ }
+
+ /*
+ * Return the size of this array.
+ */
+ auto size(this array const &self) -> size_type
+ {
+ return ::ucl_array_size(self.get_ucl_object());
+ }
+
+ /*
+ * Test if this array is empty.
+ */
+ 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 &&v) -> void
+ {
+ // There's no real benefit to moving the object here, but
+ // move it anyway to preserve the expected semantics.
+ auto copy = std::move(v);
+ self.push_back(copy);
+ }
+
+ 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 &&v) -> void
+ {
+ // There's no real benefit to moving the object here, but
+ // move it anyway to preserve the expected semantics.
+ auto copy = std::move(v);
+ self.push_front(copy);
+ }
+
+ 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.
+ */
+ 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 error("failed to fetch UCL array index");
+
+ return T(::ucl_object_ref(uobj));
+ }
+
+ auto operator[] (this array const &self, size_type idx) -> T
+ {
+ return self.at(idx);
+ }
+
+ /*
+ * Return the first element.
+ */
+ auto front(this array const &self) -> T
+ {
+ return self.at(0);
+ }
+
+ /*
+ * Return the last element.
+ */
+ 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>
+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;
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/boolean.ccm b/nihil.ucl/boolean.ccm
new file mode 100644
index 0000000..db6c864
--- /dev/null
+++ b/nihil.ucl/boolean.ccm
@@ -0,0 +1,77 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+#include <string>
+
+#include <ucl.h>
+
+export module nihil.ucl:boolean;
+
+import :object;
+
+namespace nihil::ucl {
+
+export struct boolean final : object {
+ using value_type = bool;
+
+ inline static constexpr object_type ucl_type = object_type::boolean;
+
+ boolean(value_type value)
+ : object(::ucl_object_frombool(value))
+ {
+ if (_object == nullptr)
+ throw error("failed to create UCL object");
+ }
+
+ explicit boolean(::ucl_object_t *uobj) : object(uobj)
+ {
+ assert(type() == object_type::boolean);
+ }
+
+ auto value(this boolean const &self) -> value_type
+ {
+ auto v = value_type{};
+ auto const *uobj = self.get_ucl_object();
+
+ if (::ucl_object_toboolean_safe(uobj, &v))
+ return v;
+
+ std::abort();
+ }
+};
+
+/*
+ * Comparison operators.
+ */
+
+export auto operator== (boolean const &a, boolean const &b)
+ -> bool
+{
+ return a.value() == b.value();
+}
+
+export auto operator<=> (boolean const &a, boolean const &b)
+ -> std::strong_ordering
+{
+ return a.value() <=> b.value();
+}
+
+export auto operator== (boolean const &a, boolean::value_type b)
+ -> bool
+{
+ return a.value() == b;
+}
+
+export auto operator<=> (boolean const &a, boolean::value_type b)
+ -> std::strong_ordering
+{
+ return a.value() <=> b;
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/emit.ccm b/nihil.ucl/emit.ccm
new file mode 100644
index 0000000..8fdf616
--- /dev/null
+++ b/nihil.ucl/emit.ccm
@@ -0,0 +1,141 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <array>
+#include <charconv>
+#include <cstdlib>
+#include <iterator>
+#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 (for example, returning errors?) 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);
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/error.ccm b/nihil.ucl/error.ccm
new file mode 100644
index 0000000..4eda774
--- /dev/null
+++ b/nihil.ucl/error.ccm
@@ -0,0 +1,26 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <format>
+#include <utility>
+
+export module nihil.ucl:error;
+
+import nihil;
+
+namespace nihil::ucl {
+
+/*
+ * Exception thrown when an issue occurs with UCL.
+ */
+export struct error : generic_error {
+ template<typename... Args>
+ error(std::format_string<Args...> fmt, Args &&...args)
+ : generic_error(fmt, std::forward<Args>(args)...)
+ {}
+};
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/integer.ccm b/nihil.ucl/integer.ccm
new file mode 100644
index 0000000..d3009a1
--- /dev/null
+++ b/nihil.ucl/integer.ccm
@@ -0,0 +1,77 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+#include <string>
+
+#include <ucl.h>
+
+export module nihil.ucl:integer;
+
+import :object;
+
+namespace nihil::ucl {
+
+export struct integer final : object {
+ using value_type = std::int64_t;
+
+ inline static constexpr object_type ucl_type = object_type::integer;
+
+ integer(value_type value)
+ : object(::ucl_object_fromint(value))
+ {
+ if (_object == nullptr)
+ throw error("failed to create UCL object");
+ }
+
+ explicit integer(::ucl_object_t *uobj) : object(uobj)
+ {
+ assert(type() == object_type::integer);
+ }
+
+ auto value(this integer const &self) -> value_type
+ {
+ auto v = value_type{};
+ auto const *uobj = self.get_ucl_object();
+
+ if (::ucl_object_toint_safe(uobj, &v))
+ return v;
+
+ std::abort();
+ }
+};
+
+/*
+ * Comparison operators.
+ */
+
+export auto operator== (integer const &a, integer const &b)
+ -> bool
+{
+ return a.value() == b.value();
+}
+
+export auto operator<=> (integer const &a, integer const &b)
+ -> std::strong_ordering
+{
+ return a.value() <=> b.value();
+}
+
+export auto operator== (integer const &a, integer::value_type b)
+ -> bool
+{
+ return a.value() == b;
+}
+
+export auto operator<=> (integer const &a, integer::value_type b)
+ -> std::strong_ordering
+{
+ return a.value() <=> b;
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm
index 23151e0..d0924d8 100644
--- a/nihil.ucl/nihil.ucl.ccm
+++ b/nihil.ucl/nihil.ucl.ccm
@@ -5,5 +5,12 @@
module;
export module nihil.ucl;
+export import :emit;
+export import :object;
+export import :parser;
-// TODO: Implement.
+export import :array;
+export import :boolean;
+export import :integer;
+export import :real;
+export import :string;
diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm
new file mode 100644
index 0000000..0b8c95f
--- /dev/null
+++ b/nihil.ucl/object.ccm
@@ -0,0 +1,279 @@
+/*
+ * 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 <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <format>
+#include <string>
+#include <utility>
+
+#include <ucl.h>
+
+export module nihil.ucl:object;
+
+import nihil;
+import :error;
+
+namespace nihil::ucl {
+
+export struct parser;
+
+/***********************************************************************
+ * The basic object type.
+ */
+
+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,
+};
+
+template<typename T>
+concept datatype = requires(T o) {
+ { T::ucl_type } -> std::convertible_to<object_type>;
+};
+
+export struct object {
+ inline static constexpr object_type ucl_type = object_type::object;
+
+ // Free our object on destruction.
+ virtual ~object() {
+ if (_object)
+ ::ucl_object_unref(_object);
+ }
+
+ // Movable.
+ object(object &&other) noexcept
+ : _object(std::exchange(other._object, nullptr))
+ {}
+
+ auto operator=(this object &self, object &&other) noexcept
+ -> object &
+ {
+ if (&self != &other)
+ self._object = std::exchange(other._object, nullptr);
+ return self;
+ }
+
+ // Copyable.
+ object(object const &other) noexcept
+ : _object(nullptr)
+ {
+ *this = other;
+ }
+
+ auto operator=(this object &self, object const &other)
+ -> object &
+ {
+ if (&self != &other) {
+ if (self._object != nullptr)
+ ::ucl_object_unref(self._object);
+
+ if (other._object != nullptr) {
+ self._object = ::ucl_object_copy(other._object);
+ if (self._object == nullptr)
+ throw error("failed to copy UCL object");
+ } else {
+ self._object = nullptr;
+ }
+ }
+
+ return self;
+ }
+
+ // Return the type of this object.
+ auto type(this object const &self) -> object_type
+ {
+ switch (ucl_object_type(self.get_ucl_object())) {
+ case UCL_OBJECT:
+ return object_type::object;
+ case UCL_ARRAY:
+ return object_type::array;
+ case UCL_INT:
+ return object_type::integer;
+ case UCL_FLOAT:
+ return object_type::real;
+ case UCL_STRING:
+ return object_type::string;
+ case UCL_BOOLEAN:
+ return object_type::boolean;
+ case UCL_TIME:
+ return object_type::time;
+ case UCL_USERDATA:
+ return object_type::userdata;
+ case UCL_NULL:
+ return object_type::null;
+ default:
+ std::abort();
+ }
+ }
+
+ // Return the underlying object.
+ auto get_ucl_object(this object &self) -> ::ucl_object_t *
+ {
+ return self._object;
+ }
+
+ auto get_ucl_object(this object const &self) -> ::ucl_object_t const *
+ {
+ return self._object;
+ }
+
+ // Return the key of this object.
+ auto key(this object const &self) -> std::string_view
+ {
+ auto dlen = std::size_t{};
+ auto const *dptr = ::ucl_object_keyl(self._object, &dlen);
+ return {dptr, dlen};
+ }
+
+ // Return a sub-object of this one.
+ auto lookup(this object const &self, std::string_view key)
+ -> std::optional<object>
+ {
+ auto const *obj = ::ucl_object_lookup_any(
+ self._object, key.data(), key.size());
+ if (obj == nullptr)
+ return {};
+
+ return {object(::ucl_object_ref(obj))};
+ }
+
+protected:
+ // Create an object from an existing ucl_object_t. We assume the
+ // object has already been referenced.
+ object(::ucl_object_t *object) : _object(object) {}
+
+ // The object we're wrapping.
+ ::ucl_object_t *_object = nullptr;
+
+private:
+
+ friend struct parser;
+ friend struct iterator;
+};
+
+/***********************************************************************
+ * Object iteration.
+ */
+
+export struct iterator {
+ using difference_type = std::ptrdiff_t;
+ using value_type = object;
+ using reference = value_type &;
+ using const_reference = value_type const &;
+ using pointer = value_type *;
+ using const_pointer = value_type const *;
+
+ struct sentinel{};
+
+ explicit iterator(object const &obj)
+ {
+ _state = std::make_shared<state>(obj);
+ ++(*this);
+ }
+
+ auto operator==(this iterator const &self, sentinel) -> bool
+ {
+ return (self._state->cur == nullptr);
+ }
+
+ auto operator++(this iterator &self) -> iterator &
+ {
+ self._state->next();
+ return self;
+ }
+
+ auto operator++(this iterator &self, int) -> iterator &
+ {
+ self._state->next();
+ return self;
+ }
+
+ auto operator*(this iterator const &self) -> object {
+ auto *ptr = ::ucl_object_ref(self._state->cur);
+ return object(ptr);
+ }
+
+private:
+ struct state {
+ state(object const &obj)
+ {
+ auto const *uobj = obj.get_ucl_object();
+ if ((iter = ::ucl_object_iterate_new(uobj)) == nullptr)
+ throw error("failed to create UCL iterator");
+ }
+
+ 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> _state;
+};
+
+static_assert(std::input_iterator<iterator>);
+
+export auto begin(object const &o) -> iterator
+{
+ return iterator(o);
+}
+
+export auto end(object const &) -> iterator::sentinel
+{
+ return {};
+}
+
+/***********************************************************************
+ * Value access by object_cast.
+ */
+
+// Exception thrown when object_cast fails.
+export struct bad_object_cast : error {
+ bad_object_cast()
+ : error("bad object_cast<>: object does not match target type")
+ {}
+};
+
+//export template<typename To>
+////auto object_cast(object const &from) -> To = delete;
+
+export template<datatype To>
+auto object_cast(object const &from) -> To
+{
+ if (from.type() != To::ucl_type)
+ throw bad_object_cast();
+
+ auto const *uobj = from.get_ucl_object();
+ auto *refptr = ::ucl_object_ref(uobj);
+ return To(refptr);
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/parser.ccm b/nihil.ucl/parser.ccm
new file mode 100644
index 0000000..17ed79c
--- /dev/null
+++ b/nihil.ucl/parser.ccm
@@ -0,0 +1,170 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <format>
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <ucl.h>
+
+export module nihil.ucl:parser;
+
+import nihil;
+import :error;
+import :object;
+
+namespace nihil::ucl {
+
+/*
+ * Exception thrown when an issue occurs parsing UCL.
+ */
+export struct parse_error : error {
+ template<typename... Args>
+ parse_error(std::format_string<Args...> fmt, Args &&...args)
+ : error(fmt, std::forward<Args>(args)...)
+ {}
+};
+
+// 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;
+
+export struct parser;
+
+// 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
+ {
+ auto handler = static_cast<macro_handler *>(ud);
+ auto string = std::string_view(
+ reinterpret_cast<char const *>(data),
+ len);
+ return handler->callback(string);
+ }
+};
+
+/*
+ * A UCL parser. This wraps the C ucl_parser API.
+ */
+export struct parser {
+
+ // Create a new parser with the given flags.
+ parser(int flags) {
+ if ((_parser = ::ucl_parser_new(flags)) != nullptr)
+ return;
+
+ throw error("failed to create UCL parser");
+ }
+
+ // Create a new parser with the default flags.
+ parser() : parser(0) {}
+
+ // Destroy our parser when we're destroyed.
+ ~parser()
+ {
+ if (_parser)
+ ::ucl_parser_free(_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::move(func));
+
+ auto cname = std::string(name);
+ ::ucl_parser_register_macro(self._parser, cname.c_str(),
+ &macro_handler::handle,
+ handler.get());
+
+ self._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
+ {
+ ::ucl_parser_register_variable(self._parser,
+ std::string(variable).c_str(),
+ std::string(value).c_str());
+ }
+
+ // Add data to the parser.
+ auto add(this parser &self,
+ std::ranges::contiguous_range auto &&data)
+ -> void
+ // Only bytes (chars) are permitted.
+ requires(sizeof(std::ranges::range_value_t<decltype(data)>) == 1)
+ {
+ // UCL accepts unsigned chars, but this is quite unhelpful
+ // when reading from files or strings.
+ auto dptr = reinterpret_cast<unsigned char const *>(
+ std::ranges::data(data));
+
+ auto ret = ::ucl_parser_add_chunk(self._parser, dptr,
+ std::ranges::size(data));
+ if (ret == false)
+ throw parse_error("{}",
+ ::ucl_parser_get_error(self._parser));
+ }
+
+ auto add(this parser &self, std::ranges::range auto &&data)
+ -> void
+ requires (!std::ranges::contiguous_range<decltype(data)>)
+ {
+ auto cdata = std::vector<char>(
+ std::from_range,
+ std::forward<decltype(data)>(data));
+ return self.add(std::move(cdata));
+ }
+
+ // Return the top object of this parser.
+ auto top(this parser &self) -> object
+ {
+ if (self._parser == nullptr)
+ throw error("attempt to call top() on an empty parser");
+
+ auto obj = ::ucl_parser_get_object(self._parser);
+ if (obj == nullptr)
+ throw error("attempt to call top() on an empty parser");
+
+ return {obj};
+ }
+
+private:
+ // The parser object. Should never be null, unless we've been
+ // moved-from.
+ ucl_parser *_parser = nullptr;
+
+ // Functions added by register_macro. We have to store these as
+ // pointers because we pass the address to libucl.
+ std::vector<std::unique_ptr<macro_handler>> _macros;
+};
+
+// Utility function to parse something and return the object.
+export auto parse(std::ranges::range auto &&data) -> object {
+ auto p = parser();
+ p.add(std::forward<decltype(data)>(data));
+ return p.top();
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/real.ccm b/nihil.ucl/real.ccm
new file mode 100644
index 0000000..4639109
--- /dev/null
+++ b/nihil.ucl/real.ccm
@@ -0,0 +1,76 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cassert>
+#include <cstdlib>
+#include <string>
+
+#include <ucl.h>
+
+export module nihil.ucl:real;
+
+import :object;
+
+namespace nihil::ucl {
+
+export struct real final : object {
+ using value_type = double;
+
+ inline static constexpr object_type ucl_type = object_type::real;
+
+ real(value_type value)
+ : object(::ucl_object_fromdouble(value))
+ {
+ if (_object == nullptr)
+ throw error("failed to create UCL object");
+ }
+
+ explicit real(::ucl_object_t *uobj) : object(uobj)
+ {
+ assert(type() == object_type::real);
+ }
+
+ auto value(this real const &self) -> value_type
+ {
+ auto v = value_type{};
+ auto const *uobj = self.get_ucl_object();
+
+ if (::ucl_object_todouble_safe(uobj, &v))
+ return v;
+
+ std::abort();
+ }
+};
+
+/*
+ * Comparison operators.
+ */
+
+export auto operator== (real const &a, real const &b)
+ -> bool
+{
+ return a.value() == b.value();
+}
+
+export auto operator<=> (real const &a, real const &b)
+ -> std::partial_ordering
+{
+ return a.value() <=> b.value();
+}
+
+export auto operator== (real const &a, real::value_type b)
+ -> bool
+{
+ return a.value() == b;
+}
+
+export auto operator<=> (real const &a, real::value_type b)
+ -> std::partial_ordering
+{
+ return a.value() <=> b;
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/string.ccm b/nihil.ucl/string.ccm
new file mode 100644
index 0000000..e41c70f
--- /dev/null
+++ b/nihil.ucl/string.ccm
@@ -0,0 +1,78 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cassert>
+#include <cstdlib>
+#include <string>
+
+#include <ucl.h>
+
+export module nihil.ucl:string;
+
+import :object;
+
+namespace nihil::ucl {
+
+export struct string final : object {
+ using value_type = std::string_view;
+
+ inline static constexpr object_type ucl_type = object_type::string;
+
+ string(value_type value)
+ : object(::ucl_object_fromstring_common(
+ value.data(), value.size(), UCL_STRING_RAW))
+ {
+ if (_object == nullptr)
+ throw error("failed to create UCL object");
+ }
+
+ explicit string(::ucl_object_t *uobj) : object(uobj)
+ {
+ assert(type() == object_type::string);
+ }
+
+ auto value(this string const &self) -> value_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};
+
+ std::abort();
+ }
+};
+
+/*
+ * Comparison operators.
+ */
+
+export auto operator== (string const &a, string const &b)
+ -> bool
+{
+ return a.value() == b.value();
+}
+
+export auto operator<=> (string const &a, string const &b)
+ -> std::strong_ordering
+{
+ return a.value() <=> b.value();
+}
+
+export auto operator== (string const &a, string::value_type b)
+ -> bool
+{
+ return a.value() == b;
+}
+
+export auto operator<=> (string const &a, string::value_type b)
+ -> std::strong_ordering
+{
+ return a.value() <=> b;
+}
+
+} // namespace nihil::ucl
diff --git a/nihil.ucl/tests/CMakeLists.txt b/nihil.ucl/tests/CMakeLists.txt
new file mode 100644
index 0000000..2c4ec5d
--- /dev/null
+++ b/nihil.ucl/tests/CMakeLists.txt
@@ -0,0 +1,23 @@
+# 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
+ real.cc
+ string.cc
+)
+
+target_link_libraries(nihil.ucl.test PRIVATE
+ nihil 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..687b02c
--- /dev/null
+++ b/nihil.ucl/tests/array.cc
@@ -0,0 +1,109 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.ucl;
+
+TEST_CASE("ucl: array: construct", "[ucl]")
+{
+ auto arr = nihil::ucl::array<nihil::ucl::integer>();
+ REQUIRE(arr.size() == 0);
+}
+
+TEST_CASE("ucl: array: push_back", "[ucl]")
+{
+ auto arr = nihil::ucl::array<nihil::ucl::integer>();
+ REQUIRE(arr.size() == 0);
+
+ arr.push_back(nihil::ucl::integer(1));
+ arr.push_back(nihil::ucl::integer(42));
+ arr.push_back(nihil::ucl::integer(666));
+
+ REQUIRE(arr.size() == 3);
+ REQUIRE(arr[0].value() == 1);
+ REQUIRE(arr[1].value() == 42);
+ REQUIRE(arr[2].value() == 666);
+
+ REQUIRE_THROWS_AS(arr[3], std::out_of_range);
+
+ REQUIRE(arr.front() == 1);
+ REQUIRE(arr.back() == 666);
+
+}
+
+TEST_CASE("ucl: array: compare", "[ucl]")
+{
+ auto arr = nihil::ucl::array<nihil::ucl::integer>();
+ arr.push_back(1);
+ arr.push_back(42);
+ arr.push_back(666);
+
+ auto arr2 = nihil::ucl::array<nihil::ucl::integer>();
+ REQUIRE(arr != arr2);
+
+ arr2.push_back(1);
+ arr2.push_back(42);
+ arr2.push_back(666);
+ REQUIRE(arr == arr2);
+
+ auto arr3 = nihil::ucl::array<nihil::ucl::integer>();
+ arr3.push_back(1);
+ arr3.push_back(1);
+ arr3.push_back(1);
+ REQUIRE(arr != arr3);
+}
+
+TEST_CASE("ucl: array: iterator", "[ucl]")
+{
+ auto arr = nihil::ucl::array<nihil::ucl::integer>{1, 42, 666};
+
+ auto it = arr.begin();
+ REQUIRE(*it == 1);
+
+ ++it;
+ REQUIRE(*it == 42);
+
+ ++it;
+ REQUIRE(*it == 666);
+
+ --it;
+ REQUIRE(*it == 42);
+}
+
+TEST_CASE("ucl: array: parse", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "value = [1, 42, 666]"sv;
+ auto obj = nihil::ucl::parse(input);
+ auto v = obj.lookup("value");
+
+ REQUIRE(v);
+ REQUIRE(v->key() == "value");
+
+ auto arr = object_cast<nihil::ucl::array<nihil::ucl::integer>>(*v);
+ REQUIRE(arr.size() == 3);
+ REQUIRE(arr[0] == 1);
+ REQUIRE(arr[1] == 42);
+ REQUIRE(arr[2] == 666);
+}
+
+TEST_CASE("ucl: array: emit", "[ucl]")
+{
+ auto ucl = nihil::ucl::parse("array = [1, 42, 666];");
+
+ auto output = std::string();
+ emit(ucl, nihil::ucl::emitter::configuration,
+ std::back_inserter(output));
+
+ REQUIRE(output ==
+"array [\n"
+" 1,\n"
+" 42,\n"
+" 666,\n"
+"]\n");
+}
diff --git a/nihil.ucl/tests/boolean.cc b/nihil.ucl/tests/boolean.cc
new file mode 100644
index 0000000..c023498
--- /dev/null
+++ b/nihil.ucl/tests/boolean.cc
@@ -0,0 +1,50 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.ucl;
+
+TEST_CASE("ucl: boolean: construct", "[ucl]")
+{
+ auto obj = nihil::ucl::boolean(true);
+ REQUIRE(object_cast<nihil::ucl::boolean>(obj).value() == true);
+}
+
+TEST_CASE("ucl: boolean: compare", "[ucl]")
+{
+ auto b = nihil::ucl::boolean(true);
+
+ REQUIRE(b == nihil::ucl::boolean(true));
+ REQUIRE(b == true);
+
+ REQUIRE(b != nihil::ucl::boolean(false));
+ REQUIRE(b != false);
+}
+
+TEST_CASE("ucl: boolean: parse", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "value = true"sv;
+ auto obj = nihil::ucl::parse(input);
+
+ auto v = obj.lookup("value");
+ REQUIRE(v);
+ REQUIRE(v->key() == "value");
+ REQUIRE(object_cast<nihil::ucl::boolean>(*v).value() == true);
+}
+
+TEST_CASE("ucl: boolean: emit", "[ucl]")
+{
+ auto ucl = nihil::ucl::parse("bool = true;");
+
+ auto output = std::string();
+ emit(ucl, nihil::ucl::emitter::configuration,
+ std::back_inserter(output));
+
+ REQUIRE(output == "bool = true;\n");
+}
diff --git a/nihil.ucl/tests/emit.cc b/nihil.ucl/tests/emit.cc
new file mode 100644
index 0000000..13f5914
--- /dev/null
+++ b/nihil.ucl/tests/emit.cc
@@ -0,0 +1,8 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+import nihil.ucl;
diff --git a/nihil.ucl/tests/integer.cc b/nihil.ucl/tests/integer.cc
new file mode 100644
index 0000000..da119e6
--- /dev/null
+++ b/nihil.ucl/tests/integer.cc
@@ -0,0 +1,50 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.ucl;
+
+TEST_CASE("ucl: integer: construct", "[ucl]")
+{
+ auto obj = nihil::ucl::integer(42);
+ REQUIRE(object_cast<nihil::ucl::integer>(obj).value() == 42);
+}
+
+TEST_CASE("ucl: integer: compare", "[ucl]")
+{
+ auto i = nihil::ucl::integer(42);
+
+ REQUIRE(i == nihil::ucl::integer(42));
+ REQUIRE(i != nihil::ucl::integer(1));
+ REQUIRE((i == 42) == true);
+
+ REQUIRE(i > nihil::ucl::integer(40));
+ REQUIRE(i > 40);
+}
+
+TEST_CASE("ucl: parse: integer", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "value = 42"sv;
+ auto obj = nihil::ucl::parse(input);
+ auto v = obj.lookup("value");
+ REQUIRE(v);
+ REQUIRE(v->key() == "value");
+ REQUIRE(object_cast<nihil::ucl::integer>(*v).value() == 42);
+}
+
+TEST_CASE("ucl: integer: emit", "[ucl]")
+{
+ auto ucl = nihil::ucl::parse("int = 42;");
+
+ auto output = std::string();
+ emit(ucl, nihil::ucl::emitter::configuration,
+ std::back_inserter(output));
+
+ REQUIRE(output == "int = 42;\n");
+}
diff --git a/nihil.ucl/tests/object.cc b/nihil.ucl/tests/object.cc
new file mode 100644
index 0000000..f9cef9f
--- /dev/null
+++ b/nihil.ucl/tests/object.cc
@@ -0,0 +1,22 @@
+/*
+ * 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())>);
+}
diff --git a/nihil.ucl/tests/parse.cc b/nihil.ucl/tests/parse.cc
new file mode 100644
index 0000000..3a4f061
--- /dev/null
+++ b/nihil.ucl/tests/parse.cc
@@ -0,0 +1,50 @@
+/*
+ * 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;
+import nihil.ucl;
+
+TEST_CASE("ucl parse: iterate array", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "value = [1, 42, 666];"sv;
+ auto obj = nihil::ucl::parse(input);
+
+ auto array = obj.lookup("value");
+ REQUIRE(array);
+ REQUIRE(array->key() == "value");
+
+ auto vec = std::vector<nihil::ucl::object>();
+ std::ranges::copy(*array, std::back_inserter(vec));
+ REQUIRE(vec.size() == 3);
+ REQUIRE(object_cast<nihil::ucl::integer>(vec[0]).value() == 1);
+ REQUIRE(object_cast<nihil::ucl::integer>(vec[1]).value() == 42);
+ REQUIRE(object_cast<nihil::ucl::integer>(vec[2]).value() == 666);
+}
+
+TEST_CASE("ucl parse: iterate hash", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "int = 42; bool = true; str = \"test\";"sv;
+ auto obj = nihil::ucl::parse(input);
+
+ for (auto &&value : obj) {
+ if (value.key() == "int")
+ REQUIRE(object_cast<nihil::ucl::integer>(value).value()
+ == 42);
+ else if (value.key() == "bool")
+ REQUIRE(object_cast<nihil::ucl::boolean>(value).value()
+ == true);
+ else if (value.key() == "str")
+ REQUIRE(object_cast<nihil::ucl::string>(value).value()
+ == "test");
+ }
+}
diff --git a/nihil.ucl/tests/real.cc b/nihil.ucl/tests/real.cc
new file mode 100644
index 0000000..275684a
--- /dev/null
+++ b/nihil.ucl/tests/real.cc
@@ -0,0 +1,53 @@
+/*
+ * 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: real: construct", "[ucl]")
+{
+ auto obj = nihil::ucl::real(42.1);
+ REQUIRE_THAT(object_cast<nihil::ucl::real>(obj).value(),
+ Catch::Matchers::WithinRel(42.1));
+}
+
+TEST_CASE("ucl: real: compare", "[ucl]")
+{
+ auto i = nihil::ucl::real(42);
+
+ REQUIRE(i == nihil::ucl::real(42));
+ REQUIRE(i != nihil::ucl::real(1));
+ REQUIRE((i == 42) == true);
+
+ REQUIRE(i > nihil::ucl::real(40));
+ REQUIRE(i > 40);
+}
+
+TEST_CASE("ucl: real: parse", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "value = 42.1"sv;
+ auto obj = nihil::ucl::parse(input);
+ auto v = obj.lookup("value");
+ REQUIRE(v);
+ REQUIRE(v->key() == "value");
+ REQUIRE_THAT(object_cast<nihil::ucl::real>(*v).value(),
+ Catch::Matchers::WithinRel(42.1));
+}
+
+TEST_CASE("ucl: real: emit", "[ucl]")
+{
+ auto ucl = nihil::ucl::parse("real = 42.2");
+
+ auto output = std::string();
+ emit(ucl, nihil::ucl::emitter::configuration,
+ std::back_inserter(output));
+
+ REQUIRE(output == "real = 42.2;\n");
+}
diff --git a/nihil.ucl/tests/string.cc b/nihil.ucl/tests/string.cc
new file mode 100644
index 0000000..4385dbb
--- /dev/null
+++ b/nihil.ucl/tests/string.cc
@@ -0,0 +1,38 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <string>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.ucl;
+
+TEST_CASE("ucl: string: construct", "[ucl]")
+{
+ auto obj = nihil::ucl::string("testing");
+ REQUIRE(object_cast<nihil::ucl::string>(obj).value() == "testing");
+}
+
+TEST_CASE("ucl: string: parse", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto input = "value = \"str\""sv;
+ auto obj = nihil::ucl::parse(input);
+ auto v = obj.lookup("value");
+ REQUIRE(v);
+ REQUIRE(v->key() == "value");
+ REQUIRE(object_cast<nihil::ucl::string>(*v).value() == "str");
+}
+
+TEST_CASE("ucl: string: emit", "[ucl]")
+{
+ auto ucl = nihil::ucl::parse("str = \"te\\\"st\";");
+
+ auto output = std::string();
+ emit(ucl, nihil::ucl::emitter::configuration,
+ std::back_inserter(output));
+
+ REQUIRE(output == "str = \"te\\\"st\";\n");
+}