Integrating Cizen with Phoenix (Part 2)
In the previous article “Integrating Cizen with Phoenix”, I introduced Cizen and explained how to integrate with Phoenix Framework through building an example application “Hello World”. Most of you feel that the example is too simple because it’s not an interactive application. So, in this article and upcoming articles, I’d like to add more features to the application to explore other Cizen’s features.
- Ask visitor’s name before greeting (handling HTTP POST request)
- Show hit counter (using a state of automaton)
- One-to-one: automaton per client (launching automaton dynamically)
Ask visitor’s name before greeting
Let’s start with a simple but interactive feature; Asking visitor’s name before greeting. For this feature, Greeting
automaton needs another event, so we add MyNameIs
event which has name
field to get visitor’s name.
defmodule HelloWorld.Events.MyNameIs do
defstruct [:name]
end
Greeting
automaton subscribes the new event in the spawn/2
function. Since the original “Hello World” application doesn’t have a state, the spawn/2
function simply returned :loop
atom. To keep a name of a visitor in the automaton’s state, we return a map with a field name
. This is a way to have a state in Cizen.
Notice that the automaton has a possibility to receive two events; Greeting
and MyNameIs
. Pattern matching is useful for switching what to do based on an event type. In case of receiving a MyNameIs
event, the yield/2
function returns a map with the given name to update the state. When you receive a Greeting
event, you need to dispatch a response event Reply
and return the current state without modifying.
defmodule HelloWorld.Greeting do
use Cizen.Automaton defstruct [] @impl true
def spawn(id, _) do
# Subscribe "MyNameIs" event
perform id, %Subscribe{
event_filter: Filter.new(
fn %Event{body: %HelloWorld.Events.MyNameIs{}} -> true end
)
} # Subscribe "Greeting" event
perform id, %Subscribe{
event_filter: Filter.new(
fn %Event{body: %HelloWorld.Events.Greeting{}} -> true end
)
} # Blank on init
%{name: ""}
end @impl true
def yield(id, %{name: name}) do
# Wait for events
event = perform id, %Receive{} case event.body do
%HelloWorld.Events.MyNameIs{name: name} ->
# Update state with the given name
%{name: name} %HelloWorld.Events.Greeting{} ->
# Prepare a message based on the current state
message = case name do
"" -> "What is your name?"
_ -> "Hello #{name}"
end # Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Events.Greeting.Reply{
greeting_id: event.id,
message: message,
name: name
}
} # No changes
%{name: name}
end
end
end
CurrentGreeting
automaton has a single yield/2
function, but you can define multiple yield/2
functions to reduce a number of combinations with a received events and possible states. For example, we can consider to change the above automaton so that it has two states; :dont_know
(instead of %{name: “”}
) and {:know, %{name: "kuy"}}
. As a slightly complicated behavior, we think about an automaton that accepts MyNameIs
event only once. You cannot change the name after the automaton transited to :know
state. Here is a transition diagram that represents what I explained.
And this is an example code that implements separated yield/2
functions.
@impl true
def yield(id, :dont_know) do
# Wait for events
event = perform id, %Receive{} case event.body do
%HelloWorld.Events.MyNameIs{name: name} ->
# Update state to :know with the name
{:know, %{name: name}}
%HelloWorld.Events.Greeting{} ->
# Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Events.Greeting.Reply{
greeting_id: event.id,
message: "What is your name?",
name: ""
}
} # No changes
:dont_know
end
end@impl true
def yield(id, {:know, %{name: name}}) do
# Wait for events
event = perform id, %Receive{} case event.body do
%HelloWorld.Events.Greeting{} ->
# Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Events.Greeting.Reply{
greeting_id: event.id,
message: "Hello #{name}",
name: name
}
} # No changes
{:know, %{name: name}} # Ignore "MyNameIs" event
_ -> {:know, %{name: name}}
end
end
The full code of this implementation is here.
Let’s take a look at PageController
to connect both Cizen and Phoenix worlds. Before adding tell
action to the controller, we need to add it in the router first.
scope "/", HelloWorldWeb do
pipe_through :browser get "/", PageController, :index
post "/name", PageController, :tell
end
Okay, add tell
action to receive a name of a visitor through an HTTP POST request. On a tell
action, we use Dispatch
effect to send a MyNameIs
event with the visitor’s name. The difference between Request
and Dispatch
effects is that Dispatch
effect doesn’t wait for a response event.
alias Cizen.Effects.{Dispatch, Request}
alias HelloWorld.Eventsdefmodule HelloWorldWeb.PageController do
use HelloWorldWeb, :controller # Required to use "handle" below
use Cizen.Effectful def index(conn, _params) do
# Ask a message
%{body: %{message: message, name: name}} = handle fn id ->
perform id, %Request{body: %Events.Greeting{}}
end render(conn, "index.html", message: message, name: name, token: get_csrf_token())
end def tell(conn, %{"name" => name}) do
# Tell visitor's name
handle fn id ->
perform id, %Dispatch{body: %Events.MyNameIs{name: name}}
end redirect(conn, to: "/")
end
end
We also change the template to add a form.
<h1><%= @message %></h1>
<p>
<form method="POST" action="/name">
<input type="hidden" name="_csrf_token" value="<%= @token %>" />
<input type="text" name="name" value="<%= @name %>" />
<input type="submit" value="Tell" />
</form>
</p>
That’s all! Finally, start Phoenix’s dev server mix phx.server
and open http://localhost:4000
in your browser. If you’re running Version 1 (single yield/2
function), you can change your name anytime. In contrast, Version 2 (separated yield/2
functions) doesn’t allow you to change your name multiple times.
By the way, close your browser tab/window, and then open http://localhost:4000 again. Can you still see your name? It’s like a session! Greeting
automaton still alive after closing the browser tab. You will see the same name even if you open more tabs because we share the same instance of the automaton for all visitors. An upcoming article will show you that the application launches an automaton for each client.
Here is the full code for this post: https://github.com/kuy/cizen-phoenix-hw2