├── CMakeLists.txt ├── README.md ├── Set-Toolset.ps1 ├── examples ├── CMakeLists.txt ├── bug_tracker │ ├── CMakeLists.txt │ ├── bug.cpp │ ├── bug.hpp │ └── main.cpp ├── motor │ ├── CMakeLists.txt │ ├── Motor.jpg │ └── main.cpp ├── on_off │ ├── CMakeLists.txt │ └── main.cpp └── telephone_call │ ├── CMakeLists.txt │ └── main.cpp ├── stateless++ ├── CMakeLists.txt ├── detail │ ├── no_guard.hpp │ ├── state_representation.hpp │ ├── transition.hpp │ └── trigger_behaviour.hpp ├── error.hpp ├── print_state.hpp ├── print_trigger.hpp ├── state_configuration.hpp ├── state_machine.hpp └── trigger_with_parameters.hpp └── test ├── CMakeLists.txt ├── dynamic_trigger_behaviour_fixture.cpp ├── gtest-1.6.0 └── gtest │ ├── gtest-all.cc │ └── gtest.h ├── main.cpp ├── state.hpp ├── state_machine_fixture.cpp ├── state_representation_fixture.cpp ├── transition_fixture.cpp ├── trigger.hpp ├── trigger_behaviour_fixture.cpp └── trigger_with_parameters_fixture.cpp /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Top level CMake file for stateless++. 16 | 17 | cmake_minimum_required(VERSION 2.8) 18 | 19 | project(stateless++) 20 | 21 | enable_testing() 22 | 23 | if (CYGWIN AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") 24 | include_directories(BEFORE SYSTEM /usr/lib/gcc/i686-pc-cygwin/4.7.2/include/c++) 25 | include_directories(BEFORE SYSTEM /usr/lib/gcc/i686-pc-cygwin/4.7.2/include/c++/i686-pc-cygwin) 26 | include_directories(BEFORE SYSTEM /usr/lib/gcc/i686-pc-cygwin/4.7.2/include/c++/backward) 27 | include_directories(BEFORE SYSTEM /usr/lib/gcc/i686-pc-cygwin/4.7.2/include) 28 | include_directories(BEFORE SYSTEM /usr/lib/gcc/i686-pc-cygwin/4.7.2/include-fixed) 29 | endif (CYGWIN AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") 30 | 31 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") 32 | include_directories(BEFORE SYSTEM /usr/include/x86_64-linux-gnu/c++/4.7) 33 | endif (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") 34 | message(STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO = " ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}) 35 | message(STATUS "CMAKE_BUILD_TYPE = " ${CMAKE_BUILD_TYPE}) 36 | 37 | if (CYGWIN) 38 | add_definitions(--std=gnu++11) 39 | endif (CYGWIN) 40 | 41 | if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 42 | add_definitions(--std=c++11 --stdlib=libc++ -DGTEST_USE_OWN_TR1_TUPLE) 43 | set (CMAKE_EXE_LINKER_FLAGS -lc++) 44 | endif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 45 | 46 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") 47 | add_definitions(--std=c++11) 48 | endif (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") 49 | 50 | if (MSVC) 51 | # Note that setting the toolset doesn't have any effect at the moment 52 | # but it probably will do in future. 53 | set(CMAKE_VS_PLATFORM_TOOLSET "v120_CTP_Nov2012") 54 | endif (MSVC) 55 | 56 | add_subdirectory(examples) 57 | add_subdirectory(stateless++) 58 | add_subdirectory(test) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cpp-stateless 2 | ============= 3 | 4 | Port of the [C# Stateless library](https://code.google.com/p/stateless/) to C++11. 5 | It's a lightweight state machine implementation with a fluent configuration interface. 6 | 7 | The goal of the project is to provide an API that is as close as possible to that of the original 8 | C# library using only standard C++11 features. No external dependencies are required. 9 | 10 | A simple example: 11 | ```cpp 12 | #include 13 | ... 14 | std::string on("On"), off("Off"); 15 | const char space(' '); 16 | 17 | // Create a state machine with state type string and trigger type char. 18 | // The state and trigger types can be any type that is 19 | // - default constructible 20 | // - assignable and copyable 21 | // - equality comparable 22 | // - less than comparable 23 | state_machine on_off_switch(off); 24 | 25 | // Set up using fluent configuration interface. 26 | on_off_switch.configure(off).permit(space, on); 27 | on_off_switch.configure(on).permit(space, off); 28 | 29 | // Drive the machine by firing triggers. 30 | on_off_switch.fire(space); // <-- state is now "On" 31 | ... 32 | ``` 33 | 34 | See the [bug tracker example](examples/bug_tracker/bug.cpp) for a more comprehensive use of the configuration API including 35 | parameterized triggers, sub-states and entry and exit actions. 36 | 37 | License 38 | ------- 39 | The library is licensed under the terms of the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 40 | 41 | Acknowledgements 42 | ---------------- 43 | Thanks to [Nicholas Blumhardt](http://nblumhardt.com/) for writing the original library in C# 44 | and making it available under a permissive license. 45 | 46 | Supported Platforms 47 | ------------------- 48 | [CMake](http://www.cmake.org/) build files are supplied to provide portability with minimal effort. 49 | 50 | The library, example code and tests have been built and run on the following platforms: 51 | 52 | - gcc 4.7.2 on Cygwin, gcc 4.7.3 on Ubuntu 12.04 53 | 54 | No known issues. 55 | 56 | - Clang 3.1 on Cygwin 57 | 58 | Use the patch attached to [this bug report](http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=678033) to allow use of --std=gnu++11. 59 | 60 | - Clang 3.2 on Ubuntu 12.04 61 | 62 | No known issues. 63 | 64 | - Clang Apple LLVM version 4.2 on OS X, Darwin 12.4.0 65 | 66 | No known issues. 67 | 68 | - Visual Studio 2012 on Windows 7 69 | 70 | Requires the [Microsoft Visual C++ Compiler Nov 2012 CTP Toolset](http://www.microsoft.com/en-gb/download/details.aspx?id=35515). 71 | The cmake build script attempts to configure this toolset but the [cmake CMAKE_VS_PLATFORM_TOOLSET variable is currently 72 | read-only](http://www.cmake.org/Bug/view.php?id=13774#c31828) so you have to manually update the toolset in each project file 73 | to "Microsoft Visual C++ Compiler Nov 2012 CTP (v120_CTP_Nov2012)". [This PowerShell script](Set-Toolset.ps1) automates the process. 74 | If you want to run the script you may need to run PowerShell as Administrator and run ```Set-ExecutionPolicy Unrestricted``` first. 75 | 76 | Build and Install 77 | ----------------- 78 | The library itself is header file only. 79 | The examples are built by default but this can be skipped if you just want to install the library header files. 80 | The unit tests use [GoogleTest](https://code.google.com/p/googletest/) version 1.6.0. The project includes the fused gtest code so no additional dependencies need to be installed. 81 | 82 | The instructions for UNIX-like platforms are: 83 | ``` 84 | git clone https://github.com/mattmason/cpp-stateless 85 | mkdir build && cd build # Build without polluting the source tree 86 | cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local/ ../cpp-stateless 87 | ``` 88 | To build examples, build and run unit tests, and install the headers: 89 | ``` 90 | make && make test && make install # sudo may be required for make install 91 | ``` 92 | To install the headers without building examples and tests: 93 | ``` 94 | cd stateless++ && make install # sudo may be required for make install 95 | ``` 96 | For Visual Studio 2012 use the generated project files to build from within the IDE or on the command line. 97 | 98 | Contributions 99 | ------------- 100 | Please feel free to contribute to the project. It's configured to build on [drone.io](https://drone.io/github.com/mattmason/cpp-stateless) 101 | after each commit so be prepared to receive emails to inform you of the outcome of your commit. Please don't 102 | exclude yourself from email notifications! 103 | 104 | The state machine is currently quite rudimentary when compared to, for example, boost statechart. However, it's 105 | not intended to provide all the features of UML, or other, state machine specifications. Nevertheless, if you'd 106 | like to see a feature included, then please, go ahead and implement it. I'm happy to get involved too. In the 107 | first instance, create an issue or wiki page to share your idea. 108 | 109 | One feature that would be useful is states with history. I haven't given it much thought yet, but it shouldn't 110 | be too hard to implement. 111 | 112 | Tasks 113 | ---- 114 | - [x] Dynamic destination state selection. 115 | -------------------------------------------------------------------------------- /Set-Toolset.ps1: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Run this script from your MSVC build directory to set the VC++ Toolset. 16 | $files = Get-ChildItem -name -recurse -include *.vcxproj 17 | foreach ($file in $files) 18 | { 19 | $content = Get-Content -path $file 20 | $content | foreach { $_ -replace "v110", "v120_CTP_Nov2012" } | Set-Content $file 21 | } 22 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Build stateless++ examples. 16 | 17 | add_subdirectory(bug_tracker) 18 | add_subdirectory(motor) 19 | add_subdirectory(on_off) 20 | add_subdirectory(telephone_call) 21 | -------------------------------------------------------------------------------- /examples/bug_tracker/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Build bug tracker example. 16 | 17 | include_directories(${stateless++_SOURCE_DIR}) 18 | 19 | add_executable(bug_tracker bug.cpp main.cpp) 20 | -------------------------------------------------------------------------------- /examples/bug_tracker/bug.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "bug.hpp" 18 | 19 | #include 20 | 21 | namespace bug_tracker_example 22 | { 23 | 24 | using namespace stateless; 25 | using namespace std::placeholders; 26 | 27 | bug::bug(const std::string& title) 28 | : state_(state::open) 29 | , title_(title) 30 | , assignee_() 31 | , state_machine_(std::bind(&bug::get_state, this), std::bind(&bug::set_state, this, _1)) 32 | , assign_trigger_() 33 | , resolve_trigger_() 34 | { 35 | assign_trigger_ = state_machine_.set_trigger_parameters(trigger::assign); 36 | resolve_trigger_ = state_machine_.set_trigger_parameters(trigger::resolve); 37 | 38 | state_machine_.configure(state::open) 39 | .permit(trigger::assign, state::assigned); 40 | 41 | state_machine_.configure(state::assigned) 42 | .sub_state_of(state::open) 43 | .on_entry_from(assign_trigger_, std::bind(&bug::on_assigned, this, _1, _2)) 44 | .permit_reentry(trigger::assign) 45 | .permit(trigger::resolve, state::resolved) 46 | .permit(trigger::close, state::closed) 47 | .permit(trigger::defer, state::deferred) 48 | .on_exit(std::bind(&bug::on_deassigned, this)); 49 | 50 | state_machine_.configure(state::deferred) 51 | .on_entry([=](const TTransition& t){ assignee_.reset(); }) 52 | .permit(trigger::assign, state::assigned); 53 | 54 | state_machine_.configure(state::resolved) 55 | .on_entry(std::bind(&bug::on_resolved, this, _1, _2)) 56 | .permit(trigger::close, state::closed) 57 | .permit(trigger::open, state::open); 58 | 59 | state_machine_.configure(state::closed) 60 | .permit(trigger::open, state::open); 61 | } 62 | 63 | void bug::close() 64 | { 65 | state_machine_.fire(trigger::close); 66 | } 67 | 68 | void bug::assign(const std::string& assignee) 69 | { 70 | state_machine_.fire(assign_trigger_, assignee); 71 | } 72 | 73 | bool bug::can_assign() const 74 | { 75 | return state_machine_.can_fire(trigger::assign); 76 | } 77 | 78 | void bug::defer() 79 | { 80 | state_machine_.fire(trigger::defer); 81 | } 82 | 83 | void bug::resolve(const std::string& assignee) 84 | { 85 | state_machine_.fire(resolve_trigger_, assignee); 86 | } 87 | 88 | void bug::open() 89 | { 90 | state_machine_.fire(trigger::open); 91 | } 92 | 93 | void bug::on_assigned(const bug::TTransition& transition, const std::string& assignee) 94 | { 95 | if (assignee_ != nullptr && assignee != *assignee_) 96 | { 97 | send_email_to_assignee("Don't forget to help the new guy."); 98 | } 99 | assignee_ = std::make_shared(assignee); 100 | send_email_to_assignee("You own it."); 101 | } 102 | 103 | void bug::on_resolved(const bug::TTransition& transition, const std::string& assignee) 104 | { 105 | assignee_ = std::make_shared(assignee); 106 | send_email_to_assignee("It's fixed and ready for test."); 107 | } 108 | 109 | void bug::on_deassigned() 110 | { 111 | send_email_to_assignee("You're off the hook."); 112 | } 113 | 114 | void bug::send_email_to_assignee(const std::string& message) 115 | { 116 | std::cout << "To: " << *assignee_ << " Re: " << title_ << std::endl 117 | << "--" << std::endl << message << std::endl; 118 | } 119 | 120 | const bug::state& bug::get_state() const 121 | { 122 | return state_; 123 | } 124 | 125 | void bug::set_state(const bug::state& new_state) 126 | { 127 | state_ = new_state; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /examples/bug_tracker/bug.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | 21 | namespace bug_tracker_example 22 | { 23 | 24 | class bug 25 | { 26 | public: 27 | enum class state { open, assigned, deferred, resolved, closed }; 28 | 29 | enum class trigger { open, assign, defer, resolve, close }; 30 | 31 | typedef stateless::state_machine TStateMachine; 32 | 33 | typedef TStateMachine::TTransition TTransition; 34 | 35 | bug(const std::string& title); 36 | 37 | void close(); 38 | 39 | void assign(const std::string& assignee); 40 | 41 | bool can_assign() const; 42 | 43 | void defer(); 44 | 45 | void resolve(const std::string& assignee); 46 | 47 | void open(); 48 | 49 | const state& get_state() const; 50 | 51 | private: 52 | void on_assigned(const bug::TTransition& transition, const std::string& assignee); 53 | 54 | void on_resolved(const bug::TTransition& transition, const std::string& assignee); 55 | 56 | void on_deassigned(); 57 | 58 | void send_email_to_assignee(const std::string& message); 59 | 60 | void set_state(const state& new_state); 61 | 62 | state state_; 63 | std::string title_; 64 | std::shared_ptr assignee_; 65 | 66 | TStateMachine state_machine_; 67 | 68 | typedef std::shared_ptr> TAssignTrigger; 69 | TAssignTrigger assign_trigger_; 70 | TAssignTrigger resolve_trigger_; 71 | }; 72 | 73 | } 74 | -------------------------------------------------------------------------------- /examples/bug_tracker/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "bug.hpp" 18 | 19 | #include 20 | #include 21 | 22 | using namespace bug_tracker_example; 23 | 24 | int main(int argc, char* argv[]) 25 | { 26 | bug bug("Incorrect stock count"); 27 | 28 | bug.assign("Joe"); 29 | bug.defer(); 30 | bug.assign("Harry"); 31 | bug.assign("Fred"); 32 | bug.resolve("Mike"); 33 | bug.close(); 34 | 35 | std::cout << "Press enter to quit..." << std::endl; 36 | char c; 37 | std::cin.get(c); 38 | 39 | return EXIT_SUCCESS; 40 | } 41 | -------------------------------------------------------------------------------- /examples/motor/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Build motor switch example. 16 | 17 | include_directories(${stateless++_SOURCE_DIR}) 18 | 19 | add_executable(motor main.cpp) 20 | -------------------------------------------------------------------------------- /examples/motor/Motor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattmason/cpp-stateless/9e7138f8fe164d64deb73b2b23994eeb9abe3e0b/examples/motor/Motor.jpg -------------------------------------------------------------------------------- /examples/motor/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Example inspired by 18 | // http://www.drdobbs.com/cpp/state-machine-design-in-c/184401236#. 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | using namespace stateless; 28 | using namespace std::placeholders; 29 | 30 | namespace 31 | { 32 | 33 | enum class state { idle, stopped, started, running }; 34 | 35 | std::ostream& operator<<(std::ostream& os, const state& s) 36 | { 37 | static const char* name[] = { "idle", "stopped", "started", "running" }; 38 | os << name[(int)s]; 39 | return os; 40 | } 41 | 42 | enum class trigger { start, stop, set_speed, halt }; 43 | 44 | std::ostream& operator<<(std::ostream& os, const trigger& t) 45 | { 46 | static const char* name[] = { "start", "stop", "set_speed", "halt" }; 47 | os << name[(int)t]; 48 | return os; 49 | } 50 | 51 | } 52 | 53 | namespace stateless 54 | { 55 | 56 | template<> void print_state(std::ostream& os, const state& s) 57 | { os << s; } 58 | 59 | template<> void print_trigger(std::ostream& os, const trigger& t) 60 | { os << t; } 61 | 62 | } 63 | 64 | namespace 65 | { 66 | 67 | class motor 68 | { 69 | public: 70 | motor() 71 | : sm_(state::idle) 72 | , set_speed_trigger_(sm_.set_trigger_parameters(trigger::set_speed)) 73 | , speed_(0) 74 | { 75 | sm_.configure(state::idle) 76 | .permit(trigger::start, state::started); 77 | 78 | sm_.configure(state::stopped) 79 | .on_entry([=](const TTransition&) { speed_ = 0; }) 80 | .permit(trigger::halt, state::idle); 81 | 82 | sm_.configure(state::started) 83 | .permit(trigger::set_speed, state::running) 84 | .permit(trigger::stop, state::stopped); 85 | 86 | sm_.configure(state::running) 87 | .on_entry_from( 88 | set_speed_trigger_, 89 | [=](const TTransition& t, int speed) { speed_ = speed; }) 90 | .permit(trigger::stop, state::stopped) 91 | .permit_reentry(trigger::set_speed); 92 | 93 | // Register a callback for state transitions (the default does nothing). 94 | sm_.on_transition([](const TTransition& t) 95 | { 96 | std::cout << "transition from [" << t.source() << "] to [" 97 | << t.destination() << "] via trigger [" << t.trigger() << "]" 98 | << std::endl; 99 | }); 100 | 101 | // Override the default behaviour of throwing when a trigger is unhandled. 102 | sm_.on_unhandled_trigger([](const state& s, const trigger& t) 103 | { 104 | std::cerr << "ignore unhandled trigger [" << t << "] in state [" << s 105 | << "]" << std::endl; 106 | }); 107 | } 108 | 109 | void start(int speed) 110 | { 111 | sm_.fire(trigger::start); 112 | set_speed(speed); 113 | } 114 | 115 | void stop() 116 | { 117 | sm_.fire(trigger::stop); 118 | sm_.fire(trigger::halt); 119 | } 120 | 121 | void set_speed(int speed) 122 | { 123 | sm_.fire(set_speed_trigger_, speed); 124 | } 125 | 126 | std::string print() const 127 | { 128 | std::ostringstream oss; 129 | print(oss); 130 | return oss.str(); 131 | } 132 | 133 | void print(std::ostream& os) const 134 | { 135 | os << "Motor " << sm_ << " speed = " << speed_; 136 | } 137 | 138 | private: 139 | typedef state_machine TStateMachine; 140 | typedef TStateMachine::TTransition TTransition; 141 | typedef std::shared_ptr> TSetSpeedTrigger; 142 | 143 | TStateMachine sm_; 144 | TSetSpeedTrigger set_speed_trigger_; 145 | int speed_; 146 | }; 147 | 148 | std::ostream& operator<<(std::ostream& os, const motor& m) 149 | { 150 | m.print(os); 151 | return os; 152 | } 153 | 154 | } 155 | 156 | int main(int argc, char* argv[]) 157 | { 158 | motor m; 159 | std::cout << m << std::endl; 160 | m.start(10); 161 | std::cout << m << std::endl; 162 | m.set_speed(20); 163 | std::cout << m << std::endl; 164 | m.stop(); 165 | std::cout << m << std::endl; 166 | m.stop(); 167 | std::cout << m << std::endl; 168 | 169 | std::cout << "Press enter to quit..." << std::endl; 170 | char c; 171 | std::cin.get(c); 172 | 173 | return EXIT_SUCCESS; 174 | } 175 | 176 | -------------------------------------------------------------------------------- /examples/on_off/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Build on off switch example. 16 | 17 | include_directories(${stateless++_SOURCE_DIR}) 18 | 19 | add_executable(on_off main.cpp) 20 | -------------------------------------------------------------------------------- /examples/on_off/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace stateless; 24 | 25 | int main(int argc, char* argv[]) 26 | { 27 | try 28 | { 29 | std::string on("On"), off("Off"); 30 | const char space(' '); 31 | 32 | state_machine on_off_switch(off); 33 | 34 | on_off_switch.configure(off).permit(space, on); 35 | on_off_switch.configure(on).permit(space, off); 36 | 37 | std::cout << "Press to toggle the switch. Any other key will raise an error" << std::endl; 38 | 39 | while (true) 40 | { 41 | std::cout << "switch is in state " << on_off_switch.state() << std::endl; 42 | char c; 43 | std::cin.get(c); 44 | std::cin.ignore(); 45 | on_off_switch.fire(c); 46 | } 47 | } 48 | catch (const std::exception& e) 49 | { 50 | std::cout << "Exception: " << e.what() << std::endl; 51 | std::cout << "Press enter to quit..." << std::endl; 52 | char c; 53 | std::cin.get(c); 54 | } 55 | return EXIT_SUCCESS; 56 | } 57 | -------------------------------------------------------------------------------- /examples/telephone_call/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Build bug tracker example. 16 | 17 | include_directories(${stateless++_SOURCE_DIR}) 18 | 19 | add_executable(telephone_call main.cpp) 20 | -------------------------------------------------------------------------------- /examples/telephone_call/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | namespace 26 | { 27 | 28 | enum class state 29 | { 30 | off_hook, 31 | ringing, 32 | connected, 33 | on_hold, 34 | phone_destroyed 35 | }; 36 | 37 | const char* state_name[] = 38 | { 39 | "off_hook", 40 | "ringing", 41 | "connected", 42 | "on_hold", 43 | "phone_destroyed" 44 | }; 45 | 46 | std::ostream& operator<<(std::ostream& os, const state& s) 47 | { 48 | os << state_name[(int)s]; 49 | return os; 50 | } 51 | 52 | enum class trigger 53 | { 54 | call_dialled, 55 | hung_up, 56 | call_connected, 57 | left_message, 58 | placed_on_hold, 59 | taken_off_hold, 60 | phone_hurled_against_wall 61 | }; 62 | 63 | const char* trigger_name[] = 64 | { 65 | "call_dialled", 66 | "hung_up", 67 | "call_connected", 68 | "left_message", 69 | "placed_on_hold", 70 | "taken_off_hold", 71 | "phone_hurled_against_wall" 72 | }; 73 | 74 | std::ostream& operator<<(std::ostream& os, const trigger& t) 75 | { 76 | os << trigger_name[(int)t]; 77 | return os; 78 | } 79 | 80 | } 81 | 82 | namespace stateless 83 | { 84 | 85 | template<> void print_state(std::ostream& os, const state& s) 86 | { os << s; } 87 | 88 | template<> void print_trigger(std::ostream& os, const trigger& t) 89 | { os << t; } 90 | 91 | } 92 | 93 | namespace 94 | { 95 | 96 | using namespace stateless; 97 | 98 | #if _WIN32 99 | using std::put_time; 100 | #else 101 | // No std::put_time on gcc4.7 / cygwin. 102 | std::string put_time(std::tm* t, const char* fmt) 103 | { 104 | std::time_t now = std::time(nullptr); 105 | std::tm* info = std::gmtime(&now); 106 | char buffer[80] = { 0 }; 107 | std::strftime(buffer, 80, fmt, info); 108 | return buffer; 109 | } 110 | #endif 111 | 112 | void start_call_timer() 113 | { 114 | std::time_t now = std::time(nullptr); 115 | std::cout << "Call started at " << put_time(std::gmtime(&now), "%c") << std::endl; 116 | } 117 | 118 | void stop_call_timer() 119 | { 120 | std::time_t now = std::time(nullptr); 121 | std::cout << "Call ended at " << put_time(std::gmtime(&now), "%c") << std::endl; 122 | } 123 | 124 | void fire(state_machine& sm, const trigger& trigger, std::ostream& os = std::cout) 125 | { 126 | os << "Firing [" << trigger << "]" << std::endl; 127 | sm.fire(trigger); 128 | } 129 | 130 | } 131 | 132 | int main(int argc, char* argv[]) 133 | { 134 | state_machine phone_call(state::off_hook); 135 | 136 | phone_call.configure(state::off_hook) 137 | .permit(trigger::call_dialled, state::ringing); 138 | 139 | phone_call.configure(state::ringing) 140 | .permit(trigger::hung_up, state::off_hook) 141 | .permit(trigger::call_connected, state::connected); 142 | 143 | phone_call.configure(state::connected) 144 | .on_entry(std::bind(start_call_timer)) 145 | .on_exit(std::bind(stop_call_timer)) 146 | .permit(trigger::left_message, state::off_hook) 147 | .permit(trigger::hung_up, state::off_hook) 148 | .permit(trigger::placed_on_hold, state::on_hold); 149 | 150 | phone_call.configure(state::on_hold) 151 | .sub_state_of(state::connected) 152 | .permit(trigger::taken_off_hold, state::connected) 153 | .permit(trigger::hung_up, state::off_hook) 154 | .permit(trigger::phone_hurled_against_wall, state::phone_destroyed); 155 | 156 | std::cout << phone_call << std::endl; 157 | fire(phone_call, trigger::call_dialled); 158 | std::cout << phone_call << std::endl; 159 | fire(phone_call, trigger::call_connected); 160 | std::cout << phone_call << std::endl; 161 | fire(phone_call, trigger::placed_on_hold); 162 | std::cout << phone_call << std::endl; 163 | fire(phone_call, trigger::taken_off_hold); 164 | std::cout << phone_call << std::endl; 165 | fire(phone_call, trigger::hung_up); 166 | std::cout << phone_call << std::endl; 167 | 168 | std::cout << "Press enter to quit..." << std::endl; 169 | char c; 170 | std::cin.get(c); 171 | 172 | return EXIT_SUCCESS; 173 | } 174 | -------------------------------------------------------------------------------- /stateless++/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Install stateless++ header files. 16 | 17 | install(DIRECTORY . 18 | DESTINATION "include/stateless++" 19 | FILES_MATCHING PATTERN "*.hpp") 20 | -------------------------------------------------------------------------------- /stateless++/detail/no_guard.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_DETAIL_NO_GUARD_HPP 18 | #define STATELESS_DETAIL_NO_GUARD_HPP 19 | 20 | namespace stateless 21 | { 22 | 23 | namespace detail 24 | { 25 | 26 | /// Convenience function for no-op guard clause. 27 | inline bool no_guard() 28 | { 29 | return true; 30 | } 31 | 32 | } 33 | 34 | } 35 | 36 | #endif // STATELESS_DETAIL_NO_GUARD_HPP 37 | -------------------------------------------------------------------------------- /stateless++/detail/state_representation.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_DETAIL_STATE_REPRESENTATION_HPP 18 | #define STATELESS_DETAIL_STATE_REPRESENTATION_HPP 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "../error.hpp" 28 | #include "transition.hpp" 29 | #include "trigger_behaviour.hpp" 30 | 31 | namespace stateless 32 | { 33 | 34 | namespace detail 35 | { 36 | 37 | class abstract_entry_action 38 | { 39 | public: 40 | virtual ~abstract_entry_action() = 0; 41 | }; 42 | 43 | inline abstract_entry_action::~abstract_entry_action() 44 | {} 45 | 46 | template 47 | struct entry_action : public abstract_entry_action 48 | { 49 | entry_action(const std::function& action) 50 | : execute(action) 51 | {} 52 | 53 | std::function execute; 54 | }; 55 | 56 | template 57 | class state_representation 58 | { 59 | public: 60 | typedef transition TTransition; 61 | typedef std::shared_ptr TTriggerBehaviour; 62 | typedef std::shared_ptr TEntryAction; 63 | typedef std::function TExitAction; 64 | 65 | state_representation(const TState& state) 66 | : state_(state) 67 | , trigger_behaviours_() 68 | , entry_actions_() 69 | , exit_actions_() 70 | , super_state_(nullptr) 71 | , sub_states_() 72 | {} 73 | 74 | bool can_handle(const TTrigger& trigger) const 75 | { 76 | return try_find_handler(trigger) != nullptr; 77 | } 78 | 79 | const TTriggerBehaviour try_find_handler(const TTrigger& trigger) const 80 | { 81 | auto handler = try_find_local_hander(trigger); 82 | if (handler == nullptr && super_state_ != nullptr) 83 | { 84 | handler = super_state_->try_find_handler(trigger); 85 | } 86 | return handler; 87 | } 88 | 89 | template 90 | void add_entry_action(TCallable action) 91 | { 92 | auto ea = std::make_shared>(action); 93 | entry_actions_.push_back(ea); 94 | } 95 | 96 | template 97 | void add_entry_action(const TTrigger& trigger, TCallable action) 98 | { 99 | auto wrapper = 100 | [=](const TTransition& transition, TArgs&&... args) 101 | { 102 | if (transition.trigger() == trigger) 103 | { 104 | #if _WIN32 105 | // Workaround for error C3849 http://msdn.microsoft.com/en-us/library/031k84se.aspx 106 | typedef typename std::remove_cv::type TCVRemoved; 107 | ((TCVRemoved)(action))(transition, args...); 108 | #else 109 | action(transition, std::forward(args)...); 110 | #endif 111 | } 112 | }; 113 | auto ea = std::make_shared>(wrapper); 114 | entry_actions_.push_back(ea); 115 | } 116 | 117 | void add_exit_action(const TExitAction& exit_action) 118 | { 119 | exit_actions_.push_back(exit_action); 120 | } 121 | 122 | template 123 | void enter(const TTransition& transition, TArgs&&... args) const 124 | { 125 | if (transition.is_reentry()) 126 | { 127 | execute_entry_actions(transition, std::forward(args)...); 128 | } 129 | else if (!includes(transition.source())) 130 | { 131 | if (super_state_ != nullptr) 132 | { 133 | super_state_->enter(transition, std::forward(args)...); 134 | } 135 | execute_entry_actions(transition, std::forward(args)...); 136 | } 137 | } 138 | 139 | void exit(const TTransition& transition) const 140 | { 141 | if (transition.is_reentry()) 142 | { 143 | execute_exit_actions(transition); 144 | } 145 | else if (!includes(transition.destination())) 146 | { 147 | execute_exit_actions(transition); 148 | if (super_state_ != nullptr) 149 | { 150 | super_state_->exit(transition); 151 | } 152 | } 153 | } 154 | 155 | void add_trigger_behaviour(const TTrigger& trigger, const TTriggerBehaviour trigger_behaviour) 156 | { 157 | trigger_behaviours_[trigger].push_back(trigger_behaviour); 158 | } 159 | 160 | const state_representation& super_state() const 161 | { 162 | return *super_state_; 163 | } 164 | 165 | void set_super_state(const state_representation* super_state) 166 | { 167 | super_state_ = super_state; 168 | } 169 | 170 | const TState& underlying_state() const 171 | { 172 | return state_; 173 | } 174 | 175 | void add_sub_state(const state_representation* sub_state) 176 | { 177 | sub_states_.push_back(sub_state); 178 | } 179 | 180 | bool includes(const TState& state) const 181 | { 182 | if (state == state_) 183 | { 184 | return true; 185 | } 186 | for (const auto sub_state : sub_states_) 187 | { 188 | if (sub_state->includes(state)) 189 | { 190 | return true; 191 | } 192 | } 193 | return false; 194 | } 195 | 196 | bool is_included_in(const TState& state) const 197 | { 198 | return (state == state_ || 199 | (super_state_ != nullptr && super_state_->is_included_in(state))); 200 | } 201 | 202 | std::set permitted_triggers() const 203 | { 204 | std::set local; 205 | for (auto& trigger_behaviour_list : trigger_behaviours_) 206 | { 207 | for (auto& trigger_behaviour : trigger_behaviour_list.second) 208 | { 209 | if (trigger_behaviour->is_condition_met()) 210 | { 211 | local.insert(trigger_behaviour_list.first); 212 | break; 213 | } 214 | } 215 | } 216 | 217 | if (super_state_ != nullptr) 218 | { 219 | auto super = super_state_->permitted_triggers(); 220 | std::set result; 221 | std::set_union( 222 | local.begin(), local.end(), 223 | super.begin(), super.end(), 224 | std::inserter(result, result.begin())); 225 | return result; 226 | } 227 | return local; 228 | } 229 | 230 | private: 231 | const TTriggerBehaviour try_find_local_hander(const TTrigger& trigger) const 232 | { 233 | TTriggerBehaviour result = nullptr; 234 | 235 | const auto& candidates = trigger_behaviours_.find(trigger); 236 | if (candidates == trigger_behaviours_.end()) 237 | { 238 | return result; 239 | } 240 | 241 | for (auto& candidate : candidates->second) 242 | { 243 | if (candidate->is_condition_met()) 244 | { 245 | if (result != nullptr) 246 | { 247 | throw error( 248 | "Multiple permitted exit transitions are " 249 | "configured from the current state. Guard " 250 | "clauses must be mutually exclusive."); 251 | } 252 | result = candidate; 253 | } 254 | } 255 | 256 | return result; 257 | } 258 | 259 | template 260 | void execute_entry_actions(const TTransition& transition, TArgs&&... args) const 261 | { 262 | for (auto& action : entry_actions_) 263 | { 264 | if (auto ea = std::dynamic_pointer_cast>(action)) 265 | { 266 | ea->execute(transition, std::forward(args)...); 267 | } 268 | } 269 | } 270 | 271 | void execute_exit_actions(const TTransition& transition) const 272 | { 273 | for (auto& action : exit_actions_) 274 | { 275 | action(transition); 276 | } 277 | } 278 | 279 | const TState state_; 280 | 281 | std::map> trigger_behaviours_; 282 | std::vector entry_actions_; 283 | std::vector exit_actions_; 284 | 285 | const state_representation* super_state_; 286 | std::vector sub_states_; 287 | }; 288 | 289 | } 290 | 291 | } 292 | 293 | #endif // STATELESS_DETAIL_STATE_REPRESENTATION_HPP 294 | -------------------------------------------------------------------------------- /stateless++/detail/transition.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_DETAIL_TRANSITION_HPP 18 | #define STATELESS_DETAIL_TRANSITION_HPP 19 | 20 | namespace stateless 21 | { 22 | 23 | namespace detail 24 | { 25 | 26 | template 27 | class transition 28 | { 29 | public: 30 | transition( 31 | const TState& source, 32 | const TState& destination, 33 | const TTrigger& trigger) 34 | : source_(source), destination_(destination), trigger_(trigger) 35 | {} 36 | 37 | const TState& source() const { return source_; } 38 | 39 | const TState& destination() const { return destination_; } 40 | 41 | const TTrigger& trigger() const { return trigger_; } 42 | 43 | bool is_reentry() const { return source_ == destination_; } 44 | 45 | private: 46 | const TState source_; 47 | const TState destination_; 48 | const TTrigger trigger_; 49 | }; 50 | 51 | } 52 | 53 | } 54 | 55 | #endif // STATELESS_DETAIL_TRANSITION_HPP 56 | -------------------------------------------------------------------------------- /stateless++/detail/trigger_behaviour.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_DETAIL_TRIGGER_BEHAVIOUR_HPP 18 | #define STATELESS_DETAIL_TRIGGER_BEHAVIOUR_HPP 19 | 20 | #include 21 | 22 | #include "../error.hpp" 23 | 24 | namespace stateless 25 | { 26 | 27 | namespace detail 28 | { 29 | 30 | class abstract_trigger_behaviour 31 | { 32 | public: 33 | typedef std::function TGuard; 34 | 35 | abstract_trigger_behaviour(const TGuard& guard) 36 | : guard_(guard) 37 | {} 38 | 39 | bool is_condition_met() const 40 | { 41 | return guard_(); 42 | } 43 | 44 | virtual ~abstract_trigger_behaviour() = 0; 45 | 46 | private: 47 | TGuard guard_; 48 | }; 49 | 50 | inline abstract_trigger_behaviour::~abstract_trigger_behaviour() 51 | {} 52 | 53 | template 54 | class trigger_behaviour 55 | : public abstract_trigger_behaviour 56 | { 57 | public: 58 | typedef std::function TDecision; 59 | 60 | trigger_behaviour( 61 | const TTrigger& trigger, 62 | const abstract_trigger_behaviour::TGuard& guard, 63 | const TDecision& decision) 64 | : abstract_trigger_behaviour(guard) 65 | , trigger_(trigger) 66 | , decision_(decision) 67 | {} 68 | 69 | const TTrigger& trigger() const 70 | { 71 | return trigger_; 72 | } 73 | 74 | bool results_in_transition_from(const TState& source, TState& destination) const 75 | { 76 | if (!decision_) 77 | { 78 | throw error("Static trigger behaviour decision is not set. " 79 | "The state machine is misconfigured."); 80 | } 81 | return decision_(source, destination); 82 | } 83 | 84 | trigger_behaviour( 85 | const TTrigger& trigger, 86 | const abstract_trigger_behaviour::TGuard& guard) 87 | : abstract_trigger_behaviour(guard) 88 | , trigger_(trigger) 89 | {} 90 | 91 | private: 92 | const TTrigger trigger_; 93 | TDecision decision_; 94 | }; 95 | 96 | template 97 | class dynamic_trigger_behaviour 98 | : public trigger_behaviour 99 | { 100 | public: 101 | typedef typename std::function TDecision; 102 | 103 | dynamic_trigger_behaviour( 104 | const TTrigger& trigger, 105 | const abstract_trigger_behaviour::TGuard& guard, 106 | const TDecision& decision) 107 | : trigger_behaviour(trigger, guard) 108 | , decision_(decision) 109 | {} 110 | 111 | bool results_in_transition_from(const TState& source, TState& destination, TArgs&&... args) const 112 | { 113 | destination = decision_(std::forward(args)...); 114 | return true; 115 | } 116 | 117 | private: 118 | TDecision decision_; 119 | }; 120 | 121 | } 122 | 123 | } 124 | 125 | #endif // STATELESS_DETAIL_TRIGGER_BEHAVIOUR_HPP 126 | -------------------------------------------------------------------------------- /stateless++/error.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_ERROR_HPP 18 | #define STATELESS_ERROR_HPP 19 | 20 | #include 21 | 22 | namespace stateless 23 | { 24 | 25 | /** 26 | * Represents an error caused by state machine configuration errors. 27 | */ 28 | class error : public std::runtime_error 29 | { 30 | public: 31 | error(const char* what) 32 | : std::runtime_error(what) 33 | {} 34 | }; 35 | 36 | } 37 | 38 | #endif // STATELESS_ERROR_HPP 39 | -------------------------------------------------------------------------------- /stateless++/print_state.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_PRINT_STATE_HPP 18 | #define STATELESS_PRINT_STATE_HPP 19 | 20 | #include 21 | #include 22 | 23 | namespace stateless 24 | { 25 | 26 | /** 27 | * Generic state value printer. 28 | * 29 | * Specialize to provide more informative printing 30 | * of a state machine's current state. 31 | */ 32 | template 33 | inline void print_state(std::ostream& os, const TState& s) 34 | { os << "TState@" << std::addressof(s); } 35 | 36 | #ifndef STATELESS_NO_PRETTY_PRINT 37 | 38 | /** 39 | * Specialize state printing for string. 40 | */ 41 | template 42 | inline void print_state( 43 | std::ostream& os, 44 | const typename std::enable_if::value, std::string>::type& s) 45 | { os << s; } 46 | 47 | /** 48 | * Specialize state printing for enum types. 49 | */ 50 | template 51 | inline void print_state( 52 | std::ostream& os, 53 | const typename std::enable_if::value, TState>::type& s) 54 | { os << static_cast::type>(s); } 55 | 56 | /** 57 | * Specialize state printing for arithmetic types. 58 | */ 59 | template 60 | inline void print_state( 61 | std::ostream& os, 62 | const typename std::enable_if::value, TState>::type& s) 63 | { os << s; } 64 | 65 | #endif // STATELESS_NO_PRETTY_PRINT 66 | 67 | } 68 | 69 | #endif // STATELESS_PRINT_STATE_HPP 70 | 71 | -------------------------------------------------------------------------------- /stateless++/print_trigger.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_PRINT_TRIGGER_HPP 18 | #define STATELESS_PRINT_TRIGGER_HPP 19 | 20 | #include 21 | #include 22 | 23 | namespace stateless 24 | { 25 | 26 | /** 27 | * Generic trigger value printer. 28 | * 29 | * Specialize to provide more informative printing 30 | * of a state machine's permitted triggers. 31 | */ 32 | template 33 | inline void print_trigger(std::ostream& os, const TTrigger& t) 34 | { os << "TTrigger@" << std::addressof(t); } 35 | 36 | #ifndef STATELESS_NO_PRETTY_PRINT 37 | 38 | /** 39 | * Specialize trigger printing for string. 40 | */ 41 | template 42 | inline void print_trigger( 43 | std::ostream& os, 44 | const typename std::enable_if::value, std::string>::type& t) 45 | { os << t; } 46 | 47 | /** 48 | * Specialize trigger printing for enum types. 49 | */ 50 | template 51 | inline void print_trigger( 52 | std::ostream& os, 53 | const typename std::enable_if::value, TTrigger>::type& t) 54 | { os << static_cast::type>(t); } 55 | 56 | /** 57 | * Specialize trigger printing for arithmetic types. 58 | */ 59 | template 60 | inline void print_trigger( 61 | std::ostream& os, 62 | const typename std::enable_if::value, TTrigger>::type& t) 63 | { os << t; } 64 | 65 | #endif // STATELESS_NO_PRETTY_PRINT 66 | 67 | } 68 | 69 | #endif // STATELESS_PRINT_TRIGGER_HPP 70 | 71 | -------------------------------------------------------------------------------- /stateless++/state_configuration.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_STATE_CONFIGURATION_HPP 18 | #define STATELESS_STATE_CONFIGURATION_HPP 19 | 20 | #include "detail/no_guard.hpp" 21 | #include "detail/state_representation.hpp" 22 | #include "detail/transition.hpp" 23 | #include "trigger_with_parameters.hpp" 24 | 25 | #include 26 | 27 | namespace stateless 28 | { 29 | 30 | template 31 | class state_machine; 32 | 33 | /** 34 | * The configuration for a single state value. 35 | * 36 | * \tparam TState The type used to represent the states. 37 | * \tparam TTrigger The type used to represent the triggers that cause state transitions. 38 | */ 39 | template 40 | class state_configuration 41 | { 42 | public: 43 | /// Parameterized state representation type. 44 | typedef detail::state_representation TStateRepresentation; 45 | 46 | /// Parameterized trigger behaviour type. 47 | typedef typename TStateRepresentation::TTriggerBehaviour TTriggerBehaviour; 48 | 49 | /// Parameterized trigger with parameters type. 50 | typedef std::shared_ptr> TTriggerWithParameters; 51 | 52 | /// Parameterized transition type. 53 | typedef typename TStateRepresentation::TTransition TTransition; 54 | 55 | /// Entry action type. 56 | typedef typename TStateRepresentation::TEntryAction TEntryAction; 57 | 58 | /// Exit action type. 59 | typedef typename TStateRepresentation::TExitAction TExitAction; 60 | 61 | /// Signature for guard function. 62 | typedef std::function TGuard; 63 | 64 | ///Signature for lookup function. 65 | typedef std::function TLookup; 66 | 67 | /** 68 | * Accept the specified trigger and transition to the destination state. 69 | * 70 | * \param trigger The accepted trigger. 71 | * \param destination_state The state that the trigger will cause a transition to. 72 | * 73 | * \return This configuration object. 74 | */ 75 | state_configuration& permit( 76 | const TTrigger& trigger, const TState& destination_state) 77 | { 78 | enforce_not_identity_transition(destination_state); 79 | return internal_permit(trigger, destination_state); 80 | } 81 | 82 | /** 83 | * Accept the specified trigger and transition to the destination state. 84 | * 85 | * \param trigger The accepted trigger. 86 | * \param destination_state The state that the trigger will cause a transition to. 87 | * \param guard Function that must return true in order for the trigger to be accepted. 88 | * 89 | * \return This configuration object. 90 | */ 91 | state_configuration& permit_if( 92 | const TTrigger& trigger, 93 | const TState& destination_state, 94 | const TGuard& guard) 95 | { 96 | enforce_not_identity_transition(destination_state); 97 | return internal_permit_if(trigger, destination_state, guard); 98 | } 99 | 100 | /** 101 | * Accept the specified trigger, execute exit actions and re-execute entry actions. 102 | * Reentry behaves as though the configured state transitions to an identical sibling state. 103 | * 104 | * \param trigger The accepted trigger. 105 | * 106 | * \return This configuration object. 107 | * 108 | * \note Applies to the current state only. Will not re-execute superstate actions, 109 | * or cause actions to execute transitioning between super- and sub-states. 110 | */ 111 | state_configuration& permit_reentry(const TTrigger& trigger) 112 | { 113 | return internal_permit(trigger, representation_->underlying_state()); 114 | } 115 | 116 | /** 117 | * Accept the specified trigger, execute exit actions and re-execute entry actions. 118 | * Reentry behaves as though the configured state transitions to an identical sibling state. 119 | * 120 | * \param trigger The accepted trigger. 121 | * \param guard Function that must return true in order for the trigger to be accepted. 122 | * 123 | * \return This configuration object. 124 | * 125 | * \note Applies to the current state only. Will not re-execute superstate actions, 126 | * or cause actions to execute transitioning between super- and sub-states. 127 | */ 128 | state_configuration& permit_reentry_if( 129 | const TTrigger& trigger, const TGuard& guard) 130 | { 131 | return internal_permit_if( 132 | trigger, representation_->underlying_state(), guard); 133 | } 134 | 135 | /** 136 | * Ignore the specified trigger when in the configured state. 137 | * 138 | * \param trigger The trigger to ignore. 139 | * 140 | * \return This configuration object. 141 | */ 142 | state_configuration& ignore(const TTrigger& trigger) 143 | { 144 | return ignore_if(trigger, detail::no_guard); 145 | } 146 | 147 | /** 148 | * Ignore the specified trigger when in the configured state. 149 | * 150 | * \param trigger The trigger to ignore. 151 | * \param guard Function that must return true in order for the trigger to be accepted. 152 | * 153 | * \return This configuration object. 154 | */ 155 | state_configuration& ignore_if(const TTrigger& trigger, const TGuard& guard) 156 | { 157 | auto decision = 158 | [=](const TState& source, TState& destination) 159 | { 160 | return false; 161 | }; 162 | auto behaviour = std::make_shared>( 163 | trigger, guard, decision); 164 | representation_->add_trigger_behaviour(trigger, behaviour); 165 | return *this; 166 | } 167 | 168 | /** 169 | * Specify an action that will execute when transitioning into the configured state. 170 | * 171 | * \param entry_action Action to execute, providing details of the transition. 172 | * 173 | * \return This configuration object. 174 | */ 175 | template 176 | state_configuration& on_entry(TCallable entry_action) 177 | { 178 | representation_->template add_entry_action(entry_action); 179 | return *this; 180 | } 181 | 182 | /** 183 | * Specify an action that will execute when transitioning into the configured state. 184 | * 185 | * \param trigger The trigger by which the state must be entered in order for the action to execute. 186 | * \param entry_action Action to execute, providing details of the transition. 187 | * 188 | * \return This configuration object. 189 | */ 190 | template 191 | state_configuration& on_entry_from( 192 | const TTrigger& trigger, TCallable entry_action) 193 | { 194 | representation_->template add_entry_action(trigger, entry_action); 195 | return *this; 196 | } 197 | 198 | /** 199 | * Specify an action that will execute when transitioning into the configured state. 200 | * 201 | * \param trigger The trigger by which the state must be entered in order for the action to execute. 202 | * \param entry_action Action to execute, providing details of the transition. 203 | * 204 | * \return This configuration object. 205 | */ 206 | template 207 | state_configuration& on_entry_from( 208 | const std::shared_ptr>& trigger, 209 | TCallable entry_action) 210 | { 211 | representation_->template add_entry_action(trigger->trigger(), entry_action); 212 | return *this; 213 | } 214 | 215 | /** 216 | * Specify an action that will execute when transitioning from the configured state. 217 | * 218 | * \param exit_action Action to execute, providing details of the transition. 219 | * 220 | * \return This configuration object. 221 | */ 222 | template 223 | state_configuration& on_exit(TCallable exit_action) 224 | { 225 | representation_->add_exit_action(exit_action); 226 | return *this; 227 | } 228 | 229 | /** 230 | * Set the superstate that the configured state is a substate of. 231 | * 232 | * Substates inherit the allowed transitions of their superstate. 233 | * When entering directly into a substate from outside of the superstate, 234 | * entry actions for the superstate are executed. 235 | * Likewise when leaving from the substate to outside the supserstate, 236 | * exit actions for the superstate will execute. 237 | * 238 | * \param superstate The superstate. 239 | * 240 | * \return This configuration object. 241 | */ 242 | state_configuration& sub_state_of(const TState& super_state) 243 | { 244 | auto super_representation = lookup_(super_state); 245 | representation_->set_super_state(super_representation); 246 | super_representation->add_sub_state(representation_); 247 | return *this; 248 | } 249 | 250 | /** 251 | * Accept the specified trigger and transition to the destination state, calculated 252 | * dynamically by the supplied function. 253 | * 254 | * \param trigger The accepted trigger. 255 | * \param decision Function to calculate the state that the trigger will cause a transition to. 256 | * 257 | * \return This configuration object. 258 | */ 259 | template 260 | state_configuration& permit_dynamic(const TTrigger& trigger, TCallable decision) 261 | { 262 | return this->template internal_permit_dynamic_if<>( 263 | trigger, detail::no_guard, decision); 264 | } 265 | 266 | /** 267 | * Accept the specified trigger and transition to the destination state, calculated 268 | * dynamically by the supplied function. 269 | * 270 | * \param trigger The accepted trigger. 271 | * \param decision Function to calculate the state that the trigger will cause a transition to. 272 | * 273 | * \return This configuration object. 274 | */ 275 | template 276 | state_configuration& permit_dynamic( 277 | const std::shared_ptr>& trigger, 278 | TCallable decision) 279 | { 280 | return this->template internal_permit_dynamic_if( 281 | trigger->trigger(), detail::no_guard, decision); 282 | } 283 | 284 | /** 285 | * Accept the specified trigger and transition to the destination state, calculated 286 | * dynamically by the supplied function. 287 | * 288 | * \param trigger The accepted trigger. 289 | * \param guard Function that must return true in order for the trigger to be accepted. 290 | * \param decision Function to calculate the state that the trigger will cause a transition to. 291 | * 292 | * \return This configuration object. 293 | */ 294 | template 295 | state_configuration& permit_dynamic_if(const TTrigger& trigger, const TGuard& guard, TCallable decision) 296 | { 297 | return this->template internal_permit_dynamic_if<>( 298 | trigger, guard, decision); 299 | } 300 | 301 | /** 302 | * Accept the specified trigger and transition to the destination state, calculated 303 | * dynamically by the supplied function. 304 | * 305 | * \param trigger The accepted trigger. 306 | * \param guard Function that must return true in order for the trigger to be accepted. 307 | * \param decision Function to calculate the state that the trigger will cause a transition to. 308 | * 309 | * \return This configuration object. 310 | */ 311 | template 312 | state_configuration& permit_dynamic_if( 313 | const std::shared_ptr>& trigger, 314 | const TGuard& guard, 315 | TCallable decision) 316 | { 317 | return this->template internal_permit_dynamic_if( 318 | trigger->trigger(), guard, decision); 319 | } 320 | 321 | private: 322 | friend state_machine; 323 | 324 | /** 325 | * Construct a configuration object for a single state. 326 | * Not for client use; configuration objects are created by the state_machine. 327 | */ 328 | state_configuration( 329 | TStateRepresentation* representation, const TLookup& lookup) 330 | : representation_(representation) 331 | , lookup_(lookup) 332 | {} 333 | 334 | void enforce_not_identity_transition(const TState& destination) 335 | { 336 | if (destination == representation_->underlying_state()) 337 | { 338 | throw error( 339 | "permit() (and permit_if()) require that the destination state is not " 340 | "equal to the source state. To accept a trigger without changing state, " 341 | "use either ignore() or permit_reentry()."); 342 | } 343 | } 344 | 345 | state_configuration& internal_permit( 346 | const TTrigger& trigger, const TState& destination_state) 347 | { 348 | return internal_permit_if( 349 | trigger, 350 | destination_state, 351 | detail::no_guard); 352 | } 353 | 354 | state_configuration& internal_permit_if( 355 | const TTrigger& trigger, 356 | const TState& destination_state, 357 | const TGuard& guard) 358 | { 359 | auto decision = 360 | [=](const TState& source, TState& destination) 361 | -> bool 362 | { 363 | destination = destination_state; 364 | return true; 365 | }; 366 | auto behaviour = std::make_shared>( 367 | trigger, guard, decision); 368 | representation_->add_trigger_behaviour(trigger, behaviour); 369 | return *this; 370 | } 371 | 372 | template 373 | state_configuration& internal_permit_dynamic_if( 374 | const TTrigger& trigger, 375 | const TGuard& guard, 376 | TCallable decision) 377 | { 378 | auto behaviour = 379 | std::make_shared>( 380 | trigger, guard, decision); 381 | representation_->add_trigger_behaviour(trigger, behaviour); 382 | return *this; 383 | } 384 | 385 | TStateRepresentation* representation_; 386 | TLookup lookup_; 387 | }; 388 | 389 | } 390 | 391 | #endif // STATELESS_STATE_CONFIGURATION_HPP 392 | -------------------------------------------------------------------------------- /stateless++/state_machine.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_STATE_MACHINE_HPP 18 | #define STATELESS_STATE_MACHINE_HPP 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "print_state.hpp" 27 | #include "print_trigger.hpp" 28 | #include "state_configuration.hpp" 29 | #include "trigger_with_parameters.hpp" 30 | 31 | namespace stateless 32 | { 33 | 34 | /** 35 | * Models behaviour as transitions between a finite set of states. 36 | * 37 | * \tparam TState The type used to represent the states. 38 | * \tparam TTrigger The type used to represent the triggers that cause state transitions. 39 | */ 40 | template 41 | class state_machine 42 | { 43 | public: 44 | /// Parameterized state configuration type. 45 | typedef state_configuration TStateConfiguration; 46 | 47 | /// Parameterized transition type. 48 | typedef typename TStateConfiguration::TTransition TTransition; 49 | 50 | /// Parameterized trigger with parameters type. 51 | typedef typename TStateConfiguration::TTriggerWithParameters TTriggerWithParameters; 52 | 53 | /// Signature for read access of externally managed state. 54 | typedef std::function TStateAccessor; 55 | 56 | /// Signature for write access to externally managed state. 57 | typedef std::function TStateMutator; 58 | 59 | /// Signature for handler for unhandled trigger. By default this throws an error. 60 | typedef std::function TUnhandledTriggerAction; 61 | 62 | /// Signature for handler for state transition. Does nothing by default. 63 | typedef std::function TTransitionAction; 64 | 65 | /** 66 | * Construct a state machine with external state storage. 67 | * 68 | * \param state_accessor A function that will be called to read the current state value. 69 | * \param state_mutator An action that will be called to write new state values. 70 | */ 71 | state_machine(const TStateAccessor& state_accessor, const TStateMutator& state_mutator) 72 | { 73 | init(state_accessor, state_mutator); 74 | } 75 | 76 | /** 77 | * Construct a state machine. 78 | * 79 | * \param initial_state The initial state. 80 | */ 81 | state_machine(const TState& initial_state) 82 | { 83 | auto state = std::make_shared(initial_state); 84 | using namespace std::placeholders; 85 | init( 86 | std::bind(&state_reference::get, state), 87 | std::bind(&state_reference::set, state, _1)); 88 | } 89 | 90 | /// The current state. 91 | const TState state() const 92 | { 93 | return state_accessor_(); 94 | } 95 | 96 | /** 97 | * Begin configuration of the entry/exit actions and allowed transitions 98 | * when the state machine is in a particular state. 99 | * 100 | * \param state The state to configure. 101 | * 102 | * \return A configuration object through which the state can be configured. 103 | */ 104 | TStateConfiguration configure(const TState& state) 105 | { 106 | using namespace std::placeholders; 107 | typedef state_machine TSelf; 108 | return TStateConfiguration( 109 | get_representation(state), 110 | std::bind(&TSelf::get_representation, this, _1)); 111 | } 112 | 113 | /** 114 | * Transition from the current state via the supplied trigger. 115 | * The target state is determined by the configuration of the current state. 116 | * Actions associated with leaving the current state and entering the new one 117 | * will be invoked. 118 | * 119 | * \param trigger The trigger to fire. 120 | * 121 | * \throw error The current state does not allow the trigger to be fired. 122 | */ 123 | void fire(const TTrigger& trigger) 124 | { 125 | internal_fire(trigger); 126 | } 127 | 128 | /** 129 | * Transition from the current state via the supplied trigger. 130 | * The target state is determined by the configuration of the current state. 131 | * Actions associated with leaving the current state and entering the new one 132 | * will be invoked. 133 | * 134 | * \param trigger The trigger to fire. 135 | * \param args The arguments to pass in the transition. 136 | * 137 | * \throw error The current state does not allow the trigger to be fired. 138 | */ 139 | template 140 | void fire( 141 | const std::shared_ptr>& trigger, 142 | TArgs... args) 143 | { 144 | internal_fire(trigger->trigger(), std::move(args)...); 145 | } 146 | 147 | /** 148 | * Register a callback that will be invoked every time the state machine 149 | * transitions from one state into another. 150 | * 151 | * \param action The action to execute, accepting the details of the transition. 152 | */ 153 | void on_transition(const TTransitionAction& action) 154 | { 155 | on_transition_ = action; 156 | } 157 | 158 | /** 159 | * Override the default behaviour of throwing an exception when an 160 | * unhandled trigger is fired. 161 | * 162 | * \param action An action to call when an unhandled trigger is fired. 163 | */ 164 | void on_unhandled_trigger(const TUnhandledTriggerAction& action) 165 | { 166 | on_unhandled_trigger_ = action; 167 | } 168 | 169 | /** 170 | * Determine whether the state machine is in the supplied state. 171 | * 172 | * \param state The state to test for. 173 | * 174 | * \return True if the current state is equal to, or a substate of, the supplied state. 175 | */ 176 | bool is_in_state(const TState& state) 177 | { 178 | return current_representation()->is_included_in(state); 179 | } 180 | 181 | /** 182 | * Determine whether supplied trigger can be fired in the current state. 183 | * 184 | * \param trigger Trigger to test. 185 | * 186 | * \return True if the trigger can be fired, false otherwise. 187 | */ 188 | bool can_fire(const TTrigger& trigger) const 189 | { 190 | return current_representation()->can_handle(trigger); 191 | } 192 | 193 | /** 194 | * Specify the arguments that must be supplied when a specific trigger is fired. 195 | * 196 | * \param trigger The underlying trigger value. 197 | * 198 | * \return An object that can be passed to the Fire() method in order to 199 | * fire the parameterised trigger. 200 | */ 201 | template 202 | std::shared_ptr> 203 | set_trigger_parameters(const TTrigger& trigger) 204 | { 205 | auto it = trigger_configuration_.find(trigger); 206 | if (it != trigger_configuration_.end()) 207 | { 208 | throw error("Cannot reconfigure trigger parameters"); 209 | } 210 | auto configuration = 211 | std::make_shared>(trigger); 212 | trigger_configuration_[trigger] = configuration; 213 | return configuration; 214 | } 215 | 216 | /** 217 | * The currently permissible trigger values. 218 | */ 219 | std::set permitted_triggers() const 220 | { 221 | return current_representation()->permitted_triggers(); 222 | } 223 | 224 | /** 225 | * A human readable representation of the state machine. 226 | * 227 | * \return A description of the current state and permitted triggers. 228 | */ 229 | std::string print() const 230 | { 231 | std::ostringstream oss; 232 | print(oss); 233 | return oss.str(); 234 | } 235 | 236 | /** 237 | * Stream output operator. 238 | */ 239 | friend inline std::ostream& operator<<( 240 | std::ostream& os, const stateless::state_machine& sm) 241 | { 242 | sm.print(os); 243 | return os; 244 | } 245 | 246 | private: 247 | /** 248 | * Wrapper class for internal state storage. 249 | */ 250 | class state_reference 251 | { 252 | public: 253 | state_reference(const TState& initial_state) 254 | : state_(initial_state) 255 | {} 256 | 257 | const TState& get() const 258 | { 259 | return state_; 260 | } 261 | 262 | void set(const TState& new_state) 263 | { 264 | state_ = new_state; 265 | } 266 | 267 | private: 268 | TState state_; 269 | }; 270 | 271 | /** 272 | * Perform initialization. 273 | * 274 | * \param state_accessor A function that will be called to read the current state value. 275 | * \param state_mutator An action that will be called to write new state values. 276 | */ 277 | void init( 278 | const TStateAccessor& state_accessor, 279 | const TStateMutator& state_mutator) 280 | { 281 | state_accessor_ = state_accessor; 282 | state_mutator_ = state_mutator; 283 | on_unhandled_trigger_ = [](const TState& state, const TTrigger& trigger) 284 | { 285 | throw error( 286 | "No valid leaving transitions are permitted for trigger. " 287 | "Consider ignoring the trigger."); 288 | }; 289 | } 290 | 291 | /// Parameterized state representation type. 292 | typedef detail::state_representation TStateRepresentation; 293 | 294 | /// The current representation. 295 | const TStateRepresentation* current_representation() const 296 | { 297 | return get_representation(state()); 298 | } 299 | 300 | /// Get the representation corresponding to the supplied state. 301 | TStateRepresentation* get_representation(const TState& state) const 302 | { 303 | auto it = state_configuration_.find(state); 304 | if (it == state_configuration_.end()) 305 | { 306 | TStateRepresentation representation(state); 307 | auto inserted = state_configuration_.insert( 308 | std::make_pair(state, representation)); 309 | return &inserted.first->second; 310 | } 311 | return &it->second; 312 | } 313 | 314 | /// Set the state. 315 | void set_state(const TState& new_state) 316 | { 317 | state_mutator_(new_state); 318 | } 319 | 320 | /// Implementation of state transition given a trigger. 321 | template 322 | void internal_fire(const TTrigger& trigger, TArgs&&... args) 323 | { 324 | auto abstract_configuration = trigger_configuration_.find(trigger); 325 | if (abstract_configuration != trigger_configuration_.end()) 326 | { 327 | typedef trigger_with_parameters TParameterizedTrigger; 328 | auto configuration = 329 | std::dynamic_pointer_cast( 330 | abstract_configuration->second); 331 | if (configuration == nullptr) 332 | { 333 | throw error("Invalid number or type of parameters."); 334 | } 335 | } 336 | 337 | auto abstract_handler = current_representation()->try_find_handler(trigger); 338 | if (abstract_handler == nullptr) 339 | { 340 | on_unhandled_trigger_( 341 | current_representation()->underlying_state(), trigger); 342 | return; 343 | } 344 | 345 | const auto& source = state(); 346 | TState destination; 347 | bool is_transition = false; 348 | 349 | typedef detail::dynamic_trigger_behaviour TDynamicTriggerBehaviour; 350 | typedef detail::trigger_behaviour TTriggerBehaviour; 351 | if (auto handler = std::dynamic_pointer_cast(abstract_handler)) 352 | { 353 | // A dynamic behaviour is configured, so forward the arguments to it. 354 | is_transition = handler->results_in_transition_from(source, destination, std::forward(args)...); 355 | } 356 | else if (auto handler = std::dynamic_pointer_cast(abstract_handler)) 357 | { 358 | // Fall back to configuration time defined transition. 359 | is_transition = handler->results_in_transition_from(source, destination); 360 | } 361 | else 362 | { 363 | throw error("Unable to find a suitable handler."); 364 | } 365 | 366 | if (is_transition) 367 | { 368 | TTransition transition(source, destination, trigger); 369 | current_representation()->exit(transition); 370 | set_state(transition.destination()); 371 | current_representation()->enter(transition, std::forward(args)...); 372 | if (on_transition_) 373 | { 374 | on_transition_(transition); 375 | } 376 | } 377 | } 378 | 379 | /// Implementation for public print and stream operator. 380 | void print(std::ostream& os) const 381 | { 382 | auto print_permitted_triggers = 383 | [&](const std::set& pts) 384 | { 385 | bool first = true; 386 | for (auto& pt : pts) 387 | { 388 | if (!first) os << ", "; 389 | first = false; 390 | print_trigger(os, pt); 391 | } 392 | }; 393 | os << "state_machine { state = "; 394 | print_state(os, state()); 395 | os << ", permitted triggers = { "; 396 | print_permitted_triggers(permitted_triggers()); 397 | os << " } }"; 398 | } 399 | 400 | /** 401 | * Mapping from state to representation. 402 | * There is exactly one representation per configured state. 403 | */ 404 | mutable std::map state_configuration_; 405 | 406 | /// Mapping of triggers with arguments to the underlying trigger. 407 | std::map trigger_configuration_; 408 | 409 | /// The state accessor. 410 | TStateAccessor state_accessor_; 411 | 412 | /// The state mutator. 413 | TStateMutator state_mutator_; 414 | 415 | /// Function to call on unhandled trigger. 416 | TUnhandledTriggerAction on_unhandled_trigger_; 417 | 418 | /// Function to call on state transition. 419 | TTransitionAction on_transition_; 420 | }; 421 | 422 | } 423 | 424 | #endif // STATELESS_STATE_MACHINE_HPP 425 | 426 | -------------------------------------------------------------------------------- /stateless++/trigger_with_parameters.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_TRIGGER_WITH_PARAMETERS_HPP 18 | #define STATELESS_TRIGGER_WITH_PARAMETERS_HPP 19 | 20 | namespace stateless 21 | { 22 | 23 | template 24 | class abstract_trigger_with_parameters 25 | { 26 | public: 27 | abstract_trigger_with_parameters(const TTrigger& underlying_trigger) 28 | : underlying_trigger_(underlying_trigger) 29 | {} 30 | 31 | virtual ~abstract_trigger_with_parameters() = 0; 32 | 33 | const TTrigger& trigger() const 34 | { 35 | return underlying_trigger_; 36 | } 37 | 38 | private: 39 | const TTrigger underlying_trigger_; 40 | }; 41 | 42 | template 43 | inline abstract_trigger_with_parameters::~abstract_trigger_with_parameters() 44 | {} 45 | 46 | template 47 | class trigger_with_parameters : public abstract_trigger_with_parameters 48 | { 49 | public: 50 | /** 51 | * Construct a parameterized trigger. 52 | * Not for client use; use state_machine::set_trigger_parameters. 53 | */ 54 | trigger_with_parameters(const TTrigger& underlying_trigger) 55 | : abstract_trigger_with_parameters(underlying_trigger) 56 | {} 57 | }; 58 | 59 | } 60 | 61 | #endif // STATELESS_TRIGGER_WITH_PARAMETERS_HPP 62 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Matt Mason 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | if (MSVC) 16 | # VS2012 faux-variadic template need to be bumped to 10 args 17 | # to support GoogleTest tuple requirements. 18 | # GoogleTest also (by default) wants us to use static runtime - 19 | # change this according to your needs. 20 | add_definitions("-D_VARIADIC_MAX=10" "/MTd") 21 | endif (MSVC) 22 | 23 | file(GLOB_RECURSE sources *.cpp) 24 | include_directories(${stateless++_SOURCE_DIR} . ./gtest-1.6.0) 25 | add_executable(test_stateless++ ${sources} ./gtest-1.6.0/gtest/gtest-all.cc) 26 | if (NOT MSVC) 27 | target_link_libraries(test_stateless++ pthread) 28 | endif (NOT MSVC) 29 | add_test("unit_test" test_stateless++) 30 | 31 | -------------------------------------------------------------------------------- /test/dynamic_trigger_behaviour_fixture.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | using namespace stateless; 25 | using namespace stateless::detail; 26 | using namespace testing; 27 | 28 | namespace 29 | { 30 | 31 | #ifdef _WIN32 32 | typedef state_machine TStateMachine; 33 | #else 34 | using TStateMachine = state_machine; 35 | #endif 36 | 37 | TEST(DynamicTriggerBehaviour, WhenPermitDynamic_ThenDestinationStateIsDynamic) 38 | { 39 | TStateMachine sm(state::A); 40 | sm.configure(state::A) 41 | .permit_dynamic(trigger::X, [](){ return state::B; }); 42 | 43 | sm.fire(trigger::X); 44 | 45 | EXPECT_EQ(state::B, sm.state()); 46 | } 47 | 48 | TEST(DynamicTriggerBehaviour, WhenTriggerHasParameters_ThenTheyAreUsedToCalculateDestinationState) 49 | { 50 | #if 0 51 | var sm = new StateMachine(State.A); 52 | var trigger = sm.SetTriggerParameters(Trigger.X); 53 | sm.Configure(State.A) 54 | .PermitDynamic(trigger, i => i == 1 ? State.B : State.C); 55 | 56 | sm.Fire(trigger, 1); 57 | 58 | Assert.AreEqual(State.B, sm.State); 59 | #endif 60 | TStateMachine sm(state::A); 61 | auto pt = sm.set_trigger_parameters(trigger::X); 62 | sm.configure(state::A) 63 | .permit_dynamic(pt, [](const int i){ return i == 1 ? state::B : state::C; }); 64 | 65 | sm.fire(pt, 1); 66 | 67 | EXPECT_EQ(state::B, sm.state()); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | int main(int argc, char* argv[]) 20 | { 21 | ::testing::InitGoogleTest(&argc, argv); 22 | return RUN_ALL_TESTS(); 23 | } 24 | -------------------------------------------------------------------------------- /test/state.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_TEST_STATE_HPP 18 | #define STATELESS_TEST_STATE_HPP 19 | 20 | #include 21 | 22 | enum class state { A, B, C }; 23 | 24 | inline std::ostream& operator<<(std::ostream&os, const state& s) 25 | { 26 | static const char* state_name[] = { "A", "B", "C" }; 27 | os << state_name[(int)s]; 28 | return os; 29 | } 30 | 31 | #endif // STATELESS_TEST_STATE_HPP 32 | -------------------------------------------------------------------------------- /test/state_machine_fixture.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | using namespace stateless; 25 | using namespace testing; 26 | 27 | namespace 28 | { 29 | 30 | #ifdef _WIN32 31 | typedef state_machine TStateMachine; 32 | #else 33 | using TStateMachine = state_machine; 34 | #endif 35 | 36 | TEST(StateMachine, WhenFireTrigger_ThenTransitionsToConfiguredDestinationState) 37 | { 38 | const std::string state_a("A"), state_b("B"), state_c("C"); 39 | const std::string trigger_x("X"), trigger_y("Y"); 40 | 41 | state_machine sm(state_a); 42 | 43 | sm.configure(state_a).permit(trigger_x, state_b); 44 | 45 | sm.fire(trigger_x); 46 | 47 | ASSERT_EQ(state_b, sm.state()); 48 | } 49 | 50 | TEST(StateMachine, WhenConstructed_ThenInitialStateIsCurrent) 51 | { 52 | TStateMachine sm(state::B); 53 | ASSERT_EQ(state::B, sm.state()); 54 | } 55 | 56 | TEST(StateMachine, WhenStateIsStoredExternally_ThenItIsRetrieved) 57 | { 58 | state s = state::B; 59 | TStateMachine sm([&](){ return s; }, [&](const state& new_s){ s = new_s; }); 60 | 61 | sm.configure(state::B).permit(trigger::X, state::C); 62 | 63 | ASSERT_EQ(state::B, sm.state()); 64 | 65 | sm.fire(trigger::X); 66 | 67 | ASSERT_EQ(state::C, sm.state()); 68 | } 69 | 70 | TEST(StateMachine, WhenSubstate_ThenItIsIncludedInCurrentState) 71 | { 72 | TStateMachine sm(state::B); 73 | sm.configure(state::B).sub_state_of(state::C); 74 | 75 | ASSERT_EQ(state::B, sm.state()); 76 | ASSERT_TRUE(sm.is_in_state(state::C)); 77 | } 78 | 79 | TEST(StateMachine, WhenInSubstate_ThenTriggerIgnoredInSuperstateRemainsInSubstate) 80 | { 81 | TStateMachine sm(state::B); 82 | sm.configure(state::B).sub_state_of(state::C); 83 | sm.configure(state::C).ignore(trigger::X); 84 | sm.fire(trigger::X); 85 | 86 | ASSERT_EQ(state::B, sm.state()); 87 | } 88 | 89 | TEST(StateMachine, WhenPermittedTriggersIncludeSuperstate_ThenPermittedTriggersAreIncluded) 90 | { 91 | TStateMachine sm(state::B); 92 | sm.configure(state::A).permit(trigger::Z, state::B); 93 | sm.configure(state::B).sub_state_of(state::C).permit(trigger::X, state::A); 94 | sm.configure(state::C).permit(trigger::Y, state::A); 95 | 96 | auto permitted = sm.permitted_triggers(); 97 | 98 | EXPECT_NE(permitted.end(), permitted.find(trigger::X)); 99 | EXPECT_NE(permitted.end(), permitted.find(trigger::Y)); 100 | EXPECT_EQ(permitted.end(), permitted.find(trigger::Z)); 101 | } 102 | 103 | TEST(StateMachine, WhenPermittedTriggers_ThenTheyAreDistinctValues) 104 | { 105 | TStateMachine sm(state::B); 106 | sm.configure(state::B).sub_state_of(state::C).permit(trigger::X, state::A); 107 | sm.configure(state::C).permit(trigger::X, state::B); 108 | 109 | auto permitted = sm.permitted_triggers(); 110 | 111 | EXPECT_EQ(1, permitted.size()); 112 | EXPECT_EQ(trigger::X, *permitted.begin()); 113 | } 114 | 115 | TEST(StateMachine, WhenPermittedTriggerIncludesGuard_ThenGuardIsRespected) 116 | { 117 | TStateMachine sm(state::B); 118 | sm.configure(state::B).permit_if(trigger::X, state::A, [](){ return false; }); 119 | 120 | EXPECT_EQ(0, sm.permitted_triggers().size()); 121 | } 122 | 123 | TEST(StateMachine, WhenDiscriminatedByGuard_ThenChoosesPermittedTransition) 124 | { 125 | TStateMachine sm(state::B); 126 | sm.configure(state::B) 127 | .permit_if(trigger::X, state::A, [](){ return false; }) 128 | .permit_if(trigger::X, state::C, [](){ return true; }); 129 | sm.fire(trigger::X); 130 | 131 | ASSERT_EQ(state::C, sm.state()); 132 | } 133 | 134 | TEST(StateMachine, WhenTriggerIsIgnored_ThenActionsAreNotExecuted) 135 | { 136 | TStateMachine sm(state::B); 137 | 138 | bool fired = false; 139 | sm.configure(state::B) 140 | .on_entry([&](const TStateMachine::TTransition&){ fired = true; }) 141 | .ignore(trigger::X); 142 | sm.fire(trigger::X); 143 | 144 | ASSERT_FALSE(fired); 145 | } 146 | 147 | TEST(StateMachine, WhenSelfTransitionPermited_ThenActionsFire) 148 | { 149 | TStateMachine sm(state::B); 150 | 151 | bool fired = false; 152 | sm.configure(state::B) 153 | .on_entry([&](const TStateMachine::TTransition&){ fired = true; }) 154 | .permit_reentry(trigger::X); 155 | sm.fire(trigger::X); 156 | 157 | ASSERT_TRUE(fired); 158 | } 159 | 160 | TEST(StateMachine, WhenImplicitReentryIsAttempted_ThenErrorIsRaised) 161 | { 162 | TStateMachine sm(state::B); 163 | ASSERT_THROW( 164 | sm.configure(state::B).permit(trigger::X, state::B), 165 | stateless::error); 166 | } 167 | 168 | TEST(StateMachine, WhenTriggerParametersAreSet_ThenTheyAreImmutable) 169 | { 170 | TStateMachine sm(state::B); 171 | sm.set_trigger_parameters(trigger::X); 172 | 173 | ASSERT_THROW( 174 | sm.set_trigger_parameters(trigger::X), 175 | stateless::error); 176 | } 177 | 178 | TEST(StateMachine, WhenParametersSuppliedToFire_ThenTheyArePassedToEntryAction) 179 | { 180 | TStateMachine sm(state::B); 181 | auto x = sm.set_trigger_parameters(trigger::X); 182 | sm.configure(state::B).permit(trigger::X, state::C); 183 | 184 | std::string assigned_string; 185 | int assigned_int = 0; 186 | sm.configure(state::C) 187 | .on_entry_from( 188 | x, 189 | [&](const TStateMachine::TTransition& transition, const std::string& s, int i) 190 | { 191 | assigned_string = s; 192 | assigned_int = i; 193 | }); 194 | 195 | const std::string supplied_string = "something"; 196 | const int supplied_int = 42; 197 | 198 | sm.fire(x, supplied_string, supplied_int); 199 | 200 | ASSERT_EQ(supplied_string, assigned_string); 201 | ASSERT_EQ(supplied_int, assigned_int); 202 | } 203 | 204 | TEST(StateMachine, WhenUnhandledTriggerIsFired_ThenTheProvidedHandlerIsCalledWithStateAndTrigger) 205 | { 206 | TStateMachine sm(state::B); 207 | 208 | state unhandled_state = state::A; 209 | trigger unhandled_trigger = trigger::X; 210 | 211 | sm.on_unhandled_trigger( 212 | [&](const state& s, const trigger& t) 213 | { 214 | unhandled_state = s; 215 | unhandled_trigger = t; 216 | }); 217 | 218 | sm.fire(trigger::Z); 219 | 220 | ASSERT_EQ(state::B, unhandled_state); 221 | ASSERT_EQ(trigger::Z, unhandled_trigger); 222 | } 223 | 224 | TEST(StateMachine, WhenTransitionOccurs_ThenTheOnTransitionEventFires) 225 | { 226 | TStateMachine sm(state::B); 227 | 228 | sm.configure(state::B).permit(trigger::X, state::A); 229 | sm.on_transition( 230 | [](const TStateMachine::TTransition& t) 231 | { 232 | ASSERT_EQ(trigger::X, t.trigger()); 233 | ASSERT_EQ(state::B, t.source()); 234 | ASSERT_EQ(state::A, t.destination()); 235 | }); 236 | 237 | sm.fire(trigger::X); 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /test/state_representation_fixture.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | #include 26 | 27 | using namespace stateless::detail; 28 | using namespace testing; 29 | 30 | namespace 31 | { 32 | 33 | #ifdef _WIN32 34 | typedef state_representation TSR; 35 | typedef transition TTransition; 36 | typedef trigger_behaviour TTB; 37 | #else 38 | using TSR = state_representation; 39 | using TTransition = transition; 40 | using TTB = trigger_behaviour; 41 | #endif 42 | 43 | TEST(StateRepresentation, WhenEntering_ThenEnteringActionsExecute) 44 | { 45 | TSR sr(state::B); 46 | TTransition t(state::A, state::B, trigger::X); 47 | bool executed = false; 48 | sr.add_entry_action([&](const TTransition&){ executed = true; }); 49 | sr.enter(t); 50 | ASSERT_TRUE(executed); 51 | } 52 | 53 | TEST(StateRepresentation, WhenLeaving_ThenEnteringActionsDoNotExecute) 54 | { 55 | TSR sr(state::B); 56 | TTransition t(state::A, state::B, trigger::X); 57 | bool executed = false; 58 | sr.add_entry_action([&](const TTransition&){ executed = true; }); 59 | sr.exit(t); 60 | ASSERT_FALSE(executed); 61 | } 62 | 63 | TEST(StateRepresentation, WhenLeaving_ThenLeavingActionsExecute) 64 | { 65 | TSR sr(state::A); 66 | TTransition t(state::A, state::B, trigger::X); 67 | bool executed = false; 68 | sr.add_exit_action([&](const TTransition&){ executed = true; }); 69 | sr.exit(t); 70 | ASSERT_TRUE(executed); 71 | } 72 | 73 | TEST(StateRepresentation, WhenEntering_ThenLeavingActionsDoNotExecute) 74 | { 75 | TSR sr(state::A); 76 | TTransition t(state::A, state::B, trigger::X); 77 | bool executed = false; 78 | sr.add_exit_action([&](const TTransition&){ executed = true; }); 79 | sr.enter(t); 80 | 81 | ASSERT_FALSE(executed); 82 | } 83 | 84 | TEST(StateRepresentation, WhenSetup_ThenIncludesUnderlyingState) 85 | { 86 | TSR sr(state::B); 87 | 88 | ASSERT_TRUE(sr.includes(state::B)); 89 | } 90 | 91 | TEST(StateRepresentation, WhenSetup_ThenDoesNotIncludeUnrelatedState) 92 | { 93 | TSR sr(state::B); 94 | 95 | ASSERT_FALSE(sr.includes(state::C)); 96 | } 97 | 98 | TEST(StateRepresentation, WhenSubstate_ThenIncludesSubstate) 99 | { 100 | TSR sr_b(state::B); 101 | TSR sr_c(state::C); 102 | sr_b.add_sub_state(&sr_c); 103 | 104 | ASSERT_TRUE(sr_b.includes(state::C)); 105 | } 106 | 107 | TEST(StateRepresentation, WhenSuperstate_ThenDoesNotIncludeSuperstate) 108 | { 109 | TSR sr_b(state::B); 110 | TSR sr_c(state::C); 111 | sr_b.set_super_state(&sr_c); 112 | 113 | ASSERT_FALSE(sr_b.includes(state::C)); 114 | } 115 | 116 | TEST(StateRepresentation, WhenSetup_ThenIsIncludedInUnderlyingState) 117 | { 118 | TSR sr(state::B); 119 | 120 | ASSERT_TRUE(sr.is_included_in(state::B)); 121 | } 122 | 123 | TEST(StateRepresentation, WhenSetup_ThenIsNotIncludedInUnrelatedState) 124 | { 125 | TSR sr(state::B); 126 | 127 | ASSERT_FALSE(sr.is_included_in(state::C)); 128 | } 129 | 130 | TEST(StateRepresentation, WhenSubstate_ThenIsNotIncludedInSubstate) 131 | { 132 | TSR sr_b(state::B); 133 | TSR sr_c(state::C); 134 | sr_b.add_sub_state(&sr_c); 135 | 136 | ASSERT_FALSE(sr_b.is_included_in(state::C)); 137 | } 138 | 139 | TEST(StateRepresentation, WhenSuperstate_ThenIsIncludedInSuperstate) 140 | { 141 | TSR sr_b(state::B); 142 | TSR sr_c(state::C); 143 | sr_b.set_super_state(&sr_c); 144 | 145 | ASSERT_TRUE(sr_b.is_included_in(state::C)); 146 | } 147 | 148 | #define CREATE_SUPER_SUB_PAIR() \ 149 | TSR super(state::A), sub(state::B); \ 150 | super.add_sub_state(&sub); \ 151 | sub.set_super_state(&super); 152 | 153 | TEST(StateRepresentation, WhenTransitioningFromSubToSuperstate_ThenSubstateEntryActionsExecute) 154 | { 155 | CREATE_SUPER_SUB_PAIR(); 156 | bool executed = false; 157 | sub.add_entry_action([&](const TTransition&){ executed = true; }); 158 | TTransition transition(super.underlying_state(), sub.underlying_state(), trigger::X); 159 | sub.enter(transition); 160 | 161 | ASSERT_TRUE(executed); 162 | } 163 | 164 | TEST(StateRepresentation, WhenTransitioningFromSubToSuperstate_ThenSubstateExitActionsExecute) 165 | { 166 | CREATE_SUPER_SUB_PAIR(); 167 | bool executed = false; 168 | sub.add_exit_action([&](const TTransition&){ executed = true; }); 169 | TTransition transition(sub.underlying_state(), super.underlying_state(), trigger::X); 170 | sub.exit(transition); 171 | 172 | ASSERT_TRUE(executed); 173 | } 174 | 175 | TEST(StateRepresentation, WhenTransitioningToSuperFromSubstate_ThenSuperEntryActionsDoNotExecute) 176 | { 177 | CREATE_SUPER_SUB_PAIR(); 178 | bool executed = false; 179 | super.add_entry_action([&](const TTransition&){ executed = true; }); 180 | TTransition transition(super.underlying_state(), sub.underlying_state(), trigger::X); 181 | super.enter(transition); 182 | 183 | ASSERT_FALSE(executed); 184 | } 185 | 186 | TEST(StateRepresentation, WhenTransitioningFromSuperToSubstate_ThenSuperExitActionsDoNotExecute) 187 | { 188 | CREATE_SUPER_SUB_PAIR(); 189 | bool executed = false; 190 | super.add_exit_action([&](const TTransition&){ executed = true; }); 191 | TTransition transition(super.underlying_state(), sub.underlying_state(), trigger::X); 192 | super.exit(transition); 193 | 194 | ASSERT_FALSE(executed); 195 | } 196 | 197 | TEST(StateRepresentation, WhenEnteringSubstate_ThenSuperEntryActionsExecute) 198 | { 199 | CREATE_SUPER_SUB_PAIR(); 200 | bool executed = false; 201 | super.add_entry_action([&](const TTransition&){ executed = true; }); 202 | TTransition transition(state::C, sub.underlying_state(), trigger::X); 203 | sub.enter(transition); 204 | 205 | ASSERT_TRUE(executed); 206 | } 207 | 208 | TEST(StateRepresentation, WhenLeavingSubstate_ThenSuperExitActionsExecute) 209 | { 210 | CREATE_SUPER_SUB_PAIR(); 211 | bool executed = false; 212 | super.add_exit_action([&](const TTransition&){ executed = true; }); 213 | TTransition transition(sub.underlying_state(), state::C, trigger::X); 214 | sub.exit(transition); 215 | 216 | ASSERT_TRUE(executed); 217 | } 218 | 219 | TEST(StateRepresentation, WhenEntering_ThenEntryActionsExecuteInOrder) 220 | { 221 | std::vector actual; 222 | 223 | TSR sr(state::B); 224 | sr.add_entry_action([&](const TTransition&){ actual.push_back(0); }); 225 | sr.add_entry_action([&](const TTransition&){ actual.push_back(1); }); 226 | 227 | sr.enter(TTransition(state::A, state::B, trigger::X)); 228 | 229 | ASSERT_EQ(2, actual.size()); 230 | EXPECT_EQ(0, actual.at(0)); 231 | EXPECT_EQ(1, actual.at(1)); 232 | } 233 | 234 | TEST(StateRepresentation, WhenLeaving_ThenExitActionsExecuteInOrder) 235 | { 236 | std::vector actual; 237 | 238 | TSR sr(state::B); 239 | sr.add_exit_action([&](const TTransition&){ actual.push_back(0); }); 240 | sr.add_exit_action([&](const TTransition&){ actual.push_back(1); }); 241 | 242 | sr.exit(TTransition(state::B, state::C, trigger::X)); 243 | 244 | ASSERT_EQ(2, actual.size()); 245 | EXPECT_EQ(0, actual.at(0)); 246 | EXPECT_EQ(1, actual.at(1)); 247 | } 248 | 249 | TEST(StateRepresentation, WhenTransitionExists_ThenTriggerCanBeFired) 250 | { 251 | TSR sr(state::B); 252 | auto tb = std::make_shared( 253 | trigger::X, [](){ return true; }, [](const state&, state&){ return false; }); 254 | sr.add_trigger_behaviour(trigger::X, tb); 255 | 256 | ASSERT_TRUE(sr.can_handle(trigger::X)); 257 | 258 | } 259 | 260 | TEST(StateRepresentation, WhenTransitionDoesNotExist_ThenTriggerCannotBeFired) 261 | { 262 | TSR sr(state::B); 263 | 264 | ASSERT_FALSE(sr.can_handle(trigger::X)); 265 | } 266 | 267 | TEST(StateRepresentation, WhenTransitionExistsInSupersate_ThenTriggerCanBeFired) 268 | { 269 | TSR sr_b(state::B); 270 | auto tb = std::make_shared( 271 | trigger::X, [](){ return true; }, [](const state&, state&){ return false; }); 272 | sr_b.add_trigger_behaviour(trigger::X, tb); 273 | TSR sub(state::C); 274 | sub.set_super_state(&sr_b); 275 | sr_b.add_sub_state(&sub); 276 | 277 | ASSERT_TRUE(sub.can_handle(trigger::X)); 278 | } 279 | 280 | TEST(StateRepresentation, WhenEnteringSubstate_ThenSuperstateEntryActionsExecuteBeforeSubstate) 281 | { 282 | CREATE_SUPER_SUB_PAIR(); 283 | int order = 0, sub_order = 0, super_order = 0; 284 | super.add_entry_action([&](const TTransition&){ super_order = order++; }); 285 | sub.add_entry_action([&](const TTransition&){ sub_order = order++; }); 286 | TTransition transition(state::C, sub.underlying_state(), trigger::X); 287 | sub.enter(transition); 288 | 289 | ASSERT_LT(super_order, sub_order); 290 | } 291 | 292 | TEST(StateRepresentation, WhenExitingSubstate_ThenSubstateEntryActionsExecuteBeforeSuperstate) 293 | { 294 | CREATE_SUPER_SUB_PAIR(); 295 | int order = 0, sub_order = 0, super_order = 0; 296 | super.add_exit_action([&](const TTransition&){ super_order = order++; }); 297 | sub.add_exit_action([&](const TTransition&){ sub_order = order++; }); 298 | TTransition transition(sub.underlying_state(), state::C, trigger::X); 299 | sub.exit(transition); 300 | 301 | ASSERT_LT(sub_order, super_order); 302 | } 303 | 304 | } 305 | -------------------------------------------------------------------------------- /test/transition_fixture.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | 21 | using namespace stateless::detail; 22 | using namespace testing; 23 | 24 | namespace 25 | { 26 | 27 | #ifdef _WIN32 28 | typedef transition TTransition; 29 | #else 30 | using TTransition = transition; 31 | #endif 32 | 33 | TEST(Transition, GivenSameSourceAndDestination_ThenTransitionIsReentry) 34 | { 35 | TTransition t(1, 1, 0); 36 | ASSERT_TRUE(t.is_reentry()); 37 | } 38 | 39 | TEST(Transition, GivenDifferentSourceAndDestination_ThenTransitionIsNotReentry) 40 | { 41 | TTransition t(1, 2, 0); 42 | ASSERT_FALSE(t.is_reentry()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /test/trigger.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef STATELESS_TEST_TRIGGER_HPP 18 | #define STATELESS_TEST_TRIGGER_HPP 19 | 20 | #include 21 | 22 | enum class trigger { X, Y, Z }; 23 | 24 | inline std::ostream& operator<<(std::ostream&os, const trigger& t) 25 | { 26 | static const char* trigger_name[] = { "X", "Y", "Z" }; 27 | os << trigger_name[(int)t]; 28 | return os; 29 | } 30 | 31 | #endif // STATELESS_TEST_TRIGGER_HPP 32 | -------------------------------------------------------------------------------- /test/trigger_behaviour_fixture.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | using namespace stateless::detail; 25 | using namespace testing; 26 | 27 | namespace 28 | { 29 | 30 | #ifdef _WIN32 31 | typedef trigger_behaviour TTB; 32 | #else 33 | using TTB = trigger_behaviour; 34 | #endif 35 | 36 | TEST(TriggerBehaviour, WhenUnderlyingTriggerIsSet_ThenItIsExposed) 37 | { 38 | TTB trigger_behaviour( 39 | trigger::X, 40 | []() { return true; }, 41 | [](const state& source, state& destination) { return true; }); 42 | ASSERT_EQ(trigger::X, trigger_behaviour.trigger()); 43 | } 44 | 45 | TEST(TriggerBehaviour, WhenGuardConditionFalse_ThenIsGuardConditionMetIsFalse) 46 | { 47 | TTB trigger_behaviour( 48 | trigger::X, 49 | []() { return false; }, 50 | [](const state& source, state& destination) { return true; }); 51 | ASSERT_FALSE(trigger_behaviour.is_condition_met()); 52 | } 53 | 54 | TEST(TriggerBehaviour, WhenGuardConditionTrue_ThenIsGuardConditionMetIsTrue) 55 | { 56 | TTB trigger_behaviour( 57 | trigger::X, 58 | []() { return true; }, 59 | [](const state& source, state& destination) { return true; }); 60 | ASSERT_TRUE(trigger_behaviour.is_condition_met()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /test/trigger_with_parameters_fixture.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Matt Mason 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | using namespace stateless; 23 | using namespace testing; 24 | 25 | namespace 26 | { 27 | 28 | #ifdef _WIN32 29 | typedef trigger_with_parameters TTWP; 30 | #else 31 | using TTWP = trigger_with_parameters; 32 | #endif 33 | 34 | TEST(TriggerWithParameters, WhenTriggerIsSet_ThenDescribesUnderlyingTrigger) 35 | { 36 | TTWP t(trigger::X); 37 | EXPECT_EQ(trigger::X, t.trigger()); 38 | } 39 | 40 | } 41 | --------------------------------------------------------------------------------