LCOV - code coverage report
Current view: top level - wirestead/config - config_manager.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 91.3 % 254 232
Test Date: 2026-08-30 10:35:09 Functions: 93.5 % 31 29
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 78.3 % 138 108

             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
        

Generated by: LCOV version 2.0-1