Skip to content

Commit 4d3c046

Browse files
f3schsawenzelclaude
authored
Extend configurable params for std-container types (#15525)
* Common: allow for dynamic types * Common: offer suggested on correct spelling * ConfigurableParam: reject malformed container fields, drop redundant parseSet * Some follow-up improvements in review process: 1. split() no longer silently drops empty fields. A stray delimiter such as "[1,,3]" previously parsed to a 2-element vector with no error, and "key:" collapsed an empty map value away. Both now produce an empty token that is rejected by parseScalar / the map-syntax check, so malformed configuration surfaces as an error instead of corrupting element counts. The empty container case ("[]" / "{}") is still short-circuited by the callers before split() runs, so well-formed input is unaffected. 2. Removed the redundant parseSet() branch (and the now-unused HasPushBack concept). Every non-map Container already satisfies SequenceContainer, so the parseSet dispatch was reached for all sequence and set types and re-parsed the string into a throwaway std::vector before copying element-by-element. parseSequence inserts at end(), which std::set / std::unordered_set accept just as well, so all non-map containers now share a single, single-pass path. Adds regression tests: set/unordered_set/list/deque parsing through the unified parseSequence path (incl. set de-duplication), and rejection of empty tokens in both sequence and map parsing. Note: container parse failures remain non-fatal (logged), matching the existing scalar setValue behaviour; this commit does not change that contract. --------- Signed-off-by: Felix Schlepper <felix.schlepper@cern.ch> Co-authored-by: Sandro Wenzel <sandro.wenzel@cern.ch> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 530c830 commit 4d3c046

6 files changed

Lines changed: 999 additions & 33 deletions

File tree

Common/Utils/include/CommonUtils/ConfigurableParam.h

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,24 @@
99
// granted to it by virtue of its status as an Intergovernmental Organization
1010
// or submit itself to any jurisdiction.
1111

12-
//first version 8/2018, Sandro Wenzel
12+
// first version 8/2018, Sandro Wenzel
1313

1414
#ifndef COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAM_H_
1515
#define COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAM_H_
1616

17-
#include <vector>
17+
#include <algorithm>
1818
#include <cassert>
19+
#include <cctype>
20+
#include <concepts>
21+
#include <cstdint>
22+
#include <limits>
1923
#include <map>
24+
#include <sstream>
25+
#include <stdexcept>
26+
#include <string>
27+
#include <type_traits>
2028
#include <unordered_map>
29+
#include <vector>
2130
#include <boost/property_tree/ptree_fwd.hpp>
2231
#include <typeinfo>
2332
#include <iostream>
@@ -136,6 +145,214 @@ class EnumRegistry
136145
std::unordered_map<std::string, EnumLegalValues> entries;
137146
};
138147

148+
template <typename T>
149+
concept Container = !std::is_same_v<std::remove_cvref_t<T>, std::string> && requires(T t) {
150+
typename T::value_type;
151+
typename T::iterator;
152+
{ t.begin() } -> std::same_as<typename T::iterator>;
153+
{ t.end() } -> std::same_as<typename T::iterator>;
154+
};
155+
156+
template <typename T>
157+
concept MapLike = Container<T> && requires {
158+
typename T::key_type;
159+
typename T::mapped_type;
160+
};
161+
162+
template <typename T>
163+
concept SequenceContainer = Container<T> && !MapLike<T>;
164+
165+
template <typename>
166+
inline constexpr bool AlwaysFalse = false;
167+
168+
class ContainerParser
169+
{
170+
public:
171+
template <typename T>
172+
static T parse(const std::string& str)
173+
{
174+
if constexpr (MapLike<T>) {
175+
return parseMap<T>(str);
176+
} else if constexpr (Container<T>) {
177+
// Covers vector/list/deque as well as set/unordered_set: parseSequence
178+
// inserts at end(), which both sequence and associative-set containers
179+
// accept. (Any non-map Container is a SequenceContainer, so the previous
180+
// separate parseSet branch was unreachable and re-parsed into a temporary
181+
// vector first.)
182+
return parseSequence<T>(str);
183+
} else {
184+
return parseScalar<T>(str);
185+
}
186+
}
187+
188+
static std::string trim(const std::string& str)
189+
{
190+
auto start = str.find_first_not_of(" \t\n\r\f\v");
191+
if (start == std::string::npos) {
192+
return "";
193+
}
194+
auto end = str.find_last_not_of(" \t\n\r\f\v");
195+
return str.substr(start, end - start + 1);
196+
}
197+
198+
private:
199+
// Parse sequence and set containers (vector, list, deque, set, unordered_set)
200+
template <SequenceContainer SequenceT>
201+
static SequenceT parseSequence(const std::string& str)
202+
{
203+
SequenceT result;
204+
using ValueType = typename SequenceT::value_type;
205+
std::string cleaned = str;
206+
if (!cleaned.empty() && cleaned.front() == '[' && cleaned.back() == ']') { // removed brackets [1,2,3] -> 1,2,3
207+
cleaned = cleaned.substr(1, cleaned.length() - 2);
208+
}
209+
if (cleaned.empty() || cleaned == "{}") { // nothing to do
210+
return result;
211+
}
212+
if constexpr (Container<ValueType>) {
213+
static_assert(AlwaysFalse<ValueType>, "Nested containers are not supported as configurable parameters");
214+
}
215+
auto tokens = split(cleaned, ',');
216+
for (const auto& token : tokens) {
217+
std::string trimmed = trim(token);
218+
result.insert(result.end(), parseScalar<ValueType>(trimmed));
219+
}
220+
return result;
221+
}
222+
223+
// Parse map, unordered_map, multimap
224+
template <MapLike MapT>
225+
static MapT parseMap(const std::string& str)
226+
{
227+
MapT result;
228+
using KeyType = typename MapT::key_type;
229+
using ValueType = typename MapT::mapped_type;
230+
std::string cleaned = str;
231+
if (!cleaned.empty() && cleaned.front() == '{' && cleaned.back() == '}') { // stip braces {a:1,b:2} -> a:1,b:2
232+
cleaned = cleaned.substr(1, cleaned.length() - 2);
233+
}
234+
if (cleaned.empty()) { // nothing to do
235+
return result;
236+
}
237+
if constexpr (Container<KeyType> || Container<ValueType>) {
238+
static_assert(AlwaysFalse<MapT>, "Nested containers are not supported as configurable parameters");
239+
}
240+
auto pairs = split(cleaned, ',');
241+
for (const auto& pair_str : pairs) {
242+
auto kv = split(pair_str, ':');
243+
if (kv.size() != 2) {
244+
throw std::runtime_error("Invalid map syntax: " + pair_str + ". Expected 'key:value' format, got ");
245+
}
246+
KeyType key = parseScalar<KeyType>(trim(kv[0]));
247+
result[key] = parseScalar<ValueType>(trim(kv[1]));
248+
}
249+
return result;
250+
}
251+
252+
// Parse scalar types
253+
template <typename T>
254+
static T parseScalar(const std::string& str)
255+
{
256+
if constexpr (std::is_same_v<T, std::string>) {
257+
return str;
258+
} else if constexpr (std::is_same_v<T, bool>) {
259+
std::string lower = str;
260+
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
261+
if (lower == "true" || lower == "1") {
262+
return true;
263+
}
264+
if (lower == "false" || lower == "0") {
265+
return false;
266+
}
267+
throw std::runtime_error("Invalid boolean value: " + str);
268+
} else if constexpr (std::is_same_v<T, char> || std::is_same_v<T, signed char>) {
269+
size_t pos = 0;
270+
long long value = std::stoll(str, &pos);
271+
if (pos != str.size()) {
272+
throw std::runtime_error("Failed to parse '" + str + "' as char type");
273+
}
274+
if (value < std::numeric_limits<T>::min() || value > std::numeric_limits<T>::max()) {
275+
throw std::runtime_error("Value out of range for char type: " + str);
276+
}
277+
return static_cast<T>(value);
278+
} else if constexpr (std::is_same_v<T, unsigned char>) {
279+
size_t pos = 0;
280+
unsigned long long value = std::stoull(str, &pos);
281+
if (pos != str.size()) {
282+
throw std::runtime_error("Failed to parse '" + str + "' as unsigned char type");
283+
}
284+
if (value > std::numeric_limits<T>::max()) {
285+
throw std::runtime_error("Value out of range for unsigned char type: " + str);
286+
}
287+
return static_cast<T>(value);
288+
} else if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>) {
289+
if (!str.empty() && str.front() == '-') {
290+
throw std::runtime_error("Value out of range for unsigned integer type: " + str);
291+
}
292+
size_t pos = 0;
293+
unsigned long long value = std::stoull(str, &pos);
294+
if (pos != str.size() || value > std::numeric_limits<T>::max()) {
295+
throw std::runtime_error("Failed to parse '" + str + "' as unsigned integer type");
296+
}
297+
return static_cast<T>(value);
298+
} else if constexpr (std::is_integral_v<T>) {
299+
size_t pos = 0;
300+
long long value = std::stoll(str, &pos);
301+
if (pos != str.size() || value < std::numeric_limits<T>::min() || value > std::numeric_limits<T>::max()) {
302+
throw std::runtime_error("Failed to parse '" + str + "' as signed integer type");
303+
}
304+
return static_cast<T>(value);
305+
} else if constexpr (std::is_floating_point_v<T>) {
306+
size_t pos = 0;
307+
long double value = std::stold(str, &pos);
308+
if (pos != str.size()) {
309+
throw std::runtime_error("Failed to parse '" + str + "' as floating point type");
310+
}
311+
return static_cast<T>(value);
312+
} else {
313+
std::istringstream iss(str);
314+
T value;
315+
iss >> value;
316+
iss >> std::ws;
317+
if (iss.fail() || !iss.eof()) {
318+
throw std::runtime_error("Failed to parse '" + str + "' as " + typeid(T).name());
319+
}
320+
return value;
321+
}
322+
}
323+
324+
// Split respecting nested brackets and braces
325+
static std::vector<std::string> split(const std::string& str, char delimiter)
326+
{
327+
std::vector<std::string> tokens;
328+
std::string current;
329+
int bracket_depth = 0;
330+
int brace_depth = 0;
331+
for (char c : str) {
332+
if (c == '[') {
333+
bracket_depth++;
334+
} else if (c == ']') {
335+
bracket_depth--;
336+
} else if (c == '{') {
337+
brace_depth++;
338+
} else if (c == '}') {
339+
brace_depth--;
340+
} else if (c == delimiter && bracket_depth == 0 && brace_depth == 0) {
341+
// Keep empty fields: a stray delimiter (e.g. "[1,,3]" or "key:") must
342+
// surface as a parse error downstream rather than silently dropping an
343+
// element. The empty-container case ("[]"/"{}") is handled by the
344+
// callers before split() is ever reached.
345+
tokens.push_back(current);
346+
current.clear();
347+
continue;
348+
}
349+
current += c;
350+
}
351+
tokens.push_back(current);
352+
return tokens;
353+
}
354+
};
355+
139356
class ConfigurableParam
140357
{
141358
public:
@@ -247,14 +464,21 @@ class ConfigurableParam
247464
static void setValue(std::string const& key, std::string const& valuestring);
248465
static void setEnumValue(const std::string&, const std::string&);
249466
static void setArrayValue(const std::string&, const std::string&);
467+
static void setContainerValue(const std::string&, const std::string&);
468+
static bool isRegisteredContainerType(const std::string& typeName);
469+
static void registerContainerType(const std::string& key, const std::string& typeName);
470+
static std::string getRegisteredContainerType(const std::string& key);
471+
static bool assignRegisteredContainer(const std::string& typeName, void* target, const void* source);
472+
static bool areRegisteredContainersEqual(const std::string& typeName, const void* lhs, const void* rhs);
473+
static std::string registeredContainerAsString(const std::string& typeName, const void* source);
250474

251475
// update the storagemap from a vector of key/value pairs, calling setValue for each pair
252476
static void setValues(std::vector<std::pair<std::string, std::string>> const& keyValues);
253477

254478
// initializes the parameter database
255479
static void initialize();
256480

257-
// create CCDB snapsnot
481+
// create CCDB snapshot
258482
static void toCCDB(std::string filename);
259483
// load from (CCDB) snapshot
260484
static void fromCCDB(std::string filename);

Common/Utils/include/CommonUtils/ConfigurableParamHelper.h

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,59 @@
99
// granted to it by virtue of its status as an Intergovernmental Organization
1010
// or submit itself to any jurisdiction.
1111

12-
//first version 8/2018, Sandro Wenzel
12+
// first version 8/2018, Sandro Wenzel
1313

1414
#ifndef COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAMHELPER_H_
1515
#define COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAMHELPER_H_
1616

1717
#include "CommonUtils/ConfigurableParam.h"
18-
#include "TClass.h"
18+
19+
#include <algorithm>
1920
#include <memory>
21+
#include <numeric>
22+
#include <string_view>
23+
#include <TClass.h>
24+
#include <TFile.h>
25+
#include <TDataMember.h>
2026
#include <type_traits>
2127
#include <typeinfo>
22-
#include "TFile.h"
28+
#include <utility>
2329

24-
namespace o2
30+
namespace o2::conf
2531
{
26-
namespace conf
32+
33+
// ----------------------------------------------------------------
34+
35+
inline std::size_t damerauLevenshteinDistance(std::string_view a, std::string_view b)
2736
{
37+
const std::size_t n = a.size();
38+
const std::size_t m = b.size();
39+
if (n == 0) {
40+
return m;
41+
}
42+
if (m == 0) {
43+
return n;
44+
}
45+
std::vector<std::size_t> prev(m + 1), curr(m + 1), prev2(m + 1);
46+
std::iota(prev.begin(), prev.end(), 0);
47+
for (std::size_t i = 1; i <= n; ++i) {
48+
curr[0] = i;
49+
for (std::size_t j = 1; j <= m; ++j) {
50+
std::size_t cost = (a[i - 1] == b[j - 1]) ? 0 : 1;
51+
curr[j] = std::min({prev[j] + 1,
52+
curr[j - 1] + 1,
53+
prev[j - 1] + cost});
54+
if (i > 1 && j > 1 && a[i - 1] == b[j - 2] &&
55+
a[i - 2] == b[j - 1]) {
56+
curr[j] = std::min(curr[j], prev2[j - 2] + 1);
57+
}
58+
}
59+
prev2 = std::move(prev);
60+
prev = std::move(curr);
61+
curr.assign(m + 1, 0);
62+
}
63+
return prev[m];
64+
}
2865

2966
// ----------------------------------------------------------------
3067

@@ -342,7 +379,19 @@ class ConfigurableParamPromoter : public Base, virtual public ConfigurableParam
342379
}
343380
};
344381

345-
} // namespace conf
346-
} // namespace o2
382+
inline bool isContainer(const std::string& typeName)
383+
{
384+
return ConfigurableParam::isRegisteredContainerType(typeName);
385+
}
386+
387+
inline bool isContainer(TDataMember const& dm)
388+
{
389+
if (auto* cl = dm.GetClass(); cl && isContainer(cl->GetName())) {
390+
return true;
391+
}
392+
return isContainer(dm.GetTrueTypeName()) || isContainer(dm.GetFullTypeName());
393+
}
394+
395+
} // namespace o2::conf
347396

348397
#endif /* COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAMHELPER_H_ */

Common/Utils/include/CommonUtils/ConfigurableParamTest.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
#include "CommonUtils/ConfigurableParam.h"
1616
#include "CommonUtils/ConfigurableParamHelper.h"
1717

18+
#include <array>
19+
#include <cstdint>
20+
#include <map>
21+
#include <set>
22+
#include <vector>
23+
1824
namespace o2::conf::test
1925
{
2026
struct TestParam : public o2::conf::ConfigurableParamHelper<TestParam> {
@@ -37,6 +43,11 @@ struct TestParam : public o2::conf::ConfigurableParamHelper<TestParam> {
3743
int iValueProvenanceTest{0};
3844
TestEnum eValue = TestEnum::C;
3945
int caValue[3] = {0, 1, 2};
46+
std::vector<int> vec;
47+
std::vector<uint8_t> u8vec;
48+
std::map<int, uint32_t> map;
49+
std::map<std::string, uint32_t> smap;
50+
std::set<uint16_t> set;
4051

4152
O2ParamDef(TestParam, "TestParam");
4253
};

0 commit comments

Comments
 (0)