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.