Webmachine in Elixir Tutorial, Part 4
by Sean Cribbs
In the previous installment, we displayed some dynamic
content in our webpage that was loaded from an ets
table. Even a
social network themed around The Wire is no fun if you can’t add
your favorite quotes to it. Let’s hook up a form so that we can submit
to post new tweets, and a resource to accept that POST.
Creating tweets
Our HTML file already has portions of a form in it, but let’s add the ability to select your character.
<div id="add-tweet-form" class="add-tweet-form">
<select id="add-tweet-avatar" class="person">
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/f/f4/The_Wire_Jimmy_McNulty.jpg/250px-The_Wire_Jimmy_McNulty.jpg">Jimmy</option>
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/1/15/The_Wire_Bunk.jpg/250px-The_Wire_Bunk.jpg">Bunk</option>
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/6/6c/The_Wire_Kima.jpg/250px-The_Wire_Kima.jpg">Kima</option>
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/7/73/The_Wire_Bubbles.jpg/250px-The_Wire_Bubbles.jpg">Bubbles</option>
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/2/2f/The_Wire_Avon.jpg/250px-The_Wire_Avon.jpg">Avon</option>
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/b/b7/The_Wire_Stringer_Bell.jpg/250px-The_Wire_Stringer_Bell.jpg">Stringer</option>
<option value="http://upload.wikimedia.org/wikipedia/en/thumb/7/78/The_Wire_Omar.jpg/250px-The_Wire_Omar.jpg">Omar</option>
</select>
<input id="add-tweet-message" type="text" class="message" />
<a id="add-tweet-submit" class="button post">POST!</a>
</div>
Now we need some JavaScript to show and hide the “form”, and submit it.
// Toggle the visibility of the form
$('#add-tweet').click(function() {
$('#add-tweet-form').toggle();
});
// Submit the form on click of the "POST!" button
$('#add-tweet-submit').click(function() {
var tweetMessageField = $('#add-tweet-message');
var tweetMessageAvatar = $('#add-tweet-avatar');
var tweetMessageForm = $('#add-tweet-form');
var tweetMessage = tweetMessageField.val();
var tweetAvatar = tweetMessageAvatar.val();
$.ajax({
type: 'POST',
url: '/tweets',
contentType: 'application/json',
data: JSON.stringify({ tweet: {
avatar: tweetAvatar,
message: tweetMessage }}),
success: function(d) {
tweetMessageField.val('');
tweetMessageForm.toggle();
}
});
});
If you refresh the browser and click the button to “POST!”, you should
get an error in the JavaScript console, specifically 405 Method Not
Allowed
. This is because our TweetList
resource doesn’t accept
POST!
We have two options here, we can modify our existing resource or create a new resource to handle the form submission. The difference will be whether we can tolerate the extra logic for accepting the form amongst the logic for producing a list of tweets, or whether we prefer to create a resource that only accepts the form. Personally, I could handle either way (and potentially change my mind later) but I’m going to choose the latter for clarity and to demonstrate another feature of Webmachine. Let’s create our new resource!
defmodule Tweeter.Resources.Tweet do
# Boilerplate again! In this case the state is the contents of our
# tweet, initially an empty identifier and attribute list.
def init(_), do: {:ok, {nil, []}}
def ping(req_data, state), do: {:pong, req_data, state}
end
Now, the whole goal of this resource was to accept POST
requests, so
we better allow them.
# This resource supports POST for creating tweets. We colon-prefix
# the POST atom to ensure the Elixir compiler doesn't treat it as
# a module name:
#
# iex> :io.format('~s~n', [POST])
# Elixir.POST
# :ok
# iex> :io.format('~s~n', [:POST])
# POST
# :ok
# iex> POST == :POST
# false
def allowed_methods(req_data, state) do
{[:POST], req_data, state}
end
Now we can talk about what our options are for accepting the form. We can take two paths: first, we assume the accepting resource always exists and simply handles the POST in its own way; second, we assume the resource CREATES new resources, and treat it as a PUT to a new URI.
The latter way will feel very familiar if you’ve used Rails before,
except that you need to pick the new URI before the request body is
accepted! This trips developers up frequently, but is easy to work
around. Since we already know how to allocate unique identifiers using
monotonic_time
, constructing a new unique URI should be
straightforward.
Our first step is to tell Webmachine that our resource doesn’t exist, that POST means creating a new resource, and that it’s okay to allow POST when the resource doesn’t exist:
# When we accept POST, our resource is missing!
def resource_exists(req_data, state) do
{false, req_data, state}
end
# Accepting POST means creating a resource.
def post_is_create(req_data, state) do
{true, req_data, state}
end
# Allow POST to missing resources
def allow_missing_post(req_data, state) do
{true, req_data, state}
end
Now we should pick the URI where our new tweet will live. Whether or not we allow fetching that new URI is another question entirely! We might revisit that later in the tutorial.
# Generate the path for the new resource, populating the ID in the
# state as well.
def create_path(req_data, {_id, attrs}) do
new_id = System.monotonic_time
{'/tweets/#{new_id}', req_data, {new_id, attrs}}
end
If our application were backed by a database like PostgreSQL, we
could use an SQL query here to fetch the next ID in the table’s
sequence. Instead, we just call monotonic_time
. Note how we
capture the generated ID in the resource state for when we insert
the tweet into the ETS table.
We’re submitting application/json
from the Ajax request, but
Webmachine doesn’t know that it’s ok to accept it, or what to do with
it when it arrives. Similar to content_types_provided
, we can
specify this with the content_types_accepted
callback.
# We take JSON
def content_types_accepted(req_data, state) do
{[{'application/json', :from_json}], req_data, state}
end
Finally, we parse the incoming JSON, extract the fields, and put the data in the ETS table.
def from_json(req_data, {id, attrs}) do
# We use a try here so that our pattern match throws if we fail to
# decode or extract something from the request body.
try do
# Parse the request body, extracting the attributes of the tweet
# First fetch the request body
req_body = :wrq.req_body(req_data)
# Second, decode the JSON and destructure it
{:struct, [{"tweet", {:struct, attrs}}]} = :mochijson2.decode(req_body)
# Now fetch the message and avatar attributes from the JSON
{"message", message} = List.keyfind(attrs, "message", 0)
{"avatar", avatar} = List.keyfind(attrs, "avatar", 0)
# Finally construct the data to go into ETS
new_attrs = [avatar: avatar, message: message, time: :erlang.timestamp]
# Insert into ETS and return true
:ets.insert(:tweets, [{id, new_attrs}])
{true, req_data, {id, new_attrs}}
rescue
# If we threw from the above block, we should fail the request
# from the client. MatchError could be raised from our
# pattern-match when decoding JSON, or from :mochijson2
# itself. CaseClauseError is raised by :mochijson2 alone when
# we get bad JSON.
err in [MatchError, CaseClauseError] ->
{false, req_data, {id, attrs}}
end
end
Before our new resource will work, however, we need to dispatch to it!
Since we wanted to use the same URI as the TweetList
resource, we
need to make sure that only POST
requests make it to the Tweet
resource. This is where a route guard comes in. Route guard functions
take one argument, the req_data
we’ve been passing around, and
should return a boolean. If the route is a 4-tuple, with the second
element being a route guard function, that guard will be tested before
dispatching is done (but after the path has matched).
# --- lib/tweeter.ex ---
# Some configuration that Webmachine needs
web_config = [ip: {127, 0, 0, 1},
port: 8080,
dispatch: [
# Note the guard function in the second position
{['tweets'], &(:wrq.method(&1) == :POST), Tweeter.Resources.Tweet, []},
{['tweets'], Tweeter.Resources.TweetList, []},
{[], Tweeter.Resources.Assets, []},
{[:'*'], Tweeter.Resources.Assets, []}
]]
Now reload mix
and see if you can post a tweet! (You might need to
reload the page after posting too.)
Up next
In our next and final installment, we’ll learn how to deliver live updates to the client.