diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3efbee4..c7c19ec 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,7 +21,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y lcov - name: Configure CMake with coverage - run: cmake -S . -B build -DENABLE_COVERAGE=ON + run: cmake -S . -B build -DENABLE_TEST=ON -DENABLE_COVERAGE=ON - name: Build run: cmake --build build --config Debug diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3cc2c8..0405d0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: uses: jwlawson/actions-setup-cmake@v1 - name: Configure CMake - run: cmake -S . -B build + run: cmake -DENABLE_TEST=ON -DENABLE_SINGLE_HEADER=ON -DSTATIC_LIB=ON -S . -B build - name: Build run: cmake --build build --config Release diff --git a/.gitignore b/.gitignore index 45db2d1..737bbae 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ cmake_install.cmake Makefile build/ +# Single Include +single_include/ + diff --git a/CMakeLists.txt b/CMakeLists.txt index 424115d..02a38fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,43 +4,77 @@ project(CXXStateTree VERSION 0.4.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_library(CXXStateTree INTERFACE) -target_include_directories(CXXStateTree INTERFACE include) +file (GLOB_RECURSE SRC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") -add_executable(basic examples/basic.cpp) -target_link_libraries(basic PRIVATE CXXStateTree) +option(STATIC_LIB "Enable Static Library instead of Dynamic" OFF) +if(STATIC_LIB) + add_library(CXXStateTree STATIC ${SRC_FILES}) + target_include_directories(CXXStateTree INTERFACE include ${CMAKE_CURRENT_SOURCE_DIR}/include ) +else() + add_library(CXXStateTree SHARED ${SRC_FILES}) + target_include_directories(CXXStateTree INTERFACE include ${CMAKE_CURRENT_SOURCE_DIR}/include ) +endif() -add_executable(nested examples/nested.cpp) -target_link_libraries(nested PRIVATE CXXStateTree) -add_executable(export_dot_example examples/export_dot.cpp) -target_include_directories(export_dot_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) -add_executable(export_dot_nested_example examples/export_dot_nested.cpp) -target_include_directories(export_dot_nested_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) -add_executable(export_dot_context_example examples/export_dot_context.cpp) -target_include_directories(export_dot_context_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) -add_executable(context_example examples/context_example.cpp) -target_include_directories(context_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) - -# GoogleTest setup -include(FetchContent) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/refs/heads/main.zip +set_target_properties(CXXStateTree PROPERTIES + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release" + ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release" + LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release" ) -# For Windows: Prevent overriding the parent project's compiler/linker settings -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) +option(ENABLE_TEST "Enable Test" OFF) + +if(ENABLE_TEST) + # GoogleTest setup + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/heads/main.zip + DOWNLOAD_EXTRACT_TIMESTAMP true + ) + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + + + enable_testing() + add_executable(state_tree_test tests/state_tree_test.cpp) + target_link_libraries(state_tree_test PRIVATE CXXStateTree gtest_main) + + include(GoogleTest) + gtest_discover_tests(state_tree_test) + +endif() -enable_testing() -add_executable(state_tree_test tests/state_tree_test.cpp) -target_link_libraries(state_tree_test PRIVATE CXXStateTree gtest_main) -include(GoogleTest) -gtest_discover_tests(state_tree_test) +option(ENABLE_EXAMPLE "Enable Example" OFF) + +if(ENABLE_EXAMPLE) + add_executable(basic examples/basic.cpp) + target_include_directories(basic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(basic PRIVATE CXXStateTree) + + add_executable(nested examples/nested.cpp) + target_include_directories(basic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(nested PRIVATE CXXStateTree) + + add_executable(export_dot_example examples/export_dot.cpp) + target_include_directories(export_dot_example PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(export_dot_example PRIVATE CXXStateTree) + add_executable(export_dot_nested_example examples/export_dot_nested.cpp) + target_include_directories(export_dot_nested_example PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(export_dot_nested_example PRIVATE CXXStateTree) + add_executable(export_dot_context_example examples/export_dot_context.cpp) + target_include_directories(export_dot_context_example PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(export_dot_context_example PRIVATE CXXStateTree) + + add_executable(context_example examples/context_example.cpp) + target_include_directories(context_example PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + target_link_libraries(context_example PRIVATE CXXStateTree) + +endif() option(ENABLE_COVERAGE "Enable coverage reporting" OFF) @@ -48,4 +82,61 @@ if(ENABLE_COVERAGE) message(STATUS "Building with coverage flags") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 --coverage") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") +endif() + +option(ENABLE_SINGLE_HEADER "Enable Single Header Generation" OFF) + +if(ENABLE_SINGLE_HEADER) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + include(FetchContent) + + # ----- Download the script (configure‑time, once, cached in build dir) ----- + FetchContent_Declare( + edlund_amalgamate + GIT_REPOSITORY https://github.com/edlund/amalgamate.git + GIT_TAG master # ↔ pin a commit / tag for reproducible builds + ) + FetchContent_MakeAvailable(edlund_amalgamate) # populates edlund_amalgamate_SOURCE_DIR + + set(AMALGAMATE_PY "${edlund_amalgamate_SOURCE_DIR}/amalgamate.py") # :contentReference[oaicite:0]{index=0} + + set(AMALGAMATE_CFG "${CMAKE_CURRENT_SOURCE_DIR}/config_CXXStateTree.json") + set(AMALGAMATE_PRO "${CMAKE_CURRENT_SOURCE_DIR}/config_CXXStateTree.prologue") + set(AMALGAMATE_OUT "${CMAKE_CURRENT_SOURCE_DIR}/single_include/CXXStateTree.hpp") + + add_custom_command( + OUTPUT ${AMALGAMATE_OUT} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/single_include # ensure /dist + COMMAND python3 ${AMALGAMATE_PY} -c ${AMALGAMATE_CFG} -s ${CMAKE_CURRENT_SOURCE_DIR} -p ${AMALGAMATE_PRO} --verbose yes + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS ${AMALGAMATE_PY} ${AMALGAMATE_CFG} + COMMENT "Generating single‑header CXXStateTree.hpp with edlund/amalgamate" + VERBATIM + ) + + add_custom_target(amalgamate ALL DEPENDS ${AMALGAMATE_OUT}) + + if(ENABLE_TEST) + # GoogleTest setup + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/heads/main.zip + DOWNLOAD_EXTRACT_TIMESTAMP true + ) + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + + + enable_testing() + add_executable(state_tree_singleheader_test tests/state_tree_singleheader_test.cpp) + target_include_directories(state_tree_singleheader_test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/single_include) + target_link_libraries(state_tree_singleheader_test PRIVATE CXXStateTree gtest_main) + + include(GoogleTest) + gtest_discover_tests(state_tree_singleheader_test) + + endif() + endif() \ No newline at end of file diff --git a/README.md b/README.md index eb61061..ac44133 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ * 🧪 Google Test integration * 📈 Code coverage with Codecov * 🌳 Designed for extensibility: nested states, DOT export coming soon +* 🔧 Deployed as Shared Library and as Single Header-Only library --- @@ -26,7 +27,7 @@ using namespace CXXStateTree; int main() { - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Idle") .state("Idle", [](State& s) { s.on("Start", "Running", nullptr, []() { @@ -47,10 +48,36 @@ int main() { --- +## 🛠️ Building Shared Library + +```bash +cmake -S . -B build +cmake --build build +``` + +After these command the Shared Library can be found in `build` directory + +Please Note: in future release cmake will have the ability to install the library automatically, for now it is necessary to do it manually + +--- + +## 🛠️ Building Single Header-Only Library + +```bash +cmake -S . -B build -DENABLE_SINGLE_HEADER=ON +cmake --build build +``` + +After these command the Single Header-Only Library can be found in `single_include` directory with the name CXXStateTree.hpp + +Please Note: in future release cmake will have the ability to install the library automatically, for now it is necessary to do it manually + +--- + ## 🧪 Running Tests ```bash -cmake -S . -B build -DENABLE_COVERAGE=ON +cmake -S . -B build -DENABLE_TEST=ON -DENABLE_COVERAGE=ON cmake --build build cd build && ctest ``` diff --git a/config_CXXStateTree.json b/config_CXXStateTree.json new file mode 100644 index 0000000..466dcd3 --- /dev/null +++ b/config_CXXStateTree.json @@ -0,0 +1,11 @@ +{ + "project": "CXXStateTree", + "target": "single_include/CXXStateTree.hpp", + "sources": [ + "src/CXXStateTree/State.cpp", + "src/CXXStateTree/StateTree.cpp" + ], + "include_paths": [ + "include" + ] +} \ No newline at end of file diff --git a/config_CXXStateTree.prologue b/config_CXXStateTree.prologue new file mode 100644 index 0000000..c62d951 --- /dev/null +++ b/config_CXXStateTree.prologue @@ -0,0 +1,3 @@ +/** + * CXXStateTree Single Header (c) 2025 ZigRazor + */ \ No newline at end of file diff --git a/examples/basic.cpp b/examples/basic.cpp index 6cb5737..c61efde 100644 --- a/examples/basic.cpp +++ b/examples/basic.cpp @@ -1,13 +1,12 @@ // File: examples/basic.cpp #include -#include "CXXStateTree/StateTree.hpp" -#include "CXXStateTree/Builder.hpp" +#include "CXXStateTree/StateTree.h" using namespace CXXStateTree; int main() { - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Idle") .state("Idle", [](State &s) { s.on("Start", "Running", nullptr, [](const std::any &) diff --git a/examples/context_example.cpp b/examples/context_example.cpp index 155ec52..0b155a4 100644 --- a/examples/context_example.cpp +++ b/examples/context_example.cpp @@ -1,4 +1,4 @@ -#include "CXXStateTree/Builder.hpp" +#include "CXXStateTree/StateTree.h" #include #include #include @@ -24,7 +24,7 @@ int main() { UserAuthorizedGuard auth_guard; - auto sm = Builder() + auto sm = StateTree::Builder() .initial("Idle") .state("Idle", [&](State &s) { s.on("login", "Dashboard", &auth_guard, [](const std::any &ctx) diff --git a/examples/export_dot.cpp b/examples/export_dot.cpp index e7204ce..0c84863 100644 --- a/examples/export_dot.cpp +++ b/examples/export_dot.cpp @@ -1,5 +1,4 @@ -#include "CXXStateTree/Builder.hpp" -#include "CXXStateTree/StateTree.hpp" +#include "CXXStateTree/StateTree.h" #include #include @@ -7,7 +6,7 @@ using namespace CXXStateTree; int main() { - auto tree = Builder() + auto tree = StateTree::Builder() .initial("App") .state("App", [](State &app) { app.initial_substate("Idle") diff --git a/examples/export_dot_context.cpp b/examples/export_dot_context.cpp index 02199c9..6714622 100644 --- a/examples/export_dot_context.cpp +++ b/examples/export_dot_context.cpp @@ -1,5 +1,5 @@ -#include "CXXStateTree/Builder.hpp" -#include "CXXStateTree/StateTree.hpp" + +#include "CXXStateTree/StateTree.h" #include #include @@ -21,7 +21,7 @@ class UserAuthorizedGuard : public IGuard int main() { UserAuthorizedGuard auth_guard; - auto tree = Builder() + auto tree = StateTree::Builder() .initial("Idle") .state("Idle", [&](State &s) { s.on("login", "Dashboard", &auth_guard, [](const std::any &ctx) diff --git a/examples/export_dot_nested.cpp b/examples/export_dot_nested.cpp index 59dd8be..d1ab982 100644 --- a/examples/export_dot_nested.cpp +++ b/examples/export_dot_nested.cpp @@ -1,5 +1,4 @@ -#include "CXXStateTree/Builder.hpp" -#include "CXXStateTree/StateTree.hpp" +#include "CXXStateTree/StateTree.h" #include #include @@ -7,7 +6,7 @@ using namespace CXXStateTree; int main() { - auto tree = Builder() + auto tree = StateTree::Builder() .initial("Main") .state("Main", [](State &s) { s.initial_substate("Idle") diff --git a/examples/nested.cpp b/examples/nested.cpp index 27a2e71..29615e8 100644 --- a/examples/nested.cpp +++ b/examples/nested.cpp @@ -1,12 +1,12 @@ // File: examples/basic.cpp #include -#include "CXXStateTree/Builder.hpp" +#include "CXXStateTree/StateTree.h" using namespace CXXStateTree; int main() { - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Main") .state("Main", [](State &s) { s.initial_substate("Idle") diff --git a/include/CXXStateTree/Builder.hpp b/include/CXXStateTree/Builder.hpp deleted file mode 100644 index a81777c..0000000 --- a/include/CXXStateTree/Builder.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include "StateTree.hpp" - -namespace CXXStateTree -{ - - class Builder - { - public: - Builder &initial(const std::string &name) - { - initial_state_ = name; - return *this; - } - - Builder &state(const std::string &name, std::function config) - { - states_.emplace_back(name); - config(states_.back()); - return *this; - } - - StateTree build() - { - StateTree tree; - tree.states_ = std::move(states_); - tree.current_ = tree.find_state(initial_state_); - if (tree.current_ && tree.current_->initial_substate()) - { - tree.current_ = tree.current_->find_substate(*tree.current_->initial_substate()); - } - return tree; - } - - private: - std::list states_; - std::string initial_state_; - }; -} // namespace CXXStateTree diff --git a/include/CXXStateTree/ContextUtil.hpp b/include/CXXStateTree/ContextUtil.hpp index 7a8118b..de196d6 100644 --- a/include/CXXStateTree/ContextUtil.hpp +++ b/include/CXXStateTree/ContextUtil.hpp @@ -1,4 +1,5 @@ -#pragma once +#ifndef CXXSTATETREE_CONTEXTUTIL_HPP +#define CXXSTATETREE_CONTEXTUTIL_HPP #include #include @@ -28,3 +29,5 @@ namespace CXXStateTree } } // namespace CXXStateTree + +#endif // CXXSTATETREE_CONTEXTUTIL_H diff --git a/include/CXXStateTree/IGuard.h b/include/CXXStateTree/IGuard.h index 7342dc9..4fc70be 100644 --- a/include/CXXStateTree/IGuard.h +++ b/include/CXXStateTree/IGuard.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef CXXSTATETREE_IGUARD_H +#define CXXSTATETREE_IGUARD_H #include #include @@ -15,4 +16,6 @@ namespace CXXStateTree virtual bool evaluate(const std::any &context) const = 0; }; -} // namespace CXXStateTree \ No newline at end of file +} // namespace CXXStateTree + +#endif // CXXSTATETREE_IGUARD_H diff --git a/include/CXXStateTree/State.h b/include/CXXStateTree/State.h new file mode 100644 index 0000000..383e398 --- /dev/null +++ b/include/CXXStateTree/State.h @@ -0,0 +1,38 @@ +#ifndef CXXSTATETREE_STATE_H +#define CXXSTATETREE_STATE_H + +#include +#include "Transition.h" + +namespace CXXStateTree +{ + + class State + { + public: + explicit State(std::string name, State *parent = nullptr); + + State &on(const std::string &event, const std::string &target, + IGuard *guard = nullptr, Action action = nullptr); + State &initial_substate(const std::string &name); + State &substate(const std::string &name, std::function config); + const std::string &name() const; + std::string fullName() const; + std::string baseName() const; + const std::list &substates() const; + const std::unordered_map &transitions() const; + const std::optional &initial_substate() const; + const State *parent() const; + const State *find_substate(const std::string &name) const; + void collect_transitions(std::vector> &all_transitions, const std::string &full_name, const std::string &base_name) const; + void collect_states(std::ostream &os, const std::string &prefix = "") const; + + private: + std::string name_; + State *parent_ = nullptr; + std::list substates_; + std::unordered_map transitions_; + std::optional initial_substate_; + }; +} // namespace CXXStateTree +#endif // CXXSTATETREE_STATE_H \ No newline at end of file diff --git a/include/CXXStateTree/State.hpp b/include/CXXStateTree/State.hpp deleted file mode 100644 index 187c76c..0000000 --- a/include/CXXStateTree/State.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -#include -#include "Transition.hpp" - -namespace CXXStateTree -{ - - class State - { - public: - explicit State(std::string name, State *parent = nullptr) : name_(std::move(name)), parent_(parent) {} - - State &on(const std::string &event, const std::string &target, - IGuard *guard = nullptr, Action action = nullptr) - { - transitions_[event] = {std::move(target), guard, std::move(action)}; - return *this; - } - - State &initial_substate(const std::string &name) - { - initial_substate_ = name; - return *this; - } - - State &substate(const std::string &name, std::function config) - { - substates_.emplace_back(name, this); - State &new_substate = substates_.back(); - config(new_substate); - return *this; - } - - const std::string &name() const { return name_; } - std::string fullName() const - { - if (parent_ != nullptr) - { - std::string result = parent_->fullName() + "." + name_; - return result; - } - else - { - return name_; - } - } - - std::string baseName() const - { - if (parent_ != nullptr) - { - std::string result = parent_->baseName() + "." + name_; - return result; - } - else - { - return ""; - } - } - const std::list &substates() const { return substates_; } - const std::unordered_map &transitions() const { return transitions_; } - const std::optional &initial_substate() const { return initial_substate_; } - const State *parent() const { return parent_; } - - const State *find_substate(const std::string &name) const - { - for (const auto &s : substates_) - { - if (s.name() == name) - return &s; - } - if (parent_ != nullptr) - { - return parent_->find_substate(name); - } - return nullptr; - } - - void collect_transitions(std::vector> &all_transitions, const std::string &full_name, const std::string &base_name) const - { - for (const auto &[event, t] : transitions_) - { - bool has_guard = t.guard != nullptr; - all_transitions.emplace_back(full_name, base_name != "" ? base_name + "." + t.target : t.target, event, has_guard); - } - for (const auto &sub : substates_) - { - std::string sub_full_name = full_name + "." + sub.name(); - std::string sub_base_name = full_name; - ; - sub.collect_transitions(all_transitions, sub_full_name, sub_base_name); - } - } - - void collect_states(std::ostream &os, const std::string &prefix = "") const - { - std::string full_name = prefix.empty() ? name_ : prefix + "." + name_; - - if (!substates_.empty()) - { - os << "\tsubgraph cluster_" << full_name << " {\n"; - os << "\t\tlabel = \"" << full_name << "\";\n"; - for (const auto &sub : substates_) - { - sub.collect_states(os, full_name); - } - // Add virtual entry/exit nodes for clusters - os << "\t\"" << full_name << "_entry\" [label=\"\", shape=point, style=invis];\n"; - os << "\t\"" << full_name << "_exit\" [label=\"\", shape=point, style=invis];\n"; - - os << "\t}\n"; - } - else - { - os << "\t\"" << full_name << "\";\n"; - } - } - - private: - std::string name_; - State *parent_ = nullptr; - std::list substates_; - std::unordered_map transitions_; - std::optional initial_substate_; - }; -} // namespace CXXStateTree diff --git a/include/CXXStateTree/StateTree.h b/include/CXXStateTree/StateTree.h new file mode 100644 index 0000000..c715ca1 --- /dev/null +++ b/include/CXXStateTree/StateTree.h @@ -0,0 +1,39 @@ +#ifndef CXXSTATETREE_STATETREE_H +#define CXXSTATETREE_STATETREE_H + +#include + +#include +#include "State.h" + +namespace CXXStateTree +{ + class StateTree + { + public: + class Builder + { + public: + Builder &initial(const std::string &name); + Builder &state(const std::string &name, std::function config); + StateTree build(); + + private: + std::list states_; + std::string initial_state_; + }; + + void send(const std::string &event, const std::any &context = {}); + const State ¤t_state() const; + std::string export_dot() const; + + private: + std::list states_; + const State *current_ = nullptr; + + const State *find_state(const std::string &name) const; + void sendToParent(const std::string &event, const std::any &context, const State *parent); + }; + +} // namespace CXXStateTree +#endif // CXXSTATETREE_STATETREE_H \ No newline at end of file diff --git a/include/CXXStateTree/StateTree.hpp b/include/CXXStateTree/StateTree.hpp deleted file mode 100644 index c6f397a..0000000 --- a/include/CXXStateTree/StateTree.hpp +++ /dev/null @@ -1,193 +0,0 @@ -// File: include/libstatetree/state_tree.hpp -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "State.hpp" - -namespace CXXStateTree -{ - // Forward declaration of Builder class - // This allows us to use Builder in StateTree without including its full definition here. - // The full definition of Builder will be in Builder.hpp, which will be included in the - // implementation file or wherever the Builder is actually used. - class Builder; - - class StateTree - { - public: - // friend class declaration - friend class Builder; - - void send(const std::string &event, const std::any &context = {}) - { - if (!current_) - return; - const auto &transitions = current_->transitions(); - auto it = transitions.find(event); - if (it != transitions.end()) - { - const auto &trans = it->second; - if (trans.guard && !trans.guard->evaluate(context)) - return; - if (trans.action) - trans.action(context); - current_ = find_state(trans.target); - if (current_ && current_->initial_substate()) - { - current_ = current_->find_substate(*current_->initial_substate()); - } - } - // try to send the event to parent states - else if (current_->parent()) - { - sendToParent(event, context, current_->parent()); - } - else - { - throw std::runtime_error("Event '" + event + "' not handled in state '" + current_->name() + "'"); - } - } - - const State ¤t_state() const - { - return *current_; - } - - std::string export_dot() const - { - std::ostringstream os; - os << "digraph StateTree {\n"; - os << "\trankdir=LR;\n"; - os << "\tnode [shape=box];\n"; - - // Collect cluster structure - for (const auto &s : states_) - { - s.collect_states(os); - } - - std::vector> transitions; - for (const auto &s : states_) - { - std::string name = s.name(); - s.collect_transitions(transitions, name, s.baseName()); - } - - std::set cluster_roots; - for (const auto &s : states_) - { - if (!s.substates().empty()) - { - cluster_roots.insert(s.name()); - } - } - - for (const auto &[from, to, event, has_guard] : transitions) - { - std::string label = event; - if (has_guard) - { - label += " [guard]"; - } - - bool from_is_cluster = cluster_roots.count(from); - bool to_is_cluster = cluster_roots.count(to); - - std::string from_node = from_is_cluster ? from + "_exit" : "\"" + from + "\""; - std::string to_node = to_is_cluster ? to + "_entry" : "\"" + to + "\""; - - os << "\t" << from_node << " -> " << to_node; - - if (from_is_cluster || to_is_cluster) - { - os << " [label=\"" << label << "\""; - if (from_is_cluster) - os << ", ltail=cluster_" << from; - if (to_is_cluster) - os << ", lhead=cluster_" << to; - os << "]"; - } - else - { - os << " [label=\"" << label << "\"]"; - } - - os << ";\n"; - } - - os << "}\n"; - return os.str(); - } - - private: - std::list states_; - const State *current_ = nullptr; - - const State *find_state(const std::string &name) const - { - - if (!current_ || current_->parent() == nullptr) - { - for (const auto &s : states_) - { - if (s.name() == name) - return &s; - } - return nullptr; - } - else - { - - auto foundState = current_->parent()->find_substate(name); - if (foundState) - { - return foundState; - } - - for (const auto &s : states_) - { - if (s.name() == name) - return &s; - } - } - - return nullptr; - } - - void sendToParent(const std::string &event, const std::any &context, const State *parent) - { - if (!parent) - return; - const auto &transitions = parent->transitions(); - auto it = transitions.find(event); - if (it != transitions.end()) - { - const auto &trans = it->second; - if (trans.guard && !trans.guard->evaluate(context)) - return; - if (trans.action) - trans.action(context); - current_ = find_state(trans.target); - if (current_ && current_->initial_substate()) - { - current_ = current_->find_substate(*current_->initial_substate()); - } - } - else if (parent->parent()) - { - sendToParent(event, context, parent->parent()); - } - else - { - throw std::runtime_error("Event '" + event + "' not handled in parent state '" + parent->name() + "'"); - } - } - }; - -} // namespace CXXStateTree diff --git a/include/CXXStateTree/Transition.hpp b/include/CXXStateTree/Transition.h similarity index 76% rename from include/CXXStateTree/Transition.hpp rename to include/CXXStateTree/Transition.h index 61cd575..7d00ddd 100644 --- a/include/CXXStateTree/Transition.hpp +++ b/include/CXXStateTree/Transition.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef CXXSTATETREE_TRANSITION_H +#define CXXSTATETREE_TRANSITION_H #include #include @@ -17,3 +18,4 @@ namespace CXXStateTree Action action = nullptr; }; } // namespace CXXStateTree +#endif // CXXSTATETREE_TRANSITION_H \ No newline at end of file diff --git a/src/CXXStateTree/State.cpp b/src/CXXStateTree/State.cpp new file mode 100644 index 0000000..f4df5b3 --- /dev/null +++ b/src/CXXStateTree/State.cpp @@ -0,0 +1,113 @@ +#include "../../include/CXXStateTree/State.h" +#include + +namespace CXXStateTree +{ + State::State(std::string name, State *parent) : name_(std::move(name)), parent_(parent) {} + + State &State::on(const std::string &event, const std::string &target, + IGuard *guard, Action action) + { + transitions_[event] = {std::move(target), guard, std::move(action)}; + return *this; + } + + State &State::initial_substate(const std::string &name) + { + initial_substate_ = name; + return *this; + } + + State &State::substate(const std::string &name, std::function config) + { + substates_.emplace_back(name, this); + State &new_substate = substates_.back(); + config(new_substate); + return *this; + } + + const std::string &State::name() const { return name_; } + std::string State::fullName() const + { + if (parent_ != nullptr) + { + std::string result = parent_->fullName() + "." + name_; + return result; + } + else + { + return name_; + } + } + + std::string State::baseName() const + { + if (parent_ != nullptr) + { + std::string result = parent_->baseName() + "." + name_; + return result; + } + else + { + return ""; + } + } + const std::list &State::substates() const { return substates_; } + const std::unordered_map &State::transitions() const { return transitions_; } + const std::optional &State::initial_substate() const { return initial_substate_; } + const State *State::parent() const { return parent_; } + + const State *State::find_substate(const std::string &name) const + { + for (const auto &s : substates_) + { + if (s.name() == name) + return &s; + } + if (parent_ != nullptr) + { + return parent_->find_substate(name); + } + return nullptr; + } + + void State::collect_transitions(std::vector> &all_transitions, const std::string &full_name, const std::string &base_name) const + { + for (const auto &[event, t] : transitions_) + { + bool has_guard = t.guard != nullptr; + all_transitions.emplace_back(full_name, base_name != "" ? base_name + "." + t.target : t.target, event, has_guard); + } + for (const auto &sub : substates_) + { + std::string sub_full_name = full_name + "." + sub.name(); + std::string sub_base_name = full_name; + ; + sub.collect_transitions(all_transitions, sub_full_name, sub_base_name); + } + } + + void State::collect_states(std::ostream &os, const std::string &prefix) const + { + std::string full_name = prefix.empty() ? name_ : prefix + "." + name_; + + if (!substates_.empty()) + { + os << "\tsubgraph cluster_" << full_name << " {\n"; + os << "\t\tlabel = \"" << full_name << "\";\n"; + for (const auto &sub : substates_) + { + sub.collect_states(os, full_name); + } + // Add virtual entry/exit nodes for clusters + os << "\t\"" << full_name << "_entry\" [label=\"\", shape=point, style=invis];\n"; + os << "\t\"" << full_name << "_exit\" [label=\"\", shape=point, style=invis];\n"; + + os << "\t}\n"; + } + else + { + os << "\t\"" << full_name << "\";\n"; + } + } +} \ No newline at end of file diff --git a/src/CXXStateTree/StateTree.cpp b/src/CXXStateTree/StateTree.cpp new file mode 100644 index 0000000..768a138 --- /dev/null +++ b/src/CXXStateTree/StateTree.cpp @@ -0,0 +1,195 @@ + +#include +#include +#include + +#include "../../include/CXXStateTree/StateTree.h" + +namespace CXXStateTree +{ + void StateTree::send(const std::string &event, const std::any &context) + { + if (!current_) + return; + const auto &transitions = current_->transitions(); + auto it = transitions.find(event); + if (it != transitions.end()) + { + const auto &trans = it->second; + if (trans.guard && !trans.guard->evaluate(context)) + return; + if (trans.action) + trans.action(context); + current_ = find_state(trans.target); + if (current_ && current_->initial_substate()) + { + current_ = current_->find_substate(*current_->initial_substate()); + } + } + // try to send the event to parent states + else if (current_->parent()) + { + sendToParent(event, context, current_->parent()); + } + else + { + throw std::runtime_error("Event '" + event + "' not handled in state '" + current_->name() + "'"); + } + } + + const State &StateTree::current_state() const + { + return *current_; + } + + std::string StateTree::export_dot() const + { + std::ostringstream os; + os << "digraph StateTree {\n"; + os << "\trankdir=LR;\n"; + os << "\tnode [shape=box];\n"; + + // Collect cluster structure + for (const auto &s : states_) + { + s.collect_states(os); + } + + std::vector> transitions; + for (const auto &s : states_) + { + std::string name = s.name(); + s.collect_transitions(transitions, name, s.baseName()); + } + + std::set cluster_roots; + for (const auto &s : states_) + { + if (!s.substates().empty()) + { + cluster_roots.insert(s.name()); + } + } + + for (const auto &[from, to, event, has_guard] : transitions) + { + std::string label = event; + if (has_guard) + { + label += " [guard]"; + } + + bool from_is_cluster = cluster_roots.count(from); + bool to_is_cluster = cluster_roots.count(to); + + std::string from_node = from_is_cluster ? from + "_exit" : "\"" + from + "\""; + std::string to_node = to_is_cluster ? to + "_entry" : "\"" + to + "\""; + + os << "\t" << from_node << " -> " << to_node; + + if (from_is_cluster || to_is_cluster) + { + os << " [label=\"" << label << "\""; + if (from_is_cluster) + os << ", ltail=cluster_" << from; + if (to_is_cluster) + os << ", lhead=cluster_" << to; + os << "]"; + } + else + { + os << " [label=\"" << label << "\"]"; + } + + os << ";\n"; + } + + os << "}\n"; + return os.str(); + } + + const State *StateTree::find_state(const std::string &name) const + { + + if (!current_ || current_->parent() == nullptr) + { + for (const auto &s : states_) + { + if (s.name() == name) + return &s; + } + return nullptr; + } + else + { + + auto foundState = current_->parent()->find_substate(name); + if (foundState) + { + return foundState; + } + + for (const auto &s : states_) + { + if (s.name() == name) + return &s; + } + } + + return nullptr; + } + + void StateTree::sendToParent(const std::string &event, const std::any &context, const State *parent) + { + if (!parent) + return; + const auto &transitions = parent->transitions(); + auto it = transitions.find(event); + if (it != transitions.end()) + { + const auto &trans = it->second; + if (trans.guard && !trans.guard->evaluate(context)) + return; + if (trans.action) + trans.action(context); + current_ = find_state(trans.target); + if (current_ && current_->initial_substate()) + { + current_ = current_->find_substate(*current_->initial_substate()); + } + } + else if (parent->parent()) + { + sendToParent(event, context, parent->parent()); + } + else + { + throw std::runtime_error("Event '" + event + "' not handled in parent state '" + parent->name() + "'"); + } + } + + StateTree::Builder &StateTree::Builder::initial(const std::string &name) + { + initial_state_ = name; + return *this; + } + + StateTree::Builder &StateTree::Builder::state(const std::string &name, std::function config) + { + states_.emplace_back(name); + config(states_.back()); + return *this; + } + + StateTree StateTree::Builder::build() + { + StateTree tree; + tree.states_ = std::move(states_); + tree.current_ = tree.find_state(initial_state_); + if (tree.current_ && tree.current_->initial_substate()) + { + tree.current_ = tree.current_->find_substate(*tree.current_->initial_substate()); + } + return tree; + } +} \ No newline at end of file diff --git a/tests/state_tree_singleheader_test.cpp b/tests/state_tree_singleheader_test.cpp new file mode 100644 index 0000000..45c974c --- /dev/null +++ b/tests/state_tree_singleheader_test.cpp @@ -0,0 +1,75 @@ +// File: tests/state_tree_singleheader_test.cpp +#include +#include "CXXStateTree.hpp" + +using namespace CXXStateTree; + +TEST(StateTreeSingleHeaderTest, InitialStateIsSetCorrectly) +{ + auto machine = StateTree::Builder() + .initial("Idle") + .state("Idle", [](State &s) + { s.on("Start", "Running"); }) + .state("Running", [](State &s) + { s.on("Stop", "Idle"); }) + .build(); + + EXPECT_EQ(machine.current_state().name(), "Idle"); +} + +TEST(StateTreeSingleHeaderTest, TransitionChangesState) +{ + auto machine = StateTree::Builder() + .initial("Idle") + .state("Idle", [](State &s) + { s.on("Start", "Running"); }) + .state("Running", [](State &s) + { s.on("Stop", "Idle"); }) + .build(); + + machine.send("Start"); + EXPECT_EQ(machine.current_state().name(), "Running"); + + machine.send("Stop"); + EXPECT_EQ(machine.current_state().name(), "Idle"); +} + +TEST(StateTreeSingleHeaderTest, GuardPreventsTransition) +{ + class NotPassGuard : public IGuard + { + public: + bool evaluate(const std::any &context) const override + { + return false; + } + }; + auto machine = StateTree::Builder() + .initial("Idle") + .state("Idle", [](State &s) + { s.on("Start", "Running", new NotPassGuard()); }) + .state("Running", [](State &s) + { s.on("Stop", "Idle"); }) + .build(); + + machine.send("Start"); + EXPECT_EQ(machine.current_state().name(), "Idle"); // Guard blocks transition +} + +TEST(StateTreeSingleHeaderTest, ActionIsCalled) +{ + bool actionCalled = false; + + auto machine = StateTree::Builder() + .initial("Idle") + .state("Idle", [&](State &s) + { s.on("Start", "Running", nullptr, [&](const std::any &) + { actionCalled = true; }); }) + .state("Running", [](State &s) + { s.on("Stop", "Idle"); }) + .build(); + + machine.send("Start"); + EXPECT_TRUE(actionCalled); + EXPECT_EQ(machine.current_state().name(), "Running"); +} diff --git a/tests/state_tree_test.cpp b/tests/state_tree_test.cpp index 07ff9db..550fae6 100644 --- a/tests/state_tree_test.cpp +++ b/tests/state_tree_test.cpp @@ -1,13 +1,12 @@ // File: tests/state_tree_test.cpp #include -#include "CXXStateTree/StateTree.hpp" -#include "CXXStateTree/Builder.hpp" +#include "CXXStateTree/StateTree.h" using namespace CXXStateTree; TEST(StateTreeTest, InitialStateIsSetCorrectly) { - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Idle") .state("Idle", [](State &s) { s.on("Start", "Running"); }) @@ -20,7 +19,7 @@ TEST(StateTreeTest, InitialStateIsSetCorrectly) TEST(StateTreeTest, TransitionChangesState) { - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Idle") .state("Idle", [](State &s) { s.on("Start", "Running"); }) @@ -45,7 +44,7 @@ TEST(StateTreeTest, GuardPreventsTransition) return false; } }; - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Idle") .state("Idle", [](State &s) { s.on("Start", "Running", new NotPassGuard()); }) @@ -61,7 +60,7 @@ TEST(StateTreeTest, ActionIsCalled) { bool actionCalled = false; - auto machine = Builder() + auto machine = StateTree::Builder() .initial("Idle") .state("Idle", [&](State &s) { s.on("Start", "Running", nullptr, [&](const std::any &)