Coin-toss.org / Simple Phoenix application - Part 2

Summary

The purpose of this post is to document the creation process of the Coin-toss.org website using the Phoenix Web application framework and the Elixir programming languages. Part 2 steps include:

  • Replacing the existing template engine
  • Creating a new controller and adding relevant functions

Technology used:

  • Elixir 1.2
  • Phoenix 1.1.1
  • Postgres 9.4

Replacing the existing template engine

EEx is the default template engine in Phoenix, much like Erb is the default engine in Ruby on Rails. However, I have been using HTML for over 20 years and any second spent on writing a closing tag or reading non-indented source code is a second lost. Rails comes with a lot of great alternatives like HAML and Slim. Both of these have been moved to Elixir as well, with at the time of writing, the HAML implementation being less complete than the Slim one. I personally prefer HAML but Slim will do for now. Let me include the Elixir Slim package in my mix.deps:

defp deps do
  [
   {:base62, "~> 1.1"},
   {:phoenix, "~> 1.1.1"},
   {:phoenix_ecto, "~> 2.0"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_html, "~> 2.3"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:phoenix_slime, "~> 0.4.1"},
   {:gettext, "~> 0.9"},
   {:cowboy, "~> 1.0"}
  ]
end

and also activate the engine in config/config.exs

config :phoenix, :template_engines,
  slim: PhoenixSlime.Engine,
  slime: PhoenixSlime.Engine

as well as add the extension to the watcher in config/dev.exs

# Watch static and templates for browser reloading.
config :coin_toss, CoinToss.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{web/views/.*(ex)$},
      ~r{web/templates/.*(eex|slim|slime)$}
    ]
  ]

now we just need to change our existing file from .eex to .slim in web/templates. We can use the following for web/templates/layout/app.html.slim:

doctype html
html lang="en"
  head
    meta charset="utf-8"
    meta content="IE=edge" http-equiv="X-UA-Compatible"
    meta content="width=device-width, initial-scale=1" name="viewport"
    meta content="" name="description"
    meta content="" name="author"
    title Hello CoinToss!
    link rel="stylesheet" href="#{static_path(@conn, "/css/app.css")}"
  body
    .container
      header.header
        nav role="navigation"
          ul.nav.nav-pills.pull-right
            li
              a href="http://www.phoenixframework.org/docs" Get Started
        span.logo
      p.alert.alert-info role="alert"= get_flash(@conn, :info)
      p.alert.alert-danger role="alert"= get_flash(@conn, :error)
      main role="main"
        = render @view_module, @view_template, assigns
    script src="#{static_path(@conn, "/js/app.js")}"

and the following for web/tempates/page/index.html.slim:

.jumbotron
  h2= gettext("Welcome to %{name}", name: "Phoenix!")
  p.lead
    | A productive web framework that
    br
    | does not compromise speed and maintainability.
.row.marketing
  .col-lg-6
    h4 Resources
    ul
      li
        a href="http://phoenixframework.org/docs/overview" Guides
      li
        a href="http://hexdocs.pm/phoenix" Docs
      li
        a href="https://github.com/phoenixframework/phoenix" Source
  .col-lg-6
    h4 Help
    ul
      li
        a href="http://groups.google.com/group/phoenix-talk" Mailing list
      li
        a href="http://webchat.freenode.net/?channels=elixir-lang" #elixir-lang on freenode IRC
      li
        a href="https://twitter.com/elixirphoenix" @elixirphoenix

Once again let us make sure this all has not broken anything fundamental:

$ mix test
Compiled web/views/page_view.ex
Compiled web/views/layout_view.ex
.................

Finished in 0.2 seconds (0.2s on load, 0.04s on tests)
17 tests, 0 failures

Creating a new controller and adding relevant functions

Now we could just use the default PageController to do all parts of the application but where is the fun in that? Also for clarity let us create a new controller called TossController. This also clearly links the controller to the Toss model. In terms of functions we really only need three:

  • new - This will render the initial form for a new coin toss
  • create - This will execute the coin toss from the posted form information
  • show - This will show the result for an executed coin toss.

Let us go about scaffolding our three functions. In web/controllers/toss_controller.ex I will put:

defmodule CoinToss.TossController do
  use CoinToss.Web, :controller

  alias CoinToss.Toss

  def create(conn, %{"toss" => toss_params}) do
  end

  def new(conn, _params) do
  end

  def show(conn, %{"id" => id}) do
  end
end

You can see I am using pattern matching on the second passed value to cherry pick the values I want. This also ensures that only correct requests are processed as any non-matching request gets ignored. We should also create the view controller and template files. Ex. web/views/toss_view.ex do:

defmodule CoinToss.TossView do
  use CoinToss.Web, :view
end

and add empty new.html.slim and show.html.slim files to web/templates/toss/.

Lets start getting this all hooked up and tested by focusing on the easiest function first: new. I made the following change to web/controllers/toss_controller.ex:

def new(conn, _params) do
  changeset = Toss.changeset(%Toss{})
  render conn, "new.html", changeset: changeset
end

As we want to render a form of the Toss model we need to pass the view a changeset of the model. This will allow the Phoenix form helper to create a form for the model in HTML. Before we go any further we should also add our controller to web/router.ex file so we can start testing. Let us add it as a resource with limited methods:

scope "/", CoinToss do
  pipe_through :browser # Use the default browser stack

  get "/", PageController, :index
  resources "/toss", TossController, only: [:new, :create, :show]
end

If we run mix phoenix.routes we can now see the effect:

$ mix phoenix.routes
page_path  GET   /          CoinToss.PageController :index
toss_path  GET   /toss/new  CoinToss.TossController :new
toss_path  GET   /toss/:id  CoinToss.TossController :show
toss_path  POST  /toss      CoinToss.TossController :create

At this point we might as well change the default path to the TossController.new/2 function but replacing get "/", PageController, :index with get "/", TossController, :new

$ mix phoenix.routes
toss_path  GET   /          CoinToss.TossController :new
toss_path  GET   /toss/new  CoinToss.TossController :new
toss_path  GET   /toss/:id  CoinToss.TossController :show
toss_path  POST  /toss      CoinToss.TossController :create

At this point we might as well remove all the remnants of the PageController as there are no more paths leading to it. Delete the following files:

web/controller/page_controller.ex
test/controller/page_controller_test.ex
web/view/page_view.ex
web/templates/page

and mix.test

$ mix test
Generated coin_toss app
................

Finished in 0.2 seconds (0.2s on load, 0.02s on tests)
16 tests, 0 failures

Next let us create a new test for the TossController.new/1 function. In test/controllers/toss_controller_test.exs add:

defmodule CoinToss.TossControllerTest do
  use CoinToss.ConnCase

  alias CoinToss.{Repo, Toss}

  test "GET /", %{conn: conn} do
    conn = get conn, "/"
    assert html_response(conn, 200)
  end

  test "GET /toss/new", %{conn: conn} do
    conn = get conn, toss_path(conn, :new)
    assert html_response(conn, 200)
  end
end

and mix test:

$ mix test
..................

Finished in 0.3 seconds (0.2s on load, 0.05s on tests)
18 tests, 0 failures

The great thing about MVC frameworks is that you can test most of the code without ever writing any HTML. Is it a good idea in the long run? No - but in the short term it makes sense. The next function to tackle is .show/2 as we can easily create a new model using our test suite. Change the existing code for show/2 in toss_controller.ex to something like this:

def show(conn, %{"id" => id}) do
  toss = Repo.get!(Toss, id)
  render conn, "show.html", toss: toss
end

All we are doing is getting a Toss model for a specific ID. As we are using the get! call on Repo we are expecting one result back or an error if it is not found. Let’s add the the tests to toss_controller_test.exs:

test "GET /toss/:id when a toss exists", %{conn: conn} do
  toss = Toss.changeset(%Toss{}, %{heads: "A", tails: "B", ip: "C"}) |> Repo.insert!
  conn = get conn, toss_path(conn, :show, toss)
  assert html_response(conn, 200)
end

test "GET /toss/:id when a toss does not exists", %{conn: conn} do
  assert_raise(Ecto.NoResultsError, fn -> get(conn, toss_path(conn, :show, 1)) end)
end

and again our friend mix test:

$ mix test
....................

Finished in 0.3 seconds (0.2s on load, 0.07s on tests)
20 tests, 0 failures

All that leaves is the create/2 function to implement. I will go ahead and do that right now, replacing the function in toss_controller.ex:

def create(conn, %{"toss" => toss_params}) do
  ip = get_ip(conn)
  changeset = Toss.changeset(%Toss{}, Map.merge(toss_params, %{"ip" => ip}))
  case Repo.insert(changeset) do
    {:ok, toss} ->
      conn
      |> redirect(to: toss_path(conn, :show, toss))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

and adding in a new private function, get_ip/1 at the bottom of the file:

defp get_ip(conn) do
  conn.remote_ip
    |> Tuple.to_list
    |> Enum.join(".")
end

All we are doing here is taking the toss_params and using them to create a new changeset. We also merge the IP address in which is contained in the conn struct that is passed to all requests in the application. We then insert the changeset and in case it fails render the new template again with the changeset. In case it succeeds we redirect to the show/2 function. Let us add the remaining tests to toss_controller_test.exs:

test "POST /toss creates a new toss if all the required fields are available", %{conn: conn} do
  conn = post conn, toss_path(conn, :create), %{toss: %{heads: "A", tails: "B"}}
  assert Repo.get_by(Toss, heads: "A")
  assert html_response(conn, 302)
end

test "POST /toss does not creates a new toss if any of the required fields are missing", %{conn: conn} do
  conn = post conn, toss_path(conn, :create), %{toss: %{heads: "A"}}
  refute Repo.get_by(Toss, heads: "A")
  assert html_response(conn, 200)
end

and as usual mix test:

$ mix test
......................

Finished in 0.3 seconds (0.2s on load, 0.07s on tests)
22 tests, 0 failures

Looks like we are all good.

Conclusion

We have replaced the default template engine, replace the existing EEx files and scaffolded some basic controller functions. In the next part we will Add additional Changeset functions to the Toss model and create a new new custom Ecto data type for results.