dwyl / phoenix-uk-postcode-finder-example

πŸ“An example/tutorial application showing how to rapidly find your nearest X by typing your postcode.
GNU General Public License v2.0
8 stars 2 forks source link

phoenix-uk-postcode-finder-example

An example/tutorial application showing how to rapidly find your nearest X by typing a postcode.

This readme will take you through the steps needed to create a store finder using Phoenix and ETS (Erlang Term Storage).

Prerequisites?

The only pre-requisites are:

And to make sure you have the following installed on your machine:

Now, let's get started πŸ‘

Getting started

In your terminal, make a new Phoenix application by running the command...

mix phx.new store_finder

Type y when asked if you want to install the dependencies. This may take a few seconds. Once this is done, cd into the directory.

cd store_finder

Currently we are not using the database (may change in the near future) but in the interest of avoiding unnecessary complication, let's create one with...

mix ecto.create

Creating a store cache

Now that we have created our app, the next thing that we need to do is get a list of store information. We have provided a default list of random postcodes which we will be using as our "store information" for the purposes of this example.

That list can be found here

In the lib/store_finder folder, create a file called store_cache.ex and add the following code...

defmodule StoreFinder.StoreCache do
  use GenServer

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg, name: StoreCache)
  end

  def init(initial_state) do
    :ets.new(:store_cache, [:set, :public, :named_table])
    :ets.insert(:store_cache, store_list())

    {:ok, initial_state}
  end

  def store_list do
    ... # Copy the list info from the above link into this function
  end
end

Let's take a look at this code in a little more detail.

The use GenServer allows us to create a GenServer. This will allow the code in this module to run when the application is started. We still need to do a little more for this to happen but we will touch on that a little later. You can find out more about GenServers here.

What we are going to focus on for this example is the logic inside the init/1 function, specifically the two lines starting with :ets. These are the lines that are going to create our ets table and store all of our "store" information.

As mentioned above, ETS stands for erlang term storage. In elixir, if you want to call an erlang function we put a : before the name of that module and then call the function as we would any other elixir function.

So first we call the :ets.new/2 function which creates a new table for us. The arguments we give to this function are the name that we want to call the table, :store_cache, and a list of options.

The list of options we used are:

Now that we have created our table we can insert data into it. This is where the line below comes in.

:ets.insert/2, as the name suggests, inserts data into a table. The first argument is the table name, :store_cache. The second argument is a list of tuples, in our case the store list we added. And that is it. These two lines create and store all of the info we need.

The last thing we need to do for these functions to be called when our app is started is to update our application.ex file.

Open the application.ex. You should see something that resembles this... (I have removed the comments for the tutorial to make it more concise but it should look about the same)

defmodule StoreFinder.Application do
  use Application

  def start(_type, _args) do
    children = [
      StoreFinder.Repo,
      StoreFinderWeb.Endpoint
    ]

    opts = [strategy: :one_for_one, name: StoreFinder.Supervisor]
    Supervisor.start_link(children, opts)
  end

  ...
end

We will want to add our module to the list of children. To do this simply add the module name to the list of children like so...

children = [
  StoreFinder.Repo,
  StoreFinderWeb.Endpoint,
  StoreFinder.StoreCache # <==== This line
]

Now if we start our application we will create an ets table, so let's give it a go.

As we are not currently doing anything with the data from our ets table we will not be able to tell if it is working by just starting the app. We should (and will) write tests to check it is working but for right now open your terminal and type...

iex -S mix

This will compile your application and start an interactive elixir shell. In here type the following command...

:ets.match_object(:store_cache, :_)

The above function is just saying to return all the elements from the table that match the second argument. In elixir if we pass an _ when pattern matching it matches any value. The only difference here is that because this is an erlang function we need us :_.

This will return a list of all the "stores" saved in the ets table. As you can see you didn't have to do anything to be able to access this data, it was just available when the application started 😁

Testing ets table creation

Now let's add tests to check our ets table is being created as expected. We don't need/want to test the ets functions themselves. What we want to do is test that the table exists.

First let's run the tests that were generated when the app was created. Run the tests with...

mix test

It should log the following...

...

Finished in 0.09 seconds
3 tests, 0 failures

Now that we have seen that the tests are working we can add our test for the ets table. This will be a really quick and easy test to write as it will be very similar to what we did in our terminal in the end of the previous section.

We will need to start by creating a new file for this test. Create the file test/store_finder/store_cache_test.exs. In this file add the following code...

defmodule StoreFinder.StoreCacheTest do
  use StoreFinderWeb.ConnCase

  test "ets table exists when application starts" do
    stores = :ets.match_object(:store_cache, :_)
    assert is_list(stores)
    refute Enum.empty?(stores)
  end
end

This test is very simple. We first assign all records from the store_cache to the variable stores. In reality this enough to prove the table has been created. If it didn't exist then we would get an error that looked something like...

** (ArgumentError) argument error
    (stdlib) :ets.match_object(:incorrect_name, :_)

But we should make sure that our table returns what we expect it to. We do this with the next two lines.

First we assert that stores is a list. assert expects a truthy value. is_list/1 returns true if the argument is a list and false otherwise.

Next we confirm that this list is not empty. We are using refute for this. refute is the opposite to assert and expects a falsy value. We are calling the Enum.empty?/1 function which returns true if the enumerable passed in is empty. As our list should not be empty we expect this to return false.

Now all we need to do is run the tests again. Run mix test again in your terminal. You should now have 4 passing tests.

....

Finished in 0.09 seconds
4 tests, 0 failures

We can now be certain that the table is being created as we would like πŸŽ‰πŸŽ‰πŸŽ‰πŸŽ‰πŸŽ‰

You can feel free to add more specific tests, for example, to make sure that a value you expect to be in the table actually is.