Coin-toss.org / Simple Phoenix application - Part 3
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 3 steps include:
- Adding
Changeset
modifier functions to theToss
model - Querying similar tosses from the
Toss
model
Technology used:
- Elixir 1.2
- Phoenix 1.1.1
- Postgres 9.4
Adding Changeset
modifier functions to the Toss
model
In the last part of the application we saved a new model to the database by using
the Repo.insert!(changeset)
function in the create/2
function of our
toss_controller.ex
. This, however, only saves the heads
, tails
, and ip
attributes to the database. What is missing are the hash
and result
attributes.
These should be generated by the Toss
module automatically as they need to be run
on every changeset
insert. Let us create two functions that update an existing
changeset
as it is created and before it is inserted. Modify the toss.ex
file
as follows:
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> decide_result
|> hash_params
end
defp decide_result(changeset) do
put_change(changeset, :result, flip)
end
defp flip do
:random.seed(:erlang.timestamp)
if :random.uniform >= 0.5, do: 0, else: 1
end
defp hash_params(changeset) do
hash = [changeset.params["heads"], changeset.params["tails"]]
|> Enum.sort
|> Enum.join(":")
|> String.downcase
|> String.replace(" ", "")
put_change(changeset, :hash, hash)
end
The changeset
gets passed to the decide_result/1
function which will get a
random number between 0 and 1 and then insert it into the result
attribute.
It then gets passed to the hash_params/1
function which normalizes the
heads
and tails
attributes by putting them in order, downcasing them,
and removing spaces. There are obviously many ways to game the hash, but this is
supposed to be more for exercise purposes than anything else. Next we need to add
our test to make sure it works. Add the following to test\models\toss_test.exs
:
test "changeset/2 adds a result to the changeset" do
changeset = Toss.changeset(%Toss{}, @valid_attrs)
assert Ecto.Changeset.get_field(changeset, :result)
assert [0,1] |> Enum.member?(Ecto.Changeset.get_field(changeset, :result))
end
test "changeset/2 adds a hash of heads and tails to the changeset" do
changeset = Toss.changeset(%Toss{}, @valid_attrs)
assert Ecto.Changeset.get_field(changeset, :hash)
assert Ecto.Changeset.get_field(changeset, :hash) == "abcd:efgh"
end
and mix.test
yields:
$ mix test
........................
Finished in 0.3 seconds (0.2s on load, 0.07s on tests)
24 tests, 0 failures
Now we are certain that the models gets a result
and a hash
before it is
inserted.
Querying similar tosses from the Toss
model
As part of our requirement we want to query similar Toss
models. “Similar” is
determined by a match in hashes and created within a certain time frame. In Ecto
you can create composable query functions that can be placed in the model and
then called from a controller. Let us add two queries to the Toss
module, one
for matching hashes, and one for time frames:
def created_in_last_n_days(query, days) do
age = 0 - days
from t in query,
where: t.inserted_at > datetime_add(^Ecto.DateTime.utc, ^age, "day")
end
def with_specific_hash(query, hash) do
from t in query,
where: t.hash == ^hash
end
We can also test these in the test/toss_test.exs
file:
test "created_in_last_n_days/2 creates an Ecto.Query" do
assert Toss.created_in_last_n_days(Toss, 7).__struct__ == Ecto.Query
end
test "with_specific_hash/2 creates an Ecto.Query" do
assert Toss.with_specific_hash(Toss, "").__struct__ == Ecto.Query
end
test "created_in_last_n_days/2 and with_specific_hash/2 are composable" do
toss = Toss.changeset(%Toss{}, @valid_attrs) |> Repo.insert!
assert Toss
|> Toss.created_in_last_n_days(7)
|> Toss.with_specific_hash(toss.hash)
|> Repo.one!
end
Now we can modify our show/2
function in web/controllers/toss_controller.ex
to include this functionality:
def show(conn, %{"id" => id}) do
toss = Repo.get!(Toss, id)
similar = Toss
|> Toss.created_in_last_n_days(2)
|> Toss.with_specific_hash(toss.hash)
|> Repo.all
render conn, "show.html", toss: toss, similar: similar
end
Now this function will also return the original Toss
that we are querying
against - we will want to show this in our view, but we need to keep it in mind
for our test in test\controllers\toss_controller_test.exs
which we can add now:
test "GET /toss/:id queries similar tosses", %{conn: conn} do
_toss = Toss.changeset(%Toss{}, %{heads: "A", tails: "B", ip: "C"}) |> Repo.insert!
toss = Toss.changeset(%Toss{}, %{heads: "A", tails: "B", ip: "C"}) |> Repo.insert!
conn = get conn, toss_path(conn, :show, toss)
assert conn.assigns[:similar] |> length == 2
end
test "GET /toss/:id ignores different tosses", %{conn: conn} do
_toss = Toss.changeset(%Toss{}, %{heads: "E", tails: "F", ip: "C"}) |> Repo.insert!
toss = Toss.changeset(%Toss{}, %{heads: "A", tails: "B", ip: "C"}) |> Repo.insert!
conn = get conn, toss_path(conn, :show, toss)
assert conn.assigns[:similar] |> length == 1
end
Conclusion
In this part we added some new functions that modify the changeset
of the
Toss
module. We also used composable queries to query similar Toss
models.
In the next part we will implement the JSON API.