Skip to content

Commit e34f3da

Browse files
committed
feat: add formatter specialization
1 parent dbf9592 commit e34f3da

File tree

4 files changed

+278
-35
lines changed

4 files changed

+278
-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.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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727

2828
#include <concepts>
2929
#include <format>
30+
#include <map>
3031
#include <string_view>
32+
#include <unordered_map>
33+
#include <vector>
3134

3235
#include "iceberg/util/formattable.h"
3336

@@ -40,3 +43,102 @@ struct std::formatter<Derived> : std::formatter<std::string_view> {
4043
return std::formatter<string_view>::format(obj.ToString(), ctx);
4144
}
4245
};
46+
47+
/// \brief std::formatter specialization for std::vector
48+
template <typename T>
49+
struct std::formatter<std::vector<T>> : std::formatter<std::string_view> {
50+
template <class FormatContext>
51+
auto format(const std::vector<T>& vec, FormatContext& ctx) const {
52+
std::string result = "[";
53+
54+
bool first = true;
55+
for (const auto& item : vec) {
56+
if (!first) {
57+
std::format_to(std::back_inserter(result), ", ");
58+
}
59+
if constexpr (requires { *item; }) {
60+
if (item) {
61+
std::format_to(std::back_inserter(result), "{}", *item);
62+
} else {
63+
std::format_to(std::back_inserter(result), "null");
64+
}
65+
} else {
66+
std::format_to(std::back_inserter(result), "{}", item);
67+
}
68+
first = false;
69+
}
70+
71+
std::format_to(std::back_inserter(result), "]");
72+
return std::formatter<std::string_view>::format(result, ctx);
73+
}
74+
};
75+
76+
/// \brief Helper template for formatting map-like containers
77+
template <typename MapType>
78+
std::string FormatMap(const MapType& map) {
79+
std::string result = "{";
80+
81+
bool first = true;
82+
for (const auto& [key, value] : map) {
83+
if (!first) {
84+
std::format_to(std::back_inserter(result), ", ");
85+
}
86+
87+
// Format key (handle if it's a smart pointer)
88+
if constexpr (requires { *key; }) {
89+
if (key) {
90+
std::format_to(std::back_inserter(result), "{}: ", *key);
91+
} else {
92+
std::format_to(std::back_inserter(result), "null: ");
93+
}
94+
} else {
95+
std::format_to(std::back_inserter(result), "{}: ", key);
96+
}
97+
98+
// Format value (handle if it's a smart pointer)
99+
if constexpr (requires { *value; }) {
100+
if (value) {
101+
std::format_to(std::back_inserter(result), "{}", *value);
102+
} else {
103+
std::format_to(std::back_inserter(result), "null");
104+
}
105+
} else {
106+
std::format_to(std::back_inserter(result), "{}", value);
107+
}
108+
109+
first = false;
110+
}
111+
112+
std::format_to(std::back_inserter(result), "}}");
113+
return result;
114+
}
115+
116+
/// \brief std::formatter specialization for std::map
117+
template <typename K, typename V>
118+
struct std::formatter<std::map<K, V>> : std::formatter<std::string_view> {
119+
template <class FormatContext>
120+
auto format(const std::map<K, V>& map, FormatContext& ctx) const {
121+
return std::formatter<std::string_view>::format(FormatMap(map), ctx);
122+
}
123+
};
124+
125+
/// \brief std::formatter specialization for std::unordered_map
126+
template <typename K, typename V>
127+
struct std::formatter<std::unordered_map<K, V>> : std::formatter<std::string_view> {
128+
template <class FormatContext>
129+
auto format(const std::unordered_map<K, V>& map, FormatContext& ctx) const {
130+
return std::formatter<std::string_view>::format(FormatMap(map), ctx);
131+
}
132+
};
133+
134+
/// \brief std::formatter specialization for any type that has a ToString function
135+
template <typename T>
136+
requires requires(const T& t) {
137+
{ ToString(t) } -> std::convertible_to<std::string>;
138+
}
139+
struct std::formatter<T> : std::formatter<std::string_view> {
140+
template <class FormatContext>
141+
auto format(const T& value, FormatContext& ctx) const {
142+
return std::formatter<std::string_view>::format(ToString(value), ctx);
143+
}
144+
};

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 "iceberg/util/formatter.h"
21+
22+
#include <format>
23+
#include <map>
24+
#include <memory>
25+
#include <string>
26+
#include <unordered_map>
27+
#include <vector>
28+
29+
#include <gtest/gtest.h>
30+
31+
#include "iceberg/statistics_file.h"
32+
33+
namespace iceberg {
34+
35+
// Tests for the std::format specializations
36+
TEST(FormatterTest, VectorFormat) {
37+
std::vector<int> empty;
38+
EXPECT_EQ("[]", std::format("{}", empty));
39+
40+
std::vector<int> nums = {1, 2, 3, 4, 5};
41+
EXPECT_EQ("[1, 2, 3, 4, 5]", std::format("{}", nums));
42+
43+
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
44+
EXPECT_EQ("[Alice, Bob, Charlie]", std::format("{}", names));
45+
}
46+
47+
TEST(FormatterTest, MapFormat) {
48+
std::map<std::string, int> empty;
49+
EXPECT_EQ("{}", std::format("{}", empty));
50+
51+
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
52+
EXPECT_EQ("{Alice: 30, Bob: 25, Charlie: 35}", std::format("{}", ages));
53+
}
54+
55+
TEST(FormatterTest, UnorderedMapFormat) {
56+
std::unordered_map<std::string, double> empty;
57+
EXPECT_EQ("{}", std::format("{}", empty));
58+
59+
std::unordered_map<std::string, double> scores = {
60+
{"Alice", 95.5}, {"Bob", 87.0}, {"Charlie", 92.3}};
61+
std::string str = std::format("{}", scores);
62+
EXPECT_TRUE(str.find("Alice: 95.5") != std::string::npos);
63+
EXPECT_TRUE(str.find("Bob: 87") != std::string::npos);
64+
EXPECT_TRUE(str.find("Charlie: 92.3") != std::string::npos);
65+
}
66+
67+
TEST(FormatterTest, NestedContainersFormat) {
68+
std::vector<std::map<std::string, int>> nested = {{{"a", 1}, {"b", 2}},
69+
{{"c", 3}, {"d", 4}}};
70+
71+
EXPECT_EQ("[{a: 1, b: 2}, {c: 3, d: 4}]", std::format("{}", nested));
72+
73+
std::map<std::string, std::vector<int>> nested_map = {
74+
{"primes", {2, 3, 5, 7, 11}}, {"fibonacci", {1, 1, 2, 3, 5, 8, 13}}};
75+
std::string result = std::format("{}", nested_map);
76+
EXPECT_TRUE(result.find("primes") != std::string::npos);
77+
EXPECT_TRUE(result.find("fibonacci") != std::string::npos);
78+
EXPECT_TRUE(result.find("[2, 3, 5, 7, 11]") != std::string::npos);
79+
EXPECT_TRUE(result.find("[1, 1, 2, 3, 5, 8, 13]") != std::string::npos);
80+
}
81+
82+
TEST(FormatterTest, EdgeCasesFormat) {
83+
std::vector<int> single_vec = {42};
84+
EXPECT_EQ("[42]", std::format("{}", single_vec));
85+
86+
std::map<std::string, int> single_map = {{"key", 42}};
87+
EXPECT_EQ("{key: 42}", std::format("{}", single_map));
88+
89+
std::vector<std::vector<int>> nested_empty = {{}, {1, 2}, {}};
90+
EXPECT_EQ("[[], [1, 2], []]", std::format("{}", nested_empty));
91+
}
92+
93+
TEST(FormatterTest, SmartPointerFormat) {
94+
std::vector<std::shared_ptr<int>> int_ptrs;
95+
int_ptrs.push_back(std::make_shared<int>(42));
96+
int_ptrs.push_back(std::make_shared<int>(123));
97+
int_ptrs.push_back(nullptr);
98+
EXPECT_EQ("[42, 123, null]", std::format("{}", int_ptrs));
99+
100+
std::vector<std::shared_ptr<std::string>> str_ptrs;
101+
str_ptrs.push_back(std::make_shared<std::string>("hello"));
102+
str_ptrs.push_back(std::make_shared<std::string>("world"));
103+
str_ptrs.push_back(nullptr);
104+
EXPECT_EQ("[hello, world, null]", std::format("{}", str_ptrs));
105+
106+
std::map<std::string, std::shared_ptr<int>> map_with_ptr_values;
107+
map_with_ptr_values["one"] = std::make_shared<int>(1);
108+
map_with_ptr_values["two"] = std::make_shared<int>(2);
109+
map_with_ptr_values["null"] = nullptr;
110+
EXPECT_EQ("{null: null, one: 1, two: 2}", std::format("{}", map_with_ptr_values));
111+
112+
std::unordered_map<std::string, std::shared_ptr<double>> scores;
113+
scores["Alice"] = std::make_shared<double>(95.5);
114+
scores["Bob"] = std::make_shared<double>(87.0);
115+
scores["Charlie"] = nullptr;
116+
std::string str = std::format("{}", scores);
117+
EXPECT_TRUE(str.find("Alice: 95.5") != std::string::npos);
118+
EXPECT_TRUE(str.find("Bob: 87") != std::string::npos);
119+
EXPECT_TRUE(str.find("Charlie: null") != std::string::npos);
120+
121+
std::vector<std::map<std::string, std::shared_ptr<int>>> nested;
122+
std::map<std::string, std::shared_ptr<int>> map1;
123+
map1["a"] = std::make_shared<int>(1);
124+
map1["b"] = std::make_shared<int>(2);
125+
std::map<std::string, std::shared_ptr<int>> map2;
126+
map2["c"] = std::make_shared<int>(3);
127+
map2["d"] = nullptr;
128+
nested.push_back(std::move(map1));
129+
nested.push_back(std::move(map2));
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)