fix: Array parsing in new config (#1896)

Improving array parsing in config:
- Allow null values in arrays for optional fields
- Allow empty array even for required field
- Allow to not put an empty array in config even if array contains
required fields
This commit is contained in:
Sergey Kuznetsov
2025-02-18 13:29:43 +00:00
committed by GitHub
parent fcebd715ba
commit 4b178805de
13 changed files with 328 additions and 132 deletions

View File

@@ -17,10 +17,13 @@
*/
//==============================================================================
#include "util/LoggerFixtures.hpp"
#include "util/newconfig/Array.hpp"
#include "util/newconfig/ArrayView.hpp"
#include "util/newconfig/ConfigDefinition.hpp"
#include "util/newconfig/ConfigDescription.hpp"
#include "util/newconfig/ConfigFileJson.hpp"
#include "util/newconfig/ConfigValue.hpp"
#include "util/newconfig/FakeConfigData.hpp"
#include "util/newconfig/Types.hpp"
#include "util/newconfig/ValueView.hpp"
@@ -29,10 +32,12 @@
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <algorithm>
#include <cstdint>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <unordered_set>
@@ -304,19 +309,144 @@ TEST_F(IncorrectOverrideValues, InvalidJsonErrors)
EXPECT_TRUE(errors.has_value());
// Expected error messages
std::unordered_set<std::string_view> const expectedErrors{
std::set<std::string_view> const expectedErrors{
"dosguard.whitelist.[] value does not match type string",
"higher.[].low.section key is required in user Config",
"higher.[].low.admin key is required in user Config",
"array.[].sub key is required in user Config",
"header.port value does not match type integer",
"header.admin value does not match type boolean",
"optional.withDefault value does not match type double"
};
std::unordered_set<std::string_view> actualErrors;
std::set<std::string_view> actualErrors;
for (auto const& error : errors.value()) {
actualErrors.insert(error.error);
}
EXPECT_EQ(expectedErrors, actualErrors);
}
struct ClioConfigDefinitionParseArrayTest : NoLoggerFixture {
ClioConfigDefinition config{
{"array.[].int", Array{ConfigValue{ConfigType::Integer}}},
{"array.[].string", Array{ConfigValue{ConfigType::String}.optional()}}
};
};
TEST_F(ClioConfigDefinitionParseArrayTest, emptyArray)
{
auto const configJson = boost::json::parse(R"json({
"array": []
})json").as_object();
auto const result = config.parse(ConfigFileJson{configJson});
EXPECT_FALSE(result.has_value());
}
TEST_F(ClioConfigDefinitionParseArrayTest, emptyJson)
{
auto const configJson = boost::json::object{};
auto const result = config.parse(ConfigFileJson{configJson});
EXPECT_FALSE(result.has_value());
}
TEST_F(ClioConfigDefinitionParseArrayTest, fullArray)
{
auto const configJson = boost::json::parse(R"json({
"array": [
{"int": 1, "string": "one"},
{"int": 2, "string": "two"}
]
})json").as_object();
auto const result = config.parse(ConfigFileJson{configJson});
EXPECT_FALSE(result.has_value());
EXPECT_EQ(config.arraySize("array.[]"), 2);
}
TEST_F(ClioConfigDefinitionParseArrayTest, onlyRequiredFields) {
auto const configJson = boost::json::parse(R"json({
"array": [
{"int": 1},
{"int": 2}
]
})json").as_object();
auto const configFile = ConfigFileJson{configJson};
auto const result = config.parse(configFile);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(config.arraySize("array.[]"), 2);
EXPECT_EQ(config.getArray("array.[].int").valueAt(0).asIntType<int>(), 1);
EXPECT_EQ(config.getArray("array.[].int").valueAt(1).asIntType<int>(), 2);
EXPECT_FALSE(config.getArray("array.[].string").valueAt(0).hasValue());
EXPECT_FALSE(config.getArray("array.[].string").valueAt(1).hasValue());
}
TEST_F(ClioConfigDefinitionParseArrayTest, someOptionalFieldsMissing)
{
auto const configJson = boost::json::parse(R"json({
"array": [
{"int": 1, "string": "one"},
{"int": 2}
]
})json").as_object();
auto const configFile = ConfigFileJson{configJson};
auto const result = config.parse(configFile);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(config.arraySize("array.[]"), 2);
EXPECT_EQ(config.getArray("array.[].int").valueAt(0).asIntType<int>(), 1);
EXPECT_EQ(config.getArray("array.[].int").valueAt(1).asIntType<int>(), 2);
EXPECT_EQ(config.getArray("array.[].string").valueAt(0).asString(), "one");
EXPECT_FALSE(config.getArray("array.[].string").valueAt(1).hasValue());
}
TEST_F(ClioConfigDefinitionParseArrayTest, optionalFieldMissingAtFirstPosition) {
auto const configJson = boost::json::parse(R"json({
"array": [
{"int": 1},
{"int": 2, "string": "two"}
]
})json").as_object();
auto const configFile = ConfigFileJson{configJson};
auto const result = config.parse(configFile);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(config.arraySize("array.[]"), 2);
EXPECT_EQ(config.getArray("array.[].int").valueAt(0).asIntType<int>(), 1);
EXPECT_EQ(config.getArray("array.[].int").valueAt(1).asIntType<int>(), 2);
EXPECT_FALSE(config.getArray("array.[].string").valueAt(0).hasValue());
EXPECT_EQ(config.getArray("array.[].string").valueAt(1).asString(), "two");
}
TEST_F(ClioConfigDefinitionParseArrayTest, missingRequiredFields) {
auto const configJson = boost::json::parse(R"json({
"array": [
{"int": 1},
{"string": "two"}
]
})json").as_object();
auto const configFile = ConfigFileJson{configJson};
auto const result = config.parse(configFile);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->size(), 1);
EXPECT_THAT(result->at(0).error, testing::StartsWith("array.[].int"));
}
TEST_F(ClioConfigDefinitionParseArrayTest, missingAllRequiredFields) {
auto const configJson = boost::json::parse(R"json({
"array": [
{"string": "one"},
{"string": "two"}
]
})json").as_object();
auto const configFile = ConfigFileJson{configJson};
auto const result = config.parse(configFile);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result->size(), 1);
EXPECT_THAT(result->at(0).error, testing::StartsWith("array.[].int"));
}