Skip to content

Commit e96193c

Browse files
manuzhangclaude
andcommitted
refactor: parse booleans through a single shared helper
Boolean values were parsed ad hoc in four places, in two incompatible flavors: `arrow_s3_file_io.cc` rejected anything that was not "true" or "false", while `config.h`, `auth_managers.cc` and `rest_catalog.cc` read any unrecognized value as false. Each site spelled out the "true"/"false" literals itself, and `StringUtils` covered only numbers. Add `StringUtils::ParseBoolean()`, a strict case-insensitive parser that mirrors `ParseNumber()`, and `PropertyUtil::PropertyAsBoolean()` / `PropertyUtil::PropertyAsOptionalBoolean()` for the property-map pattern, with the optional variant distinguishing an unset property from "false". Route the existing sites through them. The sites that cannot fail keep their lenient contract via `.value_or(false)`, so behavior is unchanged and no public header changes shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 168cb52 commit e96193c

10 files changed

Lines changed: 194 additions & 24 deletions

File tree

‎src/iceberg/arrow/s3/arrow_s3_file_io.cc‎

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "iceberg/arrow/s3/s3_properties.h"
3838
#include "iceberg/logging/log_macros.h"
3939
#include "iceberg/util/macros.h"
40+
#include "iceberg/util/property_util.h"
4041
#include "iceberg/util/string_util.h"
4142

4243
namespace iceberg::arrow {
@@ -52,22 +53,6 @@ const std::string* FindProperty(
5253
return it == properties.end() ? nullptr : &it->second;
5354
}
5455

55-
Result<std::optional<bool>> ParseOptionalBool(
56-
const std::unordered_map<std::string, std::string>& properties,
57-
std::string_view key) {
58-
const auto* value = FindProperty(properties, key);
59-
if (value == nullptr) {
60-
return std::nullopt;
61-
}
62-
if (StringUtils::EqualsIgnoreCase(*value, "true")) {
63-
return true;
64-
}
65-
if (StringUtils::EqualsIgnoreCase(*value, "false")) {
66-
return false;
67-
}
68-
return InvalidArgument(R"("{}" must be "true" or "false")", key);
69-
}
70-
7156
Status EnsureS3Initialized() {
7257
static const ::arrow::Status init_status = []() {
7358
auto options = ::arrow::fs::S3GlobalOptions::Defaults();
@@ -137,14 +122,16 @@ Result<::arrow::fs::S3Options> ConfigureS3Options(
137122
}
138123

139124
ICEBERG_ASSIGN_OR_RAISE(const auto path_style_access,
140-
ParseOptionalBool(properties, S3Properties::kPathStyleAccess));
125+
PropertyUtil::PropertyAsOptionalBoolean(
126+
properties, S3Properties::kPathStyleAccess));
141127
if (path_style_access.has_value()) {
142128
options.force_virtual_addressing = !*path_style_access;
143129
}
144130

145131
// Explicit `s3.ssl.enabled` overrides any endpoint-derived scheme.
146-
ICEBERG_ASSIGN_OR_RAISE(const auto ssl_enabled,
147-
ParseOptionalBool(properties, S3Properties::kSslEnabled));
132+
ICEBERG_ASSIGN_OR_RAISE(
133+
const auto ssl_enabled,
134+
PropertyUtil::PropertyAsOptionalBoolean(properties, S3Properties::kSslEnabled));
148135
if (ssl_enabled.has_value()) {
149136
options.scheme = *ssl_enabled ? "https" : "http";
150137
}

‎src/iceberg/catalog/rest/auth/auth_managers.cc‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include "iceberg/catalog/rest/auth/auth_manager_internal.h"
2525
#include "iceberg/catalog/rest/auth/auth_properties.h"
26+
#include "iceberg/util/property_util.h"
2627
#include "iceberg/util/string_util.h"
2728

2829
namespace iceberg::rest::auth {
@@ -46,9 +47,11 @@ const std::unordered_set<std::string, StringHash, StringEqual>& KnownAuthTypes()
4647
// Infer the authentication type from properties.
4748
std::string InferAuthType(
4849
const std::unordered_map<std::string, std::string>& properties) {
49-
// Deprecated alias: rest.sigv4-enabled=true forces SigV4.
50-
if (auto it = properties.find(AuthProperties::kSigV4Enabled);
51-
it != properties.end() && StringUtils::EqualsIgnoreCase(it->second, "true")) {
50+
// Deprecated alias: rest.sigv4-enabled=true forces SigV4. A value that is not a
51+
// boolean reads as false rather than being rejected, since inference cannot fail.
52+
auto sigv4_enabled = PropertyUtil::PropertyAsBoolean(
53+
properties, AuthProperties::kSigV4Enabled, /*default_value=*/false);
54+
if (sigv4_enabled.value_or(false)) {
5255
return AuthProperties::kAuthTypeSigV4;
5356
}
5457

‎src/iceberg/catalog/rest/rest_catalog.cc‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,9 @@ Result<std::shared_ptr<MetricsReporter>> RestCatalog::MakeTableReporter(
523523
const TableIdentifier& identifier,
524524
const std::shared_ptr<auth::AuthSession>& table_session) const {
525525
auto metrics_enabled = config_.Get(RestCatalogProperties::kMetricsReportingEnabled);
526-
if (StringUtils::ToLower(metrics_enabled) == "true" &&
526+
// A value that is not a boolean reads as false rather than being rejected, since
527+
// building the reporter cannot fail on a malformed property.
528+
if (StringUtils::ParseBoolean(metrics_enabled).value_or(false) &&
527529
supported_endpoints_.contains(Endpoint::ReportMetrics())) {
528530
ICEBERG_ASSIGN_OR_RAISE(auto path, paths_->Metrics(identifier));
529531
auto post = [client = client_](const std::string& endpoint, const std::string& body,

‎src/iceberg/test/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ add_iceberg_test(util_test
143143
roaring_position_bitmap_test.cc
144144
position_delete_index_test.cc
145145
position_delete_range_consumer_test.cc
146+
property_util_test.cc
146147
resolving_file_io_test.cc
147148
retry_util_test.cc
148149
string_util_test.cc
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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/property_util.h"
21+
22+
#include <optional>
23+
#include <string>
24+
#include <string_view>
25+
#include <unordered_map>
26+
27+
#include <gtest/gtest.h>
28+
29+
#include "iceberg/test/matchers.h"
30+
31+
namespace iceberg {
32+
33+
namespace {
34+
35+
constexpr std::string_view kKey = "test.enabled";
36+
37+
std::unordered_map<std::string, std::string> Properties(std::string_view value) {
38+
return {{std::string(kKey), std::string(value)}};
39+
}
40+
41+
Result<bool> PropertyAsBoolean(std::string_view value, bool default_value) {
42+
return PropertyUtil::PropertyAsBoolean(Properties(value), kKey, default_value);
43+
}
44+
45+
Result<std::optional<bool>> PropertyAsOptionalBoolean(std::string_view value) {
46+
return PropertyUtil::PropertyAsOptionalBoolean(Properties(value), kKey);
47+
}
48+
49+
} // namespace
50+
51+
TEST(PropertyUtilTest, BooleanDefaultsWhenPropertyIsAbsent) {
52+
const std::unordered_map<std::string, std::string> empty;
53+
EXPECT_THAT(PropertyUtil::PropertyAsBoolean(empty, kKey, true),
54+
HasValue(::testing::Eq(true)));
55+
EXPECT_THAT(PropertyUtil::PropertyAsBoolean(empty, kKey, false),
56+
HasValue(::testing::Eq(false)));
57+
EXPECT_THAT(PropertyUtil::PropertyAsOptionalBoolean(empty, kKey),
58+
HasValue(::testing::Eq(std::optional<bool>{})));
59+
}
60+
61+
TEST(PropertyUtilTest, BooleanIgnoresCase) {
62+
for (const auto* value : {"true", "TRUE", "TrUe"}) {
63+
EXPECT_THAT(PropertyAsBoolean(value, false), HasValue(::testing::Eq(true))) << value;
64+
EXPECT_THAT(PropertyAsOptionalBoolean(value),
65+
HasValue(::testing::Eq(std::optional<bool>{true})))
66+
<< value;
67+
}
68+
for (const auto* value : {"false", "FALSE", "FaLsE"}) {
69+
EXPECT_THAT(PropertyAsBoolean(value, true), HasValue(::testing::Eq(false))) << value;
70+
EXPECT_THAT(PropertyAsOptionalBoolean(value),
71+
HasValue(::testing::Eq(std::optional<bool>{false})))
72+
<< value;
73+
}
74+
}
75+
76+
TEST(PropertyUtilTest, BooleanRejectsNonBooleanValue) {
77+
for (const auto* value : {"", " true", "yes", "1", "maybe"}) {
78+
EXPECT_THAT(PropertyAsBoolean(value, true), IsError(ErrorKind::kInvalidArgument))
79+
<< value;
80+
EXPECT_THAT(PropertyAsOptionalBoolean(value), IsError(ErrorKind::kInvalidArgument))
81+
<< value;
82+
}
83+
}
84+
85+
TEST(PropertyUtilTest, BooleanErrorReportsKeyAndValue) {
86+
auto result = PropertyAsBoolean("maybe", /*default_value=*/true);
87+
EXPECT_THAT(result, HasErrorMessage("test.enabled"));
88+
EXPECT_THAT(result, HasErrorMessage("maybe"));
89+
}
90+
91+
} // namespace iceberg

‎src/iceberg/test/string_util_test.cc‎

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

2222
#include <gtest/gtest.h>
2323

24+
#include "iceberg/test/matchers.h"
25+
2426
namespace iceberg {
2527

2628
TEST(StringUtilsTest, ToLower) {
@@ -32,6 +34,21 @@ TEST(StringUtilsTest, ToLower) {
3234
ASSERT_EQ(StringUtils::ToLower("123"), "123");
3335
}
3436

37+
TEST(StringUtilsTest, ParseBoolean) {
38+
for (const auto* value : {"true", "TRUE", "TrUe"}) {
39+
EXPECT_THAT(StringUtils::ParseBoolean(value), HasValue(::testing::Eq(true))) << value;
40+
}
41+
for (const auto* value : {"false", "FALSE", "FaLsE"}) {
42+
EXPECT_THAT(StringUtils::ParseBoolean(value), HasValue(::testing::Eq(false)))
43+
<< value;
44+
}
45+
for (const auto* value : {"", " true", "true ", "yes", "no", "1", "0", "truthy"}) {
46+
EXPECT_THAT(StringUtils::ParseBoolean(value), IsError(ErrorKind::kInvalidArgument))
47+
<< value;
48+
}
49+
EXPECT_THAT(StringUtils::ParseBoolean("maybe"), HasErrorMessage("'maybe'"));
50+
}
51+
3552
TEST(StringUtilsTest, ToUpper) {
3653
ASSERT_EQ(StringUtils::ToUpper("abc"), "ABC");
3754
ASSERT_EQ(StringUtils::ToUpper("A-bC"), "A-BC");

‎src/iceberg/util/config.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ U DefaultFromString(const std::string& val) {
5454
if constexpr (std::is_same_v<U, std::string>) {
5555
return val;
5656
} else if constexpr (std::is_same_v<U, bool>) {
57-
return StringUtils::EqualsIgnoreCase(val, "true");
57+
// Unlike the numeric conversions, an unrecognized value is not an error here: any
58+
// value other than "true" reads as false, matching Java's Boolean.parseBoolean.
59+
return StringUtils::ParseBoolean(val).value_or(false);
5860
} else if constexpr ((std::is_signed_v<U> && std::is_integral_v<U>) ||
5961
std::is_floating_point_v<U>) {
6062
ICEBERG_ASSIGN_OR_THROW(auto res, StringUtils::ParseNumber<U>(val));

‎src/iceberg/util/property_util.cc‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
#include "iceberg/util/property_util.h"
2121

2222
#include <cstdint>
23+
#include <string>
2324

2425
#include "iceberg/table_properties.h"
26+
#include "iceberg/util/macros.h"
2527
#include "iceberg/util/string_util.h"
2628

2729
namespace iceberg {
@@ -46,4 +48,26 @@ Status PropertyUtil::ValidateCommitProperties(
4648
return {};
4749
}
4850

51+
Result<bool> PropertyUtil::PropertyAsBoolean(
52+
const std::unordered_map<std::string, std::string>& properties, std::string_view key,
53+
bool default_value) {
54+
ICEBERG_ASSIGN_OR_RAISE(auto value, PropertyAsOptionalBoolean(properties, key));
55+
return value.value_or(default_value);
56+
}
57+
58+
Result<std::optional<bool>> PropertyUtil::PropertyAsOptionalBoolean(
59+
const std::unordered_map<std::string, std::string>& properties,
60+
std::string_view key) {
61+
auto it = properties.find(std::string(key));
62+
if (it == properties.end()) {
63+
return std::nullopt;
64+
}
65+
auto value = StringUtils::ParseBoolean(it->second);
66+
if (!value) {
67+
return InvalidArgument(R"(Property {} must be "true" or "false", but got {})", key,
68+
it->second);
69+
}
70+
return *value;
71+
}
72+
4973
} // namespace iceberg

‎src/iceberg/util/property_util.h‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
/// \file iceberg/util/property_util.h
2323
/// \brief Provide property conversion helpers.
2424

25+
#include <optional>
2526
#include <string>
27+
#include <string_view>
2628
#include <unordered_map>
2729

2830
#include "iceberg/iceberg_export.h"
@@ -34,6 +36,31 @@ class ICEBERG_EXPORT PropertyUtil {
3436
public:
3537
static Status ValidateCommitProperties(
3638
const std::unordered_map<std::string, std::string>& properties);
39+
40+
/// \brief Read a boolean property from a property map.
41+
///
42+
/// The value must be "true" or "false" ignoring case; any other value is an error.
43+
///
44+
/// \param properties The property map to read from.
45+
/// \param key The property key.
46+
/// \param default_value Returned when the property is absent.
47+
/// \return The parsed value, or an error if the property is set to a non-boolean.
48+
static Result<bool> PropertyAsBoolean(
49+
const std::unordered_map<std::string, std::string>& properties,
50+
std::string_view key, bool default_value);
51+
52+
/// \brief Read a boolean property that may be unset.
53+
///
54+
/// Like PropertyAsBoolean, but returns std::nullopt when the property is absent so
55+
/// callers can distinguish an unset property from an explicit "false".
56+
///
57+
/// \param properties The property map to read from.
58+
/// \param key The property key.
59+
/// \return The parsed value, std::nullopt if the property is absent, or an error if
60+
/// the property is set to a non-boolean.
61+
static Result<std::optional<bool>> PropertyAsOptionalBoolean(
62+
const std::unordered_map<std::string, std::string>& properties,
63+
std::string_view key);
3764
};
3865

3966
} // namespace iceberg

‎src/iceberg/util/string_util.h‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ class ICEBERG_EXPORT StringUtils {
168168
return value;
169169
}
170170

171+
/// \brief Parse a boolean from its string representation, ignoring case.
172+
///
173+
/// Only "true" and "false" are accepted; any other input is an error, mirroring
174+
/// ParseNumber. Call sites that must not fail on an unrecognized value, where
175+
/// anything other than "true" means false, can use `.value_or(false)`.
176+
static Result<bool> ParseBoolean(std::string_view str) {
177+
if (EqualsIgnoreCase(str, "true")) {
178+
return true;
179+
}
180+
if (EqualsIgnoreCase(str, "false")) {
181+
return false;
182+
}
183+
return InvalidArgument(
184+
R"(Failed to parse bool from string '{}': expected "true" or "false")", str);
185+
}
186+
171187
private:
172188
// ASCII-only case mappings. These avoid std::toupper/std::tolower, which are
173189
// locale-dependent and have undefined behavior for negative char values.

0 commit comments

Comments
 (0)