Skip to content

Commit 60bff25

Browse files
authored
feat: add formatter specialization (#86)
- Added std::formatter specialization for std::map, std::unordered_map and std::vector when their elements are also formattable. - Added std::formatter specialization for classes that have a ToString at hands.
1 parent dbf9592 commit 60bff25

File tree

5 files changed

+309
-35
lines changed

5 files changed

+309
-35
lines changed

src/iceberg/statistics_file.cc

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
#include <format>
2323

24+
#include "iceberg/util/formatter_internal.h"
25+
2426
namespace iceberg {
2527

2628
std::string ToString(const BlobMetadata& blob_metadata) {
@@ -29,26 +31,9 @@ std::string ToString(const BlobMetadata& blob_metadata) {
2931
"type='{}',sourceSnapshotId={},sourceSnapshotSequenceNumber={},",
3032
blob_metadata.type, blob_metadata.source_snapshot_id,
3133
blob_metadata.source_snapshot_sequence_number);
32-
std::format_to(std::back_inserter(repr), "fields=[");
33-
for (auto iter = blob_metadata.fields.cbegin(); iter != blob_metadata.fields.cend();
34-
++iter) {
35-
if (iter != blob_metadata.fields.cbegin()) {
36-
std::format_to(std::back_inserter(repr), ",{}", *iter);
37-
} else {
38-
std::format_to(std::back_inserter(repr), "{}", *iter);
39-
}
40-
}
41-
std::format_to(std::back_inserter(repr), "],properties=[");
42-
for (auto iter = blob_metadata.properties.cbegin();
43-
iter != blob_metadata.properties.cend(); ++iter) {
44-
const auto& [key, value] = *iter;
45-
if (iter != blob_metadata.properties.cbegin()) {
46-
std::format_to(std::back_inserter(repr), ",{}:{}", key, value);
47-
} else {
48-
std::format_to(std::back_inserter(repr), "{}:{}", key, value);
49-
}
50-
}
51-
repr += "]]";
34+
std::format_to(std::back_inserter(repr), "fields={},", blob_metadata.fields);
35+
std::format_to(std::back_inserter(repr), "properties={}", blob_metadata.properties);
36+
std::format_to(std::back_inserter(repr), "]");
5237
return repr;
5338
}
5439

@@ -59,16 +44,9 @@ std::string ToString(const StatisticsFile& statistics_file) {
5944
statistics_file.snapshot_id, statistics_file.path,
6045
statistics_file.file_size_in_bytes,
6146
statistics_file.file_footer_size_in_bytes);
62-
std::format_to(std::back_inserter(repr), "blobMetadata=[");
63-
for (auto iter = statistics_file.blob_metadata.cbegin();
64-
iter != statistics_file.blob_metadata.cend(); ++iter) {
65-
if (iter != statistics_file.blob_metadata.cbegin()) {
66-
std::format_to(std::back_inserter(repr), ",{}", ToString(*iter));
67-
} else {
68-
std::format_to(std::back_inserter(repr), "{}", ToString(*iter));
69-
}
70-
}
71-
repr += "]]";
47+
std::format_to(std::back_inserter(repr), "blobMetadata={}",
48+
statistics_file.blob_metadata);
49+
std::format_to(std::back_inserter(repr), "]");
7250
return repr;
7351
}
7452

src/iceberg/util/formatter.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,15 @@ struct std::formatter<Derived> : std::formatter<std::string_view> {
4040
return std::formatter<string_view>::format(obj.ToString(), ctx);
4141
}
4242
};
43+
44+
/// \brief std::formatter specialization for any type that has a ToString function
45+
template <typename T>
46+
requires requires(const T& t) {
47+
{ ToString(t) } -> std::convertible_to<std::string>;
48+
}
49+
struct std::formatter<T> : std::formatter<std::string_view> {
50+
template <class FormatContext>
51+
auto format(const T& value, FormatContext& ctx) const {
52+
return std::formatter<std::string_view>::format(ToString(value), ctx);
53+
}
54+
};
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
#include <concepts>
23+
#include <format>
24+
#include <map>
25+
#include <ranges>
26+
#include <sstream>
27+
#include <string_view>
28+
#include <unordered_map>
29+
#include <vector>
30+
31+
#include "iceberg/util/formatter.h"
32+
33+
/// \brief Concept for smart pointer types
34+
template <typename T>
35+
concept SmartPointerType = requires(T t) {
36+
{ t.operator->() } -> std::same_as<typename T::element_type*>;
37+
{ *t } -> std::convertible_to<typename T::element_type&>;
38+
{ static_cast<bool>(t) } -> std::same_as<bool>;
39+
typename T::element_type;
40+
};
41+
42+
/// \brief Helper function to format an item using concepts to differentiate types
43+
template <typename T>
44+
std::string FormatItem(const T& item) {
45+
if constexpr (SmartPointerType<T>) {
46+
if (item) {
47+
return std::format("{}", *item);
48+
} else {
49+
return "null";
50+
}
51+
} else {
52+
return std::format("{}", item);
53+
}
54+
}
55+
56+
/// \brief Generic function to join a range of elements with a separator and wrap with
57+
/// delimiters
58+
template <std::ranges::input_range Range>
59+
std::string FormatRange(const Range& range, std::string_view separator,
60+
std::string_view prefix, std::string_view suffix) {
61+
if (std::ranges::empty(range)) {
62+
return std::format("{}{}", prefix, suffix);
63+
}
64+
65+
std::stringstream ss;
66+
ss << prefix;
67+
68+
bool first = true;
69+
for (const auto& element : range) {
70+
if (!first) {
71+
ss << separator;
72+
}
73+
ss << element;
74+
first = false;
75+
}
76+
77+
ss << suffix;
78+
return ss.str();
79+
}
80+
81+
/// \brief Helper template for formatting map-like containers
82+
template <typename MapType>
83+
std::string FormatMap(const MapType& map) {
84+
// Transform the map items into formatted key-value pairs
85+
std::ranges::transform_view formatted_range =
86+
map | std::views::transform([](const auto& pair) -> std::string {
87+
const auto& [key, value] = pair;
88+
return std::format("{}: {}", FormatItem(key), FormatItem(value));
89+
});
90+
return FormatRange(formatted_range, ", ", "{", "}");
91+
}
92+
93+
/// \brief std::formatter specialization for std::map
94+
template <typename K, typename V>
95+
struct std::formatter<std::map<K, V>> : std::formatter<std::string_view> {
96+
template <class FormatContext>
97+
auto format(const std::map<K, V>& map, FormatContext& ctx) const {
98+
return std::formatter<std::string_view>::format(FormatMap(map), ctx);
99+
}
100+
};
101+
102+
/// \brief std::formatter specialization for std::unordered_map
103+
template <typename K, typename V>
104+
struct std::formatter<std::unordered_map<K, V>> : std::formatter<std::string_view> {
105+
template <class FormatContext>
106+
auto format(const std::unordered_map<K, V>& map, FormatContext& ctx) const {
107+
return std::formatter<std::string_view>::format(FormatMap(map), ctx);
108+
}
109+
};
110+
111+
/// \brief std::formatter specialization for std::vector
112+
template <typename T>
113+
struct std::formatter<std::vector<T>> : std::formatter<std::string_view> {
114+
template <class FormatContext>
115+
auto format(const std::vector<T>& vec, FormatContext& ctx) const {
116+
auto formatted_range =
117+
vec | std::views::transform([](const auto& item) { return FormatItem(item); });
118+
return std::formatter<std::string_view>::format(
119+
FormatRange(formatted_range, ", ", "[", "]"), ctx);
120+
}
121+
};

test/CMakeLists.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ target_sources(schema_test
4242
target_link_libraries(schema_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock)
4343
add_test(NAME schema_test COMMAND schema_test)
4444

45-
add_executable(expected_test)
46-
target_sources(expected_test PRIVATE expected_test.cc)
47-
target_link_libraries(expected_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock)
48-
add_test(NAME expected_test COMMAND expected_test)
49-
5045
add_executable(expression_test)
5146
target_sources(expression_test PRIVATE expression_test.cc)
5247
target_link_libraries(expression_test PRIVATE iceberg_static GTest::gtest_main
@@ -61,6 +56,11 @@ target_link_libraries(json_serde_test PRIVATE iceberg_static GTest::gtest_main
6156
GTest::gmock)
6257
add_test(NAME json_serde_test COMMAND json_serde_test)
6358

59+
add_executable(util_test)
60+
target_sources(util_test PRIVATE expected_test.cc formatter_test.cc)
61+
target_link_libraries(util_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock)
62+
add_test(NAME util_test COMMAND util_test)
63+
6464
if(ICEBERG_BUILD_BUNDLE)
6565
add_executable(avro_test)
6666
target_sources(avro_test PRIVATE avro_test.cc)

test/formatter_test.cc

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include <format>
21+
#include <map>
22+
#include <memory>
23+
#include <string>
24+
#include <unordered_map>
25+
#include <vector>
26+
27+
#include <gmock/gmock-matchers.h>
28+
#include <gmock/gmock.h>
29+
#include <gtest/gtest.h>
30+
31+
#include "iceberg/statistics_file.h"
32+
#include "iceberg/util/formatter_internal.h"
33+
34+
namespace iceberg {
35+
36+
// Tests for the std::format specializations
37+
TEST(FormatterTest, VectorFormat) {
38+
std::vector<int> empty;
39+
EXPECT_EQ("[]", std::format("{}", empty));
40+
41+
std::vector<int> nums = {1, 2, 3, 4, 5};
42+
EXPECT_EQ("[1, 2, 3, 4, 5]", std::format("{}", nums));
43+
44+
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
45+
EXPECT_EQ("[Alice, Bob, Charlie]", std::format("{}", names));
46+
}
47+
48+
TEST(FormatterTest, MapFormat) {
49+
std::map<std::string, int> empty;
50+
EXPECT_EQ("{}", std::format("{}", empty));
51+
52+
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
53+
EXPECT_EQ("{Alice: 30, Bob: 25, Charlie: 35}", std::format("{}", ages));
54+
}
55+
56+
TEST(FormatterTest, UnorderedMapFormat) {
57+
std::unordered_map<std::string, double> empty;
58+
EXPECT_EQ("{}", std::format("{}", empty));
59+
60+
std::unordered_map<std::string, double> scores = {
61+
{"Alice", 95.5}, {"Bob", 87.0}, {"Charlie", 92.3}};
62+
std::string str = std::format("{}", scores);
63+
EXPECT_THAT(str, ::testing::HasSubstr("Alice: 95.5"));
64+
EXPECT_THAT(str, ::testing::HasSubstr("Bob: 87"));
65+
EXPECT_THAT(str, ::testing::HasSubstr("Charlie: 92.3"));
66+
}
67+
68+
TEST(FormatterTest, NestedContainersFormat) {
69+
std::vector<std::map<std::string, int>> nested = {{{"a", 1}, {"b", 2}},
70+
{{"c", 3}, {"d", 4}}};
71+
72+
EXPECT_EQ("[{a: 1, b: 2}, {c: 3, d: 4}]", std::format("{}", nested));
73+
74+
std::map<std::string, std::vector<int>> nested_map = {
75+
{"primes", {2, 3, 5, 7, 11}}, {"fibonacci", {1, 1, 2, 3, 5, 8, 13}}};
76+
std::string result = std::format("{}", nested_map);
77+
EXPECT_THAT(result, ::testing::HasSubstr("primes"));
78+
EXPECT_THAT(result, ::testing::HasSubstr("fibonacci"));
79+
EXPECT_THAT(result, ::testing::HasSubstr("[2, 3, 5, 7, 11]"));
80+
EXPECT_THAT(result, ::testing::HasSubstr("[1, 1, 2, 3, 5, 8, 13]"));
81+
}
82+
83+
TEST(FormatterTest, EdgeCasesFormat) {
84+
std::vector<int> single_vec = {42};
85+
EXPECT_EQ("[42]", std::format("{}", single_vec));
86+
87+
std::map<std::string, int> single_map = {{"key", 42}};
88+
EXPECT_EQ("{key: 42}", std::format("{}", single_map));
89+
90+
std::vector<std::vector<int>> nested_empty = {{}, {1, 2}, {}};
91+
EXPECT_EQ("[[], [1, 2], []]", std::format("{}", nested_empty));
92+
}
93+
94+
TEST(FormatterTest, SmartPointerFormat) {
95+
std::vector<std::shared_ptr<int>> int_ptrs = {
96+
std::make_shared<int>(42),
97+
std::make_shared<int>(123),
98+
nullptr,
99+
};
100+
EXPECT_EQ("[42, 123, null]", std::format("{}", int_ptrs));
101+
102+
std::vector<std::shared_ptr<std::string>> str_ptrs = {
103+
std::make_shared<std::string>("hello"),
104+
std::make_shared<std::string>("world"),
105+
nullptr,
106+
};
107+
EXPECT_EQ("[hello, world, null]", std::format("{}", str_ptrs));
108+
109+
std::map<std::string, std::shared_ptr<int>> map_with_ptr_values = {
110+
{"one", std::make_shared<int>(1)},
111+
{"two", std::make_shared<int>(2)},
112+
{"null", nullptr},
113+
};
114+
EXPECT_EQ("{null: null, one: 1, two: 2}", std::format("{}", map_with_ptr_values));
115+
116+
std::unordered_map<std::string, std::shared_ptr<double>> scores = {
117+
{"Alice", std::make_shared<double>(95.5)},
118+
{"Bob", std::make_shared<double>(87.0)},
119+
{"Charlie", nullptr},
120+
};
121+
std::string str = std::format("{}", scores);
122+
EXPECT_THAT(str, ::testing::HasSubstr("Alice: 95.5"));
123+
EXPECT_THAT(str, ::testing::HasSubstr("Bob: 87"));
124+
EXPECT_THAT(str, ::testing::HasSubstr("Charlie: null"));
125+
126+
std::vector<std::map<std::string, std::shared_ptr<int>>> nested = {
127+
{{"a", std::make_shared<int>(1)}, {"b", std::make_shared<int>(2)}},
128+
{{"c", std::make_shared<int>(3)}, {"d", nullptr}},
129+
};
130+
EXPECT_EQ("[{a: 1, b: 2}, {c: 3, d: null}]", std::format("{}", nested));
131+
}
132+
133+
TEST(FormatterTest, StatisticsFileFormat) {
134+
StatisticsFile statistics_file{
135+
.snapshot_id = 123,
136+
.path = "test_path",
137+
.file_size_in_bytes = 100,
138+
.file_footer_size_in_bytes = 20,
139+
.blob_metadata = {BlobMetadata{.type = "type1",
140+
.source_snapshot_id = 1,
141+
.source_snapshot_sequence_number = 1,
142+
.fields = {1, 2, 3},
143+
.properties = {{"key1", "value1"}}},
144+
BlobMetadata{.type = "type2",
145+
.source_snapshot_id = 2,
146+
.source_snapshot_sequence_number = 2,
147+
.fields = {4, 5, 6},
148+
.properties = {}}}};
149+
150+
const std::string expected =
151+
"StatisticsFile["
152+
"snapshotId=123,path=test_path,fileSizeInBytes=100,fileFooterSizeInBytes=20,"
153+
"blobMetadata=["
154+
"BlobMetadata[type='type1',sourceSnapshotId=1,sourceSnapshotSequenceNumber=1,"
155+
"fields=[1, 2, 3],properties={key1: value1}], "
156+
"BlobMetadata[type='type2',sourceSnapshotId=2,sourceSnapshotSequenceNumber=2,"
157+
"fields=[4, 5, 6],properties={}]"
158+
"]"
159+
"]";
160+
EXPECT_EQ(expected, std::format("{}", statistics_file));
161+
}
162+
163+
} // namespace iceberg

0 commit comments

Comments
 (0)