Let's start by creating a new Elixir module with the following content. If you have watched the videos, you should know what the filename and location should be within the project directory:
defmodule App.Planets do
@moduledoc """
A context to retrieve data of our solar system.
- `distance` is the relative distance to the Sun compared to Earth's distance to the sun.
- `orbital_period` is the time it takes for a planet to orbit the sun (in Earth years)
"""
@planets [
%{id: 1, name: "Mercury", distance: 0.39, orbital_period: 0.24},
%{id: 2, name: "Venus", distance: 0.72, orbital_period: 0.61},
%{id: 3, name: "Earth", distance: 1.0, orbital_period: 1.0},
%{id: 4, name: "Mars", distance: 1.52, orbital_period: 1.88},
%{id: 5, name: "Jupiter", distance: 5.02, orbital_period: 11.86},
%{id: 6, name: "Saturn", distance: 9.54, orbital_period: 29.46},
%{id: 7, name: "Uranus", distance: 19.18, orbital_period: 84.01},
%{id: 8, name: "Neptune", distance: 30.06, orbital_period: 164.8}
]
def list, do: @planets
def list(:sorted_by_name) do
Enum.sort_by(@planets, fn planet -> planet.name end)
end
@doc """
Returns a string containing all planet names, sorted alphabetically in ascending order, separated by semicolon.
## Examples
Planets.names()
"Earth, Jupiter, Mars, ..."
"""
def names do
:sorted_by_name
|> list
|> Enum.map(& &1.name)
|> Enum.join(", ")
end
@doc """
Returns a planet by id.
## Examples
Planets.get(1)
%{id: 1, name: "Mercury", distance: 0.39, orbital_period: 0.24}
"""
def get(id), do: Enum.find(@planets, &(&1.id == id))
end
Next look up every Enum
function you can find in that file in the documentation. Have a lenghty discussion with your partner on what they do and inspect the examples in the documentation. Ask your professor or AI if you need more clarification. It is very important that we get familar with those Enum functions as soon as possible and also remember them so we can connect the dots and use them when we need to.
Notice also that some function require a function as a parameter:
Enum.sort_by(@planets, fn planet -> planet.name end)
When that happens the function usually iterates through each element in a collection, in this case we iterate through planets and make planet.name
the sort key. We could have added an optional parameter to change sorting to descending :desc
but we left it at the default :asc
.
Between fn
and ->
we find the parameters of the anonymous function. The function is anonymous because it doesn't have a name. Notice also that we used fn
instead of def
to further make that clear. The function body rests between ->
and end
. In Elixir we use functions as paramters a lot so there is short form for that:
Enum.sort_by(@planets, & &1.name)
This is the same function call as before. &
indicates a function-parameter will be used and &1
represent the first parameter of that function, which was the planet.
&1.name
thus means we are retrieving the name
of the current planet. &2
, &3
and so on do not exist in this function so attempting to use them will result in an exception.
There can be only one &
(at the very beginning) and what follows (&1.planet
) is always the function body.
If at this point you feel confused, rest assured, I totally get it. With time you will learn to appreciate the patterns. 3-4 years you have learned about object-oriented langugages and functional programming can be a learning curve.
Objectives (1-3)
So now that we have done the legwork its time to discuss what to deliver for next lecture.
Your mission, should you choose to accept it, is to create a new controller PlanetController
yourself that will handle three different scenarios:
/planets
should show a list of planets or planet names similar to the previous exercise./planets/random
should show data of a random planet/planets/:n
should show data of the planet with the given id (such as1
) if an matching planet exists.
Here is how you could start your Controller:
defmodule AppWeb.PlanetController do
use AppWeb, :controller
alias App.Planets
# TODO
end
- Don't forget to add routes to the router.
- Don't forget to create a view
planet_html.ex
similar to thepage_html
- Don't forget to add template files. You might need two of them such as:
list.html.heex
(for showing all planets) andshow.html.heex
(for showing a single entry)
Regarding the /planets/random
part the random function might be what you are looking for.
This should yield you the first three points.
Objectives 4 and 5
The videos were about testing so this is the time we want to put that to practice. Once your functions work as expected (or even if not) you can work on your tests.
Start by installing excoveralls like I showed in the video and open the coveralls.html
page under /cover
once you have ran MIX_ENV=test mix coveralls
for the first time.
I highly recommend you to bookmark that html file in your browser. You are going to open this file a lot in the future. If you see a report great. 1 point. That was easy.
Last we want to write a few tests in a similar fashion as seen in the videos with the ultimate objective to achieve 100%
test coverage on our PlanetController
. Ignore all other untested files.
Think about how you could test the randomness of the /planet/random` url. Some ideas:
test "testing /planets/random", %{conn: conn} do
# Set a seed so we always get the same "random" numbers when testing
:rand.seed(:exsss, {100, 101, 102})
conn = get(conn, ~p"/planets/random")
# assert for as specific planet in your output (the one determined by seed)
# call conn = get(...) again
# assert for a different planet in your output
end
Generally testing randomness is tricky and usually requires you to set a seed. The reason I have added this part of the excersise is, to show you how it can be done.