From 5df04d463499d92f2fb650dc8536208bde4a8f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=BCckl?= Date: Tue, 7 Jul 2020 17:15:05 +0200 Subject: [PATCH] first very hacky poc --- include/fmt/structured.h | 146 ++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 3 + test/reflect-struct-test.cc | 83 ++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 include/fmt/structured.h create mode 100644 test/reflect-struct-test.cc diff --git a/include/fmt/structured.h b/include/fmt/structured.h new file mode 100644 index 00000000..3c469563 --- /dev/null +++ b/include/fmt/structured.h @@ -0,0 +1,146 @@ +#ifndef FMT_STRUCTURED_H +#define FMT_STRUCTURED_H + +#include +#include + +#include "format.h" +#include "ranges.h" // fixme dont rely on ranges?! + +FMT_BEGIN_NAMESPACE + +/** + * Structure that holds reflection information about type T. + * @tparam T + */ +template struct reflection { + static constexpr bool available = false; +}; + +// fixme probably the reflection should provide accessors to the data instead of +// (name, value) pairs. i.e. NamedField may have an operator() that returns the +// value of a field given the object the field belongs to. +template struct NamedField { + const std::string name; + const T value; + + bool operator==(const NamedField& other) const { + return value == other.value && name == other.name; + }; +}; + +template struct Extended { const T& value; }; + +// fixme: Would probably be better to visit all name field elements directly in +// the Extended formatter instead of relying on user space specialization +// mechanism +/** + * Format a field name/value pair as ".{name}={value}" + * @tparam T + */ +template struct fmt::formatter> { + template + auto format(NamedField const& t, FormatContext& ctx) { + *ctx.out()++ = '.'; + std::copy(t.name.begin(), t.name.end(), ctx.out()); + *ctx.out()++ = '='; + if constexpr (reflection::available) { + return fmt::formatter>{}.format(t.value, ctx); + } else { + return fmt::formatter{}.format(t.value, ctx); + } + } +}; + +/** + * Formats objects as they would be written using designated initializers. + * + * E.g. {:e} will output Outer{.a=1, b=2, .inner=Inner{.x=3, .y=4, .z=5}} + * + * @tparam T The object to format. + * @tparam C The character type used. + */ +// fixme remove T template parameter +template +struct fmt::formatter, C, + std::enable_if_t::available, void>> { + template constexpr auto parse(ParseContext& ctx) { + auto it = ctx.begin(); + if (*it != '}') throw format_error("configuration not yet supported"); + return it; + } + + template + auto format(T const& t, FormatContext& ctx) { + std::string name = reflection::name(); + + std::copy(name.begin(), name.end(), ctx.out()); + *ctx.out()++ = '{'; + using base = formatter::fields(t), ", "))>; + base{}.format(fmt::join(reflection::fields(t), ", "), ctx); + *ctx.out()++ = '}'; + return ctx.out(); + } +}; + +// fixme remove this class and merge it with the fallback formatter in detail +// namespace +template +struct fmt::formatter::available, void>> { + using extended = fmt::formatter>; + + template constexpr auto parse(ParseContext& ctx) { + // Parse the presentation format and store it in the formatter: + auto it = ctx.begin(), end = ctx.end(); + if (*(it++) != 'e') { + throw format_error("invalid format"); + } + _extended = extended{}; + ctx.advance_to(it); + _extended.parse(ctx); + + // Check if reached the end of the range: + if (it != end && *it != '}') { + throw format_error("invalid format"); + } + + // Return an iterator past the end of the parsed range: + return it; + } + + template + auto format(T const& t, FormatContext& ctx) { + return _extended.format(t, ctx); + } + + private: + extended _extended; +}; + +namespace detail { +template +struct fallback_formatter::available>> { + using extended = fmt::formatter>; + + FMT_CONSTEXPR auto parse(basic_format_parse_context& ctx) + -> decltype(ctx.begin()) { + auto it = formatter, Char>::parse(ctx); + // fixme: not sure what to return, if the closing '}' remains (like required + // for formatter specialization) + // some internals will throw from ErrorHandler::on_error("invalid + // type specifier"); + ctx.advance_to(it++); // fixme: also not sure + return it; + } + + template + auto format(const T& value, FormatContext& ctx) { + return extended{}.format(value, ctx); + } +}; +} // namespace detail + +FMT_END_NAMESPACE + +#endif // FMT_STRUCTURED_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 89176633..c84cbed7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -103,6 +103,9 @@ if (NOT (MSVC AND BUILD_SHARED_LIBS)) endif () add_fmt_test(locale-test) add_fmt_test(ostream-test) +add_fmt_test(reflect-struct-test) +# currently this requires constexpr if, variadic templates and potentially std::variant +set_property(TARGET reflect-struct-test PROPERTY CXX_STANDARD 17) add_fmt_test(compile-test) add_fmt_test(printf-test) add_fmt_test(custom-formatter-test) diff --git a/test/reflect-struct-test.cc b/test/reflect-struct-test.cc new file mode 100644 index 00000000..fefff862 --- /dev/null +++ b/test/reflect-struct-test.cc @@ -0,0 +1,83 @@ +// Formatting library for C++ - std::ostream support tests +// +// Copyright (c) 2012 - present, Victor Zverovich +// All rights reserved. +// +// For the license information refer to format.h. + +#define FMT_STRING_ALIAS 1 +#include "fmt/format.h" + +struct Inner { + int x; + double y; + std::string z; +}; + +struct Outer { + std::string a; + std::string b; + Inner inner; +}; + +// fixme: if this is enabled, then then fmt::format("{:e}", Outer{...}) fails, +// because internally formatter will be used to parse the "{:e}" +// format string, which obviously is not a valid int format string. +// This issue should be resolved by not using the NamedField formatter +// specialization in structured.h +//// Test that there is no issues with specializations when fmt/ostream.h is +//// included after fmt/format.h. +// namespace fmt { +// template <> struct formatter : formatter { +// template +// typename FormatContext::iterator format(const Inner&, FormatContext& ctx) { +// return formatter::format(42, ctx); +// } +//}; +//} // namespace fmt + +#include + +#include "fmt/structured.h" +#include "gmock.h" +#include "gtest-extra.h" + +using fmt::format; +using fmt::format_error; + +// provide explicit reflection for custom types +template <> struct fmt::reflection { + static constexpr bool available = true; + static auto name() { return "Outer"; } + static auto fields(const Outer& outer) { + return std::make_tuple(NamedField{"a", outer.a}, + NamedField{"b", outer.b}, + NamedField{"inner", outer.inner}); + } + static auto values(const Outer& outer) { + return std::make_tuple(outer.a, outer.b, outer.inner); + } +}; + +template <> struct fmt::reflection { + static constexpr bool available = true; + static auto name() { return "Inner"; } + static auto fields(const Inner& inner) { + return std::make_tuple(NamedField{"x", inner.x}, + NamedField{"y", inner.y}, + NamedField{"z", inner.z}); + } + + static auto values(const Inner& inner) { + return std::make_tuple(inner.x, inner.y, inner.z); + } +}; + +using namespace std::string_literals; + +TEST(StructuredTest, FallbackPickup) { + Inner inner{1, 3.1, "hello"}; + Outer outer{"a", "b", inner}; + EXPECT_EQ("Outer{.a=a, .b=b, .inner=Inner{.x=1, .y=3.1, .z=hello}}", + fmt::format("{:e}", outer)); +} \ No newline at end of file