Skip to content

Commit cad1163

Browse files
tweskapgeier
andcommitted
Add config parser utilities
Co-authored-by: Philipp Geier <[email protected]>
1 parent 77ccb70 commit cad1163

File tree

5 files changed

+505
-0
lines changed

5 files changed

+505
-0
lines changed

src/multio/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ list( APPEND multio_library_srcs
2222
)
2323

2424
list( APPEND multio_util_srcs
25+
util/ConfigParser.cc
26+
util/ConfigParser.h
2527
util/DateTime.cc
2628
util/DateTime.h
2729
util/Hash.h

src/multio/util/ConfigParser.cc

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#include "ConfigParser.h"
2+
3+
#include <cstdint>
4+
#include <string>
5+
6+
#include "eckit/exception/Exceptions.h"
7+
8+
namespace multio::util::config {
9+
10+
bool parseField(std::string& value, const std::string& key, const eckit::LocalConfiguration& config) {
11+
if (!config.has(key)) {
12+
return false;
13+
}
14+
if (!config.isString(key)) {
15+
throw eckit::UserError{"String key is of wrong type", Here()}; // TODO(knobel): Improve error message!
16+
}
17+
value = config.getString(key);
18+
return true;
19+
}
20+
21+
bool parseField(std::int64_t& value, const std::string& key, const eckit::LocalConfiguration& config) {
22+
if (!config.has(key)) {
23+
return false;
24+
}
25+
if (config.isIntegral(key)) {
26+
value = config.getInt64(key);
27+
return true;
28+
}
29+
if (config.isString(key)) {
30+
value = std::stol(config.getString(key)); // TODO(knobel): Use strict mapping
31+
return true;
32+
}
33+
throw eckit::UserError{"Integer key is of wrong type", Here()}; // TODO(knobel): Improve error message!
34+
}
35+
36+
bool parseField(double& value, const std::string& key, const eckit::LocalConfiguration& config) {
37+
if (!config.has(key)) {
38+
return false;
39+
}
40+
if (config.isFloatingPoint(key)) {
41+
value = config.getDouble(key);
42+
return true;
43+
}
44+
if (config.isString(key)) {
45+
value = std::stod(config.getString(key)); // TODO(knobel): Use strict mapping
46+
return true;
47+
}
48+
throw eckit::UserError{"Double key is of wrong type", Here()}; // TODO(knobel): Improve error message!
49+
}
50+
51+
bool parseField(bool& value, const std::string& key, const eckit::LocalConfiguration& config) {
52+
if (!config.has(key)) {
53+
return false;
54+
}
55+
if (config.isBoolean(key)) {
56+
value = config.getBool(key);
57+
return true;
58+
}
59+
if (config.isIntegral(key)) {
60+
const int64_t configValue = config.getInt64(key);
61+
if (configValue == 0) {
62+
value = false;
63+
return true;
64+
}
65+
if (configValue == 1) {
66+
value = true;
67+
return true;
68+
}
69+
// TODO(knobel): Improve error message!
70+
throw eckit::UserError{"Integer value of boolean key must be 1 or 0", Here()};
71+
}
72+
if (config.isString(key)) {
73+
const std::string& configValue = config.getString(key);
74+
if (configValue == "0") {
75+
value = false;
76+
return true;
77+
}
78+
if (configValue == "1") {
79+
value = true;
80+
return true;
81+
}
82+
// TODO(knobel): Improve error message!
83+
throw eckit::UserError{"String value of boolean key must be \"1\" or \"0\"", Here()};
84+
}
85+
throw eckit::UserError{"Boolean key is of wrong type", Here()}; // TODO(knobel): Improve error message!
86+
}
87+
88+
} // namespace multio::util::config

src/multio/util/ConfigParser.h

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* (C) Copyright 2025- ECMWF.
3+
*
4+
* This software is licensed under the terms of the Apache Licence Version 2.0
5+
* which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
6+
* In applying this licence, ECMWF does not waive the privileges and immunities
7+
* granted to it by virtue of its status as an intergovernmental organisation nor
8+
* does it submit to any jurisdiction.
9+
*/
10+
11+
#pragma once
12+
13+
#include <optional>
14+
#include <string>
15+
#include <string_view>
16+
#include <type_traits>
17+
18+
#include "eckit/config/LocalConfiguration.h"
19+
#include "eckit/exception/Exceptions.h"
20+
21+
22+
namespace multio::util::config {
23+
24+
25+
template <typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, bool> = true>
26+
struct EnumTrait;
27+
28+
bool parseField(std::string& value, const std::string& key, const eckit::LocalConfiguration& config);
29+
bool parseField(std::int64_t& value, const std::string& key, const eckit::LocalConfiguration& config);
30+
bool parseField(double& value, const std::string& key, const eckit::LocalConfiguration& config);
31+
bool parseField(bool& value, const std::string& key, const eckit::LocalConfiguration& config);
32+
33+
template <typename T, std::enable_if_t<std::is_enum_v<T>, bool> = true>
34+
bool parseField(T& value, const std::string& key, const eckit::LocalConfiguration& config) {
35+
if (!config.has(key)) {
36+
return false;
37+
}
38+
if (!config.isString(key)) {
39+
throw eckit::UserError{"Enum values must be strings", Here()}; // TODO(knobel): Improve error message!
40+
}
41+
const auto configValue = config.getString(key);
42+
for (const auto& pair : EnumTrait<T>::values) {
43+
if (pair.second == configValue) {
44+
value = pair.first;
45+
return true;
46+
}
47+
}
48+
throw eckit::UserError{"Unknown option for enum value", Here()}; // TODO(knobel): Improve error message!
49+
}
50+
51+
template <typename T>
52+
bool parseField(std::optional<T>& value, const std::string& key, const eckit::LocalConfiguration& config) {
53+
T result;
54+
if (parseField(result, key, config)) {
55+
value = result;
56+
return true;
57+
}
58+
return false;
59+
}
60+
61+
template <typename T>
62+
void parseRequired(T& value, const std::string& key, const eckit::LocalConfiguration& config) {
63+
if (!parseField(value, key, config)) {
64+
throw eckit::UserError{"Field is missing", Here()}; // TODO(knobel): Improve error message!
65+
}
66+
}
67+
68+
template <typename T>
69+
void parseOptional(T& value, const std::string& key, const eckit::LocalConfiguration& config) {
70+
parseField(value, key, config);
71+
}
72+
73+
template <typename TConfig, typename TValue>
74+
struct ConfigField {
75+
const std::string_view key;
76+
TValue TConfig::* value;
77+
const bool required;
78+
79+
TValue& get(TConfig& obj) const { return obj.*value; }
80+
};
81+
82+
/**
83+
* Register a required field in the configuration
84+
* @param key String value of this field
85+
* @param value Location where parsed value will be stored
86+
*/
87+
template <typename TConfig, typename TValue>
88+
constexpr ConfigField<TConfig, TValue> requiredField(const std::string_view& key, TValue TConfig::* value) {
89+
return {key, value, true};
90+
}
91+
92+
/**
93+
* Register an optional (or defaulted) field in the configuration
94+
* @param key String value of this field
95+
* @param value Location where parsed value will be stored
96+
*/
97+
template <typename TConfig, typename TValue>
98+
constexpr ConfigField<TConfig, TValue> optionalField(const std::string_view& key, TValue TConfig::* value) {
99+
return {key, value, false};
100+
}
101+
102+
template <typename TConfig, typename TValue>
103+
void parseConfigField(const ConfigField<TConfig, TValue>& field, TConfig& config,
104+
const eckit::LocalConfiguration& localConfig) {
105+
if (field.required) {
106+
parseRequired(field.get(config), std::string{field.key}, localConfig);
107+
}
108+
else {
109+
parseOptional(field.get(config), std::string{field.key}, localConfig);
110+
}
111+
}
112+
113+
/**
114+
* Create a parsed configuration object from the eckit::LocalConfiguration
115+
* @param localConfig An unparsed eckit::LocalConfiguration
116+
* @returns A parsed configuration of the desired type
117+
* @throws If an unknown key is present in the localConfig, or if a value cannot be parsed
118+
*/
119+
template <typename TConfig>
120+
TConfig parseConfig(const eckit::LocalConfiguration& localConfig) {
121+
TConfig config;
122+
std::set<std::string> allowedKeys;
123+
124+
std::apply(
125+
[&](const auto&... field) {
126+
(parseConfigField(field, config, localConfig), ...);
127+
(allowedKeys.emplace(field.key), ...);
128+
},
129+
config.fields_);
130+
131+
for (const auto& key : localConfig.keys()) {
132+
if (allowedKeys.find(key) == allowedKeys.end()) {
133+
// TODO(knobel): Improve error message!
134+
throw eckit::UserError{"Found unknown key in configuration", Here()};
135+
}
136+
}
137+
138+
return config;
139+
}
140+
141+
142+
} // namespace multio::util::config

tests/multio/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ ecbuild_add_test( TARGET test_multio_datamod_models
8888
SOURCES test_multio_datamod_models.cc
8989
LIBS multio )
9090

91+
92+
# Test Config Parser
93+
94+
ecbuild_add_test( TARGET test_multio_config_parser
95+
SOURCES test_multio_config_parser.cc
96+
LIBS multio )
97+
98+
9199
# Test ring buffer
92100

93101
ecbuild_add_test( TARGET test_multio_ringbuffer

0 commit comments

Comments
 (0)