From 6b2e69177bccb9581bb1a3d4470d341297c1c0f7 Mon Sep 17 00:00:00 2001 From: Kefu Chai Date: Thu, 21 Nov 2024 16:44:18 +0800 Subject: [PATCH] json_formatter: Add support for standard range containers This commit extends the JSON formatting capabilities to support all standard range containers through the C++20 ranges concept. Key improvements: - Add generic range support using std::ranges::input_range concept - Enable JSON formatting for any range type without materialization - Maintain existing support for vector, map, and unordered_map - Provide a flexible, type-agnostic serialization approach Example use cases: - Formatting std::span - Serializing range views - Converting filter and transform views directly to JSON Signed-off-by: Kefu Chai --- include/seastar/json/formatter.hh | 149 +++++++++++++++++++----------- tests/unit/json_formatter_test.cc | 16 ++++ 2 files changed, 113 insertions(+), 52 deletions(-) diff --git a/include/seastar/json/formatter.hh b/include/seastar/json/formatter.hh index 26c07b505d..3ae431ab4d 100644 --- a/include/seastar/json/formatter.hh +++ b/include/seastar/json/formatter.hh @@ -22,7 +22,9 @@ #pragma once #ifndef SEASTAR_MODULE +#include #include +#include #include #include #include @@ -37,6 +39,33 @@ namespace seastar { +namespace internal { + +template +concept is_map = requires { + typename T::mapped_type; +}; + +template +concept is_pair_like = requires { + typename std::tuple_size::type; + requires std::tuple_size_v == 2; +}; + +template +concept is_string_like = + std::convertible_to && + requires (T s) { + { s.data() } -> std::same_as; + // sstring::length() and sstring::find() return size_t instead of + // size_type (i.e., uint32_t), so we cannot check their return type + // with T::size_type + s.find('a'); + s.length(); + }; + +} + namespace json { SEASTAR_MODULE_EXPORT @@ -57,15 +86,16 @@ class formatter { static sstring begin(state); static sstring end(state); - template - static sstring to_json(state s, const std::pair& p) { + template + static sstring to_json(state s, const T& p) { + auto& [key, value] = p; return s == state::array ? "{" + to_json(state::none, p) + "}" : - to_json(p.first) + ":" + to_json(p.second); + to_json(key) + ":" + to_json(value); } - template - static sstring to_json(state s, Iter i, Iter e) { + template + static sstring to_json(state s, Iterator i, Sentinel e) { std::stringstream res; res << begin(s); size_t n = 0; @@ -85,8 +115,8 @@ class formatter { return to_json(t); } - template - static future<> write(output_stream& stream, state s, const std::pair& p) { + template + static future<> write(output_stream& stream, state s, T&& p) { if (s == state::array) { return stream.write("{").then([&stream, &p] { return write(stream, state::none, p).then([&stream] { @@ -94,22 +124,47 @@ class formatter { }); }); } else { - return stream.write(to_json(p.first) + ":").then([&p, &stream] { - return write(stream, p.second); + auto& [key, value] = p; + return stream.write(to_json(key) + ":").then([&value, &stream] { + return write(stream, value); + }); + } + } + + template + static future<> write(output_stream& stream, state s, const T& p) { + if (s == state::array) { + return stream.write("{").then([&stream, p] { + return write(stream, state::none, p).then([&stream] { + return stream.write("}"); + }); + }); + } else { + auto& [key, value] = p; + return stream.write(to_json(key) + ":").then([&stream, value] { + return write(stream, value); }); } } - template - static future<> write(output_stream& stream, state s, Iter i, Iter e) { + template + static future<> write(output_stream& stream, state s, Iterator i, Sentinel e) { return do_with(true, [&stream, s, i, e] (bool& first) { return stream.write(begin(s)).then([&first, &stream, s, i, e] { - return do_for_each(i, e, [&first, &stream, s] (auto& m) { + using ref_t = std::iter_reference_t; + return do_for_each(i, e, [&first, &stream, s] (ref_t m) { auto f = (first) ? make_ready_future<>() : stream.write(","); first = false; - return f.then([&m, &stream, s] { - return write(stream, s, m); - }); + if constexpr (std::is_lvalue_reference_v) { + return f.then([&m, &stream, s] { + return write(stream, s, m); + }); + } else { + using value_t = std::iter_value_t; + return f.then([m = std::forward(m), &stream, s] { + return write(stream, s, m); + }); + } }).then([&stream, s] { return stream.write(end(s)); }); @@ -191,23 +246,18 @@ public: static sstring to_json(bool d); /** - * return a json formatted list of a given vector of params - * @param vec the vector to format - * @return the given vector in a json format + * converts a given range to a JSON-formatted string + * @param range A standard range type + * @return A string containing the JSON representation of the input range */ - template - static sstring to_json(const std::vector& vec) { - return to_json(state::array, vec.begin(), vec.end()); - } - - template - static sstring to_json(const std::map& map) { - return to_json(state::map, map.begin(), map.end()); - } - - template - static sstring to_json(const std::unordered_map& map) { - return to_json(state::map, map.begin(), map.end()); + template + requires (!internal::is_string_like) + static sstring to_json(const Range& range) { + if constexpr (internal::is_map) { + return to_json(state::map, std::ranges::begin(range), std::ranges::end(range)); + } else { + return to_json(state::array, std::ranges::begin(range), std::ranges::end(range)); + } } /** @@ -297,28 +347,23 @@ public: } /** - * return a json formatted list of a given vector of params - * @param vec the vector to format - * @return the given vector in a json format + * Converts a range to a JSON array or object and writes it to an output stream. + * @param s The output stream that will receive the JSON-formatted string + * @param range The range to convert. If the range contains key-value pairs (like std::map), + * it will be formatted as a JSON object. Otherwise, it will be formatted as + * a JSON array. + * @returns A future that will be resolved when the write operation completes + * */ - template - static future<> write(output_stream& s, std::vector vec) { - return do_with(std::move(vec), [&s] (const auto& vec) { - return write(s, state::array, vec.begin(), vec.end()); - }); - } - - template - static future<> write(output_stream& s, std::map map) { - return do_with(std::move(map), [&s] (const auto& map) { - return write(s, state::map, map.begin(), map.end()); - }); - } - - template - static future<> write(output_stream& s, std::unordered_map map) { - return do_with(std::move(map), [&s] (const auto& map) { - return write(s, state::map, map.begin(), map.end()); + template + requires (!internal::is_string_like) + static future<> write(output_stream& s, const Range& range) { + return do_with(std::move(range), [&s] (const auto& range) { + if constexpr (internal::is_map) { + return write(s, state::map, std::ranges::begin(range), std::ranges::end(range)); + } else { + return write(s, state::array, std::ranges::begin(range), std::ranges::end(range)); + } }); } diff --git a/tests/unit/json_formatter_test.cc b/tests/unit/json_formatter_test.cc index 783b29e409..69c820e18d 100644 --- a/tests/unit/json_formatter_test.cc +++ b/tests/unit/json_formatter_test.cc @@ -62,6 +62,14 @@ SEASTAR_TEST_CASE(test_collections) { return make_ready_future(); } +SEASTAR_TEST_CASE(test_ranges) { + BOOST_CHECK_EQUAL("[1,2,3,4]", formatter::to_json(std::views::iota(1, 5))); +#ifdef __cpp_lib_ranges_enumerate + BOOST_CHECK_EQUAL("[{0:5},{1:6},{2:7},{3:8}]", formatter::to_json(std::views::iota(5, 9) | std::views::enumerate)); +#endif + return make_ready_future(); +} + struct object_json : public json_base { json_element subject; json_list values; @@ -158,4 +166,12 @@ SEASTAR_THREAD_TEST_CASE(formatter_write) { formatter_check_expected("[[1,2],[3,4]]", [] (auto &out) { json::formatter::write(out, std::vector>({{1, 2}, {3, 4}})).get(); }); + formatter_check_expected("[1,2,3,4]", [] (auto& out) { + json::formatter::write(out, std::views::iota(1, 5)).get(); + }); +#ifdef __cpp_lib_ranges_enumerate + formatter_check_expected("[{0:5},{1:6},{2:7},{3:8}]", [] (auto& out) { + json::formatter::write(out, std::views::iota(5, 9) | std::views::enumerate).get(); + }); +#endif }