Telephonist.StateMachine behaviour
This module provides a Behaviour
and macros that make designing a Telephonist-compatible state machine much easier.
Usage
You can use Telephonist.StateMachine
in two ways:
Use the behaviour and implement all the callbacks yourself:
defmodule MyStateMachine do
@behaviour Telephonist.StateMachine
end
Use the __using__
macro. This will set the @behaviour
attribute, and provide default implementations for initial_state/0
and on_transition_error/3
, as well as macros for constructing states:
defmodule MyStateMachine do
uses Telephonist.StateMachine, initial_state: :introduction
state :introduction, twilio, options do
say "Welcome to my state machine!"
end
end
Callbacks
A complete Telephonist.StateMachine
must implement the following callbacks:
initial_state/0
state/3
transition/3
on_complete/3
on_transition_error/4
Summary↑
__using__(arg1) | Imports the module, sets the |
state(name, twilio, options, block) | A shortcut macro that makes it easier to define a |
Types ↑
call :: Telephonist.CallManager.call
machine :: atom
state_name :: atom
twilio :: %{}
options :: %{}
next_state :: state_name | {machine, state_name}
Macros
Imports the module, sets the @behaviour
attribute, and provides default implementations for initial_state/0
and on_transition_error/4
.
Don't forget to set an initial state!
Example
defmodule MyModule do
use Telephonist.StateMachine, initial_state: :welcome
end
A shortcut macro that makes it easier to define a state/3
function.
name
: the name of the state.twilio
: a map of Twilio request parameters.options
: the custom options provided for this state.
Examples
Macros from ExTwiml can be used in the body of the state
definition:
state :introduction, _twilio, _options do
say "Welcome!"
end
You can pattern match on the twilio
and options
parameters:
state :introduction, _twilio, %{error: message} do
say "An error occurred!" <> message
end
You can also add guards:
state :introduction, _twilio, %{error: code} when code in [:catastrophic, :terrible] do
say "The death toll is catastrophic!"
end
Under the Hood
The state
macro just defines a simple function in this format:
def state(name, twilio, options) do
xml = twiml do
# body evaluated here
end
%Telephonist.State{ ... }
end
If you prefer to define your states manually like this, just follow this pattern and everything should work fine.
Callbacks
Specs:
- initial_state :: state_name
Defines the initial state for the state machine. If you use Telephonist.StateMachine
, this will be defined for you.
Examples
Implementing it yourself:
def initial_state, do: :welcome
With __using__/1
macro:
use Telephonist.StateMachine, initial_state: :welcome
Specs:
This callback is run when Twilio reports that the call has completed. It's a good place to put any cleanup logic or final logging that you want to perform when a call finishes.
Specs:
- on_transition_error(%{}, state_name, twilio, options) :: term
Whenever a call fails to transition due to an exception, this on_transition_error
handler will be run, and will be given the error that occurred as its first argument.
It should return a new state. If you use Telephonist.StateMachine
, the default implementation will simply re-raise the exception.
Specs:
- state(state_name, twilio, options) :: Telephonist.State.t
Defines a particular state in your state machine. Can be defined easily using the state/3
macro.
Specs:
- transition(state_name, twilio, options) :: Telephonist.State.t
Defines a transition from a given state to a new state. This callback will be run when the user gives input, say, by pressing some digits on their phone. Each transition/3
definition must return a new Telephonist.State
.
Example
Suppose you have two states: :introduction
and :extension
. The :introduction
state welcomes the user and asks them to enter an extension. After they enter an extension, you validate it and send them through to the :extension
menu, where they'll be connected. You can define a state machine like this:
defmodule Extensions do
use Telephonist.StateMachine
state :introduction, _twilio, %{error: message} do
gather finish_on_key: "#" do
say message
end
end
state :introduction, _twilio, _options do
gather finish_on_key: "#" do
say """
Welcome to Company, Inc!
If you know the extension of the person you are trying to reach,
please enter it now, followed by the pound sign.
"""
end
end
state :extension, _twilio, %{extension: extension} do
say "We will now connect you."
dial extension.number
end
def transition(:introduction, %{Digits: ext_code} = twilio, options) do
case find_extension(ext_code) do # a database check?
{:ok, extension} ->
options = Map.put(options, :extension, extension)
state(:extension, twilio, options)
_ ->
options = Map.put(options, :error, "Sorry, we could not find that extension code. Please try again!")
state(:introduction, twilio, options)
end
end
end
The transition/3
handler in this example checks to make sure that the digits entered by the user are a valid extension code by looking them up in a database.
If the extension is found, it appends it to the options
, and passes it to the state/3
function for :extension
.
If the extension is not found, it appends an :error
to the options
, and returns the :introduction
state. The pattern matching on :introduction
notices that there is an :error
, and runs the appropriate version of the :introduction
state. The user can then try again.
Handling Transition Errors
It is possible that you will accidentally not define a correct transition
handler for one or more of your states. When this happens, you will want to have a recovery plan.
See on_transition_error/4
for more details on how to do this.