aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-22 15:22:23 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-22 15:22:23 +0100
commita14cb70cb715beb714e7b05d8fe631c0df4a7053 (patch)
treea2a2c80ce3e1b4a28c0868a6db5f6b10f5223350
parentf41970666675f873d7c1075efd192f22df8d17fe (diff)
downloadnihil-a14cb70cb715beb714e7b05d8fe631c0df4a7053.tar.gz
nihil-a14cb70cb715beb714e7b05d8fe631c0df4a7053.tar.bz2
nihil.ucl: add object comparison and std::format support
-rw-r--r--nihil.ucl/emit.ccm73
-rw-r--r--nihil.ucl/object.ccm23
-rw-r--r--nihil.ucl/tests/emit.cc82
-rw-r--r--nihil.ucl/tests/object.cc15
4 files changed, 193 insertions, 0 deletions
diff --git a/nihil.ucl/emit.ccm b/nihil.ucl/emit.ccm
index 8fdf616..a451f88 100644
--- a/nihil.ucl/emit.ccm
+++ b/nihil.ucl/emit.ccm
@@ -7,7 +7,9 @@ module;
#include <array>
#include <charconv>
#include <cstdlib>
+#include <format>
#include <iterator>
+#include <iostream>
#include <span>
#include <string>
#include <utility>
@@ -138,4 +140,75 @@ export auto emit(object const &object, emitter format,
&functions, nullptr);
}
+/*
+ * Basic ostream printer for UCL; default to JSON since it's probably what
+ * most people expect.
+ */
+export auto operator<<(std::ostream &stream, object const &o)
+ -> std::ostream &
+{
+ emit(o, emitter::json, std::ostream_iterator<char>(stream));
+ return stream;
+}
+
} // namespace nihil::ucl
+
+/*
+ * Specialisation of std::formatter<> for object.
+ */
+template<>
+struct std::formatter<nihil::ucl::object, 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/object.ccm b/nihil.ucl/object.ccm
index 0b8c95f..43f36fe 100644
--- a/nihil.ucl/object.ccm
+++ b/nihil.ucl/object.ccm
@@ -170,6 +170,29 @@ private:
};
/***********************************************************************
+ * Object comparison.
+ */
+
+export 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;
+}
+
+export auto operator==(object const &lhs, object const &rhs) -> bool
+{
+ return (lhs <=> rhs) == std::strong_ordering::equal;
+}
+
+/***********************************************************************
* Object iteration.
*/
diff --git a/nihil.ucl/tests/emit.cc b/nihil.ucl/tests/emit.cc
index 13f5914..c7f9757 100644
--- a/nihil.ucl/tests/emit.cc
+++ b/nihil.ucl/tests/emit.cc
@@ -2,7 +2,89 @@
* This source code is released into the public domain.
*/
+#include <format>
+#include <sstream>
+
#include <catch2/catch_test_macros.hpp>
import nihil;
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);
+ auto strm = std::ostringstream();
+ strm << obj;
+
+ REQUIRE(strm.str() ==
+"{\n"
+" \"int\": [\n"
+" 1,\n"
+" 42,\n"
+" 666\n"
+" ]\n"
+"}");
+}
+
+TEST_CASE("ucl: emit JSON with std::format", "[ucl]")
+{
+ using namespace std::literals;
+
+ auto obj = nihil::ucl::parse("int = [1, 42, 666]"sv);
+ auto str = std::format("{:j}", obj);
+
+ REQUIRE(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);
+ 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);
+ 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);
+ auto str = std::format("{:y}", obj);
+
+ REQUIRE(str ==
+"int: [\n"
+" 1,\n"
+" 42,\n"
+" 666\n"
+"]");
+}
diff --git a/nihil.ucl/tests/object.cc b/nihil.ucl/tests/object.cc
index f9cef9f..1bbcf4f 100644
--- a/nihil.ucl/tests/object.cc
+++ b/nihil.ucl/tests/object.cc
@@ -20,3 +20,18 @@ TEST_CASE("ucl object: get_ucl_object", "[ucl]")
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);
+ auto obj_42 = nihil::ucl::parse("int = 42;"sv);
+ auto obj_42_2 = nihil::ucl::parse("int = 42;"sv);
+ auto obj_43 = nihil::ucl::parse("int = 43;"sv);
+
+ REQUIRE(obj_42 == obj_42_2);
+ REQUIRE(obj_42 != obj_43);
+ REQUIRE(obj_42 < obj_43);
+ REQUIRE(obj_42 > obj_41);
+}