aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.ucl/tests
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.ucl/tests')
-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
10 files changed, 2018 insertions, 0 deletions
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");
+ }
+}