├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── examples ├── code_lock │ ├── .gitignore │ ├── README.md │ ├── lib │ │ └── code_lock.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ │ ├── code_lock_test.exs │ │ └── test_helper.exs └── turnstile │ ├── .gitignore │ ├── README.md │ ├── lib │ └── turnstile.ex │ ├── mix.exs │ ├── mix.lock │ └── test │ ├── test_helper.exs │ └── turnstile_test.exs ├── lib └── gen_fsm.ex ├── mix.exs ├── mix.lock └── test ├── gen_fsm_test.exs ├── test_helper.exs └── turnstile_example_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | doc/* 2 | .idea/* 3 | /_build 4 | /cover 5 | /deps 6 | erl_crash.dump 7 | *.ez 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Paul Hieromnimon 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # GenFSM 4 | 5 | Elixir wrapper around Erlang's OTP gen_fsm. 6 | 7 | 8 | ## Motivation 9 | 10 | Elixir [deprecated](https://github.com/elixir-lang/elixir/commit/455eb4c4ace81ce60b347558f9419fe3c33d8bf7) 11 | its wrapper around OTP's gen_fsm from the standard library because it is difficult to understand and suggested that 12 | developers seek other finite state machine implementations. 13 | 14 | This is understandable, but some of us still need/prefer to use the OTP gen_fsm. 15 | 16 | I took the basis of Elixir's old 17 | [GenFSM.Behaviour](https://github.com/elixir-lang/elixir/blob/a6f048b3de4a971c15fc8b66397cf2e4597793cb/lib/elixir/lib/gen_fsm/behaviour.ex) 18 | and added some additional convenience methods. Currently missing are the `enter_loop` methods. 19 | 20 | ## Usage 21 | 22 | The following example implement a simple state machine with two states, `martin` and `paul`. The state machine will initialize into the `martin` state, when the state machine receive `:hello` as the input it will transition between the states, from `martin` to `paul` and `"Hello, Paul"` will get printed to the console. 23 | 24 | ```elixir 25 | defmodule Conversation do 26 | use GenFSM 27 | 28 | def start_link() do 29 | GenFSM.start_link(__MODULE__, :na) 30 | end 31 | 32 | def hello(pid) do 33 | GenFSM.send_event(pid, :hello) 34 | end 35 | 36 | def init(:na), do: {:ok, :martin, nil} 37 | 38 | def martin(:hello, nil) do 39 | IO.puts "Hello, Paul" 40 | {:next_state, :paul, nil} 41 | end 42 | 43 | def paul(:hello, nil) do 44 | IO.puts "Hello, Martin" 45 | {:next_state, :martin, nil} 46 | end 47 | end 48 | ``` 49 | 50 | A conversation could go like this: 51 | 52 | ``` elixir 53 | iex(2)> {:ok, pid} = Conversation.start_link 54 | {:ok, #PID<0.165.0>} 55 | iex(3)> Conversation.hello pid 56 | Hello, Paul 57 | :ok 58 | iex(4)> Conversation.hello pid 59 | Hello, Martin 60 | :ok 61 | iex(5)> 62 | ``` 63 | 64 | ## Installation 65 | 66 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 67 | 68 | 1. Add gen_fsm to your list of dependencies in `mix.exs`: 69 | 70 | def deps do 71 | [{:gen_fsm, "~> 0.1.0"}] 72 | end 73 | 74 | ## Documentation 75 | 76 | Complete [API documentation](http://erlang.org/doc/man/gen_fsm.html) can be found at 77 | http://erlang.org/doc/man/gen_fsm.html 78 | and OTP [design principal documentation](http://erlang.org/doc/design_principles/fsm.html) 79 | lives at http://erlang.org/doc/man/gen_fsm.html 80 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :gen_fsm, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:gen_fsm, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/code_lock/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /examples/code_lock/README.md: -------------------------------------------------------------------------------- 1 | # Code Lock 2 | 3 | An Elixir port of the Code Lock example found in the Erlang OTP documentation. 4 | 5 | * http://erlang.org/doc/design_principles/fsm.html 6 | 7 | This link contain explanation of the code. 8 | 9 | Remember to run `mix deps.get` in the example folder and have a look in the test file for usage. 10 | -------------------------------------------------------------------------------- /examples/code_lock/lib/code_lock.ex: -------------------------------------------------------------------------------- 1 | defmodule CodeLock do 2 | use GenFSM 3 | 4 | def start_link(code) do 5 | GenFSM.start_link(__MODULE__, Enum.reverse(code)) 6 | end 7 | 8 | def button(pid, digit) do 9 | IO.puts "beeep!" 10 | GenFSM.send_event(pid, {:button, digit}) 11 | end 12 | 13 | def init(code) do 14 | {:ok, :locked, {[], code}} 15 | end 16 | 17 | def locked({:button, digit}, {so_far, code}) do 18 | IO.inspect {:state, [digit | so_far], code} 19 | case [digit | so_far] do 20 | ^code -> 21 | # do_unlock(...) 22 | IO.puts "unlocked!" 23 | {:next_state, :open, {[], code}, 1000} 24 | 25 | incomplete when length(incomplete) < length(code) -> 26 | IO.puts "awaiting more digits" 27 | {:next_state, :locked, {incomplete, code}} 28 | 29 | _wrong -> 30 | IO.puts "wrong!" 31 | {:next_state, :locked, {[], code}} 32 | end 33 | end 34 | 35 | def open(:timeout, state) do 36 | # do_lock(...) 37 | IO.puts "SLAM! locked!" 38 | {:next_state, :locked, state} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /examples/code_lock/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CodeLock.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :code_lock, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger]] 15 | end 16 | 17 | defp deps do 18 | [{:gen_fsm, "~> 0.1"}] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/code_lock/mix.lock: -------------------------------------------------------------------------------- 1 | %{"gen_fsm": {:hex, :gen_fsm, "0.1.0", "5097308e244e25dbb2aa0ee5b736bb1ab79c3f8ca889a9e5b3aabd8beee6fc88", [:mix], []}} 2 | -------------------------------------------------------------------------------- /examples/code_lock/test/code_lock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CodeLockTest do 2 | use ExUnit.Case 3 | 4 | test "punch in those numbers and await the timeout" do 5 | {:ok, pid} = CodeLock.start_link([1,2,3,4]) 6 | 7 | CodeLock.button(pid, 1) 8 | :timer.sleep 10 9 | CodeLock.button(pid, 2) 10 | :timer.sleep 10 11 | CodeLock.button(pid, 3) 12 | :timer.sleep 10 13 | CodeLock.button(pid, 4) 14 | # we should be open by now 15 | 16 | # wait for the timer... 17 | :timer.sleep 1100 18 | 19 | IO.puts "done!" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/code_lock/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/turnstile/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /examples/turnstile/README.md: -------------------------------------------------------------------------------- 1 | # GenFSM Turnstile Example 2 | 3 | Have a look at the module documentation in `lib/turnstile.ex` 4 | 5 | Load the project up in IEX after fetching the dependencies from hex.pm: 6 | 7 | ``` bash 8 | $ mix deps.get 9 | $ iex -S mix 10 | ``` 11 | 12 | `Turnstile` should be available. Use the `h/1` function to read about the example: 13 | 14 | ``` elixir 15 | iex(0)> h Turnstile 16 | ``` 17 | 18 | Notice that the state machine will crash if the turn-stile is emptied while in the unlocked state. 19 | -------------------------------------------------------------------------------- /examples/turnstile/lib/turnstile.ex: -------------------------------------------------------------------------------- 1 | defmodule Turnstile do 2 | use GenFSM 3 | 4 | @moduledoc """ 5 | A module that implement a turnstile that let a person enter if 6 | a coin has been inserted into the turnstile; no access will be 7 | granted if no coins has been inserted. If multiple coins are 8 | inserted it will only let one person through; it is greedy like 9 | that. 10 | 11 | Start the turnstile process using `start_link/0` 12 | 13 | iex> {:ok, pid} = Turnstile.start_link() 14 | {:ok, #PID<0.137.0>} 15 | 16 | The turnstile will start in the locked position. From the locked 17 | state it will accept the `insert_coin` input, as well as the 18 | `empty` and `enter` input--it will not let anyone pass though. 19 | 20 | If we try to empty the turnstile we will get `0` 21 | 22 | iex> Turnstile.empty(pid) 23 | 0 24 | 25 | If we enter a coin it will be put into the memory of the state 26 | machine: 27 | 28 | iex> Turnstile.insert_coin(pid) 29 | :ok 30 | 31 | And we are allowed to enter the turnstile: 32 | 33 | iex> Turnstile.enter(pid) 34 | :access_allowed 35 | 36 | We can empty the bank of the turnstile to verify that there is 37 | a coin. 38 | 39 | iex> Turnstile.empty(pid) 40 | 1 41 | 42 | Emptying it again will yield `0` because `Turnstile.empty/1` 43 | will reset the bank. 44 | 45 | Let's try entering the turnstile. When it is in the locked 46 | position we will get an `:access_denied` when using 47 | `Turnstile.enter/1`. 48 | 49 | iex> Turnstile.enter(pid) 50 | :access_denied 51 | 52 | When someone enters the turnstile while in the unlocked state 53 | it will go back to the locked state, but the bank will contain 54 | the coin: 55 | 56 | iex> Turnstile.enter(pid) 57 | :access_denied 58 | iex> Turnstile.empty(pid) 59 | 1 60 | 61 | The source code had been annotated with comments. Please open 62 | an issue if something is not clear. 63 | """ 64 | 65 | # Initialize the turnstile state machine with zero coins 66 | def start_link do 67 | GenFSM.start_link(__MODULE__, 0) 68 | end 69 | 70 | # Public API 71 | # 72 | # The user can enter and insert_coins into our turnstile. The 73 | # outcome of these actions depends on the state of the turnstile 74 | # lock. 75 | def enter(pid) do 76 | GenFSM.sync_send_event(pid, :enter) 77 | end 78 | def empty(pid) do 79 | GenFSM.sync_send_event(pid, :empty) 80 | end 81 | def insert_coin(pid) do 82 | GenFSM.send_event(pid, :coin) 83 | end 84 | 85 | # Internal API 86 | def init(number_of_coins) do 87 | # Initialize the turnstile state machine in the locked state. 88 | # The state machine is initialized with a given number of 89 | # coins in its bank. 90 | {:ok, :locked, number_of_coins} 91 | end 92 | 93 | def unlocked(:coin, state) do 94 | # If another coin is inserted we will just eat the coin. 95 | {:next_state, :unlocked, state + 1} 96 | end 97 | def unlocked(:enter, _from, state) do 98 | # One can enter the door when it is unlocked, but it will switch 99 | # state to locked when the door has been entered. 100 | {:reply, :access_allowed, :locked, state} 101 | end 102 | 103 | def locked(:coin, state) do 104 | # Increment the bank and set the state to unlocked, allowing 105 | # someone to enter. 106 | {:next_state, :unlocked, state + 1} 107 | end 108 | def locked(:enter, _from, state) do 109 | # The door will not let anyone enter before it has been unlocked 110 | # by inserting a coin. Entering in this state will not to 111 | # anything. 112 | {:reply, :access_denied, :locked, state} 113 | end 114 | def locked(:empty, _from, state) do 115 | # The turnstile can be emptied when it is locked. This will reset 116 | # the bank to zero and return the number of coins (we do not take 117 | # authorization into consideration here). When the turnstile is 118 | # emptied it will remain in the locked state. 119 | {:reply, state, :locked, 0} 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /examples/turnstile/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Turnstile.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :turnstile, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | [applications: [:logger]] 18 | end 19 | 20 | # Dependencies can be Hex packages: 21 | # 22 | # {:mydep, "~> 0.3.0"} 23 | # 24 | # Or git/path repositories: 25 | # 26 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 27 | # 28 | # Type "mix help deps" for more examples and options 29 | defp deps do 30 | [{:gen_fsm, "~> 0.1"}] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /examples/turnstile/mix.lock: -------------------------------------------------------------------------------- 1 | %{"gen_fsm": {:hex, :gen_fsm, "0.1.0"}} 2 | -------------------------------------------------------------------------------- /examples/turnstile/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/turnstile/test/turnstile_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TurnstileTest do 2 | use ExUnit.Case 3 | # doctest Turnstile 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/gen_fsm.ex: -------------------------------------------------------------------------------- 1 | defmodule GenFSM do 2 | @type state_name :: atom 3 | @type state_data :: term 4 | @type next_state_name :: atom 5 | @type new_state_data :: term 6 | @type reason :: term 7 | 8 | @callback init(args :: term) :: 9 | {:ok, state_name, state_data} | 10 | {:ok, state_name, state_data, timeout | :hibernate} | 11 | :ignore | 12 | {:stop, reason} 13 | 14 | @callback handle_event(event :: term, state_name, state_data) :: 15 | {:next_state, next_state_name, new_state_data} | 16 | {:next_state, next_state_name, new_state_data, timeout} | 17 | {:next_state, next_state_name, new_state_data, :hibernate} | 18 | {:stop, reason, new_state_data} when new_state_data: term 19 | 20 | @type reply :: term 21 | @callback handle_sync_event(event :: term, from :: {pid, tag :: term}, state_name, state_data) :: 22 | {:reply, reply, next_state_name, new_state_data} | 23 | {:reply, reply, next_state_name, new_state_data, timeout} | 24 | {:reply, reply, next_state_name, new_state_data, :hibernate} | 25 | {:next_state, next_state_name, new_state_data} | 26 | {:next_state, next_state_name, new_state_data, timeout} | 27 | {:next_state, next_state_name, new_state_data, :hibernate} | 28 | {:stop, reason, reply, new_state_data} | 29 | {:stop, reason, new_state_data} when new_state_data: term 30 | 31 | @callback handle_info(info :: term, state_name, state_data) :: 32 | {:next_state, next_state_name, new_state_data} | 33 | {:next_state, next_state_name, new_state_data, timeout} | 34 | {:next_state, next_state_name, new_state_data, :hibernate} | 35 | {:stop, reason, new_state_data} when new_state_data: term 36 | 37 | @callback terminate(reason, state_name, state_data) :: 38 | term when reason: :normal | :shutdown | {:shutdown, term} | term 39 | 40 | @callback code_change(old_vsn, state_name, state_data, extra :: term) :: 41 | {:ok, next_state_name, new_state_data} | 42 | {:error, reason} when old_vsn: term | 43 | {:down, term} 44 | 45 | @typedoc "Return values of `start*` functions" 46 | @type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term} 47 | 48 | @typedoc "Options used by the `start*` functions" 49 | @type options :: [option] 50 | 51 | @typedoc "Option values used by the `start*` functions" 52 | @type option :: {:debug, debug} | 53 | {:name, name} | 54 | {:timeout, timeout} | 55 | {:spawn_opt, Process.spawn_opt} 56 | 57 | @typedoc "The GenFSM name" 58 | @type name :: atom | {:global, term} | {:via, module, term} 59 | 60 | @typedoc "Debug options supported by the `start*` functions" 61 | @type debug :: [:trace | :log | :statistics | {:log_to_file, Path.t} | {:install, {fun, any}}] 62 | 63 | @typedoc "The fsm reference" 64 | @type fsm_ref :: name | {name, node} | pid() 65 | 66 | @type event :: term 67 | 68 | @spec start_link(module, any, options) :: on_start 69 | def start_link(module, args, options \\ []) when is_atom(module) and is_list(options) do 70 | do_start(:link, module, args, options) 71 | end 72 | 73 | @spec start(module, any, options) :: on_start 74 | def start(module, args, options \\ []) when is_atom(module) and is_list(options) do 75 | do_start(:nolink, module, args, options) 76 | end 77 | 78 | defp do_start(link, module, args, options) do 79 | case Keyword.pop(options, :name) do 80 | {nil, opts} -> 81 | :gen.start(:gen_fsm, link, module, args, opts) 82 | {atom, opts} when is_atom(atom) -> 83 | :gen.start(:gen_fsm, link, {:local, atom}, module, args, opts) 84 | {other, opts} when is_tuple(other) -> 85 | :gen.start(:gen_fsm, link, other, module, args, opts) 86 | end 87 | end 88 | 89 | @spec stop(fsm_ref, reason :: term, timeout) :: :ok 90 | def stop(fsm, reason \\ :normal, timeout \\ :infinity) do 91 | :gen.stop(fsm, reason, timeout) 92 | end 93 | 94 | @doc """ 95 | Sends an `event` to the `GenFSM` and waits until a reply arrives 96 | or a timeout occurs. 97 | """ 98 | @spec sync_send_event(fsm_ref, event) :: :ok 99 | defdelegate sync_send_event(fsm_ref, event), to: :gen_fsm 100 | 101 | @doc """ 102 | Sends an event to the `GenFSM` and waits until a reply arrives 103 | or a `timeout` occurs. 104 | """ 105 | @spec sync_send_all_state_event(fsm_ref, event, timeout | :infinity) :: :ok 106 | defdelegate sync_send_all_state_event(fsm_ref, event, timeout), to: :gen_fsm 107 | 108 | @doc """ 109 | Sends an event asynchronously to the `GenFSM` and returns 110 | `:ok` immediately. 111 | """ 112 | @spec send_event(fsm_ref, event) :: :ok 113 | defdelegate send_event(fsm_ref, event), to: :gen_fsm 114 | 115 | @doc """ 116 | This function can be used by a `GenFSM` to explicitly send a 117 | reply to a client process. 118 | """ 119 | @type from :: {pid, tag :: term} 120 | @spec reply(from, reply) :: :ok 121 | defdelegate reply(caller, reply), to: :gen_fsm 122 | 123 | @doc """ 124 | Sends a delayed event internally in the `GenFSM` that calls this 125 | function after `time` in ms. Returns immediately a `reference` that 126 | can be used to cancel the delayed send using `cancel_timer/1`. 127 | """ 128 | @spec send_event_after(integer, event) :: reference 129 | defdelegate send_event_after(time, event), to: :gen_fsm 130 | 131 | @doc """ 132 | Sends a timeout event internally in the `GenFSM` that calls this 133 | function after `time` set in ms. Returns immediately a `reference` 134 | that can be used to cancel the timer using `cancel_timer/1`. 135 | """ 136 | @spec start_timer(integer, term) :: reference 137 | defdelegate start_timer(time, message), to: :gen_fsm 138 | 139 | @doc """ 140 | Cancels an internal timer referred by `reference` in the `GenFSM` 141 | that calls this function. 142 | """ 143 | @type remaining_time :: integer 144 | @spec cancel_timer(reference) :: remaining_time | false 145 | defdelegate cancel_timer(timer_ref), to: :gen_fsm 146 | 147 | defmacro __using__(_) do 148 | quote location: :keep do 149 | @behaviour :gen_fsm 150 | 151 | @doc false 152 | def handle_event(_event, _state_name, state_data) do 153 | {:stop, :unexpected_event, state_data} 154 | end 155 | 156 | @doc false 157 | def handle_sync_event(_event, _from, _state_name, state_data) do 158 | {:stop, :unexpected_event, state_data} 159 | end 160 | 161 | @doc false 162 | def handle_info(_info, _state_name, state_data) do 163 | {:stop, :unexpected_message, state_data} 164 | end 165 | 166 | @doc false 167 | def terminate(reason, _state_name, _state_data) do 168 | reason 169 | end 170 | 171 | @doc false 172 | def code_change(_old, state_name, state_data, _extra) do 173 | {:ok, state_name, state_data} 174 | end 175 | 176 | defoverridable [handle_event: 3, handle_sync_event: 4, 177 | handle_info: 3, terminate: 3, code_change: 4] 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GenFSM.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :gen_fsm, 6 | version: "0.1.0", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | package: package, 11 | deps: deps, 12 | description: description] 13 | end 14 | 15 | def deps do 16 | [{:ex_doc, ">= 0.11.4", only: [:dev]}, 17 | {:earmark, ">= 0.0.0", only: [:dev]}] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | [applications: [:logger]] 25 | end 26 | 27 | defp description do 28 | """ 29 | Elixir wrapper around Erlang's OTP gen_fsm. 30 | """ 31 | end 32 | 33 | defp package do 34 | [# These are the default files included in the package 35 | files: ["lib", "mix.exs", "README*", "LICENSE"], 36 | maintainers: ["Paul Hieromnimon"], 37 | licenses: ["Apache 2.0"], 38 | links: %{"GitHub" => "https://github.com/pavlos/gen_fsm", 39 | "Docs" => "https://hexdocs.pm/gen_fsm/"}] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "0.2.1"}, 2 | "ex_doc": {:hex, :ex_doc, "0.11.4"}} 3 | -------------------------------------------------------------------------------- /test/gen_fsm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GenFsmTest do 2 | use ExUnit.Case 3 | import ExUnit.CaptureLog 4 | 5 | defmodule Sample do 6 | use GenFSM 7 | 8 | def init(args) do 9 | { :ok, :sample, args } 10 | end 11 | end 12 | 13 | test "sync event stops server on unknown requests" do 14 | capture_log fn-> 15 | Process.flag(:trap_exit, true) 16 | assert { :ok, pid } = GenFSM.start_link(Sample, [:hello], []) 17 | 18 | catch_exit(:gen_fsm.sync_send_all_state_event(pid, :unknown_request)) 19 | assert_receive {:EXIT, ^pid, :unexpected_event} 20 | end 21 | after 22 | Process.flag(:trap_exit, false) 23 | end 24 | 25 | test "event stops server on unknown requests" do 26 | capture_log fn-> 27 | Process.flag(:trap_exit, true) 28 | assert { :ok, pid } = GenFSM.start_link(Sample, [:hello], []) 29 | 30 | :gen_fsm.send_all_state_event(pid, :unknown_request) 31 | assert_receive {:EXIT, ^pid, :unexpected_event} 32 | end 33 | after 34 | Process.flag(:trap_exit, false) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/turnstile_example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TurnstileExampleTest do 2 | use ExUnit.Case 3 | 4 | # A module that implement a turnstile that let a person enter if 5 | # a coin has been inserted into the turnstile; no access will be 6 | # granted if no coins has been inserted. If multiple coins are 7 | # inserted it will only let one person through, it is greedy like 8 | # that. 9 | defmodule Turnstile do 10 | use GenFSM 11 | 12 | # Initialize the turnstile state machine with zero coins 13 | def start_link do 14 | GenFSM.start_link(__MODULE__, 0) 15 | end 16 | 17 | # Public API 18 | # The user can enter and insert_coins into our turnstile. The 19 | # outcome of these actions depends on the state of the turnstile 20 | # lock. 21 | def enter(pid) do 22 | GenFSM.sync_send_event(pid, :enter) 23 | end 24 | def insert_coin(pid) do 25 | GenFSM.send_event(pid, :coin) 26 | end 27 | def empty(pid) do 28 | GenFSM.sync_send_event(pid, :empty) 29 | end 30 | 31 | # Internal API 32 | def init(number_of_coins) do 33 | # Initialize the turnstile state machine in the locked state. 34 | # The state machine is initialized with a given number of 35 | # coins in its bank. 36 | {:ok, :locked, number_of_coins} 37 | end 38 | 39 | def unlocked(:coin, state) do 40 | # If another coin is inserted we will just eat the coin. 41 | {:next_state, :unlocked, state + 1} 42 | end 43 | def unlocked(:enter, _from, state) do 44 | # One can enter the door when it is unlocked, but it will switch 45 | # state to locked when the door has been entered. 46 | {:reply, :access_allowed, :locked, state} 47 | end 48 | 49 | def locked(:coin, state) do 50 | # Increment the bank and set the state to unlocked, allowing 51 | # someone to enter. 52 | {:next_state, :unlocked, state + 1} 53 | end 54 | def locked(:enter, _from, state) do 55 | # The door will not let anyone enter before it has been unlocked 56 | # by inserting a coin. Entering in this state will not to 57 | # anything. 58 | {:reply, :access_denied, :locked, state} 59 | end 60 | def locked(:empty, _from, state) do 61 | # The turnstile can be emptied when it is locked. This will reset 62 | # the bank to zero and return the number of coins (we do not take 63 | # authorization into consideration here). When the turnstile is 64 | # emptied it will remain in the locked state. 65 | {:reply, state, :locked, 0} 66 | end 67 | end 68 | 69 | test "turnstile example" do 70 | assert {:ok, pid} = Turnstile.start_link() 71 | 72 | # turnstile bank should initialize as empty 73 | assert Turnstile.empty(pid) == 0 74 | 75 | # let's try to enter without inserting a coin 76 | assert Turnstile.enter(pid) == :access_denied 77 | 78 | # lets insert some coins 79 | Turnstile.insert_coin(pid) 80 | Turnstile.insert_coin(pid) 81 | Turnstile.insert_coin(pid) 82 | 83 | # it should allow entry once; the second time it should deny 84 | assert Turnstile.enter(pid) == :access_allowed 85 | assert Turnstile.enter(pid) == :access_denied 86 | 87 | # let's check the bank! 88 | assert Turnstile.empty(pid) == 3 89 | # turnstile bank should now be empty 90 | assert Turnstile.empty(pid) == 0 91 | end 92 | end 93 | --------------------------------------------------------------------------------