diff --git a/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp b/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp index 1078a0118..ada464a92 100644 --- a/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp +++ b/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp @@ -5,7 +5,11 @@ #include // std::equal #include // std::tolower #include +#include #include // std::string_view +#include +#include +#include // Read default configuration from CMake-generated include file. @@ -25,79 +29,122 @@ bool iequals(std::string_view lhs, std::string_view rhs) { namespace YamlInputReader{ + // Helper function to get all keys from a YAML Map node + std::set getYamlKeys(const YAML::Node& node) { + std::set keys; + if (!node.IsMap()) { + return keys; + } + for (const auto& it : node) { + keys.insert(it.first.as()); + } + return keys; + } + + void validateYamlKeys(const YAML::Node& defaultNode, const YAML::Node& userNode, const std::string& currentPath = "") { + if (!userNode.IsMap()) { + // If the user node is not a map, we don't need to check its keys. + return; + } + + if (!defaultNode.IsMap()) { + // If the user node is a map but the default is not, it's an error + // because the user is trying to add a structure that doesn't exist. + throw std::runtime_error("Invalid key: '" + currentPath + "' is a map in provided YAML but not in the default input.yaml (should be a value)."); + } + + auto defaultKeys = getYamlKeys(defaultNode); + auto userKeys = getYamlKeys(userNode); + + for (const auto& key : userKeys) { + if (!defaultKeys.contains(key)) { + // The key from the user's YAML does not exist in the default YAML. + std::string errorPath = currentPath.empty() ? key : currentPath + " -> " + key; + throw std::runtime_error("Unknown key found: '" + errorPath + "'"); + } + + // Recurse to check nested maps + const YAML::Node nextUserNode = userNode[key]; + const YAML::Node nextDefaultNode = defaultNode[key]; + + if (nextUserNode.IsMap()) { + std::string nextPath = currentPath.empty() ? key : currentPath + " -> " + key; + validateYamlKeys(nextDefaultNode, nextUserNode, nextPath); + } + } + } + void readYamlInputFiles(OptInput& input, const vector &filenames){ - YAML::Node data = YAML::Load(default_input); + YAML::Node defaultData = YAML::Load(default_input); + YAML::Node mergedData = YAML::Load(default_input); + for (auto filename: filenames) { + YAML::Node userData = YAML::LoadFile(filename); + + // Validate the user's YAML file against the default structure + try { + validateYamlKeys(defaultData, userData); + } catch (const std::runtime_error& e) { + throw std::runtime_error("Invalid field in YAML input file '" + filename + "': " + e.what()); + } INPUT_FILE_PATH = std::filesystem::path(filename); - data = mergeYamlNodes(data, YAML::LoadFile(filename)); + mergedData = mergeYamlNodes(mergedData, userData); } try { - readSimMenu(input, data["SIMULATION MENU"]); + readSimMenu(input, mergedData["SIMULATION MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the SIMULATION MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the SIMULATION MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readParamMenu(input, data["PARAMETER MENU"]); + readParamMenu(input, mergedData["PARAMETER MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the PARAMETER MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the PARAMETER MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readTransportMenu(input, data["TRANSPORT MENU"]); + readTransportMenu(input, mergedData["TRANSPORT MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the TRANSPORT MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the TRANSPORT MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readChemMenu(input, data["CHEMISTRY MENU"]); + readChemMenu(input, mergedData["CHEMISTRY MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the CHEMISTRY MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the CHEMISTRY MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readAeroMenu(input, data["AEROSOL MENU"]); + readAeroMenu(input, mergedData["AEROSOL MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the AEROSOL MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the AEROSOL MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readMetMenu(input, data["METEOROLOGY MENU"]); - } - catch (const std::invalid_argument& e) { - std::cerr << "ERROR: " << e.what() << std::endl; - exit(1); + readMetMenu(input, mergedData["METEOROLOGY MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the METEOROLOGY MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the METEOROLOGY MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readDiagMenu(input, data["DIAGNOSTIC MENU"]); + readDiagMenu(input, mergedData["DIAGNOSTIC MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the DIAGNOSTIC MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the DIAGNOSTIC MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } try { - readAdvancedMenu(input, data["ADVANCED OPTIONS MENU"]); + readAdvancedMenu(input, mergedData["ADVANCED OPTIONS MENU"]); } - catch (...) { - std::cout << "Something went wrong in reading the ADVANCED OPTIONS MENU! Please double-check your input file with the reference in SampleRunDir!"; - exit(1); + catch (const std::exception& e) { + throw std::runtime_error("Something went wrong in reading the ADVANCED OPTIONS MENU! Please double-check your input file with the reference in Code.v05-00/defaults/input.yaml\n Exception: " + std::string(e.what())); } } void readSimMenu(OptInput& input, const YAML::Node& simNode){ diff --git a/Code.v05-00/tests/test1.yaml b/Code.v05-00/tests/test1.yaml index 74e9f4e1a..aafbdf300 100644 --- a/Code.v05-00/tests/test1.yaml +++ b/Code.v05-00/tests/test1.yaml @@ -100,7 +100,6 @@ METEOROLOGY MENU: Init wind shear from met. (T/F): T Wind shear time series input (T/F): T Interpolate shear met. data (T/F): T - Init vertical velocity from met. data: T Init vert. veloc. from met. data (T/F): T Vert. veloc. time series input (T/F): T Interpolate vert. veloc. met. data (T/F): T diff --git a/Code.v05-00/tests/test3.yaml b/Code.v05-00/tests/test3.yaml new file mode 100644 index 000000000..639afa9cc --- /dev/null +++ b/Code.v05-00/tests/test3.yaml @@ -0,0 +1,3 @@ +PARAMETER MENU: + Plume Process [hr] (double): 14 + INVALID YAML INPUT: 0 diff --git a/Code.v05-00/tests/test4.yaml b/Code.v05-00/tests/test4.yaml new file mode 100644 index 000000000..c80d8c7e0 --- /dev/null +++ b/Code.v05-00/tests/test4.yaml @@ -0,0 +1,9 @@ +PARAMETER MENU: + Plume Process [hr] (double): 14 + +METEOROLOGY MENU: + METEOROLOGICAL INPUT SUBMENU: + Use met. input (T/F): F + Met input file path (string): /path/to/met/input + INVALID YAML KEY: + BAD FIELD: 0 diff --git a/Code.v05-00/tests/test5.yaml b/Code.v05-00/tests/test5.yaml new file mode 100644 index 000000000..cd5baff31 --- /dev/null +++ b/Code.v05-00/tests/test5.yaml @@ -0,0 +1,7 @@ +METEOROLOGY MENU: + METEOROLOGICAL INPUT SUBMENU: + Use met. input (T/F): F + # This is a valid value in the default yaml + # but it is not a valid key! Should throw + Met input file path (string): + BAD VALUE: 0 diff --git a/Code.v05-00/tests/test_yamlreader.cpp b/Code.v05-00/tests/test_yamlreader.cpp index bb3a4c4e9..448f98710 100644 --- a/Code.v05-00/tests/test_yamlreader.cpp +++ b/Code.v05-00/tests/test_yamlreader.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include "APCEMM.h" @@ -458,3 +459,45 @@ TEST_CASE("Merge Input Files"){ REQUIRE(caseInput.coreExitTemp() == 547.3); REQUIRE(caseInput.bypassArea() == 1.804); } + +TEST_CASE("Validate Input Files"){ + OptInput input; + string validFile = string(APCEMM_TESTS_DIR)+"/test1.yaml"; + + string filename1 = string(APCEMM_TESTS_DIR)+"/test3.yaml"; + string filename2 = string(APCEMM_TESTS_DIR)+"/test4.yaml"; + string filename3 = string(APCEMM_TESTS_DIR)+"/test5.yaml"; + + SECTION("Invalid key (scalar) at the root level") { + // Check that it detects the invalid key, that it points to the correct file and that + // it prints out the name of the invalid key (here a scalar) + string invalid_file = string(APCEMM_TESTS_DIR) + "/test3.yaml"; + + REQUIRE_THROWS_WITH( + YamlInputReader::readYamlInputFiles(input, {validFile, filename1}), + Catch::Matchers::ContainsSubstring("Unknown key found") && + Catch::Matchers::ContainsSubstring("test3.yaml") && + Catch::Matchers::ContainsSubstring("INVALID YAML INPUT") + ); + } + + SECTION("Invalid key (map) in a submenu"){ + // Check that it detects the invalid key and that it prints out the name + // of the invalid key (here a map) + REQUIRE_THROWS_WITH( + YamlInputReader::readYamlInputFiles(input, {filename2}), + Catch::Matchers::ContainsSubstring("Unknown key found") && + Catch::Matchers::ContainsSubstring("INVALID YAML KEY") + ); + } + + SECTION("Valid key but wrong type (map instead of scalar)"){ + // Here we have a key that is supposed to be a scalar but instead is a map + // Check that if detects this and prints the name correctly + REQUIRE_THROWS_WITH( + YamlInputReader::readYamlInputFiles(input, {filename3}), + Catch::Matchers::ContainsSubstring("is a map in provided YAML but not in the default input.yaml") && + Catch::Matchers::ContainsSubstring("Met input file path (string)") + ); + } +}