From b9d467ecbc630c9c7e9f17c5a9b457dce7b4a7df Mon Sep 17 00:00:00 2001 From: Toni Neubert Date: Mon, 22 Jan 2024 22:36:45 +0100 Subject: [PATCH] feat(format): add formatter for common standard library types (#83) * std::optional * std::exception * std::filesystem::path * std::variant --- include/emio/detail/format/args.hpp | 15 +-- include/emio/detail/format/formatter.hpp | 32 +++-- include/emio/detail/misc.hpp | 8 ++ include/emio/emio.hpp | 1 + include/emio/std.hpp | 144 +++++++++++++++++++++++ test/unit_test/CMakeLists.txt | 1 + test/unit_test/test_std.cpp | 81 +++++++++++++ 7 files changed, 258 insertions(+), 24 deletions(-) create mode 100644 include/emio/std.hpp create mode 100644 test/unit_test/test_std.cpp diff --git a/include/emio/detail/format/args.hpp b/include/emio/detail/format/args.hpp index 16f555f6..be3d0541 100644 --- a/include/emio/detail/format/args.hpp +++ b/include/emio/detail/format/args.hpp @@ -16,20 +16,7 @@ struct format_arg_trait { using unified_type = format::unified_type_t>; static constexpr result validate(reader& format_rdr) noexcept { - // Check if a formatter exist and a correct validate method is implemented. If not, use the parse method. - if constexpr (has_formatter_v) { - if constexpr (has_validate_function_v) { - return formatter::validate(format_rdr); - } else { - static_assert(!has_any_validate_function_v, - "Formatter seems to have a validate property which doesn't fit the desired signature."); - return formatter{}.parse(format_rdr); - } - } else { - static_assert(has_formatter_v, - "Cannot format an argument. To make type T formattable provide a formatter specialization."); - return err::invalid_format; - } + return detail::format::validate_trait(format_rdr); } static constexpr result process_arg(writer& out, reader& format_rdr, const Arg& arg) noexcept { diff --git a/include/emio/detail/format/formatter.hpp b/include/emio/detail/format/formatter.hpp index 05034fe8..caed8fcc 100644 --- a/include/emio/detail/format/formatter.hpp +++ b/include/emio/detail/format/formatter.hpp @@ -548,7 +548,6 @@ inline constexpr result validate_format_specs(reader& format_rdr, format_s return err::invalid_format; } - bool fill_aligned = false; { // Parse for alignment specifier. EMIO_TRY(const char c2, format_rdr.peek()); @@ -560,7 +559,6 @@ inline constexpr result validate_format_specs(reader& format_rdr, format_s } else { specs.align = alignment::right; } - fill_aligned = true; specs.fill = c; format_rdr.pop(); EMIO_TRY(c, format_rdr.read_char()); @@ -572,7 +570,6 @@ inline constexpr result validate_format_specs(reader& format_rdr, format_s } else { specs.align = alignment::right; } - fill_aligned = true; EMIO_TRY(c, format_rdr.read_char()); } } @@ -584,8 +581,8 @@ inline constexpr result validate_format_specs(reader& format_rdr, format_s specs.alternate_form = true; EMIO_TRY(c, format_rdr.read_char()); } - if (c == '0') { // Zero flag. - if (!fill_aligned) { // If fill/align is used, the zero flag is ignored. + if (c == '0') { // Zero flag. + if (specs.align == alignment::none) { // If fill/align is used, the zero flag is ignored. specs.fill = '0'; specs.align = alignment::right; specs.zero_flag = true; @@ -629,7 +626,6 @@ inline constexpr result parse_format_specs(reader& format_rdr, format_spec return success; } - bool fill_aligned = false; { // Parse for alignment specifier. const char c2 = format_rdr.peek().assume_value(); @@ -641,7 +637,6 @@ inline constexpr result parse_format_specs(reader& format_rdr, format_spec } else { specs.align = alignment::right; } - fill_aligned = true; specs.fill = c; format_rdr.pop(); c = format_rdr.read_char().assume_value(); @@ -653,7 +648,6 @@ inline constexpr result parse_format_specs(reader& format_rdr, format_spec } else { specs.align = alignment::right; } - fill_aligned = true; c = format_rdr.read_char().assume_value(); } } @@ -665,8 +659,8 @@ inline constexpr result parse_format_specs(reader& format_rdr, format_spec specs.alternate_form = true; c = format_rdr.read_char().assume_value(); } - if (c == '0') { // Zero flag. - if (!fill_aligned) { // Ignoreable. + if (c == '0') { // Zero flag. + if (specs.align == alignment::none) { // Ignoreable. specs.fill = '0'; specs.align = alignment::right; specs.zero_flag = true; @@ -800,6 +794,24 @@ concept has_any_validate_function_v = has_static_validate_function_v || std::is_member_function_pointer_v::validate)> || has_member_validate_function_v; +template +constexpr result validate_trait(reader& format_rdr) { + // Check if a formatter exist and a correct validate method is implemented. If not, use the parse method. + if constexpr (has_formatter_v) { + if constexpr (has_validate_function_v) { + return formatter::validate(format_rdr); + } else { + static_assert(!has_any_validate_function_v, + "Formatter seems to have a validate property which doesn't fit the desired signature."); + return formatter{}.parse(format_rdr); + } + } else { + static_assert(has_formatter_v, + "Cannot format an argument. To make type T formattable provide a formatter specialization."); + return err::invalid_format; + } +} + template inline constexpr bool is_core_type_v = std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || diff --git a/include/emio/detail/misc.hpp b/include/emio/detail/misc.hpp index bcf11ff1..0316fd39 100644 --- a/include/emio/detail/misc.hpp +++ b/include/emio/detail/misc.hpp @@ -16,4 +16,12 @@ struct always_false : std::false_type {}; template inline constexpr bool always_false_v = always_false::value; +template +struct overload : Ts... { + using Ts::operator()...; +}; + +template +overload(Ts...) -> overload; + } // namespace emio::detail diff --git a/include/emio/emio.hpp b/include/emio/emio.hpp index f2cb6f22..e8a00527 100644 --- a/include/emio/emio.hpp +++ b/include/emio/emio.hpp @@ -39,5 +39,6 @@ #include "format.hpp" #include "ranges.hpp" #include "scan.hpp" +#include "std.hpp" #endif // EMIO_Z_MAIN_H diff --git a/include/emio/std.hpp b/include/emio/std.hpp new file mode 100644 index 00000000..d8a199a2 --- /dev/null +++ b/include/emio/std.hpp @@ -0,0 +1,144 @@ +// +// Copyright (c) 2024 - present, Toni Neubert +// All rights reserved. +// +// For the license information refer to emio.hpp + +#pragma once + +#include +#include +#include +#include + +#include "formatter.hpp" + +namespace emio { + +/** + * Formatter for std::optional. + * @tparam T The type to format. + */ +template + requires is_formattable_v +class formatter> { + public: + static constexpr result validate(reader& format_rdr) noexcept { + return detail::format::validate_trait(format_rdr); + } + + constexpr result parse(reader& format_rdr) noexcept { + return underlying_.parse(format_rdr); + } + + constexpr result format(writer& out, const std::optional& arg) const noexcept { + if (!arg.has_value()) { + return out.write_str(detail::sv("none")); + } else { + EMIO_TRYV(out.write_str(detail::sv("optional("))); + EMIO_TRYV(underlying_.format(out, *arg)); + return out.write_char(')'); + } + } + + private: + formatter underlying_; +}; + +/** + * Formatter for std::exception. + * @tparam T The type. + */ +template + requires std::is_base_of_v +class formatter : public formatter { + public: + result format(writer& out, const std::exception& arg) const noexcept { + return formatter::format(out, arg.what()); + } +}; + +/** + * Formatter for std::filesystem::path. + */ +template <> +class formatter : public formatter { + public: + result format(writer& out, const std::filesystem::path& arg) const noexcept { + return formatter::format(out, arg.native()); + } +}; + +/** + * Formatter for std::monostate. + */ +template <> +class formatter { + public: + static constexpr result validate(reader& format_rdr) noexcept { + return format_rdr.read_if_match_char('}'); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static): API requests this to be a member function. + constexpr result parse(reader& format_rdr) noexcept { + return format_rdr.read_if_match_char('}'); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static): API requests this to be a member function. + constexpr result format(writer& out, const std::monostate& /*arg*/) const noexcept { + return out.write_str(detail::sv("monostate")); + } +}; + +/** + * Formatter for std::variant. + * @tparam Ts The types to format. + */ +template + requires(is_formattable_v && ...) +class formatter> { + public: + static constexpr result validate(reader& format_rdr) noexcept { + return format_rdr.read_if_match_char('}'); + } + + constexpr result parse(reader& format_rdr) noexcept { + return format_rdr.read_if_match_char('}'); + } + + constexpr result format(writer& out, const std::variant& arg) noexcept { + EMIO_TRYV(out.write_str(detail::sv("variant("))); +#ifdef __EXCEPTIONS + try { +#endif + EMIO_TRYV(std::visit(detail::overload{ + [&](const char& val) -> result { + return out.write_char_escaped(val); + }, + [&](const std::string_view& val) -> result { + return out.write_str_escaped(val); + }, + [&](const auto& val) -> result { + using T = std::decay_t; + if constexpr (!std::is_null_pointer_v && + std::is_constructible_v) { + return out.write_str_escaped(std::string_view{val}); + } else { + formatter fmt; + emio::reader rdr{detail::sv("}")}; + EMIO_TRYV(fmt.parse(rdr)); + return fmt.format(out, val); + } + }, + }, + arg)); +#ifdef __EXCEPTIONS + } catch (const std::bad_variant_access&) { + EMIO_TRYV(out.write_str(detail::sv("valueless by exception"))); + } +#endif + return out.write_char(')'); + } // namespace emio +}; + +} // namespace emio diff --git a/test/unit_test/CMakeLists.txt b/test/unit_test/CMakeLists.txt index 6168613d..3cb06623 100644 --- a/test/unit_test/CMakeLists.txt +++ b/test/unit_test/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(emio_test test_reader.cpp test_result.cpp test_scan.cpp + test_std.cpp test_writer.cpp ) diff --git a/test/unit_test/test_std.cpp b/test/unit_test/test_std.cpp new file mode 100644 index 00000000..112f7af3 --- /dev/null +++ b/test/unit_test/test_std.cpp @@ -0,0 +1,81 @@ +// Unit under test. +#include + +// Other includes. +#include +#include +#include + +struct unformattable {}; + +TEST_CASE("std::optional") { + CHECK(emio::format("{}", std::optional{}) == "none"); + CHECK(emio::format("{:x}", std::optional{42}) == "optional(2a)"); + CHECK(emio::format("{:x}", std::optional{42}) == "optional(2a)"); + CHECK(emio::format(emio::runtime("{:x}"), std::optional{42}) == "optional(2a)"); + CHECK(emio::format("{}", std::optional{std::vector{'h', 'e', 'l', 'l', 'o'}}) == + "optional(['h', 'e', 'l', 'l', 'o'])"); + CHECK(emio::format("{::d}", std::optional{std::vector{'h', 'e', 'l', 'l', 'o'}}) == + "optional([104, 101, 108, 108, 111])"); + + STATIC_CHECK(emio::is_formattable_v>); + STATIC_CHECK_FALSE(emio::is_formattable_v>); +} + +TEST_CASE("std::exception") { + CHECK(emio::format("{}", std::exception{}) == "std::exception"); + CHECK(emio::format("{}", std::runtime_error{"hello"}) == "hello"); +} + +TEST_CASE("std::filesystem::path") { + using std::filesystem::path; + + CHECK(emio::format("{}", path{}) == ""); + CHECK(emio::format("{}", path{"/abc/dev"}) == "/abc/dev"); + CHECK(emio::format("{:x>11}", path{"/abc/dev"}) == "xxx/abc/dev"); + CHECK(emio::format("{:x<11?}", path{"/abc/dev"}) == "\"/abc/dev\"x"); + CHECK(emio::format(emio::runtime("{:x<11?}"), path{"/abc/dev"}) == "\"/abc/dev\"x"); +} + +namespace { + +struct throws_on_move { + throws_on_move() = default; + throws_on_move(throws_on_move&&) { + throw std::runtime_error{"As expected..."}; + } +}; + +std::string_view format_as(const throws_on_move&) { + throw std::logic_error{"Shouldn't be called."}; +} + +} // namespace + +TEST_CASE("std::variant") { + STATIC_CHECK(emio::is_formattable_v); + + std::variant v{}; + CHECK(emio::format("{}", v) == "variant(monostate)"); + CHECK(emio::format(emio::runtime("{}"), v) == "variant(monostate)"); + v = 42; + CHECK(emio::format("{}", v) == "variant(42)"); + v = 4.2; + CHECK(emio::format("{}", v) == "variant(4.2)"); + v = 'x'; + CHECK(emio::format("{}", v) == "variant('x')"); + v = nullptr; + CHECK(emio::format("{}", v) == "variant(0x0)"); + v = "abc"; + CHECK(emio::format("{}", v) == "variant(\"abc\")"); + + SECTION("valueless by exception") { + std::variant v2; + try { + throws_on_move thrower; + v2.emplace(std::move(thrower)); + } catch (const std::runtime_error&) { + } + CHECK(emio::format("{}", v2) == "variant(valueless by exception)"); + } +}