LCOV - code coverage report
Current view: top level - src/config - toml_parser.cpp (source / functions) Coverage Total Hit
Test: HPActor Coverage Lines: 77.7 % 229 178
Test Date: 2026-05-20 02:24:49 Functions: 100.0 % 7 7
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: - 0 0

             Branch data     Line data    Source code
       1                 :             : // Copyright 2026 HPActor Contributors
       2                 :             : //
       3                 :             : // Licensed under the Apache License, Version 2.0 (the "License");
       4                 :             : // you may not use this file except in compliance with the License.
       5                 :             : // You may obtain a copy of the License at
       6                 :             : //
       7                 :             : //     http://www.apache.org/licenses/LICENSE-2.0
       8                 :             : //
       9                 :             : // Unless required by applicable law or agreed to in writing, software
      10                 :             : // distributed under the License is distributed on an "AS IS" BASIS,
      11                 :             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12                 :             : // See the License for the specific language governing permissions and
      13                 :             : // limitations under the License.
      14                 :             : 
      15                 :             : #include <hpactor/config/toml_config_parser.hpp>
      16                 :             : #include <hpactor/config/toml_file_data.hpp>
      17                 :             : #include <hpactor/config/toml_parse_context.hpp>
      18                 :             : #include <hpactor/config/toml_parser.hpp>
      19                 :             : #include <hpactor/config/toml_parser_registry.hpp>
      20                 :             : #include <hpactor/config/toml_table_view.hpp>
      21                 :             : #include <hpactor/log/log_category.hpp>
      22                 :             : #include <hpactor/log/logger.hpp>
      23                 :             : 
      24                 :             : #include <toml.hpp>
      25                 :             : 
      26                 :             : #include <deque>
      27                 :             : #include <filesystem>
      28                 :             : #include <unordered_set>
      29                 :             : 
      30                 :             : namespace fs = std::filesystem;
      31                 :             : 
      32                 :             : namespace hpactor::config {
      33                 :             : 
      34                 :             : namespace {
      35                 :             : 
      36                 :             : // ---------------------------------------------------------------------------
      37                 :             : // Glob expansion — simple *-only pattern via fs::directory_iterator
      38                 :             : // ---------------------------------------------------------------------------
      39                 :           3 : static std::vector<std::string> expand_glob(const std::string& pattern) {
      40                 :           3 :     std::vector<std::string> results;
      41                 :           3 :     fs::path p(pattern);
      42                 :             : 
      43                 :           3 :     if (p.is_absolute() && fs::exists(p)) {
      44                 :           3 :         results.push_back(p.string());
      45                 :           3 :         return results;
      46                 :             :     }
      47                 :             : 
      48                 :           0 :     fs::path parent = p.parent_path();
      49                 :           0 :     if (parent.empty())
      50                 :           0 :         parent = ".";
      51                 :           0 :     std::string fname = p.filename().string();
      52                 :             : 
      53                 :           0 :     auto star_pos = fname.find('*');
      54                 :           0 :     if (star_pos == std::string::npos) {
      55                 :           0 :         if (fs::exists(p))
      56                 :           0 :             results.push_back(p.string());
      57                 :           0 :         return results;
      58                 :             :     }
      59                 :             : 
      60                 :           0 :     std::string prefix = fname.substr(0, star_pos);
      61                 :           0 :     std::string suffix = fname.substr(star_pos + 1);
      62                 :             : 
      63                 :           0 :     std::error_code ec;
      64                 :           0 :     for (const auto& entry : fs::directory_iterator(parent, ec)) {
      65                 :           0 :         if (ec)
      66                 :           0 :             break;
      67                 :           0 :         std::string entry_name = entry.path().filename().string();
      68                 :           0 :         if (entry_name.size() >= prefix.size() + suffix.size() &&
      69                 :           0 :             entry_name.starts_with(prefix) && entry_name.ends_with(suffix)) {
      70                 :           0 :             results.push_back(entry.path().string());
      71                 :             :         }
      72                 :           0 :     }
      73                 :           0 :     return results;
      74                 :           3 : }
      75                 :             : 
      76                 :             : // ---------------------------------------------------------------------------
      77                 :             : // Parse a single TOML file using the registered parser pipeline
      78                 :             : // ---------------------------------------------------------------------------
      79                 :             : static result<TomlFileData>
      80                 :          38 : parse_file_data(const std::string& filepath, bool is_entrypoint) {
      81                 :          38 :     TomlFileData data;
      82                 :             : 
      83                 :          38 :     toml::table root;
      84                 :             :     try {
      85                 :          38 :         root = toml::parse_file(filepath);
      86                 :           0 :     } catch (const toml::parse_error&) {
      87                 :           0 :         error err(errors::unknown);
      88                 :           0 :         HPACTOR_LOG_ERROR(log::LogCategory::kConfig, ActorId{0}, 0,
      89                 :             :                           "topology parse error",
      90                 :             :                           log::field_lit("error", err.message().c_str()));
      91                 :           0 :         return result<TomlFileData>::make(std::move(err));
      92                 :           0 :     }
      93                 :             : 
      94                 :          38 :     TomlParseContext ctx(filepath, is_entrypoint);
      95                 :          38 :     TomlTableView root_view = make_toml_table_view(&root);
      96                 :             : 
      97                 :             :     // Imported files must not contain [system]
      98                 :          38 :     if (!is_entrypoint && root_view.contains("system")) {
      99                 :           1 :         error err(errors::unknown);
     100                 :           1 :         return result<TomlFileData>::make(std::move(err));
     101                 :           1 :     }
     102                 :             : 
     103                 :             :     // Entrypoint must have [system]
     104                 :          37 :     if (is_entrypoint) {
     105                 :          35 :         auto system_view = root_view.table("system");
     106                 :          35 :         if (!system_view.valid()) {
     107                 :           0 :             error err(errors::unknown);
     108                 :           0 :             return result<TomlFileData>::make(std::move(err));
     109                 :           0 :         }
     110                 :             :     }
     111                 :             : 
     112                 :             :     // Snapshot registered parsers before use
     113                 :             :     auto document_parsers =
     114                 :          37 :         TomlParserRegistry::instance().create_document_parsers();
     115                 :          37 :     auto system_parsers = TomlParserRegistry::instance().create_system_parsers();
     116                 :             : 
     117                 :          37 :     if (document_parsers.empty() || (is_entrypoint && system_parsers.empty())) {
     118                 :           0 :         error err(errors::unknown);
     119                 :           0 :         HPACTOR_LOG_ERROR(log::LogCategory::kConfig, ActorId{0}, 0,
     120                 :             :                           "no registered TOML parsers found");
     121                 :           0 :         return result<TomlFileData>::make(std::move(err));
     122                 :           0 :     }
     123                 :             : 
     124                 :             :     // Run document parsers on the root table
     125                 :          74 :     for (const auto& parser : document_parsers) {
     126                 :          37 :         auto parsed = parser->parse(root_view, data, ctx);
     127                 :          37 :         if (!parsed.has_value())
     128                 :           0 :             return result<TomlFileData>::make(parsed.error());
     129                 :          37 :     }
     130                 :             : 
     131                 :             :     // Run system parsers on [system] table (entrypoint only)
     132                 :          37 :     if (is_entrypoint) {
     133                 :          35 :         auto system_view = root_view.table("system");
     134                 :         353 :         for (const auto& parser : system_parsers) {
     135                 :         318 :             auto parsed = parser->parse(system_view, data.system, ctx);
     136                 :         318 :             if (!parsed.has_value())
     137                 :           0 :                 return result<TomlFileData>::make(parsed.error());
     138                 :         318 :         }
     139                 :             :     }
     140                 :             : 
     141                 :          37 :     return result<TomlFileData>::make(std::move(data));
     142                 :          38 : }
     143                 :             : 
     144                 :             : // ---------------------------------------------------------------------------
     145                 :             : // Deep merge overrides into base
     146                 :             : // ---------------------------------------------------------------------------
     147                 :           4 : static void deep_merge(ActorDef& base, const ActorDef& overrides) {
     148                 :           4 :     if (!overrides.behavior.empty())
     149                 :           0 :         base.behavior = overrides.behavior;
     150                 :           4 :     if (!overrides.dispatcher.empty())
     151                 :           0 :         base.dispatcher = overrides.dispatcher;
     152                 :           4 :     if (!overrides.supervisor.empty())
     153                 :           0 :         base.supervisor = overrides.supervisor;
     154                 :           4 :     if (overrides.mailbox_capacity != 0)
     155                 :           0 :         base.mailbox_capacity = overrides.mailbox_capacity;
     156                 :           4 :     base.dispatch_policy = overrides.dispatch_policy;
     157                 :             : 
     158                 :           4 :     if (overrides.resources.slab_class_bytes != 0)
     159                 :           0 :         base.resources.slab_class_bytes = overrides.resources.slab_class_bytes;
     160                 :           4 :     if (overrides.resources.max_memory_kb != 0)
     161                 :           0 :         base.resources.max_memory_kb = overrides.resources.max_memory_kb;
     162                 :             : 
     163                 :           4 :     if (overrides.mailbox.policy != hpactor::mailbox::OverflowPolicy::RejectNewest)
     164                 :           0 :         base.mailbox.policy = overrides.mailbox.policy;
     165                 :           4 :     base.mailbox.priority_aware = overrides.mailbox.priority_aware;
     166                 :           4 :     if (overrides.mailbox.max_overflow_depth != 0)
     167                 :           0 :         base.mailbox.max_overflow_depth = overrides.mailbox.max_overflow_depth;
     168                 :             : 
     169                 :          10 :     for (const auto& [k, v] : overrides.args) {
     170                 :           6 :         base.args[k] = v;
     171                 :             :     }
     172                 :           4 : }
     173                 :             : 
     174                 :             : // ---------------------------------------------------------------------------
     175                 :             : // Resolve template inheritance
     176                 :             : // ---------------------------------------------------------------------------
     177                 :             : static result<std::vector<ActorDef>>
     178                 :          34 : resolve_templates(const std::vector<TomlRawActor>& raw_actors,
     179                 :             :                   const std::unordered_map<std::string, ActorDef>& templates) {
     180                 :          34 :     std::vector<ActorDef> resolved;
     181                 :          34 :     resolved.reserve(raw_actors.size());
     182                 :             : 
     183                 :          84 :     for (const auto& raw : raw_actors) {
     184                 :          51 :         if (raw.inherits.empty()) {
     185                 :          46 :             resolved.push_back(raw.def);
     186                 :          46 :             continue;
     187                 :             :         }
     188                 :             : 
     189                 :           5 :         auto tmpl_it = templates.find(raw.inherits);
     190                 :           5 :         if (tmpl_it == templates.end()) {
     191                 :           1 :             error err(errors::unknown);
     192                 :           1 :             return result<std::vector<ActorDef>>::make(std::move(err));
     193                 :           1 :         }
     194                 :             : 
     195                 :           4 :         ActorDef merged = tmpl_it->second;
     196                 :           4 :         deep_merge(merged, raw.def);
     197                 :           4 :         merged.id = raw.def.id;
     198                 :           4 :         resolved.push_back(std::move(merged));
     199                 :           4 :     }
     200                 :             : 
     201                 :          33 :     return result<std::vector<ActorDef>>::make(std::move(resolved));
     202                 :          34 : }
     203                 :             : 
     204                 :             : // ---------------------------------------------------------------------------
     205                 :             : // Validation
     206                 :             : // ---------------------------------------------------------------------------
     207                 :          33 : static bool validate(const TopologyModel& model, std::string& error_msg) {
     208                 :             :     // Duplicate/empty actor ids
     209                 :          33 :     std::unordered_set<std::string> ids;
     210                 :          82 :     for (const auto& actor : model.actors) {
     211                 :          50 :         if (actor.id.empty()) {
     212                 :           0 :             error_msg = "actor has empty id";
     213                 :           1 :             return false;
     214                 :             :         }
     215                 :          50 :         if (ids.find(actor.id) != ids.end()) {
     216                 :           1 :             error_msg = "duplicate actor id '" + actor.id + "'";
     217                 :           1 :             return false;
     218                 :             :         }
     219                 :          49 :         ids.insert(actor.id);
     220                 :          49 :         if (actor.behavior.empty()) {
     221                 :           0 :             error_msg = "actor '" + actor.id + "' has no behavior";
     222                 :           0 :             return false;
     223                 :             :         }
     224                 :             :     }
     225                 :             : 
     226                 :             :     // Dispatcher references
     227                 :          32 :     std::unordered_set<std::string> disp_names;
     228                 :          34 :     for (const auto& d : model.dispatchers)
     229                 :           2 :         disp_names.insert(d.name);
     230                 :             : 
     231                 :          79 :     for (const auto& actor : model.actors) {
     232                 :          51 :         if (!actor.dispatcher.empty() &&
     233                 :          51 :             disp_names.find(actor.dispatcher) == disp_names.end()) {
     234                 :           2 :             error_msg = "actor '" + actor.id + "' references unknown dispatcher '" +
     235                 :           2 :                         actor.dispatcher + "'";
     236                 :           1 :             return false;
     237                 :             :         }
     238                 :             :     }
     239                 :             : 
     240                 :          31 :     return true;
     241                 :          33 : }
     242                 :             : 
     243                 :             : // ---------------------------------------------------------------------------
     244                 :             : // Topological sort (Kahn's algorithm)
     245                 :             : // ---------------------------------------------------------------------------
     246                 :             : static result<std::vector<ActorDef>>
     247                 :          31 : topological_sort(std::vector<ActorDef> actors) {
     248                 :          31 :     std::unordered_map<std::string, size_t> id_to_idx;
     249                 :          78 :     for (size_t i = 0; i < actors.size(); ++i)
     250                 :          47 :         id_to_idx[actors[i].id] = i;
     251                 :             : 
     252                 :          31 :     std::unordered_map<std::string, std::vector<size_t>> children;
     253                 :          31 :     std::vector<size_t> in_degree(actors.size(), 0);
     254                 :             : 
     255                 :          77 :     for (size_t i = 0; i < actors.size(); ++i) {
     256                 :          47 :         const auto& sup = actors[i].supervisor;
     257                 :          47 :         if (!sup.empty()) {
     258                 :          14 :             auto it = id_to_idx.find(sup);
     259                 :          14 :             if (it == id_to_idx.end()) {
     260                 :           1 :                 error err(errors::unknown);
     261                 :           1 :                 return result<std::vector<ActorDef>>::make(std::move(err));
     262                 :           1 :             }
     263                 :          13 :             children[sup].push_back(i);
     264                 :          13 :             in_degree[i]++;
     265                 :             :         }
     266                 :             :     }
     267                 :             : 
     268                 :          30 :     std::deque<size_t> queue;
     269                 :          76 :     for (size_t i = 0; i < actors.size(); ++i)
     270                 :          46 :         if (in_degree[i] == 0)
     271                 :          33 :             queue.push_back(i);
     272                 :             : 
     273                 :          30 :     std::vector<ActorDef> sorted;
     274                 :          30 :     sorted.reserve(actors.size());
     275                 :             : 
     276                 :          74 :     while (!queue.empty()) {
     277                 :          44 :         size_t current = queue.front();
     278                 :          44 :         queue.pop_front();
     279                 :          44 :         sorted.push_back(std::move(actors[current]));
     280                 :             : 
     281                 :          44 :         auto child_it = children.find(sorted.back().id);
     282                 :          44 :         if (child_it != children.end()) {
     283                 :          19 :             for (size_t child_idx : child_it->second) {
     284                 :          11 :                 if (--in_degree[child_idx] == 0)
     285                 :          11 :                     queue.push_back(child_idx);
     286                 :             :             }
     287                 :             :         }
     288                 :             :     }
     289                 :             : 
     290                 :          30 :     if (sorted.size() != actors.size()) {
     291                 :           1 :         error err(errors::unknown);
     292                 :           1 :         return result<std::vector<ActorDef>>::make(std::move(err));
     293                 :           1 :     }
     294                 :             : 
     295                 :          29 :     return result<std::vector<ActorDef>>::make(std::move(sorted));
     296                 :          31 : }
     297                 :             : 
     298                 :             : } // anonymous namespace
     299                 :             : 
     300                 :             : // =============================================================================
     301                 :             : // TomlParser::parse
     302                 :             : // =============================================================================
     303                 :          35 : result<TopologyModel> TomlParser::parse(const std::string& entrypoint_path) {
     304                 :          35 :     fs::path entry_fs(entrypoint_path);
     305                 :          35 :     fs::path base_dir = entry_fs.parent_path();
     306                 :          35 :     if (base_dir.empty())
     307                 :           0 :         base_dir = ".";
     308                 :             : 
     309                 :             :     // Phase 1: Parse entrypoint
     310                 :          35 :     auto entry_parse = parse_file_data(entrypoint_path, true);
     311                 :          35 :     if (!entry_parse.has_value()) {
     312                 :           0 :         return result<TopologyModel>::make(entry_parse.error());
     313                 :             :     }
     314                 :          35 :     TomlFileData entry_data = std::move(entry_parse.value());
     315                 :             : 
     316                 :             :     // Phase 2: Collect imports' data
     317                 :          35 :     std::vector<TomlFileData> imported_data;
     318                 :          37 :     for (const auto& import_pattern : entry_data.system.imports) {
     319                 :           3 :         fs::path import_path = base_dir / import_pattern;
     320                 :           3 :         auto files = expand_glob(import_path.string());
     321                 :           5 :         for (const auto& file : files) {
     322                 :           3 :             auto fc = parse_file_data(file, false);
     323                 :           3 :             if (!fc.has_value())
     324                 :           1 :                 return result<TopologyModel>::make(fc.error());
     325                 :           2 :             imported_data.push_back(std::move(fc.value()));
     326                 :           3 :         }
     327                 :           4 :     }
     328                 :             : 
     329                 :             :     // Phase 3: Merge dispatchers (imported first, then entrypoint)
     330                 :          34 :     std::vector<DispatcherDef> all_dispatchers;
     331                 :          34 :     std::unordered_map<std::string, ActorDef> all_templates;
     332                 :          34 :     std::vector<TomlRawActor> all_raw_actors;
     333                 :             : 
     334                 :          36 :     for (auto& imp : imported_data) {
     335                 :           2 :         for (auto& d : imp.dispatchers)
     336                 :           0 :             all_dispatchers.push_back(std::move(d));
     337                 :           4 :         for (auto& a : imp.actors)
     338                 :           2 :             all_raw_actors.push_back(std::move(a));
     339                 :             :         // Templates: first-wins (imported files are processed first)
     340                 :           2 :         for (auto& [name, tmpl] : imp.templates) {
     341                 :           0 :             if (all_templates.find(name) == all_templates.end())
     342                 :           0 :                 all_templates[name] = std::move(tmpl);
     343                 :             :         }
     344                 :             :     }
     345                 :             : 
     346                 :             :     // Entrypoint dispatchers, templates, actors (last, can override templates)
     347                 :          36 :     for (auto& d : entry_data.dispatchers)
     348                 :           2 :         all_dispatchers.push_back(std::move(d));
     349                 :          83 :     for (auto& a : entry_data.actors)
     350                 :          49 :         all_raw_actors.push_back(std::move(a));
     351                 :          38 :     for (auto& [name, tmpl] : entry_data.templates) {
     352                 :           4 :         if (all_templates.find(name) == all_templates.end())
     353                 :           4 :             all_templates[name] = std::move(tmpl);
     354                 :             :     }
     355                 :             : 
     356                 :             :     // Phase 4: Resolve template inheritance
     357                 :          34 :     auto resolved_result = resolve_templates(all_raw_actors, all_templates);
     358                 :          34 :     if (!resolved_result.has_value())
     359                 :           1 :         return result<TopologyModel>::make(resolved_result.error());
     360                 :             : 
     361                 :          33 :     TopologyModel model;
     362                 :          33 :     model.system = std::move(entry_data.system);
     363                 :          33 :     model.dispatchers = std::move(all_dispatchers);
     364                 :          33 :     model.actors = std::move(resolved_result.value());
     365                 :             : 
     366                 :             :     // Phase 5: Validate
     367                 :          33 :     std::string error_msg;
     368                 :          33 :     if (!validate(model, error_msg)) {
     369                 :           2 :         error err(errors::unknown);
     370                 :           2 :         return result<TopologyModel>::make(std::move(err));
     371                 :           2 :     }
     372                 :             : 
     373                 :             :     // Phase 6: Topological sort
     374                 :          31 :     auto sorted_result = topological_sort(std::move(model.actors));
     375                 :          31 :     if (!sorted_result.has_value())
     376                 :           2 :         return result<TopologyModel>::make(sorted_result.error());
     377                 :          29 :     model.actors = std::move(sorted_result.value());
     378                 :             : 
     379                 :          29 :     HPACTOR_LOG_INFO(log::LogCategory::kConfig, ActorId{0}, 0, "topology loaded",
     380                 :             :                      log::field_lit("path", entrypoint_path.c_str()));
     381                 :             : 
     382                 :          29 :     return result<TopologyModel>::make(std::move(model));
     383                 :          35 : }
     384                 :             : 
     385                 :             : } // namespace hpactor::config
        

Generated by: LCOV version 2.0-1