Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 85 additions & 38 deletions Code.v05-00/src/YamlInputReader/YamlInputReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
#include <algorithm> // std::equal
#include <cctype> // std::tolower
#include <iostream>
#include <stdexcept>
#include <string_view> // std::string_view
#include <set>
#include <string>
#include <vector>


// Read default configuration from CMake-generated include file.
Expand All @@ -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<std::string> getYamlKeys(const YAML::Node& node) {
std::set<std::string> keys;
if (!node.IsMap()) {
return keys;
}
for (const auto& it : node) {
keys.insert(it.first.as<std::string>());
}
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<string> &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){
Expand Down
1 change: 0 additions & 1 deletion Code.v05-00/tests/test1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Code.v05-00/tests/test3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PARAMETER MENU:
Plume Process [hr] (double): 14
INVALID YAML INPUT: 0
9 changes: 9 additions & 0 deletions Code.v05-00/tests/test4.yaml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions Code.v05-00/tests/test5.yaml
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions Code.v05-00/tests/test_yamlreader.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <YamlInputReader/YamlInputReader.hpp>
#include <Core/Input.hpp>
#include "APCEMM.h"
Expand Down Expand Up @@ -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)")
);
}
}