22#include < filesystem>
33#include < iostream>
44#include < regex>
5+ #include < set>
56#include < stdexcept>
67
78namespace anolis_provider_sim {
89
910namespace fs = std::filesystem;
11+ namespace {
12+ constexpr const char *kReservedChaosControlId = " chaos_control" ;
13+ }
1014
1115// Parse simulation mode from string
1216SimulationMode parse_simulation_mode (const std::string &mode_str) {
@@ -22,6 +26,17 @@ SimulationMode parse_simulation_mode(const std::string &mode_str) {
2226 }
2327}
2428
29+ StartupPolicy parse_startup_policy (const std::string &policy_str) {
30+ if (policy_str == " strict" ) {
31+ return StartupPolicy::Strict;
32+ }
33+ if (policy_str == " degraded" ) {
34+ return StartupPolicy::Degraded;
35+ }
36+ throw std::runtime_error (" Invalid startup_policy: '" + policy_str +
37+ " '. Valid values: strict, degraded" );
38+ }
39+
2540// Parse transform type from string
2641static TransformType parse_transform_type (const std::string &type_str) {
2742 if (type_str == " first_order_lag" ) {
@@ -226,12 +241,27 @@ ProviderConfig load_config(const std::string &path) {
226241 config.provider_name = provider_name;
227242 }
228243
244+ // Parse optional startup policy (default: strict)
245+ if (yaml[" startup_policy" ]) {
246+ try {
247+ const std::string startup_policy_str =
248+ yaml[" startup_policy" ].as <std::string>();
249+ config.startup_policy = parse_startup_policy (startup_policy_str);
250+ } catch (const YAML::Exception &) {
251+ throw std::runtime_error (
252+ " [CONFIG] Invalid startup_policy: must be a string" );
253+ } catch (const std::runtime_error &e) {
254+ throw std::runtime_error (" [CONFIG] " + std::string (e.what ()));
255+ }
256+ }
257+
229258 // Parse devices section
230259 if (yaml[" devices" ]) {
231260 if (!yaml[" devices" ].IsSequence ()) {
232261 throw std::runtime_error (" 'devices' must be a sequence" );
233262 }
234263
264+ std::set<std::string> seen_device_ids;
235265 for (std::size_t i = 0 ; i < yaml[" devices" ].size (); ++i) {
236266 const auto &device_node = yaml[" devices" ][i];
237267 if (!device_node.IsMap ()) {
@@ -261,6 +291,27 @@ ProviderConfig load_config(const std::string &path) {
261291 std::to_string (i) + " ]: " + e.what ());
262292 }
263293
294+ if (spec.id .empty ()) {
295+ throw std::runtime_error (" [CONFIG] Invalid devices[" +
296+ std::to_string (i) +
297+ " ]: 'id' must not be empty" );
298+ }
299+ if (spec.type .empty ()) {
300+ throw std::runtime_error (" [CONFIG] Invalid devices[" +
301+ std::to_string (i) +
302+ " ]: 'type' must not be empty" );
303+ }
304+ if (spec.id == kReservedChaosControlId ) {
305+ throw std::runtime_error (
306+ " [CONFIG] devices[].id 'chaos_control' is reserved and cannot be "
307+ " configured explicitly" );
308+ }
309+
310+ if (!seen_device_ids.insert (spec.id ).second ) {
311+ throw std::runtime_error (" [CONFIG] Duplicate device id: '" + spec.id +
312+ " '" );
313+ }
314+
264315 // Store all other fields as configuration parameters
265316 for (const auto &kv : device_node) {
266317 std::string key = kv.first .as <std::string>();
@@ -295,9 +346,34 @@ ProviderConfig load_config(const std::string &path) {
295346 std::string (e.what ()));
296347 }
297348
349+ std::set<std::string> provided_simulation_keys;
350+ const std::set<std::string> known_simulation_keys = {
351+ " mode" , " tick_rate_hz" , " physics_config" , " ambient_temp_c" ,
352+ " ambient_signal_path" };
353+ for (const auto &kv : yaml[" simulation" ]) {
354+ const std::string key = kv.first .as <std::string>();
355+ provided_simulation_keys.insert (key);
356+
357+ if (key == " noise_enabled" || key == " update_rate_hz" ) {
358+ throw std::runtime_error (" [CONFIG] simulation." + key +
359+ " is no longer supported" );
360+ }
361+
362+ if (known_simulation_keys.find (key) == known_simulation_keys.end ()) {
363+ throw std::runtime_error (" [CONFIG] Unknown simulation key: '" + key +
364+ " '" );
365+ }
366+ }
367+
298368 // Parse simulation.tick_rate_hz (required for non_interacting and sim)
299369 if (yaml[" simulation" ][" tick_rate_hz" ]) {
300- double tick_rate = yaml[" simulation" ][" tick_rate_hz" ].as <double >();
370+ double tick_rate = 0.0 ;
371+ try {
372+ tick_rate = yaml[" simulation" ][" tick_rate_hz" ].as <double >();
373+ } catch (const YAML::Exception &) {
374+ throw std::runtime_error (" [CONFIG] simulation.tick_rate_hz must be "
375+ " numeric in range [0.1, 1000.0]" );
376+ }
301377
302378 // Validate bounds
303379 if (tick_rate < 0.1 || tick_rate > 1000.0 ) {
@@ -310,37 +386,77 @@ ProviderConfig load_config(const std::string &path) {
310386
311387 // Parse simulation.physics_config (required for sim mode)
312388 if (yaml[" simulation" ][" physics_config" ]) {
313- config.physics_config_path =
314- yaml[" simulation" ][" physics_config" ].as <std::string>();
389+ try {
390+ config.physics_config_path =
391+ yaml[" simulation" ][" physics_config" ].as <std::string>();
392+ } catch (const YAML::Exception &) {
393+ throw std::runtime_error (
394+ " [CONFIG] simulation.physics_config must be a string" );
395+ }
396+
397+ if (config.physics_config_path ->empty ()) {
398+ throw std::runtime_error (
399+ " [CONFIG] simulation.physics_config cannot be empty" );
400+ }
401+ }
402+
403+ if (yaml[" simulation" ][" ambient_temp_c" ]) {
404+ try {
405+ config.ambient_temp_c = yaml[" simulation" ][" ambient_temp_c" ].as <double >();
406+ } catch (const YAML::Exception &) {
407+ throw std::runtime_error (
408+ " [CONFIG] simulation.ambient_temp_c must be numeric" );
409+ }
315410 }
316411
412+ if (yaml[" simulation" ][" ambient_signal_path" ]) {
413+ try {
414+ config.ambient_signal_path =
415+ yaml[" simulation" ][" ambient_signal_path" ].as <std::string>();
416+ } catch (const YAML::Exception &) {
417+ throw std::runtime_error (
418+ " [CONFIG] simulation.ambient_signal_path must be a string" );
419+ }
420+
421+ if (config.ambient_signal_path ->empty ()) {
422+ throw std::runtime_error (
423+ " [CONFIG] simulation.ambient_signal_path cannot be empty" );
424+ }
425+ }
426+
427+ if (config.ambient_signal_path && !config.ambient_temp_c ) {
428+ throw std::runtime_error (" [CONFIG] simulation.ambient_signal_path requires "
429+ " simulation.ambient_temp_c" );
430+ }
431+
432+ const auto ensure_mode_allowed_keys = [&](const std::set<std::string> &keys,
433+ const std::string &mode_name) {
434+ for (const auto &provided_key : provided_simulation_keys) {
435+ if (keys.find (provided_key) == keys.end ()) {
436+ throw std::runtime_error (" [CONFIG] simulation." + provided_key +
437+ " is not valid for mode=" + mode_name);
438+ }
439+ }
440+ };
441+
317442 // Startup validation matrix
318443 switch (config.simulation_mode ) {
319444 case SimulationMode::NonInteracting:
445+ ensure_mode_allowed_keys ({" mode" , " tick_rate_hz" }, " non_interacting" );
320446 if (!config.tick_rate_hz ) {
321447 throw std::runtime_error (
322448 " [CONFIG] mode=non_interacting requires simulation.tick_rate_hz" );
323449 }
324- if (config.physics_config_path ) {
325- throw std::runtime_error (
326- " [CONFIG] mode=non_interacting cannot have physics_config (prevents "
327- " silent ignored config)" );
328- }
329450 break ;
330451
331452 case SimulationMode::Inert:
332- if (config.tick_rate_hz ) {
333- throw std::runtime_error (
334- " [CONFIG] mode=inert cannot have simulation.tick_rate_hz (prevents "
335- " ignored config)" );
336- }
337- if (config.physics_config_path ) {
338- throw std::runtime_error (" [CONFIG] mode=inert cannot have physics_config "
339- " (prevents silent ignored config)" );
340- }
453+ ensure_mode_allowed_keys ({" mode" }, " inert" );
341454 break ;
342455
343456 case SimulationMode::Sim:
457+ ensure_mode_allowed_keys ({" mode" , " tick_rate_hz" , " physics_config" ,
458+ " ambient_temp_c" , " ambient_signal_path" },
459+ " sim" );
344460 if (!config.tick_rate_hz ) {
345461 throw std::runtime_error (
346462 " [CONFIG] mode=sim requires simulation.tick_rate_hz" );
@@ -352,48 +468,6 @@ ProviderConfig load_config(const std::string &path) {
352468 break ;
353469 }
354470
355- const bool has_ambient_temp =
356- static_cast <bool >(yaml[" simulation" ][" ambient_temp_c" ]);
357- const bool has_ambient_path =
358- static_cast <bool >(yaml[" simulation" ][" ambient_signal_path" ]);
359-
360- if (config.simulation_mode != SimulationMode::Sim &&
361- (has_ambient_temp || has_ambient_path)) {
362- throw std::runtime_error (
363- " [CONFIG] simulation.ambient_* options are only valid in mode=sim" );
364- }
365-
366- if (config.simulation_mode == SimulationMode::Sim) {
367- if (has_ambient_path && !has_ambient_temp) {
368- throw std::runtime_error (" [CONFIG] simulation.ambient_signal_path "
369- " requires simulation.ambient_temp_c" );
370- }
371-
372- if (has_ambient_temp) {
373- try {
374- (void )yaml[" simulation" ][" ambient_temp_c" ].as <double >();
375- } catch (const YAML::Exception &) {
376- throw std::runtime_error (
377- " [CONFIG] simulation.ambient_temp_c must be numeric" );
378- }
379- }
380-
381- if (has_ambient_path) {
382- std::string ambient_path;
383- try {
384- ambient_path =
385- yaml[" simulation" ][" ambient_signal_path" ].as <std::string>();
386- } catch (const YAML::Exception &) {
387- throw std::runtime_error (
388- " [CONFIG] simulation.ambient_signal_path must be a string" );
389- }
390- if (ambient_path.empty ()) {
391- throw std::runtime_error (
392- " [CONFIG] simulation.ambient_signal_path cannot be empty" );
393- }
394- }
395- }
396-
397471 // Validate physics_bindings are only used in sim mode
398472 if (config.simulation_mode != SimulationMode::Sim) {
399473 for (const auto &device : config.devices ) {
@@ -405,14 +479,6 @@ ProviderConfig load_config(const std::string &path) {
405479 }
406480 }
407481
408- // Store legacy simulation params for backward compatibility
409- for (const auto &kv : yaml[" simulation" ]) {
410- std::string key = kv.first .as <std::string>();
411- if (key != " mode" && key != " tick_rate_hz" && key != " physics_config" ) {
412- config.simulation [key] = kv.second ;
413- }
414- }
415-
416482 return config;
417483}
418484
0 commit comments