diff --git a/src/iceberg/util/config.h b/src/iceberg/util/config.h new file mode 100644 index 000000000..7a3a28b40 --- /dev/null +++ b/src/iceberg/util/config.h @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#pragma once + +#include +#include +#include +#include + +#include "iceberg/exception.h" + +namespace iceberg { +namespace internal { +// Default conversion functions +template +std::string DefaultToString(const U& val) { + if constexpr ((std::is_signed_v && std::is_integral_v) || + std::is_floating_point_v) { + return std::to_string(val); + } else if constexpr (std::is_same_v) { + return val ? "true" : "false"; + } else if constexpr (std::is_same_v || + std::is_same_v) { + return val; + } else { + throw IcebergError( + std::format("Explicit to_str() is required for {}", typeid(U).name())); + } +} + +template +U DefaultFromString(const std::string& val) { + if constexpr (std::is_same_v) { + return val; + } else if constexpr (std::is_same_v) { + return val == "true"; + } else if constexpr (std::is_signed_v && std::is_integral_v) { + return static_cast(std::stoll(val)); + } else if constexpr (std::is_floating_point_v) { + return static_cast(std::stod(val)); + } else { + throw IcebergError( + std::format("Explicit from_str() is required for {}", typeid(U).name())); + } +} +} // namespace internal + +template +class ConfigBase { + public: + template + class Entry { + public: + Entry(std::string key, const T& val, + std::function to_str = internal::DefaultToString, + std::function from_str = internal::DefaultFromString) + : key_{std::move(key)}, default_{val}, to_str_{to_str}, from_str_{from_str} {} + + private: + const std::string key_; + const T default_; + const std::function to_str_; + const std::function from_str_; + + friend ConfigBase; + friend ConcreteConfig; + + public: + const std::string& key() const { return key_; } + + const T& value() const { return default_; } + }; + + template + ConfigBase& Set(const Entry& entry, const T& val) { + configs_.emplace(entry.key_, entry.to_str_(val)); + return *this; + } + + template + ConfigBase& Unset(const Entry& entry) { + configs_.erase(entry.key_); + return *this; + } + + ConfigBase& Reset() { + configs_.clear(); + return *this; + } + + template + T Get(const Entry& entry) const { + auto iter = configs_.find(entry.key_); + return iter != configs_.cend() ? entry.from_str_(iter->second) : entry.default_; + } + + const std::unordered_map& configs() const { return configs_; } + + protected: + std::unordered_map configs_; +}; + +} // namespace iceberg diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bf8caa0b0..3fc3152e6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -57,7 +57,7 @@ target_link_libraries(json_serde_test PRIVATE iceberg_static GTest::gtest_main add_test(NAME json_serde_test COMMAND json_serde_test) add_executable(util_test) -target_sources(util_test PRIVATE expected_test.cc formatter_test.cc) +target_sources(util_test PRIVATE expected_test.cc formatter_test.cc config_test.cc) target_link_libraries(util_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock) add_test(NAME util_test COMMAND util_test) diff --git a/test/config_test.cc b/test/config_test.cc new file mode 100644 index 000000000..1ee04224e --- /dev/null +++ b/test/config_test.cc @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include +#include + +namespace iceberg { + +enum class TestEnum { VALUE1, VALUE2, VALUE3 }; + +std::string EnumToString(const TestEnum& val) { + switch (val) { + case TestEnum::VALUE1: + return "VALUE1"; + case TestEnum::VALUE2: + return "VALUE2"; + case TestEnum::VALUE3: + return "VALUE3"; + default: + throw std::runtime_error("Invalid enum value"); + } +} + +TestEnum StringToEnum(const std::string& val) { + if (val == "VALUE1") { + return TestEnum::VALUE1; + } else if (val == "VALUE2") { + return TestEnum::VALUE2; + } else if (val == "VALUE3") { + return TestEnum::VALUE3; + } else { + throw std::runtime_error("Invalid enum string"); + } +} + +// Define a concrete config class for testing +class TestConfig : public ConfigBase { + public: + template + using Entry = const ConfigBase::Entry; + + inline static const Entry kStringConfig{"string_config", "default_value"}; + inline static const Entry kIntConfig{"int_config", 25}; + inline static const Entry kBoolConfig{"bool_config", false}; + inline static const Entry kEnumConfig{"enum_config", TestEnum::VALUE1, + EnumToString, StringToEnum}; + inline static const Entry kDoubleConfig{"double_config", 3.14}; +}; + +TEST(ConfigTest, BasicOperations) { + TestConfig config; + + // Test default values + ASSERT_EQ(config.Get(TestConfig::kStringConfig), std::string("default_value")); + ASSERT_EQ(config.Get(TestConfig::kIntConfig), 25); + ASSERT_EQ(config.Get(TestConfig::kBoolConfig), false); + ASSERT_EQ(config.Get(TestConfig::kEnumConfig), TestEnum::VALUE1); + ASSERT_EQ(config.Get(TestConfig::kDoubleConfig), 3.14); + + // Test setting values + config.Set(TestConfig::kStringConfig, std::string("new_value")); + config.Set(TestConfig::kIntConfig, 100); + config.Set(TestConfig::kBoolConfig, true); + config.Set(TestConfig::kEnumConfig, TestEnum::VALUE2); + config.Set(TestConfig::kDoubleConfig, 2.99); + + ASSERT_EQ(config.Get(TestConfig::kStringConfig), "new_value"); + ASSERT_EQ(config.Get(TestConfig::kIntConfig), 100); + ASSERT_EQ(config.Get(TestConfig::kBoolConfig), true); + ASSERT_EQ(config.Get(TestConfig::kEnumConfig), TestEnum::VALUE2); + ASSERT_EQ(config.Get(TestConfig::kDoubleConfig), 2.99); + + // Test unsetting a value + config.Unset(TestConfig::kIntConfig); + config.Unset(TestConfig::kEnumConfig); + config.Unset(TestConfig::kDoubleConfig); + ASSERT_EQ(config.Get(TestConfig::kIntConfig), 25); + ASSERT_EQ(config.Get(TestConfig::kStringConfig), "new_value"); + ASSERT_EQ(config.Get(TestConfig::kBoolConfig), true); + ASSERT_EQ(config.Get(TestConfig::kEnumConfig), TestEnum::VALUE1); + ASSERT_EQ(config.Get(TestConfig::kDoubleConfig), 3.14); + + // Test resetting all values + config.Reset(); + ASSERT_EQ(config.Get(TestConfig::kStringConfig), "default_value"); + ASSERT_EQ(config.Get(TestConfig::kIntConfig), 25); + ASSERT_EQ(config.Get(TestConfig::kBoolConfig), false); + ASSERT_EQ(config.Get(TestConfig::kEnumConfig), TestEnum::VALUE1); + ASSERT_EQ(config.Get(TestConfig::kDoubleConfig), 3.14); +} + +} // namespace iceberg