├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── lib └── fsm.ex ├── mix.exs ├── mix.lock └── test ├── fsm_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{lib,test}/**/*.{ex,exs}"] 3 | ] 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ebin 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /_build/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013. Saša Jurić 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fsm 2 | 3 | **This project is not maintained anymore, and I don't advise using it. Pure functional FSMs are still my preferred approach (as opposed to gen_statem), but you don't need this library for that. Regular data structures, such as maps or structs, with pattern matching in multiclauses will serve you just fine.** 4 | 5 | Fsm is pure functional finite state machine. Unlike `gen_fsm`, it doesn't run in its own process. Instead, it is a functional data structure. 6 | 7 | ## Why? 8 | 9 | In the rare cases I needed a proper fsm, I most often wanted to use it inside the already existing process, together with the already present state data. Creating another process didn't work for me because that requires additional bookkeeping such as supervising and process linking. More importantly, fsm as a process implies mutability and side effects, which is harder to deal with. In addition, `gen_fsm` introduces more complicated protocol of cross-process communication such as `send_event`, `sync_send_event`, `send_all_state_event` and `sync_send_all_state_event`. 10 | 11 | Unlike `gen_fsm`, the `Fsm` data structure has following benefits: 12 | 13 | * It is immutable and side-effect free 14 | * No need to create and manage separate processes 15 | * You can persist it, use it via ets, embed it inside `gen_server` or plain processes 16 | 17 | ## Basic example 18 | 19 | ```elixir 20 | defmodule BasicFsm do 21 | use Fsm, initial_state: :stopped 22 | 23 | defstate stopped do # opens the state scope 24 | defevent run do # defines event 25 | next_state(:running) # transition to next state 26 | end 27 | end 28 | 29 | defstate running do 30 | defevent stop do 31 | next_state(:stopped) 32 | end 33 | end 34 | end 35 | ``` 36 | 37 | Usage: 38 | 39 | Be sure to include a dependency in your mix.exs: 40 | 41 | ```elixir 42 | deps: [{:fsm, "~> 0.3.1"}, ...] 43 | ``` 44 | 45 | ```elixir 46 | # basic usage 47 | BasicFsm.new 48 | |> BasicFsm.run 49 | |> BasicFsm.stop 50 | 51 | # invalid state/event combination throws exception 52 | BasicFsm.new 53 | |> BasicFsm.run 54 | |> BasicFsm.run 55 | 56 | # you can query fsm for its state: 57 | BasicFsm.new 58 | |> BasicFsm.run 59 | |> BasicFsm.state 60 | ``` 61 | 62 | ## Data 63 | As you probably know, basic fsm is not Turing complete, and has limited uses. Therefore, `Fsm` introduces concept of data, just like `gen_fsm`: 64 | 65 | ```elixir 66 | defmodule DataFsm do 67 | use Fsm, initial_state: :stopped, initial_data: 0 68 | 69 | defstate stopped do 70 | defevent run(speed) do # events can have arguments 71 | next_state(:running, speed) # changing state and data 72 | end 73 | end 74 | 75 | defstate running do 76 | defevent slowdown(by), data: speed do # you can pattern match data with dedicated option 77 | next_state(:running, speed - by) 78 | end 79 | 80 | defevent stop do 81 | next_state(:stopped, 0) 82 | end 83 | end 84 | end 85 | 86 | DataFsm.new 87 | |> DataFsm.run(50) 88 | |> DataFsm.slowdown(10) 89 | |> DataFsm.data 90 | ``` 91 | 92 | ## Global handlers 93 | 94 | Normally, undefined state/event mapping throws an exception. You can handle this by using special `_` event definition: 95 | 96 | ```elixir 97 | defmodule BasicFsm do 98 | use Fsm, initial_state: :stopped 99 | 100 | defstate stopped do 101 | defevent run, do: next_state(:running) 102 | 103 | # called for undefined state/event mapping when inside stopped state 104 | defevent _, do: 105 | end 106 | 107 | defstate running do 108 | defevent stop, do: next_state(:stopped) 109 | end 110 | 111 | # called for some_event, regardless of the state 112 | defevent some_event, do: 113 | 114 | # called for undefined state/event mapping when inside any state 115 | defevent _, do: 116 | end 117 | ``` 118 | 119 | Keep in mind that public functions are defined only for the specified events. In the example above those are `run`, `stop`, and `some_event`. So you cannot call `BasicFsm.undefined_event`, because such event is not defined. You can explicitly define events, without adding them to state/event map: 120 | 121 | ```elixir 122 | defmodule MyFsm do 123 | defevent my_event1 # 0 arity event 124 | defevent my_event2/2 # 2 arity event 125 | end 126 | ``` 127 | 128 | In global handlers, it is often useful to know about event context: 129 | 130 | ```elixir 131 | defevent _, state: state, data: data, event: event, args: args do 132 | # now you can reference state, data, event and args 133 | ... 134 | end 135 | ``` 136 | 137 | ## Pattern matching and options 138 | 139 | Pattern matching works with event arguments, and all available options: 140 | 141 | ```elixir 142 | defstate some_state do 143 | defevent event(1), do: 144 | defevent event(2), do: 145 | 146 | defevent event(x), state: 0, do: 147 | defevent event(x), state: 1, do: 148 | end 149 | ``` 150 | 151 | It is allowed to define multiple global handlers: 152 | 153 | ```elixir 154 | defevent _, event: :event_1, do: 155 | defevent _, event: :event_2, do: 156 | defevent _, event: something_else, do: 157 | ``` 158 | 159 | You can also specify guards: 160 | ```elixir 161 | defevent my_event, when: ..., do: 162 | ``` 163 | 164 | ## Event results 165 | The result of the event handler determines the response of the event: 166 | 167 | ```elixir 168 | defevent my_event do 169 | ... 170 | next_state(:new_state) # data remains the same 171 | end 172 | 173 | defevent my_event do 174 | ... 175 | next_state(:new_state, new_data) 176 | end 177 | ``` 178 | 179 | The result of the event will be the new fsm instance: 180 | 181 | ```elixir 182 | fsm2 = MyFsm.my_event(fsm) 183 | MyFsm.another_event(fsm2, ...) 184 | ``` 185 | 186 | You can also return some result and the modified fsm instance: 187 | ```elixir 188 | respond(response) # data and state remain the same 189 | respond(response, :new_state) # data remains the same 190 | respond(response, :new_state, new_data) 191 | ``` 192 | 193 | In this case, the result of calling the event is a two elements tuple: 194 | ```elixir 195 | {response, fsm2} = MyFsm.my_event(mfs) 196 | ``` 197 | 198 | If the result of event handler is not created via `next_state` or `respond` it will be ignored, and the input fsm instance will be returned. This is useful when the event handler needs to perform some side-effect operations (file or network I/O) without changing the state or data. 199 | 200 | ## Dynamic definitions 201 | Fsm macros are runtime friendly, so you can build your fsm dynamically: 202 | 203 | ```elixir 204 | defmodule DynamicFsm do 205 | use Fsm, initial_state: :stopped 206 | 207 | # define states and transition 208 | fsm = [ 209 | stopped: [run: :running], 210 | running: [stop: :stopped] 211 | ] 212 | 213 | # loop through definition and dynamically call defstate/defevent 214 | for {state, transitions} <- fsm do 215 | defstate unquote(state) do 216 | for {event, target_state} <- transitions do 217 | defevent unquote(event) do 218 | next_state(unquote(target_state)) 219 | end 220 | end 221 | end 222 | end 223 | end 224 | ``` 225 | 226 | You might use this to define your fsm in the separate file, and in compile time read it and build the corresponding module. 227 | 228 | ## Generated functions 229 | Normally, `defevent` generates corresponding public interface function, which has the same name as the event. In addition, the multi-clause public `transition` function exists where all possible transitions are implemented. Interface functions simply delegate to the `transition` function, and their purpose is simply to have nicer looking interface. 230 | 231 | You can make interface function private: 232 | 233 | ```elixir 234 | defeventp ... 235 | ``` 236 | 237 | The `transition` function is always public. It can be used for dynamic fsm manipulation: 238 | 239 | ```elixir 240 | MyFsm.transition(fsm, :my_event, [arg1, arg2]) 241 | ``` 242 | 243 | Notice that with `transition`, you can also use undefined events, and they will be caught by global handlers (if such exist). 244 | 245 | ## Extending the module 246 | Inside your fsm module, you can add additional functions which manipulate the fsm. An fsm instance is represented by the private `fsm_rec` record: 247 | 248 | ```elixir 249 | def my_fun(fsm_rec() = fsm, ...), do: 250 | ``` 251 | 252 | ## In a separate process 253 | Fsm makes sense even when used from a separate process. Instead of relying on `gen_fsm` verbs, you can use `gen_server` simple call/cast approach. If the interface of the fsm is large, it may be tedious to create wrappers for all events. Runtime friendly [ExActor](https://github.com/sasa1977/exactor) can make your life a bit easier: 254 | 255 | ```elixir 256 | defmodule BasicFsmServer do 257 | use ExActor 258 | 259 | def init(_), do: initial_state(BasicFsm.new) 260 | 261 | # dynamic wrapping of zero arity events inside casts 262 | for event <- [:run, :stop] do 263 | defcast unquote(event), state: fsm do 264 | BasicFsm.unquote(event)(fsm) 265 | |> new_state 266 | end 267 | end 268 | 269 | # call wrapper to get the state 270 | defcall state, state: fsm, do: BasicFsm.state(fsm) 271 | end 272 | ``` 273 | -------------------------------------------------------------------------------- /lib/fsm.ex: -------------------------------------------------------------------------------- 1 | defmodule Fsm do 2 | defmacro __using__(opts) do 3 | quote do 4 | import Fsm 5 | 6 | defstruct state: unquote(opts[:initial_state]), 7 | data: unquote(opts[:initial_data]) 8 | 9 | @declaring_state nil 10 | @declared_events MapSet.new() 11 | 12 | def new(params \\ []), do: struct!(__MODULE__, params) 13 | 14 | def state(%__MODULE__{state: state}), do: state 15 | def data(%__MODULE__{data: data}), do: data 16 | 17 | # We need to suppress dialyzer warning, since in the second clause might 18 | # never match. This can happen if every single event handler uses `Fsm` 19 | # functions to explicitly set the next state. 20 | @dialyzer {:no_match, change_state: 2} 21 | defp change_state(%__MODULE__{} = fsm, {:action_responses, responses}), 22 | do: parse_action_responses(fsm, responses) 23 | 24 | defp change_state(%__MODULE__{} = fsm, _), do: fsm 25 | 26 | defp parse_action_responses(fsm, responses) do 27 | Enum.reduce(responses, fsm, fn response, fsm -> 28 | handle_action_response(fsm, response) 29 | end) 30 | end 31 | 32 | defp handle_action_response(fsm, {:next_state, next_state}) do 33 | %__MODULE__{fsm | state: next_state} 34 | end 35 | 36 | defp handle_action_response(fsm, {:new_data, new_data}) do 37 | %__MODULE__{fsm | data: new_data} 38 | end 39 | 40 | defp handle_action_response(fsm, {:respond, response}) do 41 | {response, fsm} 42 | end 43 | end 44 | end 45 | 46 | def next_state(state), do: {:action_responses, [next_state: state]} 47 | def next_state(state, data), do: {:action_responses, [next_state: state, new_data: data]} 48 | 49 | def respond(response), do: {:action_responses, [respond: response]} 50 | def respond(response, state), do: {:action_responses, [next_state: state, respond: response]} 51 | 52 | def respond(response, state, data), 53 | do: {:action_responses, [next_state: state, new_data: data, respond: response]} 54 | 55 | defmacro defstate(state, state_def) do 56 | quote do 57 | state_name = 58 | case unquote(Macro.escape(state, unquote: true)) do 59 | name when is_atom(name) -> name 60 | {name, _, _} -> name 61 | end 62 | 63 | @declaring_state state_name 64 | unquote(state_def) 65 | @declaring_state nil 66 | end 67 | end 68 | 69 | defmacro defevent(event) do 70 | decl_event(event, false) 71 | end 72 | 73 | defmacro defeventp(event) do 74 | decl_event(event, true) 75 | end 76 | 77 | defp decl_event(event, private) do 78 | quote do 79 | {event_name, arity} = 80 | case unquote(Macro.escape(event, unquote: nil)) do 81 | event_name when is_atom(event_name) -> {event_name, 0} 82 | {:/, _, [{event_name, _, _}, arity]} -> {event_name, arity} 83 | {event_name, _, _} -> {event_name, 0} 84 | end 85 | 86 | args = 87 | case arity do 88 | 0 -> [] 89 | n -> Enum.to_list(1..n) 90 | end 91 | 92 | private = unquote(private) 93 | unquote(define_interface()) 94 | end 95 | end 96 | 97 | defmacro defevent(event, opts) do 98 | do_defevent(event, opts, opts[:do]) 99 | end 100 | 101 | defmacro defevent(event, opts, do: event_def) do 102 | do_defevent(event, opts, event_def) 103 | end 104 | 105 | defmacro defeventp(event, opts) do 106 | do_defevent(event, [{:private, true} | opts], opts[:do]) 107 | end 108 | 109 | defmacro defeventp(event, opts, do: event_def) do 110 | do_defevent(event, [{:private, true} | opts], event_def) 111 | end 112 | 113 | defp do_defevent(event_decl, opts, event_def) do 114 | quote do 115 | unquote(extract_args(event_decl, opts, event_def)) 116 | unquote(define_interface()) 117 | unquote(implement_transition()) 118 | end 119 | end 120 | 121 | defp extract_args(event_decl, opts, event_def) do 122 | quote do 123 | {event_name, args} = 124 | case unquote(Macro.escape(event_decl, unquote: true)) do 125 | :_ -> {:_, []} 126 | name when is_atom(name) -> {name, []} 127 | {name, _, args} -> {name, args || []} 128 | end 129 | 130 | private = unquote(opts[:private]) 131 | state_arg = unquote(Macro.escape(opts[:state] || quote(do: _), unquote: true)) 132 | data_arg = unquote(Macro.escape(opts[:data] || quote(do: _), unquote: true)) 133 | event_arg = unquote(Macro.escape(opts[:event] || quote(do: _), unquote: true)) 134 | args_arg = unquote(Macro.escape(opts[:args] || quote(do: _), unquote: true)) 135 | event_def = unquote(Macro.escape(event_def, unquote: true)) 136 | guard = unquote(Macro.escape(opts[:when])) 137 | end 138 | end 139 | 140 | defp define_interface do 141 | quote bind_quoted: [] do 142 | unless event_name == :_ or MapSet.member?(@declared_events, {event_name, length(args)}) do 143 | interface_args = 144 | Enum.reduce(args, {0, []}, fn _, {index, args} -> 145 | { 146 | index + 1, 147 | [{:"arg#{index}", [], nil} | args] 148 | } 149 | end) 150 | |> elem(1) 151 | |> Enum.reverse() 152 | 153 | body = 154 | quote do 155 | transition(fsm, unquote(event_name), [unquote_splicing(interface_args)]) 156 | end 157 | 158 | interface_args = [quote(do: fsm) | interface_args] 159 | 160 | if private do 161 | defp unquote(event_name)(unquote_splicing(interface_args)), do: unquote(body) 162 | else 163 | def unquote(event_name)(unquote_splicing(interface_args)), do: unquote(body) 164 | end 165 | 166 | @declared_events MapSet.put(@declared_events, {event_name, length(args)}) 167 | end 168 | end 169 | end 170 | 171 | defp implement_transition do 172 | quote bind_quoted: [] do 173 | transition_args = [ 174 | if @declaring_state do 175 | quote do 176 | %__MODULE__{ 177 | state: unquote(@declaring_state) = unquote(state_arg), 178 | data: unquote(data_arg) 179 | } = fsm 180 | end 181 | else 182 | quote do 183 | %__MODULE__{state: unquote(state_arg), data: unquote(data_arg)} = fsm 184 | end 185 | end, 186 | quote do 187 | unquote(if event_name == :_, do: quote(do: _), else: event_name) = unquote(event_arg) 188 | end, 189 | quote do 190 | unquote(if event_name == :_, do: quote(do: _), else: args) = unquote(args_arg) 191 | end 192 | ] 193 | 194 | body = quote(do: change_state(fsm, unquote(event_def))) 195 | 196 | if guard do 197 | def transition(unquote_splicing(transition_args)) when unquote(guard), do: unquote(body) 198 | else 199 | def transition(unquote_splicing(transition_args)), do: unquote(body) 200 | end 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Fsm.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :fsm, 7 | version: "0.3.1", 8 | elixir: "~> 1.1", 9 | deps: deps(), 10 | package: [ 11 | maintainers: ["Saša Jurić"], 12 | licenses: ["MIT"], 13 | links: %{Github: "https://github.com/sasa1977/fsm"} 14 | ], 15 | description: "Finite state machine as a functional data structure." 16 | ] 17 | end 18 | 19 | def application do 20 | [applications: [:logger]] 21 | end 22 | 23 | defp deps do 24 | [] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{} 2 | -------------------------------------------------------------------------------- /test/fsm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FsmTest do 2 | use ExUnit.Case 3 | 4 | defmodule BasicFsm do 5 | use Fsm, initial_state: :stopped 6 | 7 | defstate stopped do 8 | defevent run do 9 | next_state(:running) 10 | end 11 | end 12 | 13 | defstate running do 14 | defevent stop do 15 | next_state(:stopped) 16 | end 17 | end 18 | end 19 | 20 | test "basic" do 21 | assert( 22 | BasicFsm.new() 23 | |> BasicFsm.state() == :stopped 24 | ) 25 | 26 | assert( 27 | BasicFsm.new() 28 | |> BasicFsm.run() 29 | |> BasicFsm.state() == :running 30 | ) 31 | 32 | assert( 33 | BasicFsm.new() 34 | |> BasicFsm.run() 35 | |> BasicFsm.stop() 36 | |> BasicFsm.state() == :stopped 37 | ) 38 | 39 | assert_raise(FunctionClauseError, fn -> 40 | BasicFsm.new() 41 | |> BasicFsm.run() 42 | |> BasicFsm.run() 43 | end) 44 | end 45 | 46 | test "initialize with other state" do 47 | assert BasicFsm.new(state: :running) 48 | |> BasicFsm.state() == :running 49 | end 50 | 51 | defmodule PrivateFsm do 52 | use Fsm, initial_state: :stopped 53 | 54 | defstate stopped do 55 | defeventp run do 56 | next_state(:running) 57 | end 58 | end 59 | 60 | def my_run(fsm), do: run(fsm) 61 | end 62 | 63 | test "private" do 64 | assert_raise(UndefinedFunctionError, fn -> 65 | PrivateFsm.new() 66 | |> PrivateFsm.run() 67 | end) 68 | 69 | assert( 70 | PrivateFsm.new() 71 | |> PrivateFsm.my_run() 72 | |> PrivateFsm.state() == :running 73 | ) 74 | end 75 | 76 | defmodule GlobalHandlers do 77 | use Fsm, initial_state: :stopped 78 | 79 | defstate stopped do 80 | defevent(undefined_event1) 81 | defevent(undefined_event2 / 2) 82 | 83 | defevent run do 84 | next_state(:running) 85 | end 86 | 87 | defevent _ do 88 | next_state(:invalid1) 89 | end 90 | end 91 | 92 | defstate running do 93 | defevent stop do 94 | next_state(:stopped) 95 | end 96 | end 97 | 98 | defevent _ do 99 | next_state(:invalid2) 100 | end 101 | end 102 | 103 | test "global handlers" do 104 | assert( 105 | GlobalHandlers.new() 106 | |> GlobalHandlers.undefined_event1() 107 | |> GlobalHandlers.state() == :invalid1 108 | ) 109 | 110 | assert( 111 | GlobalHandlers.new() 112 | |> GlobalHandlers.run() 113 | |> GlobalHandlers.undefined_event2(1, 2) 114 | |> GlobalHandlers.state() == :invalid2 115 | ) 116 | end 117 | 118 | defmodule DataFsm do 119 | use Fsm, initial_state: :stopped, initial_data: 0 120 | 121 | defstate stopped do 122 | defevent run(speed) do 123 | next_state(:running, speed) 124 | end 125 | end 126 | 127 | defstate running do 128 | defevent slowdown(by), data: speed do 129 | next_state(:running, speed - by) 130 | end 131 | 132 | defevent stop do 133 | next_state(:stopped, 0) 134 | end 135 | end 136 | end 137 | 138 | test "data" do 139 | assert( 140 | DataFsm.new() 141 | |> DataFsm.data() == 0 142 | ) 143 | 144 | assert( 145 | DataFsm.new() 146 | |> DataFsm.run(50) 147 | |> DataFsm.data() == 50 148 | ) 149 | 150 | assert( 151 | DataFsm.new() 152 | |> DataFsm.run(50) 153 | |> DataFsm.slowdown(20) 154 | |> DataFsm.data() == 30 155 | ) 156 | 157 | assert( 158 | DataFsm.new() 159 | |> DataFsm.run(50) 160 | |> DataFsm.stop() 161 | |> DataFsm.data() == 0 162 | ) 163 | end 164 | 165 | test "initialize with other data" do 166 | assert( 167 | DataFsm.new(data: 42) 168 | |> DataFsm.data() == 42 169 | ) 170 | end 171 | 172 | test "initialize with other state and data" do 173 | fsm = DataFsm.new(state: :running, data: 42) 174 | 175 | assert( 176 | fsm 177 | |> DataFsm.state() == :running 178 | ) 179 | 180 | assert( 181 | fsm 182 | |> DataFsm.data() == 42 183 | ) 184 | end 185 | 186 | defmodule ResponseFsm do 187 | use Fsm, initial_state: :stopped, initial_data: 0 188 | 189 | defstate stopped do 190 | defevent run(speed) do 191 | respond(:ok, :running, speed) 192 | end 193 | 194 | defevent _ do 195 | respond(:error) 196 | end 197 | end 198 | 199 | defstate running do 200 | defevent stop do 201 | respond(:ok, :stopped, 0) 202 | end 203 | 204 | defevent _ do 205 | respond(:error, :invalid) 206 | end 207 | end 208 | end 209 | 210 | test "response actions" do 211 | {response, fsm} = 212 | ResponseFsm.new() 213 | |> ResponseFsm.run(50) 214 | 215 | assert(response == :ok) 216 | assert(ResponseFsm.state(fsm) == :running) 217 | assert(ResponseFsm.data(fsm) == 50) 218 | 219 | {response2, fsm2} = ResponseFsm.run(fsm, 10) 220 | assert(response2 == :error) 221 | assert(ResponseFsm.state(fsm2) == :invalid) 222 | 223 | assert( 224 | ResponseFsm.new() 225 | |> ResponseFsm.stop() == {:error, %ResponseFsm{data: 0, state: :stopped}} 226 | ) 227 | end 228 | 229 | defmodule PatternMatch do 230 | use Fsm, initial_state: :running, initial_data: 10 231 | 232 | defstate running do 233 | defevent toggle_speed, data: d, when: d == 10 do 234 | next_state(:running, 50) 235 | end 236 | 237 | defevent toggle_speed, data: 50 do 238 | next_state(:running, 10) 239 | end 240 | 241 | defevent set_speed(1) do 242 | next_state(:running, 10) 243 | end 244 | 245 | defevent set_speed(x), when: x == 2 do 246 | next_state(:running, 50) 247 | end 248 | 249 | defevent(stop, do: next_state(:stopped)) 250 | end 251 | 252 | defevent dummy, state: :stopped do 253 | respond(:dummy) 254 | end 255 | 256 | defevent _, event: :toggle_speed do 257 | respond(:error) 258 | end 259 | end 260 | 261 | test "pattern match" do 262 | assert( 263 | PatternMatch.new() 264 | |> PatternMatch.toggle_speed() 265 | |> PatternMatch.data() == 50 266 | ) 267 | 268 | assert( 269 | PatternMatch.new() 270 | |> PatternMatch.toggle_speed() 271 | |> PatternMatch.toggle_speed() 272 | |> PatternMatch.data() == 10 273 | ) 274 | 275 | assert( 276 | PatternMatch.new() 277 | |> PatternMatch.set_speed(1) 278 | |> PatternMatch.data() == 10 279 | ) 280 | 281 | assert( 282 | PatternMatch.new() 283 | |> PatternMatch.set_speed(2) 284 | |> PatternMatch.data() == 50 285 | ) 286 | 287 | assert_raise(FunctionClauseError, fn -> 288 | PatternMatch.new() 289 | |> PatternMatch.set_speed(3) 290 | |> PatternMatch.data() == 50 291 | end) 292 | 293 | assert( 294 | PatternMatch.new() 295 | |> PatternMatch.stop() 296 | |> PatternMatch.dummy() == {:dummy, %PatternMatch{data: 10, state: :stopped}} 297 | ) 298 | 299 | assert( 300 | PatternMatch.new() 301 | |> PatternMatch.stop() 302 | |> PatternMatch.toggle_speed() == {:error, %PatternMatch{data: 10, state: :stopped}} 303 | ) 304 | 305 | assert_raise(FunctionClauseError, fn -> 306 | PatternMatch.new() 307 | |> PatternMatch.dummy() 308 | end) 309 | 310 | assert_raise(FunctionClauseError, fn -> 311 | PatternMatch.new() 312 | |> PatternMatch.stop() 313 | |> PatternMatch.stop() 314 | end) 315 | 316 | assert_raise(FunctionClauseError, fn -> 317 | PatternMatch.new() 318 | |> PatternMatch.stop() 319 | |> PatternMatch.set_speed(1) 320 | end) 321 | end 322 | 323 | defmodule DynamicFsm do 324 | use Fsm, initial_state: :stopped 325 | 326 | fsm = [ 327 | stopped: [run: :running], 328 | running: [stop: :stopped] 329 | ] 330 | 331 | for {state, transitions} <- fsm do 332 | defstate unquote(state) do 333 | for {event, target_state} <- transitions do 334 | defevent unquote(event) do 335 | next_state(unquote(target_state)) 336 | end 337 | end 338 | end 339 | end 340 | end 341 | 342 | test "dynamic" do 343 | assert( 344 | DynamicFsm.new() 345 | |> DynamicFsm.state() == :stopped 346 | ) 347 | 348 | assert( 349 | DynamicFsm.new() 350 | |> DynamicFsm.run() 351 | |> DynamicFsm.state() == :running 352 | ) 353 | 354 | assert( 355 | DynamicFsm.new() 356 | |> DynamicFsm.run() 357 | |> DynamicFsm.stop() 358 | |> DynamicFsm.state() == :stopped 359 | ) 360 | 361 | assert_raise(FunctionClauseError, fn -> 362 | DynamicFsm.new() 363 | |> DynamicFsm.run() 364 | |> DynamicFsm.run() 365 | end) 366 | end 367 | end 368 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------