Skip to content

Commit

Permalink
feat(format): add formatter for common standard library types (#83)
Browse files Browse the repository at this point in the history
* std::optional
* std::exception
* std::filesystem::path
* std::variant
  • Loading branch information
Viatorus authored Jan 22, 2024
1 parent ed5b341 commit b9d467e
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 24 deletions.
15 changes: 1 addition & 14 deletions include/emio/detail/format/args.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,7 @@ struct format_arg_trait {
using unified_type = format::unified_type_t<std::remove_const_t<Arg>>;

static constexpr result<void> 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<Arg>) {
if constexpr (has_validate_function_v<Arg>) {
return formatter<Arg>::validate(format_rdr);
} else {
static_assert(!has_any_validate_function_v<Arg>,
"Formatter seems to have a validate property which doesn't fit the desired signature.");
return formatter<Arg>{}.parse(format_rdr);
}
} else {
static_assert(has_formatter_v<Arg>,
"Cannot format an argument. To make type T formattable provide a formatter<T> specialization.");
return err::invalid_format;
}
return detail::format::validate_trait<Arg>(format_rdr);
}

static constexpr result<void> process_arg(writer& out, reader& format_rdr, const Arg& arg) noexcept {
Expand Down
32 changes: 22 additions & 10 deletions include/emio/detail/format/formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,6 @@ inline constexpr result<void> 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());
Expand All @@ -560,7 +559,6 @@ inline constexpr result<void> 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());
Expand All @@ -572,7 +570,6 @@ inline constexpr result<void> validate_format_specs(reader& format_rdr, format_s
} else {
specs.align = alignment::right;
}
fill_aligned = true;
EMIO_TRY(c, format_rdr.read_char());
}
}
Expand All @@ -584,8 +581,8 @@ inline constexpr result<void> 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;
Expand Down Expand Up @@ -629,7 +626,6 @@ inline constexpr result<void> 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();
Expand All @@ -641,7 +637,6 @@ inline constexpr result<void> 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();
Expand All @@ -653,7 +648,6 @@ inline constexpr result<void> parse_format_specs(reader& format_rdr, format_spec
} else {
specs.align = alignment::right;
}
fill_aligned = true;
c = format_rdr.read_char().assume_value();
}
}
Expand All @@ -665,8 +659,8 @@ inline constexpr result<void> 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;
Expand Down Expand Up @@ -800,6 +794,24 @@ concept has_any_validate_function_v =
has_static_validate_function_v<T> || std::is_member_function_pointer_v<decltype(&formatter<T>::validate)> ||
has_member_validate_function_v<T>;

template <typename Arg>
constexpr result<void> 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<Arg>) {
if constexpr (has_validate_function_v<Arg>) {
return formatter<Arg>::validate(format_rdr);
} else {
static_assert(!has_any_validate_function_v<Arg>,
"Formatter seems to have a validate property which doesn't fit the desired signature.");
return formatter<Arg>{}.parse(format_rdr);
}
} else {
static_assert(has_formatter_v<Arg>,
"Cannot format an argument. To make type T formattable provide a formatter<T> specialization.");
return err::invalid_format;
}
}

template <typename T>
inline constexpr bool is_core_type_v =
std::is_same_v<T, bool> || std::is_same_v<T, char> || std::is_same_v<T, int32_t> || std::is_same_v<T, uint32_t> ||
Expand Down
8 changes: 8 additions & 0 deletions include/emio/detail/misc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ struct always_false : std::false_type {};
template <typename T>
inline constexpr bool always_false_v = always_false<T>::value;

template <typename... Ts>
struct overload : Ts... {
using Ts::operator()...;
};

template <class... Ts>
overload(Ts...) -> overload<Ts...>;

} // namespace emio::detail
1 change: 1 addition & 0 deletions include/emio/emio.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@
#include "format.hpp"
#include "ranges.hpp"
#include "scan.hpp"
#include "std.hpp"

#endif // EMIO_Z_MAIN_H
144 changes: 144 additions & 0 deletions include/emio/std.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//
// Copyright (c) 2024 - present, Toni Neubert
// All rights reserved.
//
// For the license information refer to emio.hpp

#pragma once

#include <exception>
#include <filesystem>
#include <optional>
#include <variant>

#include "formatter.hpp"

namespace emio {

/**
* Formatter for std::optional.
* @tparam T The type to format.
*/
template <typename T>
requires is_formattable_v<T>
class formatter<std::optional<T>> {
public:
static constexpr result<void> validate(reader& format_rdr) noexcept {
return detail::format::validate_trait<T>(format_rdr);
}

constexpr result<void> parse(reader& format_rdr) noexcept {
return underlying_.parse(format_rdr);
}

constexpr result<void> format(writer& out, const std::optional<T>& 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<T> underlying_;
};

/**
* Formatter for std::exception.
* @tparam T The type.
*/
template <typename T>
requires std::is_base_of_v<std::exception, T>
class formatter<T> : public formatter<std::string_view> {
public:
result<void> format(writer& out, const std::exception& arg) const noexcept {
return formatter<std::string_view>::format(out, arg.what());
}
};

/**
* Formatter for std::filesystem::path.
*/
template <>
class formatter<std::filesystem::path> : public formatter<std::string_view> {
public:
result<void> format(writer& out, const std::filesystem::path& arg) const noexcept {
return formatter<std::string_view>::format(out, arg.native());
}
};

/**
* Formatter for std::monostate.
*/
template <>
class formatter<std::monostate> {
public:
static constexpr result<void> 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<void> 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<void> 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 <typename... Ts>
requires(is_formattable_v<Ts> && ...)
class formatter<std::variant<Ts...>> {
public:
static constexpr result<void> validate(reader& format_rdr) noexcept {
return format_rdr.read_if_match_char('}');
}

constexpr result<void> parse(reader& format_rdr) noexcept {
return format_rdr.read_if_match_char('}');
}

constexpr result<void> format(writer& out, const std::variant<Ts...>& arg) noexcept {
EMIO_TRYV(out.write_str(detail::sv("variant(")));
#ifdef __EXCEPTIONS
try {
#endif
EMIO_TRYV(std::visit(detail::overload{
[&](const char& val) -> result<void> {
return out.write_char_escaped(val);
},
[&](const std::string_view& val) -> result<void> {
return out.write_str_escaped(val);
},
[&](const auto& val) -> result<void> {
using T = std::decay_t<decltype(val)>;
if constexpr (!std::is_null_pointer_v<T> &&
std::is_constructible_v<std::string_view, T>) {
return out.write_str_escaped(std::string_view{val});
} else {
formatter<T> 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
1 change: 1 addition & 0 deletions test/unit_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ add_executable(emio_test
test_reader.cpp
test_result.cpp
test_scan.cpp
test_std.cpp
test_writer.cpp
)

Expand Down
81 changes: 81 additions & 0 deletions test/unit_test/test_std.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Unit under test.
#include <emio/std.hpp>

// Other includes.
#include <catch2/catch_test_macros.hpp>
#include <emio/format.hpp>
#include <emio/ranges.hpp>

struct unformattable {};

TEST_CASE("std::optional") {
CHECK(emio::format("{}", std::optional<int>{}) == "none");
CHECK(emio::format("{:x}", std::optional<int>{42}) == "optional(2a)");
CHECK(emio::format("{:x}", std::optional<int>{42}) == "optional(2a)");
CHECK(emio::format(emio::runtime("{:x}"), std::optional<int>{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<std::optional<int>>);
STATIC_CHECK_FALSE(emio::is_formattable_v<std::optional<unformattable>>);
}

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::string>);

std::variant<std::monostate, int, double, char, std::nullptr_t, std::string> 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<std::monostate, throws_on_move> v2;
try {
throws_on_move thrower;
v2.emplace<throws_on_move>(std::move(thrower));
} catch (const std::runtime_error&) {
}
CHECK(emio::format("{}", v2) == "variant(valueless by exception)");
}
}

0 comments on commit b9d467e

Please sign in to comment.