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

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 1 steps include:

  • Developing a data model
  • Creating a new phoenix application
  • Implementing a model using Ecto
  • Creating a custom data type
  • Testing the data model

Technology used:

  • Elixir 1.2
  • Phoenix 1.1.1
  • Postgres 9.4

Premise

The basic idea behind the coin-toss.org web is to create an application through which users can simulate a coin toss between two options and guarantee a 50/50 chance of either option being shown. There are ample applications out there that already do this, but coin-toss.org comes with an additional feature - it will record the outcome and save it with a unique link to the result can be shared.

Additionally the application will show a list of similar coin flips preformed recently which may have been made to force a particular outcome - consider it a basic device for ensuring authenticity of the flip.

The application itself it not complex or difficult to implement - but developing it will shed some insight into how a basic Phoenix application is structured.

Developing a data model

A coin flip event is straight forward - there is an option for heads and an option for tales. The data model should also record the outcome and the timestamp when the flip was made. We also have the requirement of showing similar coin flips so we are going to create hash of the coin flip that we can use to compare with other coin flips. Tracking the IP of the coin flipper might also come in handy.

Additionally we want to come up with a unique ID for the coin flip that can easily be shared. Most databases already come with a unique ID for a row in a table, but where is the fun in that. Instead we will generate a short-url type string that represent the coin flip event.

In summary we probably need the following data fields:

  • Unique ID
  • Heads Option
  • Tails Option
  • Result
  • Hash
  • IP
  • Timestamp

Using Elixir’s Ecto we can easily achieve this and I will show you how shortly but first we should create a new Phoenix application.

Creating a new phoenix application

Assuming you have installed everything as per the http://www.phoenixframework.org/docs/installation this should be straight forward process:

$ mix phoenix.new coin_toss

Once you have completed the process and run all the recommended commands ex:

mix ecto.create

I always recommend running the test suite to make sure nothing is broken from the get go:

mix test

and after compiling the app again, it should say something like

....

Finished in 0.4 seconds (0.4s on load, 0.00s on tests)
4 tests, 0 failures

Randomized with seed 621007

and you are done!

Implementing a model using Ecto

Much like in any other framework you can use a command-line tool to do most of the heavy lifting for you. To create our Toss model in Phoenix use the following command:

mix phoenix.gen.model Toss tossess heads:string tails:string result:integer hash:string ip:string

A primary ID and a timestamp are automatically added by Ecto. As we want to use our hash attribute comparatively it makes sense to add an index.

I added this to my *_create_toss.ex in priv/repo/migrations:

def change do
  create table(:tossess) do
    add :heads, :string
    add :tails, :string
    add :result, :integer
    add :hash, :string
    add :ip, :string

    timestamps
  end

  index(:tossess, [:hash])

end

Run the migration mix ecto.migrate and the let us test again to make sure all is ok:

......

Finished in 0.2 seconds (0.2s on load, 0.03s on tests)
6 tests, 0 failures

looks like two more test were added and they passed. Let us take a look at the model itself: web/models/toss.ex. You may note that all our fields are required.

@required_fields ~w(heads tails result hash ip)
@optional_fields ~w()

I changed that to:

@required_fields ~w(heads tails ip)
@optional_fields ~w()

as we only need to require heads, tails, and ip because result and hash are calculated by the model itself. If you look in your model test file: test/models/toss_test.exs you will see that we need to remove these from the @valid_attrs map and replace it with some content:

@valid_attrs %{heads: "efgh", ip: "some content", tails: "abcd"}
@invalid_attrs %{}

Creating a custom data type

You may recall our requirement of having a short-url code that references a particular coin toss event. In other frameworks you could create another field in the model, set an index on it, and be done. In Ecto we can take advantage of custom data types that will encode and decode data before it interacts with the database. There is a great introduction to custom data types here. All to say that a custom data type has to implement certain functions that convert data back and forth between your schema and something the database understands.

A short-url is a compressed url that can be expanded. Examples of url shorteners are t.co, ow.ly, and is.gd. In our case we are interested in compressing the primary ID of the coin toss so that it can be expanded again later. One really simple way to do this with a Base conversion, so taking a Base 10 number, and converting it into another Base that can use other URL safe numbers and letters, such as ABCD. As you can have one letter ex A equal a two digit number ex. 11 one sees how you can shorten a long integer into shorter and shorter strings. As this is a simple mathematical process, you can also reverse it and replace any instance you see an A with 11. A Base62 code set is a great place to start as it include all the letters and all the digits [A-Za-z0-9]. This ensures that any encoded string is URL safe.

We could easily write our own base converter, but there conveniently already exists a library for Elixir called Base62. Include that in our mix.deps file:

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},
   {:gettext, "~> 0.9"},
   {:cowboy, "~> 1.0"}
  ]
end

run mix deps.get and you are good to go. We can even play with our new library in ie IEX console to ensure that it works:

$ iex -S mix

Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Base62.encode(1234567890)
"1LY7VK"

So you can see we have gotten almost a 50% improvement on characters used.

The plan of action is is as follows: As a new record is created in the database it is assigned a positive integer primary ID. When we query that record by ID, instead of using the integer, we would like to use the Base62 conversion of that ID. Similarly when we look at the ID attribute of that record we want it to return the Base62 conversion of the ID, not the ID itself.

Let us create a new module that implements the above described behavior. In lib/ecto_short_url_id.ex add the following:

defmodule CoinToss.EctoShortUrlId do
  @behaviour Ecto.Type

  def type, do: :id

  def cast(string) when is_binary(string) do
    case Base62.decode(string) do
      {:ok, int} when int > 0 -> {:ok, int}
      _ -> :error
    end
  end

  def cast(int) when is_integer(int), do: {:ok, int}
  def cast(_), do: :error

  def dump(int) when is_integer(int), do: {:ok, int}
  def dump(_), do: :error

  def load(int) when is_integer(int), do: {:ok, Base62.encode(int)}
  def load(_), do: :error
end

As you can see I am making liberal use of Elixir’s pattern matching to make sure that only valid inputs get run through the Base62 conversion. Let us also create some tests to make sure all this works. In test/lib/ecto_short_url_id_test.exs add:

defmodule CoinToss.EctoShortUrlIdTest do
  use ExUnit.Case

  import CoinToss.EctoShortUrlId

  test "type/0 returns the :id atom" do
    assert type == :id
  end

  test "cast/1 returns an integer from a Base62 string" do
    assert cast("1LY7VK") == {:ok, 1234567890}
  end

  test "cast/1 returns an error if the string is not Base62 encoded" do
    assert cast("=") == :error
  end

  test "cast/1 returns an integer if an integer is passed" do
    assert cast(1) == {:ok, 1}
  end

  test "cast/1 returns an :error if anything but an integer or binary is passed" do
    assert cast(false) == :error
  end

  test "dump/1 returns an integer is the input is an integer" do
    assert dump(1) == {:ok, 1}
  end

  test "dump/1 returns an :error is the input is no an integer" do
    assert dump("ABCD") == :error
  end

  test "load/1 encodes an integer into Base62" do
    assert load(1234567890) == {:ok, "1LY7VK"}
  end

  test "load/1 returns an error unless an integer is passed" do
    assert load("ABCD") == :error
  end
end

running mix test we get:

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

Finished in 0.2 seconds (0.2s on load, 0.03s on tests)
15 tests, 0 failures

Testing the data model

The next step is to change our existing Toss schema to use the EctoShortUrlId as a custom ID type instead of a positive integer.

Let make the following modifications to web/models/toss.ex:

use CoinToss.Web, :model

@primary_key {:id, CoinToss.EctoShortUrlId, autogenerate: true}

schema "tossess" do

This will tell Ecto to use our custom data type as the primary key. Let me make sure I did not break anything outright:

$ mix test
Compiled web/models/toss.ex
...............

Finished in 0.2 seconds (0.2s on load, 0.03s on tests)
15 tests, 0 failures

looks like it did not break anything immediate. However, nothing specific was tested either, but at least we know that we are starting from a clean slate. Now to add some additional tests to test\models\toss_test.exs:

test "model returns a Base62 encoded version of its id" do
  toss = Toss.changeset(%Toss{}, @valid_attrs) |> Repo.insert!
  assert {:ok, _id} = Base62.decode(toss.id)
end

test "model can be queried by the Base62 version of its id" do
  toss = Toss.changeset(%Toss{}, @valid_attrs) |> Repo.insert!
  assert Repo.get!(Toss, toss.id)
end

and run our suite mix test:

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

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

Looks like we are all good.

Conclusion

In a somewhat verbose manner I have set up Phoenix, created a model, and added a custom Ecto ID type. I have also provided tests for all modifications. The next part will tackle the controller and view components of our application.