From a3c1193bbc5a67380a92ef79e0436b675e0387f9 Mon Sep 17 00:00:00 2001 From: Alexheeeee Date: Fri, 9 Jan 2026 22:31:24 -0800 Subject: [PATCH 1/6] update README.md --- README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d807ecf..953d1a9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,27 @@ ## Introduction -yaml_reader.cpp is for manipulaing lattices direcly in C++ +yaml_reader.cpp is for manipulaing lattices direcly in C++. yaml_c_wrapper.cpp wraps YAML::Node into C objects so they can be part of a shared object library to interface with other languages -yaml_c_wrapper.cpp wraps YAML::Node into C objects so they can be part of -a shared object library to interface with other languages +First install yaml-cpp by running -In terminal, run - -mkdir build; cd build +macOS: +brew install yaml-cpp -cmake .. -DYAML_BUILD_SHARED_LIBS=ON +Linux: +sudo apt-get install libyaml-cpp-dev +Windows: +choco install yaml-cpp + +Manual Install: +git clone https://github.com/jbeder/yaml-cpp.git +cd yaml-cpp/src +mkdir build && cd build +cmake .. +cmake --build . +cmake --install . + +Next, in pals-cpp, run + +mkdir build && cd build +cmake .. -DYAML_BUILD_SHARED_LIBS=ON make \ No newline at end of file From 5475a92e2e94d546e4320662ff94f6da16d4777a Mon Sep 17 00:00:00 2001 From: Alexheeeee Date: Tue, 20 Jan 2026 15:26:33 -0500 Subject: [PATCH 2/6] made yaml_c_wrapper into library --- .gitignore | 2 + CMakeLists.txt | 59 +++++++++++++++++++++-- README.md | 14 ++++-- lattice_files/ex.yaml | 18 +++++++ src/yaml_c_wrapper.cpp | 105 ++++++++++++++++++++--------------------- src/yaml_c_wrapper.h | 74 +++++++++++++++++++++++++++++ src/yaml_reader.cpp | 99 ++------------------------------------ 7 files changed, 217 insertions(+), 154 deletions(-) create mode 100644 lattice_files/ex.yaml create mode 100644 src/yaml_c_wrapper.h diff --git a/.gitignore b/.gitignore index 2fb7407..a545519 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ build/ # debug information files *.dwo + +lattice_files/expand.yaml \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index bb3e9b9..9baf83c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,63 @@ project(YAMLWrapper) set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + if(APPLE) -## add macports - set(CMAKE_INSTALL_RPATH "@executable_path/../lib;@loader_path;/usr/local/lib;/opt/homebrew/lib") + # Check for package managers + execute_process(COMMAND which brew OUTPUT_VARIABLE HOMEBREW_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process(COMMAND which port OUTPUT_VARIABLE MACPORTS_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process(COMMAND which conda OUTPUT_VARIABLE CONDA_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) + + # Error if both Homebrew and MacPorts exist + if(HOMEBREW_EXISTS AND MACPORTS_EXISTS) + message(FATAL_ERROR + "Both Homebrew and MacPorts detected. This can cause conflicts.\n" + "Please use only one package manager:\n" + " - Homebrew found at: ${HOMEBREW_EXISTS}\n" + " - MacPorts found at: ${MACPORTS_EXISTS}\n" + "Consider uninstalling one to avoid library conflicts." + ) + endif() + + # Set RPATH based on which package manager is found + set(BASE_RPATH "@executable_path/../lib;@loader_path") + + if(HOMEBREW_EXISTS) + message(STATUS "Using Homebrew package manager") + # Check if Apple Silicon or Intel + execute_process( + COMMAND uname -m + OUTPUT_VARIABLE ARCH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(ARCH STREQUAL "arm64") + set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/homebrew/lib") + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") + else() + set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") + list(APPEND CMAKE_PREFIX_PATH "/usr/local") + endif() + elseif(MACPORTS_EXISTS) + message(STATUS "Using MacPorts package manager") + set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/local/lib") + list(APPEND CMAKE_PREFIX_PATH "/opt/local") + elseif(CONDA_EXISTS) + message(STATUS "Using Conda package manager") + # Get conda prefix + execute_process( + COMMAND conda info --base + OUTPUT_VARIABLE CONDA_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(CMAKE_INSTALL_RPATH "${BASE_RPATH};${CONDA_PREFIX}/lib") + list(APPEND CMAKE_PREFIX_PATH "${CONDA_PREFIX}") + else() + message(STATUS "No package manager detected, using default paths") + set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") + endif() + set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) + message(STATUS "RPATH set to: ${CMAKE_INSTALL_RPATH}") endif() find_package(yaml-cpp REQUIRED) @@ -16,4 +69,4 @@ add_library(yaml_c_wrapper SHARED src/yaml_c_wrapper.cpp) target_link_libraries(yaml_c_wrapper yaml-cpp) add_executable(yaml_reader src/yaml_reader.cpp) -target_link_libraries(yaml_reader yaml-cpp) \ No newline at end of file +target_link_libraries(yaml_reader yaml_c_wrapper) \ No newline at end of file diff --git a/README.md b/README.md index 953d1a9..bbf3f24 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ ## Introduction -yaml_reader.cpp is for manipulaing lattices direcly in C++. yaml_c_wrapper.cpp wraps YAML::Node into C objects so they can be part of a shared object library to interface with other languages +yaml_c_wrapper.cpp wraps YAML::Node into C objects so they can be part of a shared object library to interface with other languages First install yaml-cpp by running macOS: -brew install yaml-cpp +brew install yaml-cpp +or +port install yaml-cpp Linux: sudo apt-get install libyaml-cpp-dev @@ -24,4 +26,10 @@ Next, in pals-cpp, run mkdir build && cd build cmake .. -DYAML_BUILD_SHARED_LIBS=ON -make \ No newline at end of file +make + +## Example +See yaml_reader.cpp for an example of how to use the library to read a lattice file, +perform a basic manipulation, and write to another lattice file. +Navigate to the build directly and run +./yaml_reader \ No newline at end of file diff --git a/lattice_files/ex.yaml b/lattice_files/ex.yaml new file mode 100644 index 0000000..e29fa28 --- /dev/null +++ b/lattice_files/ex.yaml @@ -0,0 +1,18 @@ +- thingB: + kind: Sextupole + +- inj_line: + kind: BeamLine + multipass: true + length: 37.8 + zero_point: thingC + line: # This item refers to the name of an element or BeamLine defined elsewhere. + - thingZ: # thingZ inherits parameters from thingB + inherit: thingB + - Q1a: # Define an element in place called Q1a + kind: Quadrupole + length: 1.03 + direction: -1 + - a_subline: # Item a_subline is repeated three times + repeat: 3 + hi: 2 \ No newline at end of file diff --git a/src/yaml_c_wrapper.cpp b/src/yaml_c_wrapper.cpp index 15022cc..0e6b457 100644 --- a/src/yaml_c_wrapper.cpp +++ b/src/yaml_c_wrapper.cpp @@ -3,6 +3,58 @@ #include #include +// ======= LATTICE EXPANSION UTILS +// === DEEP COPY === + +YAML::Node deep_copy_internal(const YAML::Node& node) { + if (node.IsScalar()) { + return YAML::Node(node.as()); + } else if (node.IsSequence()) { + YAML::Node seq(YAML::NodeType::Sequence); + for (auto x : node) { + seq.push_back(deep_copy_internal(x)); + } + return seq; + } else if (node.IsMap()) { + YAML::Node map(YAML::NodeType::Map); + for (auto x : node) { + map[x.first.as()] = deep_copy_internal(x.second); + } + return map; + } else { + return YAML::Node(); + } +} +// === REPLACE === +YAML::Node replace_internal(YAML::Node name, std::map* seen) { + std::string str = name.as(); + if (seen->count(str)) { + return deep_copy_internal(seen->at(str)); + } else { + return name; + } +} + +// === EXPAND === +YAML::Node expand_internal(YAML::Node node, std::map* seen) { + if (node.IsSequence()) { + for (int i = 0; i < node.size(); i++) { + node[i] = expand_internal(node[i], seen); + } + return node; + } else if (node.IsScalar()) { + return replace_internal(node, seen); + } else if (node.IsMap()) { + for (auto ele : node) { + seen->insert({ele.first.as(), ele.second}); + node[ele.first.as()] = expand_internal(ele.second, seen); + } + return node; + } else { + return node; + } +} + extern "C" { typedef void* YAMLNodeHandle; @@ -368,59 +420,6 @@ extern "C" { YAMLNodeHandle yaml_clone(YAMLNodeHandle handle) { return new YAML::Node(YAML::Clone(*static_cast(handle))); } - - - // ======= LATTICE EXPANSION UTILS - // === DEEP COPY === - - YAML::Node deep_copy_internal(const YAML::Node& node) { - if (node.IsScalar()) { - return YAML::Node(node.as()); - } else if (node.IsSequence()) { - YAML::Node seq(YAML::NodeType::Sequence); - for (auto x : node) { - seq.push_back(deep_copy_internal(x)); - } - return seq; - } else if (node.IsMap()) { - YAML::Node map(YAML::NodeType::Map); - for (auto x : node) { - map[x.first.as()] = deep_copy_internal(x.second); - } - return map; - } else { - return YAML::Node(); - } - } - // === REPLACE === - YAML::Node replace_internal(YAML::Node name, std::map* seen) { - std::string str = name.as(); - if (seen->count(str)) { - return deep_copy_internal(seen->at(str)); - } else { - return name; - } - } - - // === EXPAND === - YAML::Node expand_internal(YAML::Node node, std::map* seen) { - if (node.IsSequence()) { - for (int i = 0; i < node.size(); i++) { - node[i] = expand_internal(node[i], seen); - } - return node; - } else if (node.IsScalar()) { - return replace_internal(node, seen); - } else if (node.IsMap()) { - for (auto ele : node) { - seen->insert({ele.first.as(), ele.second}); - node[ele.first.as()] = expand_internal(ele.second, seen); - } - return node; - } else { - return node; - } - } // C interface for expand - handles the map internally YAMLNodeHandle yaml_expand(YAMLNodeHandle handle) { diff --git a/src/yaml_c_wrapper.h b/src/yaml_c_wrapper.h new file mode 100644 index 0000000..cbd27fc --- /dev/null +++ b/src/yaml_c_wrapper.h @@ -0,0 +1,74 @@ +#ifndef YAML_C_WRAPPER_H +#define YAML_C_WRAPPER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* YAMLNodeHandle; + +// === CREATION === +YAMLNodeHandle yaml_create_node(void); +YAMLNodeHandle yaml_create_map(void); +YAMLNodeHandle yaml_create_sequence(void); +void yaml_delete_node(YAMLNodeHandle handle); + +// === PARSING === +YAMLNodeHandle yaml_parse(const char* yaml_str); +YAMLNodeHandle yaml_parse_file(const char* filename); + +// === TYPE CHECKING === +bool yaml_is_scalar(YAMLNodeHandle handle); +bool yaml_is_sequence(YAMLNodeHandle handle); +bool yaml_is_map(YAMLNodeHandle handle); +bool yaml_is_null(YAMLNodeHandle handle); +bool yaml_is_defined(YAMLNodeHandle handle); + +// === ACCESS === +YAMLNodeHandle yaml_get_key(YAMLNodeHandle handle, const char* key); +YAMLNodeHandle yaml_get_index(YAMLNodeHandle handle, int index); +bool yaml_has_key(YAMLNodeHandle handle, const char* key); +int yaml_size(YAMLNodeHandle handle); + +// === CONVERSION === +char* yaml_as_string(YAMLNodeHandle handle); +int yaml_as_int(YAMLNodeHandle handle); +double yaml_as_float(YAMLNodeHandle handle); +bool yaml_as_bool(YAMLNodeHandle handle); +void yaml_free_string(char* str); + +// === MODIFICATION === +void yaml_set_string(YAMLNodeHandle handle, const char* key, const char* value); +void yaml_set_int(YAMLNodeHandle handle, const char* key, int value); +void yaml_set_float(YAMLNodeHandle handle, const char* key, double value); +void yaml_set_bool(YAMLNodeHandle handle, const char* key, bool value); +void yaml_set_node(YAMLNodeHandle handle, const char* key, YAMLNodeHandle value); + +void yaml_push_string(YAMLNodeHandle handle, const char* value); +void yaml_push_int(YAMLNodeHandle handle, int value); +void yaml_push_float(YAMLNodeHandle handle, double value); +void yaml_push_bool(YAMLNodeHandle handle, bool value); +void yaml_push_node(YAMLNodeHandle handle, YAMLNodeHandle value); + +// === UTILITY === +char* yaml_to_string(YAMLNodeHandle handle); +char* yaml_emit(YAMLNodeHandle handle, int indent); +YAMLNodeHandle yaml_clone(YAMLNodeHandle handle); +YAMLNodeHandle yaml_deep_copy(YAMLNodeHandle handle); +YAMLNodeHandle yaml_expand(YAMLNodeHandle handle); + +bool yaml_write_file(YAMLNodeHandle handle, const char* filename); +bool yaml_write_file_formatted(YAMLNodeHandle handle, const char* filename, + int indent, bool flow_maps, bool flow_seqs); + +// === KEY UTILITIES === +char** yaml_get_keys(YAMLNodeHandle handle, int* out_count); +void yaml_free_keys(char** keys, int count); + +#ifdef __cplusplus +} +#endif + +#endif // YAML_C_WRAPPER_H \ No newline at end of file diff --git a/src/yaml_reader.cpp b/src/yaml_reader.cpp index 412dd63..398d021 100644 --- a/src/yaml_reader.cpp +++ b/src/yaml_reader.cpp @@ -1,98 +1,7 @@ -#include "yaml-cpp/yaml.h" -#include -#include -#include - -// run cmake .. -DYAML_BUILD_SHARED_LIBS=ON in the pals directory -// cd into build, run make, ./my_project -void print_recur(YAML::Node node); -YAML::Node replace(YAML::Node name, std::map *seen); -YAML::Node expand(YAML::Node node, std::map *seen); -YAML::Node deep_copy(const YAML::Node& node); +#include "yaml_c_wrapper.h" int main() { - // reads in a lattice from a yaml file - YAML::Node config = YAML::LoadFile("ex.yaml"); - std::map seen; - // manipulates lattice - config = expand(config, &seen); - - // saves lattice to new yaml file - std::ofstream outputFile("expand.yaml"); - YAML::Emitter out; - out << config; - outputFile << out.c_str(); -} - -// makes a deep copy of a node -YAML::Node deep_copy(const YAML::Node& node) { - if (node.IsScalar()) { - return YAML::Node(node.as()); - } else if (node.IsSequence()) { - YAML::Node seq(YAML::NodeType::Sequence); - for (auto x : node) { - seq.push_back(deep_copy(x)); - } - return seq; - } else if (node.IsMap()) { - YAML::Node map(YAML::NodeType::Map); - for (auto x : node) { - map[x.first.as()] = deep_copy(x.second); - } - return map; - } else { - return YAML::Node(); - } -} - -YAML::Node replace(YAML::Node name, std::map *seen) { - std::string str = name.as(); - if (seen->count(str)) { - std::cout << seen->at(str); - return deep_copy(seen->at(str)); - } else { - return name; - } -} - -/*/ -Performs lattice expansion (not implemented correctly yet) -*/ -YAML::Node expand(YAML::Node node, std::map *seen) { - if (node.IsSequence()) { - for (int i = 0; i < node.size(); i++) { - node[i] = expand(node[i], seen); - } - return node; - } else if (node.IsScalar()) { - return replace(node, seen); - } else if (node.IsMap()) { - for (auto ele : node) { - seen->insert({ele.first.as(), ele.second}); - ele.second = expand(ele.second, seen); - } - return node; - } else { - return node; - } -} - -/*/ -Prints out a node line by line to the terminal. -*/ -void print_recur(YAML::Node node) { - if (node.IsScalar()) { - std::cout << node.as() << "\n"; - } else if (node.IsSequence()) { - for (auto x : node) { - print_recur(x); - } - } else if (node.IsMap()) { - for (auto x : node) { - std::cout << x.first.as() << "\n"; - print_recur(x.second); - } - } else { - std::cout << "Null"; - } + YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); + handle = yaml_expand(handle); + yaml_write_file(handle, "../lattice_files/expand.yaml"); } \ No newline at end of file From b7908e4f01c06f489167c43c01d2be9973253c8e Mon Sep 17 00:00:00 2001 From: Alexheeeee Date: Tue, 20 Jan 2026 15:53:36 -0500 Subject: [PATCH 3/6] examples --- src/yaml_c_wrapper.cpp | 23 +---------------------- src/yaml_c_wrapper.h | 1 - src/yaml_reader.cpp | 7 +++++++ 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/yaml_c_wrapper.cpp b/src/yaml_c_wrapper.cpp index 0e6b457..a1569f5 100644 --- a/src/yaml_c_wrapper.cpp +++ b/src/yaml_c_wrapper.cpp @@ -4,32 +4,11 @@ #include // ======= LATTICE EXPANSION UTILS -// === DEEP COPY === - -YAML::Node deep_copy_internal(const YAML::Node& node) { - if (node.IsScalar()) { - return YAML::Node(node.as()); - } else if (node.IsSequence()) { - YAML::Node seq(YAML::NodeType::Sequence); - for (auto x : node) { - seq.push_back(deep_copy_internal(x)); - } - return seq; - } else if (node.IsMap()) { - YAML::Node map(YAML::NodeType::Map); - for (auto x : node) { - map[x.first.as()] = deep_copy_internal(x.second); - } - return map; - } else { - return YAML::Node(); - } -} // === REPLACE === YAML::Node replace_internal(YAML::Node name, std::map* seen) { std::string str = name.as(); if (seen->count(str)) { - return deep_copy_internal(seen->at(str)); + return YAML::Clone(seen->at(str)); } else { return name; } diff --git a/src/yaml_c_wrapper.h b/src/yaml_c_wrapper.h index cbd27fc..e4c5e05 100644 --- a/src/yaml_c_wrapper.h +++ b/src/yaml_c_wrapper.h @@ -56,7 +56,6 @@ void yaml_push_node(YAMLNodeHandle handle, YAMLNodeHandle value); char* yaml_to_string(YAMLNodeHandle handle); char* yaml_emit(YAMLNodeHandle handle, int indent); YAMLNodeHandle yaml_clone(YAMLNodeHandle handle); -YAMLNodeHandle yaml_deep_copy(YAMLNodeHandle handle); YAMLNodeHandle yaml_expand(YAMLNodeHandle handle); bool yaml_write_file(YAMLNodeHandle handle, const char* filename); diff --git a/src/yaml_reader.cpp b/src/yaml_reader.cpp index 398d021..35226ff 100644 --- a/src/yaml_reader.cpp +++ b/src/yaml_reader.cpp @@ -1,7 +1,14 @@ #include "yaml_c_wrapper.h" +#include int main() { YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); handle = yaml_expand(handle); yaml_write_file(handle, "../lattice_files/expand.yaml"); + + // type checking: + std::cout << (yaml_is_sequence(handle)) << "\n"; + + // access: + std::cout << yaml_to_string(yaml_get_index(handle, 0)) << "\n"; } \ No newline at end of file From 470dbc741c8a1cffc7da3d10562a42025f74cdb0 Mon Sep 17 00:00:00 2001 From: Alexheeeee Date: Sun, 25 Jan 2026 21:00:29 -0500 Subject: [PATCH 4/6] docs and testing setup --- .github/workflows/ci.yaml | 24 +++++++++++++++ CMakeLists.txt | 18 +++++++++-- README.md | 33 ++++++++++++++------- examples/yaml_reader.cpp | 38 ++++++++++++++++++++++++ src/yaml_c_wrapper.cpp | 56 ++++++++++++++++++++++++----------- src/yaml_c_wrapper.h | 8 ++++- src/yaml_reader.cpp | 14 --------- tests/CMakeLists.txt | 14 +++++++++ tests/test_yaml_c_wrapper.cpp | 10 +++++++ 9 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 examples/yaml_reader.cpp delete mode 100644 src/yaml_reader.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_yaml_c_wrapper.cpp diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..e3f373e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +name: C++ CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-test: + name: C++ - ${{ github.event_name }} + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + # 1. Check out your code + - uses: actions/checkout@v4 + # # 2. Install dependencies (if using vcpkg, Conan, etc.) + # - name: Install dependencies + # run: sudo apt update && sudo apt install -y cmake g++ make + - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + - run: cmake --build build --parallel + - run: ctest --test-dir build --output-on-failure --verbose --no-compress-output diff --git a/CMakeLists.txt b/CMakeLists.txt index 9baf83c..eb6d56b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,22 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.16) project(YAMLWrapper) set(CMAKE_CXX_STANDARD 17) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +enable_testing() +include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.5.2 +) + +FetchContent_MakeAvailable(Catch2) +add_subdirectory(tests) + if(APPLE) # Check for package managers execute_process(COMMAND which brew OUTPUT_VARIABLE HOMEBREW_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) @@ -68,5 +80,5 @@ find_package(yaml-cpp REQUIRED) add_library(yaml_c_wrapper SHARED src/yaml_c_wrapper.cpp) target_link_libraries(yaml_c_wrapper yaml-cpp) -add_executable(yaml_reader src/yaml_reader.cpp) -target_link_libraries(yaml_reader yaml_c_wrapper) \ No newline at end of file +add_executable(yaml_reader examples/yaml_reader.cpp) +target_link_libraries(yaml_reader yaml_c_wrapper) diff --git a/README.md b/README.md index bbf3f24..caab6c6 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,27 @@ cmake .. cmake --build . cmake --install . -Next, in pals-cpp, run +## Usage +In pals-cpp, run -mkdir build && cd build -cmake .. -DYAML_BUILD_SHARED_LIBS=ON -make - -## Example -See yaml_reader.cpp for an example of how to use the library to read a lattice file, -perform a basic manipulation, and write to another lattice file. -Navigate to the build directly and run -./yaml_reader \ No newline at end of file +mkdir build +cmake -S . -B build +cmake --build build +ctest --test-dir build + +This builds libyaml_c_wrapper.dylib, a shared object library that can be used +by other languages. + +It also builds an executable using yaml_reader.cpp containing examples for how +to use the library to read lattice files, perform basic manipulations, and write +to other lattice files. To see the output, navigate to the build directory and run +./yaml_reader + +It will also build the tests. + +## Testing +In the root pals-cpp directory, run +ctest --test-dir build --output-on-failure + +To run a specific test, run +ctest --test-dir build -R "Test Name" diff --git a/examples/yaml_reader.cpp b/examples/yaml_reader.cpp new file mode 100644 index 0000000..1915b6b --- /dev/null +++ b/examples/yaml_reader.cpp @@ -0,0 +1,38 @@ +#include "../src/yaml_c_wrapper.h" +#include + +int main() { + // reading a lattice from a yaml file + YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); + // printing to terminal + std::cout << yaml_to_string(handle) << std::endl << std::endl; + + // type checking + std::cout << (yaml_is_sequence(handle)) << "\n"; + + // accessing sequence + YAMLNodeHandle node = yaml_get_index(handle, 0); + std::cout << "the first element is: \n" << yaml_to_string(node) << "\n"; + + // accessing map + std::cout << "\nthe value at key 'thingB' is: " << yaml_to_string(yaml_get_key(node, "thingB")) << "\n"; + + // creating a new node that's a map + YAMLNodeHandle map = yaml_create_map(); + yaml_set_int(map, "first", 1); + + // creating a new node that's a sequence + YAMLNodeHandle sequence = yaml_create_sequence(); + yaml_push_string(sequence, "magnet1"); + yaml_push_string(sequence, ""); + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_string(scalar, "magnet2"); + yaml_set_at_index(sequence, 1, scalar); + + // adding new nodes to lattice + yaml_push_node(handle, map); + yaml_push_node(handle, sequence); + + // writing modified lattice file to expand.yaml + yaml_write_file(handle, "../lattice_files/expand.yaml"); +} \ No newline at end of file diff --git a/src/yaml_c_wrapper.cpp b/src/yaml_c_wrapper.cpp index a1569f5..6fa9c15 100644 --- a/src/yaml_c_wrapper.cpp +++ b/src/yaml_c_wrapper.cpp @@ -125,9 +125,6 @@ extern "C" { return static_cast(handle)->size(); } - // === ITERATION HELPERS === - - // For iterating over maps - get all keys char** yaml_get_keys(YAMLNodeHandle handle, int* out_count) { auto node = static_cast(handle); if (!node->IsMap()) { @@ -149,20 +146,6 @@ extern "C" { return result; } - void yaml_free_keys(char** keys, int count) { - for (int i = 0; i < count; i++) { - delete[] keys[i]; - } - delete[] keys; - } - - // Set node at index for sequences - void yaml_set_at_index(YAMLNodeHandle handle, int index, YAMLNodeHandle value) { - auto node = static_cast(handle); - auto val_node = static_cast(value); - (*node)[index] = *val_node; - } - // === CONVERT TO C TYPES (caller must free returned strings) === char* yaml_as_string(YAMLNodeHandle handle) { try { @@ -225,6 +208,37 @@ extern "C" { auto val_node = static_cast(value); (*node)[key] = *val_node; } + + void yaml_set_scalar_string(YAMLNodeHandle handle, const char* value) { + if (handle == nullptr || value == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + void yaml_set_scalar_int(YAMLNodeHandle handle, int value) { + if (handle == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + void yaml_set_scalar_float(YAMLNodeHandle handle, double value) { + if (handle == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + void yaml_set_scalar_bool(YAMLNodeHandle handle, bool value) { + if (handle == nullptr) return; + auto node = static_cast(handle); + *node = value; + } + + // Set node at index for sequences + void yaml_set_at_index(YAMLNodeHandle handle, int index, YAMLNodeHandle value) { + auto node = static_cast(handle); + auto val_node = static_cast(value); + (*node)[index] = *val_node; + } void yaml_push_string(YAMLNodeHandle handle, const char* value) { auto node = static_cast(handle); @@ -395,6 +409,14 @@ extern "C" { void yaml_free_string(char* str) { delete[] str; } + + void yaml_free_keys(char** keys, int count) { + for (int i = 0; i < count; i++) { + delete[] keys[i]; + } + delete[] keys; + } + YAMLNodeHandle yaml_clone(YAMLNodeHandle handle) { return new YAML::Node(YAML::Clone(*static_cast(handle))); diff --git a/src/yaml_c_wrapper.h b/src/yaml_c_wrapper.h index e4c5e05..319a9cb 100644 --- a/src/yaml_c_wrapper.h +++ b/src/yaml_c_wrapper.h @@ -13,6 +13,7 @@ typedef void* YAMLNodeHandle; YAMLNodeHandle yaml_create_node(void); YAMLNodeHandle yaml_create_map(void); YAMLNodeHandle yaml_create_sequence(void); +YAMLNodeHandle yaml_create_scalar(); void yaml_delete_node(YAMLNodeHandle handle); // === PARSING === @@ -24,7 +25,6 @@ bool yaml_is_scalar(YAMLNodeHandle handle); bool yaml_is_sequence(YAMLNodeHandle handle); bool yaml_is_map(YAMLNodeHandle handle); bool yaml_is_null(YAMLNodeHandle handle); -bool yaml_is_defined(YAMLNodeHandle handle); // === ACCESS === YAMLNodeHandle yaml_get_key(YAMLNodeHandle handle, const char* key); @@ -46,6 +46,12 @@ void yaml_set_float(YAMLNodeHandle handle, const char* key, double value); void yaml_set_bool(YAMLNodeHandle handle, const char* key, bool value); void yaml_set_node(YAMLNodeHandle handle, const char* key, YAMLNodeHandle value); +void yaml_set_scalar_string(YAMLNodeHandle handle, const char* value); +void yaml_set_scalar_int(YAMLNodeHandle handle, int value); +void yaml_set_scalar_float(YAMLNodeHandle handle, double value); +void yaml_set_scalar_bool(YAMLNodeHandle handle, bool value); + +void yaml_set_at_index(YAMLNodeHandle handle, int index, YAMLNodeHandle value) ; void yaml_push_string(YAMLNodeHandle handle, const char* value); void yaml_push_int(YAMLNodeHandle handle, int value); void yaml_push_float(YAMLNodeHandle handle, double value); diff --git a/src/yaml_reader.cpp b/src/yaml_reader.cpp deleted file mode 100644 index 35226ff..0000000 --- a/src/yaml_reader.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "yaml_c_wrapper.h" -#include - -int main() { - YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); - handle = yaml_expand(handle); - yaml_write_file(handle, "../lattice_files/expand.yaml"); - - // type checking: - std::cout << (yaml_is_sequence(handle)) << "\n"; - - // access: - std::cout << yaml_to_string(yaml_get_index(handle, 0)) << "\n"; -} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..dad23e1 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +include(CTest) +add_executable(tests + test_yaml_c_wrapper.cpp +) + +target_link_libraries(tests + PRIVATE + Catch2::Catch2WithMain +) + +include(Catch) +Catch_discover_tests(tests) +target_compile_features(tests PRIVATE cxx_std_17) + diff --git a/tests/test_yaml_c_wrapper.cpp b/tests/test_yaml_c_wrapper.cpp new file mode 100644 index 0000000..6624460 --- /dev/null +++ b/tests/test_yaml_c_wrapper.cpp @@ -0,0 +1,10 @@ +#include + +int add(int a, int b) { + return a + b; +} + +TEST_CASE("Addition works", "[math]") { + REQUIRE(add(2, 3) == 5); + CHECK(add(-1, 1) == 0); +} \ No newline at end of file From cef5d1c3d8a15cf5f09f322b0d66ee559d1be1f7 Mon Sep 17 00:00:00 2001 From: Alexheeeee Date: Sun, 25 Jan 2026 21:25:47 -0500 Subject: [PATCH 5/6] use fetchcontent for yaml-cpp --- CMakeLists.txt | 8 ++++++-- README.md | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eb6d56b..0c46bf0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,12 +8,18 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) enable_testing() include(FetchContent) +FetchContent_Declare( + yaml-cpp + GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git + GIT_TAG 0.8.0 +) FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.5.2 ) +FetchContent_MakeAvailable(yaml-cpp) FetchContent_MakeAvailable(Catch2) add_subdirectory(tests) @@ -75,8 +81,6 @@ if(APPLE) message(STATUS "RPATH set to: ${CMAKE_INSTALL_RPATH}") endif() -find_package(yaml-cpp REQUIRED) - add_library(yaml_c_wrapper SHARED src/yaml_c_wrapper.cpp) target_link_libraries(yaml_c_wrapper yaml-cpp) diff --git a/README.md b/README.md index caab6c6..b07615d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ cmake --install . In pals-cpp, run mkdir build -cmake -S . -B build +cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -S . -B build cmake --build build ctest --test-dir build @@ -46,3 +46,7 @@ ctest --test-dir build --output-on-failure To run a specific test, run ctest --test-dir build -R "Test Name" + +## Issue +yaml-cpp's cmake only requires cmake version 3.4, which is deprecated. Warnings must +be suppressed to run properly \ No newline at end of file From 7b49390ef7186559859082b3f595118bebb05d06 Mon Sep 17 00:00:00 2001 From: Alexheeeee Date: Mon, 26 Jan 2026 21:57:43 -0500 Subject: [PATCH 6/6] added tests --- CMakeLists.txt | 109 ++--- README.md | 6 +- lattice_files/ex.yaml | 3 +- src/yaml_reader.cpp | 14 - tests/CMakeLists.txt | 4 +- tests/test_yaml_c_wrapper.cpp | 745 +++++++++++++++++++++++++++++++++- 6 files changed, 801 insertions(+), 80 deletions(-) delete mode 100644 src/yaml_reader.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c46bf0..aa850c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,63 +23,66 @@ FetchContent_MakeAvailable(yaml-cpp) FetchContent_MakeAvailable(Catch2) add_subdirectory(tests) -if(APPLE) - # Check for package managers - execute_process(COMMAND which brew OUTPUT_VARIABLE HOMEBREW_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND which port OUTPUT_VARIABLE MACPORTS_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND which conda OUTPUT_VARIABLE CONDA_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) +file(COPY ${CMAKE_SOURCE_DIR}/lattice_files/ + DESTINATION ${CMAKE_BINARY_DIR}/lattice_files/) + +# if(APPLE) +# # Check for package managers +# execute_process(COMMAND which brew OUTPUT_VARIABLE HOMEBREW_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) +# execute_process(COMMAND which port OUTPUT_VARIABLE MACPORTS_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) +# execute_process(COMMAND which conda OUTPUT_VARIABLE CONDA_EXISTS OUTPUT_STRIP_TRAILING_WHITESPACE) - # Error if both Homebrew and MacPorts exist - if(HOMEBREW_EXISTS AND MACPORTS_EXISTS) - message(FATAL_ERROR - "Both Homebrew and MacPorts detected. This can cause conflicts.\n" - "Please use only one package manager:\n" - " - Homebrew found at: ${HOMEBREW_EXISTS}\n" - " - MacPorts found at: ${MACPORTS_EXISTS}\n" - "Consider uninstalling one to avoid library conflicts." - ) - endif() +# # Error if both Homebrew and MacPorts exist +# if(HOMEBREW_EXISTS AND MACPORTS_EXISTS) +# message(FATAL_ERROR +# "Both Homebrew and MacPorts detected. This can cause conflicts.\n" +# "Please use only one package manager:\n" +# " - Homebrew found at: ${HOMEBREW_EXISTS}\n" +# " - MacPorts found at: ${MACPORTS_EXISTS}\n" +# "Consider uninstalling one to avoid library conflicts." +# ) +# endif() - # Set RPATH based on which package manager is found - set(BASE_RPATH "@executable_path/../lib;@loader_path") +# # Set RPATH based on which package manager is found +# set(BASE_RPATH "@executable_path/../lib;@loader_path") - if(HOMEBREW_EXISTS) - message(STATUS "Using Homebrew package manager") - # Check if Apple Silicon or Intel - execute_process( - COMMAND uname -m - OUTPUT_VARIABLE ARCH - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(ARCH STREQUAL "arm64") - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/homebrew/lib") - list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") - else() - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") - list(APPEND CMAKE_PREFIX_PATH "/usr/local") - endif() - elseif(MACPORTS_EXISTS) - message(STATUS "Using MacPorts package manager") - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/local/lib") - list(APPEND CMAKE_PREFIX_PATH "/opt/local") - elseif(CONDA_EXISTS) - message(STATUS "Using Conda package manager") - # Get conda prefix - execute_process( - COMMAND conda info --base - OUTPUT_VARIABLE CONDA_PREFIX - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};${CONDA_PREFIX}/lib") - list(APPEND CMAKE_PREFIX_PATH "${CONDA_PREFIX}") - else() - message(STATUS "No package manager detected, using default paths") - set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") - endif() +# if(HOMEBREW_EXISTS) +# message(STATUS "Using Homebrew package manager") +# # Check if Apple Silicon or Intel +# execute_process( +# COMMAND uname -m +# OUTPUT_VARIABLE ARCH +# OUTPUT_STRIP_TRAILING_WHITESPACE +# ) +# if(ARCH STREQUAL "arm64") +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/homebrew/lib") +# list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") +# else() +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") +# list(APPEND CMAKE_PREFIX_PATH "/usr/local") +# endif() +# elseif(MACPORTS_EXISTS) +# message(STATUS "Using MacPorts package manager") +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/opt/local/lib") +# list(APPEND CMAKE_PREFIX_PATH "/opt/local") +# elseif(CONDA_EXISTS) +# message(STATUS "Using Conda package manager") +# # Get conda prefix +# execute_process( +# COMMAND conda info --base +# OUTPUT_VARIABLE CONDA_PREFIX +# OUTPUT_STRIP_TRAILING_WHITESPACE +# ) +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};${CONDA_PREFIX}/lib") +# list(APPEND CMAKE_PREFIX_PATH "${CONDA_PREFIX}") +# else() +# message(STATUS "No package manager detected, using default paths") +# set(CMAKE_INSTALL_RPATH "${BASE_RPATH};/usr/local/lib") +# endif() - set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) - message(STATUS "RPATH set to: ${CMAKE_INSTALL_RPATH}") -endif() +# set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +# message(STATUS "RPATH set to: ${CMAKE_INSTALL_RPATH}") +# endif() add_library(yaml_c_wrapper SHARED src/yaml_c_wrapper.cpp) target_link_libraries(yaml_c_wrapper yaml-cpp) diff --git a/README.md b/README.md index b07615d..c9d2365 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Introduction yaml_c_wrapper.cpp wraps YAML::Node into C objects so they can be part of a shared object library to interface with other languages - + ## Usage In pals-cpp, run -mkdir build cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -S . -B build cmake --build build -ctest --test-dir build This builds libyaml_c_wrapper.dylib, a shared object library that can be used by other languages. diff --git a/lattice_files/ex.yaml b/lattice_files/ex.yaml index e29fa28..a9e9898 100644 --- a/lattice_files/ex.yaml +++ b/lattice_files/ex.yaml @@ -14,5 +14,4 @@ length: 1.03 direction: -1 - a_subline: # Item a_subline is repeated three times - repeat: 3 - hi: 2 \ No newline at end of file + repeat: 3 \ No newline at end of file diff --git a/src/yaml_reader.cpp b/src/yaml_reader.cpp deleted file mode 100644 index 35226ff..0000000 --- a/src/yaml_reader.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "yaml_c_wrapper.h" -#include - -int main() { - YAMLNodeHandle handle = yaml_parse_file("../lattice_files/ex.yaml"); - handle = yaml_expand(handle); - yaml_write_file(handle, "../lattice_files/expand.yaml"); - - // type checking: - std::cout << (yaml_is_sequence(handle)) << "\n"; - - // access: - std::cout << yaml_to_string(yaml_get_index(handle, 0)) << "\n"; -} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dad23e1..3cf143d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,10 +5,10 @@ add_executable(tests target_link_libraries(tests PRIVATE - Catch2::Catch2WithMain + yaml_c_wrapper + Catch2::Catch2WithMain ) include(Catch) Catch_discover_tests(tests) target_compile_features(tests PRIVATE cxx_std_17) - diff --git a/tests/test_yaml_c_wrapper.cpp b/tests/test_yaml_c_wrapper.cpp index 6624460..1fcb5f6 100644 --- a/tests/test_yaml_c_wrapper.cpp +++ b/tests/test_yaml_c_wrapper.cpp @@ -1,10 +1,745 @@ +#define CATCH_CONFIG_MAIN #include +#include +#include "../src/yaml_c_wrapper.h" +#include +#include +#include -int add(int a, int b) { - return a + b; +using Catch::Approx; + +// Helper to create test YAML files +void create_test_file(const std::string& filename, const std::string& content) { + std::ofstream file(filename); + file << content; + file.close(); +} + +// Helper to read file content +std::string read_file(const std::string& filename) { + std::ifstream file(filename); + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + return content; +} + +// Helper to clean up test files +void cleanup_file(const std::string& filename) { + std::remove(filename.c_str()); +} + +// =========================================== +// TEST SUITE: Creation and Deletion +// =========================================== + +TEST_CASE("YAML nodes can be created and deleted", "[creation]") { + SECTION("Create empty node") { + YAMLNodeHandle node = yaml_create_node(); + REQUIRE(node != nullptr); + yaml_delete_node(node); + } + + SECTION("Create map node") { + YAMLNodeHandle map = yaml_create_map(); + REQUIRE(map != nullptr); + REQUIRE(yaml_is_map(map)); + yaml_delete_node(map); + } + + SECTION("Create sequence node") { + YAMLNodeHandle seq = yaml_create_sequence(); + REQUIRE(seq != nullptr); + REQUIRE(yaml_is_sequence(seq)); + yaml_delete_node(seq); + } + + SECTION("Create scalar node") { + YAMLNodeHandle scalar = yaml_create_scalar(); + REQUIRE(scalar != nullptr); + REQUIRE(yaml_is_scalar(scalar)); + yaml_delete_node(scalar); + } +} + +// =========================================== +// TEST SUITE: Parsing +// =========================================== + +TEST_CASE("YAML can be parsed from strings", "[parsing]") { + SECTION("Parse simple map") { + const char* yaml = "key: value"; + YAMLNodeHandle node = yaml_parse(yaml); + + REQUIRE(node != nullptr); + REQUIRE(yaml_is_map(node)); + REQUIRE(yaml_has_key(node, "key")); + + yaml_delete_node(node); + } + + SECTION("Parse sequence") { + const char* yaml = "[a, b, c]"; + YAMLNodeHandle node = yaml_parse(yaml); + + REQUIRE(node != nullptr); + REQUIRE(yaml_is_sequence(node)); + REQUIRE(yaml_size(node) == 3); + + yaml_delete_node(node); + } + + SECTION("Parse invalid YAML returns nullptr") { + const char* invalid = "invalid: yaml: :"; + YAMLNodeHandle node = yaml_parse(invalid); + + REQUIRE(node == nullptr); + } +} + +TEST_CASE("YAML can be parsed from files", "[parsing][file]") { + SECTION("Parse valid file") { + YAMLNodeHandle node = yaml_parse_file("../lattice_files/ex.yaml"); + + REQUIRE(node != nullptr); + REQUIRE(yaml_is_sequence(node)); + REQUIRE(yaml_size(node) >= 2); + + yaml_delete_node(node); + } + + SECTION("Parse non-existent file returns nullptr") { + YAMLNodeHandle node = yaml_parse_file("nonexistent.yaml"); + REQUIRE(node == nullptr); + } } -TEST_CASE("Addition works", "[math]") { - REQUIRE(add(2, 3) == 5); - CHECK(add(-1, 1) == 0); +// =========================================== +// TEST SUITE: Type Checking +// =========================================== + +TEST_CASE("Node types can be checked", "[types]") { + SECTION("Check scalar type") { + YAMLNodeHandle node = yaml_parse("value"); + REQUIRE(yaml_is_scalar(node)); + REQUIRE_FALSE(yaml_is_map(node)); + REQUIRE_FALSE(yaml_is_sequence(node)); + yaml_delete_node(node); + } + + SECTION("Check map type") { + YAMLNodeHandle node = yaml_parse("key: value"); + REQUIRE(yaml_is_map(node)); + REQUIRE_FALSE(yaml_is_scalar(node)); + REQUIRE_FALSE(yaml_is_sequence(node)); + yaml_delete_node(node); + } + + SECTION("Check sequence type") { + YAMLNodeHandle node = yaml_parse("[a, b, c]"); + REQUIRE(yaml_is_sequence(node)); + REQUIRE_FALSE(yaml_is_map(node)); + REQUIRE_FALSE(yaml_is_scalar(node)); + yaml_delete_node(node); + } + + SECTION("Check null type") { + YAMLNodeHandle node = yaml_parse("null"); + REQUIRE(yaml_is_null(node)); + yaml_delete_node(node); + } +} + +// =========================================== +// TEST SUITE: Access Operations +// =========================================== + +TEST_CASE("Map keys can be accessed", "[access][map]") { + const char* yaml = "name: test\nvalue: 42"; + YAMLNodeHandle node = yaml_parse(yaml); + + SECTION("Check key existence") { + REQUIRE(yaml_has_key(node, "name")); + REQUIRE(yaml_has_key(node, "value")); + REQUIRE_FALSE(yaml_has_key(node, "nonexistent")); + } + + SECTION("Get key value") { + YAMLNodeHandle name = yaml_get_key(node, "name"); + REQUIRE(name != nullptr); + + char* str = yaml_as_string(name); + REQUIRE(std::string(str) == "test"); + + yaml_free_string(str); + yaml_delete_node(name); + } + + SECTION("Get non-existent key returns nullptr") { + YAMLNodeHandle missing = yaml_get_key(node, "missing"); + REQUIRE(missing == nullptr); + } + + SECTION("Get all keys") { + int count; + char** keys = yaml_get_keys(node, &count); + + REQUIRE(count == 2); + REQUIRE(keys != nullptr); + + bool has_name = false, has_value = false; + for (int i = 0; i < count; i++) { + if (std::string(keys[i]) == "name") has_name = true; + if (std::string(keys[i]) == "value") has_value = true; + } + + REQUIRE(has_name); + REQUIRE(has_value); + + yaml_free_keys(keys, count); + } + + yaml_delete_node(node); +} + +TEST_CASE("Sequence indices can be accessed", "[access][sequence]") { + const char* yaml = "[apple, banana, cherry]"; + YAMLNodeHandle node = yaml_parse(yaml); + + SECTION("Check size") { + REQUIRE(yaml_size(node) == 3); + } + + SECTION("Access valid index") { + YAMLNodeHandle item = yaml_get_index(node, 1); + REQUIRE(item != nullptr); + + char* str = yaml_as_string(item); + REQUIRE(std::string(str) == "banana"); + + yaml_free_string(str); + yaml_delete_node(item); + } + + SECTION("Access out of bounds index returns nullptr") { + YAMLNodeHandle item = yaml_get_index(node, 999); + REQUIRE(item == nullptr); + } + + SECTION("Access negative index returns nullptr") { + YAMLNodeHandle item = yaml_get_index(node, -1); + REQUIRE(item == nullptr); + } + + yaml_delete_node(node); +} + +// =========================================== +// TEST SUITE: Type Conversions +// =========================================== + +TEST_CASE("Values can be converted to C types", "[conversion]") { + SECTION("Convert to string") { + YAMLNodeHandle node = yaml_parse("test_value"); + char* str = yaml_as_string(node); + + REQUIRE(str != nullptr); + REQUIRE(std::string(str) == "test_value"); + + yaml_free_string(str); + yaml_delete_node(node); + } + + SECTION("Convert to int") { + YAMLNodeHandle node = yaml_parse("42"); + int val = yaml_as_int(node); + + REQUIRE(val == 42); + + yaml_delete_node(node); + } + + SECTION("Convert to float") { + YAMLNodeHandle node = yaml_parse("3.14"); + double val = yaml_as_float(node); + + REQUIRE(val == Approx(3.14)); + + yaml_delete_node(node); + } + + SECTION("Convert to bool") { + YAMLNodeHandle node_true = yaml_parse("true"); + YAMLNodeHandle node_false = yaml_parse("false"); + + REQUIRE(yaml_as_bool(node_true) == true); + REQUIRE(yaml_as_bool(node_false) == false); + + yaml_delete_node(node_true); + yaml_delete_node(node_false); + } + + SECTION("Invalid conversion returns default") { + YAMLNodeHandle node = yaml_parse("[a, b, c]"); + + // Can't convert sequence to string + char* str = yaml_as_string(node); + REQUIRE(str == nullptr); + + yaml_delete_node(node); + } +} + +// =========================================== +// TEST SUITE: Modification - Maps +// =========================================== + +TEST_CASE("Map values can be set", "[modification][map]") { + YAMLNodeHandle map = yaml_create_map(); + + SECTION("Set string value") { + yaml_set_string(map, "name", "test"); + REQUIRE(yaml_has_key(map, "name")); + + YAMLNodeHandle value = yaml_get_key(map, "name"); + char* str = yaml_as_string(value); + REQUIRE(std::string(str) == "test"); + + yaml_free_string(str); + yaml_delete_node(value); + } + + SECTION("Set int value") { + yaml_set_int(map, "count", 42); + + YAMLNodeHandle value = yaml_get_key(map, "count"); + REQUIRE(yaml_as_int(value) == 42); + + yaml_delete_node(value); + } + + SECTION("Set float value") { + yaml_set_float(map, "pi", 3.14); + + YAMLNodeHandle value = yaml_get_key(map, "pi"); + REQUIRE(yaml_as_float(value) == Catch::Approx(3.14)); + + yaml_delete_node(value); + } + + SECTION("Set bool value") { + yaml_set_bool(map, "enabled", true); + + YAMLNodeHandle value = yaml_get_key(map, "enabled"); + REQUIRE(yaml_as_bool(value) == true); + + yaml_delete_node(value); + } + + SECTION("Set nested node") { + YAMLNodeHandle nested = yaml_create_map(); + yaml_set_string(nested, "inner", "value"); + + yaml_set_node(map, "nested", nested); + + REQUIRE(yaml_has_key(map, "nested")); + YAMLNodeHandle retrieved = yaml_get_key(map, "nested"); + REQUIRE(yaml_is_map(retrieved)); + + yaml_delete_node(retrieved); + yaml_delete_node(nested); + } + + yaml_delete_node(map); +} + +// =========================================== +// TEST SUITE: Modification - Sequences +// =========================================== + +TEST_CASE("Sequence values can be pushed", "[modification][sequence]") { + YAMLNodeHandle seq = yaml_create_sequence(); + + SECTION("Push string values") { + yaml_push_string(seq, "first"); + yaml_push_string(seq, "second"); + + REQUIRE(yaml_size(seq) == 2); + + YAMLNodeHandle item = yaml_get_index(seq, 1); + char* str = yaml_as_string(item); + REQUIRE(std::string(str) == "second"); + + yaml_free_string(str); + yaml_delete_node(item); + } + + SECTION("Push int values") { + yaml_push_int(seq, 10); + yaml_push_int(seq, 20); + + REQUIRE(yaml_size(seq) == 2); + + YAMLNodeHandle item = yaml_get_index(seq, 0); + REQUIRE(yaml_as_int(item) == 10); + + yaml_delete_node(item); + } + + SECTION("Push float values") { + yaml_push_float(seq, 1.1); + yaml_push_float(seq, 2.2); + + REQUIRE(yaml_size(seq) == 2); + + YAMLNodeHandle item = yaml_get_index(seq, 1); + REQUIRE(yaml_as_float(item) == Catch::Approx(2.2)); + + yaml_delete_node(item); + } + + SECTION("Push node") { + YAMLNodeHandle node = yaml_create_map(); + yaml_set_string(node, "key", "value"); + + yaml_push_node(seq, node); + + REQUIRE(yaml_size(seq) == 1); + + YAMLNodeHandle retrieved = yaml_get_index(seq, 0); + REQUIRE(yaml_is_map(retrieved)); + + yaml_delete_node(retrieved); + yaml_delete_node(node); + } + + yaml_delete_node(seq); +} + +TEST_CASE("Sequence values can be set at index", "[modification][sequence]") { + YAMLNodeHandle seq = yaml_parse("[a, b, c]"); + + SECTION("Set string at index") { + YAMLNodeHandle replacement = yaml_parse("replaced"); + yaml_set_at_index(seq, 1, replacement); + + YAMLNodeHandle item = yaml_get_index(seq, 1); + char* str = yaml_as_string(item); + REQUIRE(std::string(str) == "replaced"); + + yaml_free_string(str); + yaml_delete_node(item); + yaml_delete_node(replacement); + } + + yaml_delete_node(seq); +} + +// =========================================== +// TEST SUITE: Scalar Editing +// =========================================== + +TEST_CASE("Scalar values can be edited directly", "[modification][scalar]") { + SECTION("Set scalar string") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_string(scalar, "new_value"); + + char* str = yaml_as_string(scalar); + REQUIRE(std::string(str) == "new_value"); + + yaml_free_string(str); + yaml_delete_node(scalar); + } + + SECTION("Set scalar int") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_int(scalar, 99); + + REQUIRE(yaml_as_int(scalar) == 99); + + yaml_delete_node(scalar); + } + + SECTION("Set scalar float") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_float(scalar, 2.718); + + REQUIRE(yaml_as_float(scalar) == Catch::Approx(2.718)); + + yaml_delete_node(scalar); + } + + SECTION("Set scalar bool") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_bool(scalar, false); + + REQUIRE(yaml_as_bool(scalar) == false); + + yaml_delete_node(scalar); + } +} + +// =========================================== +// TEST SUITE: File I/O +// =========================================== + +TEST_CASE("YAML can be written to files", "[io][file]") { + const char* test_file = "test_output.yaml"; + + SECTION("Write simple map") { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_string(map, "test", "value"); + yaml_set_int(map, "count", 5); + + REQUIRE(yaml_write_file(map, test_file)); + + // Read back and verify + YAMLNodeHandle loaded = yaml_parse_file(test_file); + REQUIRE(loaded != nullptr); + REQUIRE(yaml_has_key(loaded, "test")); + REQUIRE(yaml_has_key(loaded, "count")); + + yaml_delete_node(map); + yaml_delete_node(loaded); + cleanup_file(test_file); + } + + SECTION("Write with formatting") { + YAMLNodeHandle seq = yaml_parse("[a, b, c]"); + + REQUIRE(yaml_write_file_formatted(seq, test_file, 4, false, true)); + + std::string content = read_file(test_file); + REQUIRE(content.find("[a, b, c]") != std::string::npos); // Flow style + + yaml_delete_node(seq); + cleanup_file(test_file); + } +} + +// =========================================== +// TEST SUITE: String Conversion +// =========================================== + +TEST_CASE("YAML nodes can be converted to strings", "[conversion][string]") { + SECTION("Convert map to string") { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_string(map, "key", "value"); + + char* str = yaml_to_string(map); + REQUIRE(str != nullptr); + REQUIRE(std::string(str).find("key") != std::string::npos); + REQUIRE(std::string(str).find("value") != std::string::npos); + + yaml_free_string(str); + yaml_delete_node(map); + } + + SECTION("Emit with custom indent") { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_string(map, "test", "val"); + + char* str2 = yaml_emit(map, 2); + char* str4 = yaml_emit(map, 4); + + REQUIRE(str2 != nullptr); + REQUIRE(str4 != nullptr); + + yaml_free_string(str2); + yaml_free_string(str4); + yaml_delete_node(map); + } +} + +// =========================================== +// TEST SUITE: Cloning +// =========================================== + +TEST_CASE("YAML nodes can be cloned", "[clone]") { + SECTION("Clone simple map") { + YAMLNodeHandle original = yaml_create_map(); + yaml_set_string(original, "name", "original"); + + YAMLNodeHandle clone = yaml_clone(original); + REQUIRE(clone != nullptr); + + // Modify clone + yaml_set_string(clone, "name", "modified"); + + // Verify original unchanged + YAMLNodeHandle orig_val = yaml_get_key(original, "name"); + char* orig_str = yaml_as_string(orig_val); + REQUIRE(std::string(orig_str) == "original"); + + // Verify clone changed + YAMLNodeHandle clone_val = yaml_get_key(clone, "name"); + char* clone_str = yaml_as_string(clone_val); + REQUIRE(std::string(clone_str) == "modified"); + + yaml_free_string(orig_str); + yaml_free_string(clone_str); + yaml_delete_node(orig_val); + yaml_delete_node(clone_val); + yaml_delete_node(original); + yaml_delete_node(clone); + } + + SECTION("Clone nested structure") { + YAMLNodeHandle original = yaml_parse("outer: {inner: value}"); + YAMLNodeHandle clone = yaml_clone(original); + + REQUIRE(clone != nullptr); + REQUIRE(yaml_is_map(clone)); + + YAMLNodeHandle outer = yaml_get_key(clone, "outer"); + REQUIRE(yaml_has_key(outer, "inner")); + + yaml_delete_node(outer); + yaml_delete_node(original); + yaml_delete_node(clone); + } +} + +// =========================================== +// TEST SUITE: ex.yaml Structure Tests +// =========================================== + +TEST_CASE("ex.yaml has expected structure", "[ex.yaml][structure]") { + YAMLNodeHandle root = yaml_parse_file("../lattice_files/ex.yaml"); + REQUIRE(root != nullptr); + + SECTION("Root is a sequence") { + REQUIRE(yaml_is_sequence(root)); + REQUIRE(yaml_size(root) >= 2); + } + + SECTION("First element has thingB") { + YAMLNodeHandle first = yaml_get_index(root, 0); + REQUIRE(first != nullptr); + REQUIRE(yaml_is_map(first)); + REQUIRE(yaml_has_key(first, "thingB")); + + YAMLNodeHandle thingB = yaml_get_key(first, "thingB"); + REQUIRE(yaml_is_map(thingB)); + REQUIRE(yaml_has_key(thingB, "kind")); + + YAMLNodeHandle kind = yaml_get_key(thingB, "kind"); + char* kind_str = yaml_as_string(kind); + REQUIRE(std::string(kind_str) == "Sextupole"); + + yaml_free_string(kind_str); + yaml_delete_node(kind); + yaml_delete_node(thingB); + yaml_delete_node(first); + } + + SECTION("Second element has inj_line") { + YAMLNodeHandle second = yaml_get_index(root, 1); + REQUIRE(second != nullptr); + REQUIRE(yaml_is_map(second)); + REQUIRE(yaml_has_key(second, "inj_line")); + + YAMLNodeHandle inj_line = yaml_get_key(second, "inj_line"); + REQUIRE(yaml_is_map(inj_line)); + REQUIRE(yaml_has_key(inj_line, "kind")); + REQUIRE(yaml_has_key(inj_line, "multipass")); + REQUIRE(yaml_has_key(inj_line, "line")); + + YAMLNodeHandle kind = yaml_get_key(inj_line, "kind"); + char* kind_str = yaml_as_string(kind); + REQUIRE(std::string(kind_str) == "BeamLine"); + + YAMLNodeHandle multipass = yaml_get_key(inj_line, "multipass"); + REQUIRE(yaml_as_bool(multipass) == true); + + yaml_free_string(kind_str); + yaml_delete_node(kind); + yaml_delete_node(multipass); + yaml_delete_node(inj_line); + yaml_delete_node(second); + } + + SECTION("inj_line has line sequence") { + YAMLNodeHandle second = yaml_get_index(root, 1); + YAMLNodeHandle inj_line = yaml_get_key(second, "inj_line"); + YAMLNodeHandle line = yaml_get_key(inj_line, "line"); + + REQUIRE(yaml_is_sequence(line)); + REQUIRE(yaml_size(line) >= 3); + + yaml_delete_node(line); + yaml_delete_node(inj_line); + yaml_delete_node(second); + } + + yaml_delete_node(root); +} + +// =========================================== +// TEST SUITE: Memory Safety +// =========================================== + +TEST_CASE("Memory is properly managed", "[memory]") { + SECTION("Can safely delete nullptr") { + yaml_delete_node(nullptr); // Should not crash + } + + SECTION("Multiple operations don't leak") { + for (int i = 0; i < 100; i++) { + YAMLNodeHandle node = yaml_create_map(); + yaml_set_int(node, "test", i); + + char* str = yaml_to_string(node); + yaml_free_string(str); + + yaml_delete_node(node); + } + // No assertion - just shouldn't crash or leak + } + + SECTION("Complex structure cleanup") { + YAMLNodeHandle root = yaml_create_sequence(); + + for (int i = 0; i < 10; i++) { + YAMLNodeHandle map = yaml_create_map(); + yaml_set_int(map, "id", i); + yaml_push_node(root, map); + yaml_delete_node(map); // Safe after push_node copies + } + + yaml_delete_node(root); + } +} + +// =========================================== +// TEST SUITE: Edge Cases +// =========================================== + +TEST_CASE("Edge cases are handled correctly", "[edge_cases]") { + SECTION("Empty map") { + YAMLNodeHandle map = yaml_create_map(); + REQUIRE(yaml_size(map) == 0); + + int count; + char** keys = yaml_get_keys(map, &count); + REQUIRE(count == 0); + + yaml_delete_node(map); + } + + SECTION("Empty sequence") { + YAMLNodeHandle seq = yaml_create_sequence(); + REQUIRE(yaml_size(seq) == 0); + + YAMLNodeHandle item = yaml_get_index(seq, 0); + REQUIRE(item == nullptr); + + yaml_delete_node(seq); + } + + SECTION("Set scalar to nullptr is safe") { + YAMLNodeHandle scalar = yaml_create_scalar(); + yaml_set_scalar_string(nullptr, "test"); // Should not crash + yaml_set_scalar_string(scalar, nullptr); // Should not crash + yaml_delete_node(scalar); + } } \ No newline at end of file