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
|