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.

Comments

© 2006-present Sean CribbsGithub PagesTufte CSS