Add shared DeviceAttachmentSpec param parsing helpers.
Centralize DAP param lookup, parsing, and validation primitives so stimBuff and threshold modules can share one implementation instead of duplicating parse logic per plugin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -44,3 +44,7 @@ add_custom_command(TARGET attachmentSupport POST_BUILD
|
||||
install(TARGETS attachmentSupport
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP
|
||||
)
|
||||
|
||||
if(ENABLE_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
add_executable(deviceAttachmentSpecParams_tests
|
||||
deviceAttachmentSpecParams_tests.cpp
|
||||
)
|
||||
|
||||
target_include_directories(deviceAttachmentSpecParams_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_BINARY_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/libspinscale/tests
|
||||
)
|
||||
|
||||
target_link_libraries(deviceAttachmentSpecParams_tests
|
||||
gtest_main
|
||||
spinscale_test_support
|
||||
${Boost_LIBRARIES}
|
||||
)
|
||||
|
||||
add_dependencies(deviceAttachmentSpecParams_tests gtest_main)
|
||||
|
||||
add_test(
|
||||
NAME deviceAttachmentSpecParams_tests
|
||||
COMMAND deviceAttachmentSpecParams_tests)
|
||||
@@ -0,0 +1,413 @@
|
||||
#include <array>
|
||||
#include <gtest/gtest.h>
|
||||
#include <support/exceptionAssertions.h>
|
||||
#include <user/deviceAttachmentSpec.h>
|
||||
#include <vector>
|
||||
|
||||
namespace smo {
|
||||
namespace device {
|
||||
namespace {
|
||||
|
||||
using ParamList = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
ParamList makeParams(std::initializer_list<std::pair<const char*, const char*>> entries)
|
||||
{
|
||||
ParamList params;
|
||||
for (const auto& entry : entries)
|
||||
{
|
||||
params.emplace_back(entry.first, entry.second);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecNamesContainTest, MatchesVectorAndArraySynonyms)
|
||||
{
|
||||
const std::vector<std::string> widthSynonyms = {
|
||||
"frame-width",
|
||||
"dim-w",
|
||||
};
|
||||
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-w"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-h"));
|
||||
|
||||
const std::array<std::string_view, 2> heightSynonyms = {
|
||||
"frame-height",
|
||||
"dim-h",
|
||||
};
|
||||
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-height"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-width"));
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParamsContainTest, DetectsExactParamName)
|
||||
{
|
||||
const ParamList params = makeParams({
|
||||
{"display", "0"},
|
||||
{"screen", "1"},
|
||||
});
|
||||
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "display"));
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "screen"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::paramsContain(params, "width"));
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParamNameMatchesAnySynonymGroupTest, MatchesAcrossGroups)
|
||||
{
|
||||
const std::vector<std::string> widthSynonyms = {"dim-w", "frame-w"};
|
||||
const std::vector<std::string> heightSynonyms = {"dim-h", "frame-h"};
|
||||
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
|
||||
"dim-w", widthSynonyms, heightSynonyms));
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
|
||||
"frame-h", widthSynonyms, heightSynonyms));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup(
|
||||
"colour-space", widthSynonyms, heightSynonyms));
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecNormalizeParamTokenTest, TrimsAndLowercases)
|
||||
{
|
||||
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(" TRUE "), "true");
|
||||
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("YUV"), "yuv");
|
||||
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("480p"), "480p");
|
||||
EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(""), "");
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecFindOptionalParamTest, LattermostExactNameWins)
|
||||
{
|
||||
const ParamList params = makeParams({
|
||||
{"display", "0"},
|
||||
{"other", "ignored"},
|
||||
{"display", "2"},
|
||||
});
|
||||
|
||||
const std::optional<std::pair<std::string, std::string>> matched =
|
||||
DeviceAttachmentSpec::findOptionalParam(params, "display");
|
||||
|
||||
ASSERT_TRUE(matched.has_value());
|
||||
EXPECT_EQ(matched->first, "display");
|
||||
EXPECT_EQ(matched->second, "2");
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, LattermostSynonymWins)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {
|
||||
"cmd-timeout-ms",
|
||||
"command-timeout-ms",
|
||||
};
|
||||
|
||||
const ParamList params = makeParams({
|
||||
{"cmd-timeout-ms", "5"},
|
||||
{"command-timeout-ms", "25"},
|
||||
});
|
||||
|
||||
const std::optional<std::pair<std::string, std::string>> matched =
|
||||
DeviceAttachmentSpec::findOptionalParamWithSynonyms(params, synonyms);
|
||||
|
||||
ASSERT_TRUE(matched.has_value());
|
||||
EXPECT_EQ(matched->first, "command-timeout-ms");
|
||||
EXPECT_EQ(matched->second, "25");
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, AbsentReturnsNullopt)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {"data-port"};
|
||||
const ParamList params = makeParams({{"cmd-port", "56001"}});
|
||||
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::findOptionalParamWithSynonyms(
|
||||
params, synonyms).has_value());
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseParamValueAsIntTest, ParsesValidInteger)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseParamValueAsInt("width", "1280"),
|
||||
1280);
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseParamValueAsInt("width", "-42"),
|
||||
-42);
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseParamValueAsIntTest, InvalidIntegerThrows)
|
||||
{
|
||||
try {
|
||||
DeviceAttachmentSpec::parseParamValueAsInt("width", "not-a-number");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "Failed to parse 'width'");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseParamValueAsIntTest, OutOfRangeThrows)
|
||||
{
|
||||
try {
|
||||
DeviceAttachmentSpec::parseParamValueAsInt(
|
||||
"width",
|
||||
"999999999999999999999");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "out of range");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseParamValueAsPositiveIntTest, RejectsNonPositive)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "640"),
|
||||
640);
|
||||
|
||||
try {
|
||||
DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "0");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "must be positive");
|
||||
}
|
||||
|
||||
try {
|
||||
DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "-10");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "must be positive");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, ParsesRecognizedValues)
|
||||
{
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(""));
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("true"));
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(" YES "));
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("1"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("false"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("0"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("no"));
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool(
|
||||
"", /*emptyMeansTrue=*/false));
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, UnknownValueThrows)
|
||||
{
|
||||
EXPECT_THROW(
|
||||
DeviceAttachmentSpec::parseParamValueAsBool("maybe"),
|
||||
std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, MissingParamThrows)
|
||||
{
|
||||
const ParamList params = makeParams({{"screen", "1"}});
|
||||
|
||||
try {
|
||||
DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "No display specified in params");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, LattermostValueWins)
|
||||
{
|
||||
const ParamList params = makeParams({
|
||||
{"display", "0"},
|
||||
{"display", "3"},
|
||||
});
|
||||
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display"),
|
||||
3);
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseOptionalParamAsIntTest, ReturnsDefaultWhenAbsent)
|
||||
{
|
||||
const ParamList params = makeParams({{"screen", "1"}});
|
||||
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseOptionalParamAsInt(params, "display", 42),
|
||||
42);
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseOptionalParamAsIntWithSynonymsTest, SynonymLattermostWins)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {
|
||||
"n-dgrams-per-frame",
|
||||
"num-dgrams-per-frame",
|
||||
};
|
||||
|
||||
const ParamList params = makeParams({
|
||||
{"n-dgrams-per-frame", "10"},
|
||||
{"num-dgrams-per-frame", "99"},
|
||||
});
|
||||
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseOptionalParamAsIntWithSynonyms(
|
||||
params, synonyms, 84),
|
||||
99);
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseRequiredParamAsIntWithSynonymsTest, ParsesMatchedSynonym)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {
|
||||
"cmd-timeout-ms",
|
||||
"command-timeout-ms",
|
||||
};
|
||||
|
||||
const ParamList params = makeParams({
|
||||
{"command-timeout-ms", "15"},
|
||||
});
|
||||
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseRequiredParamAsIntWithSynonyms(
|
||||
params,
|
||||
synonyms,
|
||||
"missing timeout"),
|
||||
15);
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseRequiredParamValueWithSynonymsTest, RequiresNonEmptyValue)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {"colour-space"};
|
||||
const ParamList missing = makeParams({{"width", "640"}});
|
||||
|
||||
try {
|
||||
DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
|
||||
missing, synonyms, "colour-space required");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "colour-space required");
|
||||
}
|
||||
|
||||
const ParamList emptyValue = makeParams({{"colour-space", ""}});
|
||||
|
||||
try {
|
||||
DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
|
||||
emptyValue, synonyms, "colour-space required");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "colour-space required");
|
||||
}
|
||||
|
||||
const ParamList present = makeParams({{"colour-space", "yuv"}});
|
||||
EXPECT_EQ(
|
||||
DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms(
|
||||
present, synonyms, "colour-space required"),
|
||||
"yuv");
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseOptionalParamValueWithSynonymsTest, OptionalStringValue)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {"colour-space"};
|
||||
const ParamList absent = makeParams({{"width", "640"}});
|
||||
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
|
||||
absent, synonyms, "empty colour-space").has_value());
|
||||
|
||||
const ParamList emptyValue = makeParams({{"colour-space", ""}});
|
||||
try {
|
||||
DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
|
||||
emptyValue, synonyms, "empty colour-space");
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "empty colour-space");
|
||||
}
|
||||
|
||||
const ParamList present = makeParams({{"colour-space", "yuv"}});
|
||||
const std::optional<std::string> parsed =
|
||||
DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
|
||||
present, synonyms, "empty colour-space");
|
||||
|
||||
ASSERT_TRUE(parsed.has_value());
|
||||
EXPECT_EQ(*parsed, "yuv");
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecParseOptionalParamAsBoolWithSynonymsTest, ParsesPresenceAndValues)
|
||||
{
|
||||
const std::vector<std::string> synonyms = {
|
||||
"full-planar-is-optional",
|
||||
"opt-planar",
|
||||
};
|
||||
|
||||
const ParamList absent = makeParams({{"width", "640"}});
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
|
||||
absent, synonyms, false));
|
||||
|
||||
const ParamList presentEmpty = makeParams({{"opt-planar", ""}});
|
||||
EXPECT_TRUE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
|
||||
presentEmpty, synonyms, false));
|
||||
|
||||
const ParamList presentFalse = makeParams({{"opt-planar", "false"}});
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
|
||||
presentFalse, synonyms, true));
|
||||
|
||||
const ParamList lattermostWins = makeParams({
|
||||
{"opt-planar", "true"},
|
||||
{"full-planar-is-optional", "false"},
|
||||
});
|
||||
EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
|
||||
lattermostWins, synonyms, true));
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecRejectUnknownParamsTest, RejectsUnknownNames)
|
||||
{
|
||||
const std::vector<std::string> knownA = {"width", "dim-w"};
|
||||
const std::vector<std::string> knownB = {"height", "dim-h"};
|
||||
const ParamList params = makeParams({
|
||||
{"dim-w", "640"},
|
||||
{"unknown-param", "1"},
|
||||
});
|
||||
|
||||
try {
|
||||
DeviceAttachmentSpec::rejectUnknownParams(
|
||||
params,
|
||||
"unknown param '",
|
||||
knownA,
|
||||
knownB);
|
||||
FAIL() << "Expected std::runtime_error";
|
||||
}
|
||||
catch (const std::runtime_error& exception)
|
||||
{
|
||||
sscl::tests::expectExceptionMessageContains(
|
||||
exception, "unknown-param");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DeviceAttachmentSpecRejectUnknownParamsTest, AcceptsAllKnownNames)
|
||||
{
|
||||
const std::vector<std::string> knownA = {"width", "dim-w"};
|
||||
const std::vector<std::string> knownB = {"height", "dim-h"};
|
||||
const ParamList params = makeParams({
|
||||
{"dim-w", "640"},
|
||||
{"dim-h", "480"},
|
||||
});
|
||||
|
||||
EXPECT_NO_THROW(DeviceAttachmentSpec::rejectUnknownParams(
|
||||
params,
|
||||
"unknown param '",
|
||||
knownA,
|
||||
knownB));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace device
|
||||
} // namespace smo
|
||||
@@ -1,6 +1,10 @@
|
||||
#ifndef SENSORDEVICESPEC_H
|
||||
#define SENSORDEVICESPEC_H
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
@@ -8,6 +12,7 @@
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace smo {
|
||||
namespace device {
|
||||
@@ -138,33 +143,17 @@ public:
|
||||
* @param paramName The name of the parameter to parse
|
||||
* @return The parsed integer value
|
||||
* @throws std::runtime_error if parameter is not found or cannot be parsed
|
||||
* @note The lattermost supplied matching param wins if multiple are present
|
||||
*/
|
||||
static int parseRequiredParamAsInt(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const std::string& paramName
|
||||
)
|
||||
{
|
||||
auto it = std::find_if(
|
||||
params.begin(),
|
||||
params.end(),
|
||||
[¶mName](const auto& param) {
|
||||
return param.first == paramName;
|
||||
}
|
||||
);
|
||||
|
||||
if (it == params.end())
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"No " + paramName + " specified in params");
|
||||
}
|
||||
|
||||
try {
|
||||
return std::stoi(it->second);
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(
|
||||
"Failed to parse '" + paramName + "' param value '"
|
||||
+ it->second + "' as integer: " + e.what());
|
||||
}
|
||||
return parseRequiredParamAsIntWithSynonyms(
|
||||
params,
|
||||
std::array<std::string_view, 1>{{paramName}},
|
||||
"No " + paramName + " specified in params");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,9 +170,197 @@ public:
|
||||
int defaultValue
|
||||
)
|
||||
{
|
||||
const std::string paramNames[] = {paramName};
|
||||
return parseOptionalParamAsIntWithSynonyms(
|
||||
params, paramNames, defaultValue);
|
||||
params, std::array<std::string_view, 1>{{paramName}}, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test whether a parameter name appears in a synonym collection
|
||||
*/
|
||||
template <typename SynonymCollectionT>
|
||||
static bool namesContain(
|
||||
const SynonymCollectionT& synonymNames,
|
||||
std::string_view paramName)
|
||||
{
|
||||
return std::find(
|
||||
std::begin(synonymNames),
|
||||
std::end(synonymNames),
|
||||
paramName) != std::end(synonymNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test whether a parameter list contains an exact parameter name
|
||||
*/
|
||||
static bool paramsContain(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
std::string_view paramName)
|
||||
{
|
||||
for (const auto& param : params)
|
||||
{
|
||||
if (param.first == paramName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test whether a parameter name matches any synonym collection
|
||||
*/
|
||||
template <typename... SynonymCollectionTs>
|
||||
static bool paramNameMatchesAnySynonymGroup(
|
||||
std::string_view paramName,
|
||||
const SynonymCollectionTs&... synonymGroups)
|
||||
{
|
||||
return (namesContain(synonymGroups, paramName) || ...);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trim surrounding whitespace and lowercase a DAP param token
|
||||
*/
|
||||
static std::string normalizeParamToken(std::string token)
|
||||
{
|
||||
while (!token.empty()
|
||||
&& std::isspace(static_cast<unsigned char>(token.back())))
|
||||
{
|
||||
token.pop_back();
|
||||
}
|
||||
|
||||
while (!token.empty()
|
||||
&& std::isspace(static_cast<unsigned char>(token.front())))
|
||||
{
|
||||
token.erase(token.begin());
|
||||
}
|
||||
|
||||
for (char& character : token)
|
||||
{
|
||||
character = static_cast<char>(
|
||||
std::tolower(static_cast<unsigned char>(character)));
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find the lattermost param matching any synonym (name + value pair)
|
||||
*/
|
||||
template <typename SynonymCollectionT>
|
||||
static std::optional<std::pair<std::string, std::string>>
|
||||
findOptionalParamWithSynonyms(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const SynonymCollectionT& synonymNames)
|
||||
{
|
||||
for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt)
|
||||
{
|
||||
if (namesContain(synonymNames, paramIt->first)) {
|
||||
return *paramIt;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find the lattermost param with an exact name (name + value pair)
|
||||
*/
|
||||
static std::optional<std::pair<std::string, std::string>> findOptionalParam(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
std::string_view paramName)
|
||||
{
|
||||
return findOptionalParamWithSynonyms(
|
||||
params, std::array<std::string_view, 1>{{paramName}});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a DAP integer param value
|
||||
* @throws std::runtime_error if parsing fails
|
||||
*/
|
||||
static int parseParamValueAsInt(
|
||||
const std::string& paramName,
|
||||
const std::string& paramValue)
|
||||
{
|
||||
try {
|
||||
return std::stoi(paramValue);
|
||||
}
|
||||
catch (const std::invalid_argument&)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Failed to parse '" + paramName + "' param value '"
|
||||
+ paramValue + "' as integer");
|
||||
}
|
||||
catch (const std::out_of_range&)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"'" + paramName + "' param value '" + paramValue
|
||||
+ "' is out of range");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a DAP integer param value; result must be strictly positive
|
||||
* @throws std::runtime_error if parsing fails or value is not positive
|
||||
*/
|
||||
static int parseParamValueAsPositiveInt(
|
||||
const std::string& paramName,
|
||||
const std::string& paramValue)
|
||||
{
|
||||
const int parsedValue = parseParamValueAsInt(paramName, paramValue);
|
||||
if (parsedValue <= 0)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"'" + paramName + "' must be positive");
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a DAP boolean param value
|
||||
* @param value Raw param value; empty string means @p emptyMeansTrue
|
||||
* @throws std::runtime_error if the value is not recognized
|
||||
*/
|
||||
static bool parseParamValueAsBool(
|
||||
const std::string& value,
|
||||
bool emptyMeansTrue = true)
|
||||
{
|
||||
if (value.empty()) {
|
||||
return emptyMeansTrue;
|
||||
}
|
||||
|
||||
const std::string lowered = normalizeParamToken(value);
|
||||
if (lowered == "true" || lowered == "1" || lowered == "yes") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lowered == "false" || lowered == "0" || lowered == "no") {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw std::runtime_error(
|
||||
"Boolean param value '" + value
|
||||
+ "' is not recognized (use true/false, 1/0, yes/no, or omit value)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a required integer param using synonyms
|
||||
* @note The lattermost supplied matching param wins if multiple are present
|
||||
*/
|
||||
template <typename SynonymCollectionT>
|
||||
static int parseRequiredParamAsIntWithSynonyms(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const SynonymCollectionT& synonymNames,
|
||||
const std::string& missingParamMessage)
|
||||
{
|
||||
const std::optional<std::pair<std::string, std::string>> matchedParam =
|
||||
findOptionalParamWithSynonyms(params, synonymNames);
|
||||
|
||||
if (!matchedParam)
|
||||
{
|
||||
throw std::runtime_error(missingParamMessage);
|
||||
}
|
||||
|
||||
return parseParamValueAsInt(matchedParam->first, matchedParam->second);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,26 +378,102 @@ public:
|
||||
int defaultValue
|
||||
)
|
||||
{
|
||||
// Loop through params in reverse order; lattermost supplied param wins.
|
||||
for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt)
|
||||
{
|
||||
const auto& [paramName, paramValue] = *paramIt;
|
||||
auto synonymIt = std::find(
|
||||
std::begin(synonymNames), std::end(synonymNames), paramName);
|
||||
const std::optional<std::pair<std::string, std::string>> matchedParam =
|
||||
findOptionalParamWithSynonyms(params, synonymNames);
|
||||
|
||||
if (synonymIt == std::end(synonymNames))
|
||||
{ continue; }
|
||||
|
||||
try {
|
||||
return std::stoi(paramValue);
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(
|
||||
"Failed to parse '" + paramName + "' param value '"
|
||||
+ paramValue + "' as integer: " + e.what());
|
||||
}
|
||||
if (!matchedParam) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
return parseParamValueAsInt(matchedParam->first, matchedParam->second);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse a required non-empty param value using synonyms
|
||||
* @note The lattermost supplied matching param wins if multiple are present
|
||||
* @throws std::runtime_error if no matching param is found or value is empty
|
||||
*/
|
||||
template <typename SynonymCollectionT>
|
||||
static std::string parseRequiredParamValueWithSynonyms(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const SynonymCollectionT& synonymNames,
|
||||
const std::string& missingOrEmptyValueMessage)
|
||||
{
|
||||
const std::optional<std::pair<std::string, std::string>> matchedParam =
|
||||
findOptionalParamWithSynonyms(params, synonymNames);
|
||||
|
||||
if (!matchedParam || matchedParam->second.empty())
|
||||
{
|
||||
throw std::runtime_error(missingOrEmptyValueMessage);
|
||||
}
|
||||
|
||||
return matchedParam->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse an optional non-empty param value using synonyms
|
||||
* @return Parsed value, or std::nullopt when the param is absent
|
||||
* @throws std::runtime_error if the param is present but its value is empty
|
||||
*/
|
||||
template <typename SynonymCollectionT>
|
||||
static std::optional<std::string> parseOptionalParamValueWithSynonyms(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const SynonymCollectionT& synonymNames,
|
||||
const std::string& emptyValueMessage)
|
||||
{
|
||||
const std::optional<std::pair<std::string, std::string>> matchedParam =
|
||||
findOptionalParamWithSynonyms(params, synonymNames);
|
||||
|
||||
if (!matchedParam) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (matchedParam->second.empty()) {
|
||||
throw std::runtime_error(emptyValueMessage);
|
||||
}
|
||||
|
||||
return matchedParam->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse an optional boolean param using synonyms
|
||||
* @note The lattermost supplied matching param wins if multiple are present
|
||||
*/
|
||||
template <typename SynonymCollectionT>
|
||||
static bool parseOptionalParamAsBoolWithSynonyms(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const SynonymCollectionT& synonymNames,
|
||||
bool defaultValue = false,
|
||||
bool emptyMeansTrue = true)
|
||||
{
|
||||
const std::optional<std::pair<std::string, std::string>> matchedParam =
|
||||
findOptionalParamWithSynonyms(params, synonymNames);
|
||||
|
||||
if (!matchedParam) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return parseParamValueAsBool(matchedParam->second, emptyMeansTrue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reject params whose names are not covered by any synonym group
|
||||
*/
|
||||
template <typename... SynonymCollectionTs>
|
||||
static void rejectUnknownParams(
|
||||
const std::vector<std::pair<std::string,std::string>>& params,
|
||||
const std::string& unknownParamMessagePrefix,
|
||||
const SynonymCollectionTs&... synonymGroups)
|
||||
{
|
||||
for (const auto& param : params)
|
||||
{
|
||||
if (!paramNameMatchesAnySynonymGroup(
|
||||
param.first, synonymGroups...))
|
||||
{
|
||||
throw std::runtime_error(
|
||||
unknownParamMessagePrefix + param.first + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user