Branch data Line data Source code
1 : : /*
2 : : * Copyright 2025 Jinwoo Sung
3 : : *
4 : : * Licensed under the Apache License, Version 2.0 (the "License");
5 : : * you may not use this file except in compliance with the License.
6 : : * You may obtain a copy of the License at
7 : : *
8 : : * http://www.apache.org/licenses/LICENSE-2.0
9 : : *
10 : : * Unless required by applicable law or agreed to in writing, software
11 : : * distributed under the License is distributed on an "AS IS" BASIS,
12 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 : : * See the License for the specific language governing permissions and
14 : : * limitations under the License.
15 : : */
16 : :
17 : : #include "config_manager.hpp"
18 : :
19 : : #include <algorithm>
20 : : #include <fstream>
21 : : #include <iostream>
22 : : #include <mutex>
23 : : #include <sstream>
24 : : #include <stdexcept>
25 : : #include <unordered_map>
26 : :
27 : : #include "wirestead/diagnostics/logger.hpp"
28 : :
29 : : namespace wirestead {
30 : : namespace config {
31 : :
32 : : struct ConfigManager::Impl {
33 : : mutable std::mutex mutex_;
34 : : std::unordered_map<std::string, ConfigItem> config_items_;
35 : : std::unordered_map<std::string, ConfigChangeCallback> change_callbacks_;
36 : :
37 : 1278 : ValidationResult validate_value(const std::string& key, const std::any& value) const {
38 : 1278 : auto it = config_items_.find(key);
39 [ + + ]: 1278 : if (it != config_items_.end()) {
40 [ + + ]: 20 : if (it->second.validator) {
41 : 3 : return it->second.validator(value);
42 : : }
43 : :
44 : 17 : ConfigType expected_type = it->second.type;
45 : 17 : ConfigType actual_type = ConfigType::String;
46 : :
47 [ + + ]: 17 : if (value.type() == typeid(std::string)) {
48 : 13 : actual_type = ConfigType::String;
49 [ + + ]: 4 : } else if (value.type() == typeid(int)) {
50 : 2 : actual_type = ConfigType::Integer;
51 [ + + ]: 2 : } else if (value.type() == typeid(bool)) {
52 : 1 : actual_type = ConfigType::Boolean;
53 [ + - ]: 1 : } else if (value.type() == typeid(double)) {
54 : 1 : actual_type = ConfigType::Double;
55 : : }
56 : :
57 [ + + ]: 17 : if (expected_type != actual_type) {
58 : 7 : return ValidationResult::error("Type mismatch for key '" + key + "'");
59 : : }
60 : : }
61 : 1268 : return ValidationResult::success();
62 : : }
63 : :
64 : : // Must be called while holding mutex_. Returns an empty callback if none is
65 : : // registered for `key`.
66 : 10 : ConfigChangeCallback change_callback_for(const std::string& key) const {
67 : 10 : auto it = change_callbacks_.find(key);
68 [ + + + - ]: 10 : return it != change_callbacks_.end() ? it->second : ConfigChangeCallback{};
69 : : }
70 : :
71 : : // Must be called *without* holding mutex_: it invokes user code, and a
72 : : // callback that reenters get()/set()/has() on this ConfigManager would
73 : : // otherwise deadlock on the non-recursive mutex_.
74 : 10 : static void invoke_change_callback(const ConfigChangeCallback& callback, const std::string& key,
75 : : const std::any& old_value, const std::any& new_value) {
76 [ + + ]: 10 : if (!callback) return;
77 : : try {
78 : 3 : callback(key, old_value, new_value);
79 : 1 : } catch (const std::exception& e) {
80 : 3 : WIRESTEAD_LOG_ERROR("config_manager", "callback",
81 : : "Error in change callback for key '" + key + "': " + std::string(e.what()));
82 : 1 : }
83 : : }
84 : :
85 : 13 : std::string serialize_value(const std::any& value, ConfigType type) const {
86 : : try {
87 [ + + - - : 13 : switch (type) {
+ ]
88 : 11 : case ConfigType::String:
89 : 11 : return std::any_cast<std::string>(value);
90 : 1 : case ConfigType::Integer:
91 : 1 : return std::to_string(std::any_cast<int>(value));
92 : 0 : case ConfigType::Boolean:
93 [ # # # # : 0 : return std::any_cast<bool>(value) ? "true" : "false";
# # ]
94 : 0 : case ConfigType::Double:
95 : 0 : return std::to_string(std::any_cast<double>(value));
96 : 1 : default:
97 : 2 : return "unknown";
98 : : }
99 : 8 : } catch (const std::bad_any_cast&) {
100 : 8 : return "unknown";
101 : 8 : }
102 : : }
103 : :
104 : 16 : std::any deserialize_value(const std::string& value_str, ConfigType type) const {
105 : : try {
106 [ + + + + : 16 : switch (type) {
- ]
107 : 10 : case ConfigType::String:
108 : 10 : return std::any(value_str);
109 : 4 : case ConfigType::Integer:
110 : 4 : return std::any(std::stoi(value_str));
111 : 1 : case ConfigType::Boolean:
112 [ + - + - ]: 1 : return std::any(value_str == "true");
113 : 1 : case ConfigType::Double:
114 : 1 : return std::any(std::stod(value_str));
115 : 0 : default:
116 : 0 : return std::any(value_str);
117 : : }
118 : 3 : } catch (const std::exception&) {
119 : 3 : return std::any(value_str);
120 : 3 : }
121 : : }
122 : : };
123 : :
124 : 44 : ConfigManager::ConfigManager() : impl_(std::make_unique<Impl>()) {}
125 : 47 : ConfigManager::~ConfigManager() = default;
126 : :
127 : 0 : ConfigManager::ConfigManager(ConfigManager&&) noexcept = default;
128 : 0 : ConfigManager& ConfigManager::operator=(ConfigManager&&) noexcept = default;
129 : :
130 : 1013 : std::any ConfigManager::get(const std::string& key) const {
131 : 1013 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
132 : 1013 : auto it = get_impl()->config_items_.find(key);
133 [ + + ]: 1013 : if (it != get_impl()->config_items_.end()) {
134 : 2024 : return it->second.value;
135 : : }
136 : 1 : throw std::runtime_error("Configuration key not found: " + key);
137 : 1013 : }
138 : :
139 : 231 : std::any ConfigManager::get(const std::string& key, const std::any& default_value) const {
140 : 231 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
141 : 231 : auto it = get_impl()->config_items_.find(key);
142 [ + + ]: 231 : if (it != get_impl()->config_items_.end()) {
143 : 210 : return it->second.value;
144 : : }
145 : 21 : return default_value;
146 : 231 : }
147 : :
148 : 19 : bool ConfigManager::has(const std::string& key) const {
149 : 19 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
150 [ + - ]: 38 : return get_impl()->config_items_.find(key) != get_impl()->config_items_.end();
151 : 19 : }
152 : :
153 : 1273 : ValidationResult ConfigManager::set(const std::string& key, const std::any& value) {
154 : 1273 : std::any old_value;
155 : 1273 : bool had_key = false;
156 : 1273 : ConfigChangeCallback callback;
157 : :
158 : : {
159 : 1273 : std::lock_guard<std::mutex> lock(impl_->mutex_);
160 : :
161 : 1273 : auto validation_result = impl_->validate_value(key, value);
162 [ + + ]: 1273 : if (!validation_result.is_valid) {
163 : 5 : return validation_result;
164 : : }
165 : :
166 : 1268 : auto it = impl_->config_items_.find(key);
167 [ + + ]: 1268 : if (it != impl_->config_items_.end()) {
168 : 10 : old_value = it->second.value;
169 : 10 : had_key = true;
170 : 10 : it->second.value = value;
171 : : } else {
172 : 1258 : ConfigItem item(key, value, ConfigType::String, false);
173 : 1258 : impl_->config_items_[key] = item;
174 : 1258 : }
175 : :
176 [ + + ]: 1268 : if (had_key) {
177 : 10 : callback = impl_->change_callback_for(key);
178 : : }
179 : 1278 : }
180 : :
181 [ + + ]: 1268 : if (had_key) {
182 : 10 : Impl::invoke_change_callback(callback, key, old_value, value);
183 : : }
184 : :
185 : 1268 : return ValidationResult::success();
186 : 1273 : }
187 : :
188 : 32 : bool ConfigManager::remove(const std::string& key) {
189 : 32 : std::lock_guard<std::mutex> lock(impl_->mutex_);
190 : 32 : auto it = impl_->config_items_.find(key);
191 [ + + ]: 32 : if (it != impl_->config_items_.end()) {
192 : 21 : impl_->config_items_.erase(it);
193 : 21 : return true;
194 : : }
195 : 11 : return false;
196 : 32 : }
197 : :
198 : 1 : void ConfigManager::clear() {
199 : 1 : std::lock_guard<std::mutex> lock(impl_->mutex_);
200 : 1 : impl_->config_items_.clear();
201 : 1 : }
202 : :
203 : 2 : ValidationResult ConfigManager::validate() const {
204 : 2 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
205 : :
206 [ + + ]: 2 : for (const auto& [key, item] : get_impl()->config_items_) {
207 : 1 : auto result = get_impl()->validate_value(key, item.value);
208 [ + - ]: 1 : if (!result.is_valid) {
209 : 1 : return result;
210 : : }
211 : 1 : }
212 : :
213 : 1 : return ValidationResult::success();
214 : 2 : }
215 : :
216 : 2 : ValidationResult ConfigManager::validate(const std::string& key) const {
217 : 2 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
218 : 2 : auto it = get_impl()->config_items_.find(key);
219 [ + + ]: 2 : if (it == get_impl()->config_items_.end()) {
220 : 1 : return ValidationResult::error("Configuration key not found: " + key);
221 : : }
222 : :
223 : 1 : return get_impl()->validate_value(key, it->second.value);
224 : 2 : }
225 : :
226 : 14 : void ConfigManager::register_item(const ConfigItem& item) {
227 : 14 : std::lock_guard<std::mutex> lock(impl_->mutex_);
228 : 14 : impl_->config_items_[item.key] = item;
229 : 14 : }
230 : :
231 : 3 : void ConfigManager::register_validator(const std::string& key,
232 : : std::function<ValidationResult(const std::any&)> validator) {
233 : 3 : std::lock_guard<std::mutex> lock(impl_->mutex_);
234 : 3 : auto it = impl_->config_items_.find(key);
235 [ + + ]: 3 : if (it != impl_->config_items_.end()) {
236 : 2 : it->second.validator = validator;
237 : : }
238 : 3 : }
239 : :
240 : 5 : void ConfigManager::on_change(const std::string& key, ConfigChangeCallback callback) {
241 : 5 : std::lock_guard<std::mutex> lock(impl_->mutex_);
242 : 5 : impl_->change_callbacks_[key] = callback;
243 : 5 : }
244 : :
245 : 1 : void ConfigManager::remove_change_callback(const std::string& key) {
246 : 1 : std::lock_guard<std::mutex> lock(impl_->mutex_);
247 : 1 : impl_->change_callbacks_.erase(key);
248 : 1 : }
249 : :
250 : 5 : bool ConfigManager::save_to_file(const std::string& filepath) const {
251 : 5 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
252 : :
253 : : try {
254 : 5 : std::ofstream file(filepath);
255 [ + + ]: 5 : if (!file.is_open()) {
256 : 1 : return false;
257 : : }
258 : :
259 : 4 : file << "# wirestead configuration file\n";
260 : 4 : file << "# Generated automatically\n\n";
261 : :
262 [ + + ]: 17 : for (const auto& [key, item] : get_impl()->config_items_) {
263 : 13 : file << "# " << item.description << "\n";
264 : 13 : file << key << "=" << get_impl()->serialize_value(item.value, item.type) << "\n\n";
265 : : }
266 : :
267 : 4 : return true;
268 : 5 : } catch (const std::exception& e) {
269 : 0 : WIRESTEAD_LOG_ERROR("config_manager", "save", "Error saving configuration: " + std::string(e.what()));
270 : 0 : return false;
271 : 0 : }
272 : 5 : }
273 : :
274 : 11 : bool ConfigManager::load_from_file(const std::string& filepath) {
275 : : struct PendingNotification {
276 : : std::string key;
277 : : std::any old_value;
278 : : std::any new_value;
279 : : ConfigChangeCallback callback;
280 : : };
281 : 11 : std::vector<PendingNotification> pending_notifications;
282 : :
283 : : // Parsing and mutating config_items_ stays under the lock; change
284 : : // callbacks are invoked afterward (below) so a callback that reenters
285 : : // get()/set() on this ConfigManager doesn't deadlock on mutex_.
286 : 0 : const bool load_result = [&]() {
287 : 11 : std::lock_guard<std::mutex> lock(impl_->mutex_);
288 : :
289 : : try {
290 : 11 : std::ifstream file(filepath);
291 [ + + ]: 11 : if (!file.is_open()) {
292 : 2 : return false;
293 : : }
294 : :
295 : 9 : std::string line;
296 [ + - + - : 58 : while (std::getline(file, line)) {
+ + ]
297 [ + + + - : 49 : if (line.empty() || line[0] == '#') {
+ + + + ]
298 : 22 : continue;
299 : : }
300 : :
301 : 27 : size_t pos = line.find('=');
302 [ + + ]: 27 : if (pos != std::string::npos) {
303 : 16 : std::string key = line.substr(0, pos);
304 : 16 : std::string value_str = line.substr(pos + 1);
305 : :
306 : 16 : key.erase(0, key.find_first_not_of(" \t"));
307 : 16 : key.erase(key.find_last_not_of(" \t") + 1);
308 : 16 : value_str.erase(0, value_str.find_first_not_of(" \t"));
309 : 16 : value_str.erase(value_str.find_last_not_of(" \t") + 1);
310 : :
311 : 16 : ConfigType type = ConfigType::String;
312 : 16 : auto it = impl_->config_items_.find(key);
313 : 16 : bool exists = (it != impl_->config_items_.end());
314 : :
315 [ + + ]: 16 : if (exists) {
316 : 3 : type = it->second.type;
317 : : } else {
318 [ + - + + : 13 : if (value_str == "true" || value_str == "false") {
+ - - + +
+ ]
319 : 1 : type = ConfigType::Boolean;
320 [ + - + + ]: 12 : } else if (std::all_of(value_str.begin(), value_str.end(),
321 [ + + + + ]: 16 : [](char c) { return std::isdigit(c) || c == '-'; })) {
322 : 1 : type = ConfigType::Integer;
323 [ + - + + : 12 : } else if (std::count(value_str.begin(), value_str.end(), '.') == 1 &&
+ + ]
324 [ + - + - ]: 1 : std::all_of(value_str.begin(), value_str.end(),
325 [ + + + + : 4 : [](char c) { return std::isdigit(c) || c == '.' || c == '-'; })) {
+ - ]
326 : 1 : type = ConfigType::Double;
327 : : }
328 : : }
329 : :
330 : 16 : std::any value = impl_->deserialize_value(value_str, type);
331 : :
332 [ + + ]: 16 : if (exists) {
333 : 3 : auto result = impl_->validate_value(key, value);
334 [ + - ]: 3 : if (!result.is_valid) {
335 : 3 : WIRESTEAD_LOG_ERROR("config_manager", "load",
336 : : "Validation failed for key '" + key + "': " + result.error_message);
337 : 3 : continue;
338 : 3 : }
339 : :
340 : 0 : std::any old_value = it->second.value;
341 : 0 : it->second.value = value;
342 : 0 : auto callback = impl_->change_callback_for(key);
343 [ # # ]: 0 : if (callback) {
344 : 0 : pending_notifications.push_back({key, old_value, value, std::move(callback)});
345 : : }
346 : 3 : } else {
347 : 13 : ConfigItem item(key, value, type, false);
348 : 13 : impl_->config_items_[key] = item;
349 : 13 : }
350 : 22 : }
351 : : }
352 : :
353 : 9 : return true;
354 : 11 : } catch (const std::exception& e) {
355 : 0 : WIRESTEAD_LOG_ERROR("config_manager", "load", "Error loading configuration: " + std::string(e.what()));
356 : 0 : return false;
357 : 0 : }
358 : 22 : }(); // load_result: whether the file was read and parsed successfully.
359 : : // pending_notifications (collected above, outside this lambda) is
360 : : // drained into callback invocations next, now that mutex_ is
361 : : // released.
362 : :
363 [ - + ]: 11 : for (const auto& notification : pending_notifications) {
364 : 0 : Impl::invoke_change_callback(notification.callback, notification.key, notification.old_value,
365 : 0 : notification.new_value);
366 : : }
367 : :
368 : 11 : return load_result;
369 : 11 : }
370 : :
371 : 5 : std::vector<std::string> ConfigManager::get_keys() const {
372 : 5 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
373 : 5 : std::vector<std::string> keys;
374 : 5 : keys.reserve(get_impl()->config_items_.size());
375 : :
376 [ + + ]: 9 : for (const auto& [key, item] : get_impl()->config_items_) {
377 : 4 : keys.push_back(key);
378 : : }
379 : :
380 : 10 : return keys;
381 : 5 : }
382 : :
383 : 7 : ConfigType ConfigManager::get_type(const std::string& key) const {
384 : 7 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
385 : 7 : auto it = get_impl()->config_items_.find(key);
386 [ + + ]: 7 : if (it != get_impl()->config_items_.end()) {
387 : 12 : return it->second.type;
388 : : }
389 : 1 : throw std::runtime_error("Configuration key not found: " + key);
390 : 7 : }
391 : :
392 : 2 : std::string ConfigManager::get_description(const std::string& key) const {
393 : 2 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
394 : 2 : auto it = get_impl()->config_items_.find(key);
395 [ + + ]: 2 : if (it != get_impl()->config_items_.end()) {
396 : 1 : return it->second.description;
397 : : }
398 : 2 : return "";
399 : 2 : }
400 : :
401 : 2 : bool ConfigManager::is_required(const std::string& key) const {
402 : 2 : std::lock_guard<std::mutex> lock(get_impl()->mutex_);
403 : 2 : auto it = get_impl()->config_items_.find(key);
404 [ + + ]: 2 : if (it != get_impl()->config_items_.end()) {
405 : 1 : return it->second.required;
406 : : }
407 : 1 : return false;
408 : 2 : }
409 : :
410 : : } // namespace config
411 : : } // namespace wirestead
|