diff --git a/lib/koans/19_genservers.ex b/lib/koans/19_genservers.ex new file mode 100644 index 0000000..537e72d --- /dev/null +++ b/lib/koans/19_genservers.ex @@ -0,0 +1,141 @@ +defmodule GenServers do + use Koans + + @intro "GenServers" + + defmodule BicycleLock do + use GenServer + + @init_password 1234 + + ##### + # External API + + def start_link(init_password \\ @init_password) do + # The __MODULE__ macro returns the current module name as an atom + GenServer.start_link(__MODULE__, init_password, name: __MODULE__) + end + + def unlock(password) do + GenServer.call(__MODULE__, {:unlock, password}) + end + + def password_reset(old_password, new_password) do + GenServer.cast(__MODULE__, {:reset, old_password, new_password}) + end + + def owner_info do + GenServer.call(__MODULE__, :get_owner_info) + end + + #### + # GenServer implementation + + def init(args) do + {:ok, args && args || @init_password} + end + + def handle_call(:get_password, _from, current_password) do + {:reply, current_password, current_password} + end + + def handle_call(:get_bike_brand, _from, current_state) do + {:reply, "Tribe Bicycle Co.", current_state} + end + + def handle_call(:get_bike_name, _from, current_state) do + {:reply, "CRMO Series", current_state} + end + + def handle_call(:get_owner_info, _from, current_state) do + {:reply, {:ok, "Argh...Jack Sparrow's password is: #{current_state}"}, current_state} + end + + def handle_call(:get_multi, _from, current_state) do + {:reply, {:ok, ["this", "is", "sparta"]}, current_state} + end + + def handle_call(:name_check, _from, current_state) do + {:reply, "Congrats! Your process was successfully named.", current_state} + end + + def handle_call({:unlock, password}, _from, current_password) do + case password do + password when password === current_password -> + {:reply, {:ok, "Bicycle unlocked!"}, current_password} + _ -> + {:reply, {:error, "Incorrect password!"}, current_password} + end + end + + def handle_cast({:reset, old_password, new_password}, current_password) do + case old_password do + old_password when old_password == current_password -> + {:noreply, new_password} + _ -> + {:noreply, current_password} + end + end + end + + koan "Servers that are created and initialized successfully returns a tuple that holds the PID of the server" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil) + assert is_pid(pid) == ___ + end + + koan "When starting a GenServer you can set it's initial state" do + {:ok, pid} = GenServer.start_link(BicycleLock, "Hey Arnold!") + assert GenServer.call(pid, :get_password) == ___ + end + + koan "The handle_call callback is synchronous so it will block until a reply is received" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil) + assert GenServer.call(pid, :get_password) == ___ + end + + koan "A server can support multiple actions by implementing multiple handle_call functions" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil) + assert GenServer.call(pid, :get_bike_brand) == ___ + assert GenServer.call(pid, :get_bike_name) == ___ + end + + koan "A handler can return multiple values" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil) + {:ok, multi_values} = GenServer.call(pid, :get_multi) + assert multi_values == ___ + end + + koan "The handle_cast callback handles asynchronous messages" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil) + GenServer.cast(pid, {:reset, 1234, "Hello"}) + assert GenServer.call(pid, :get_password) == ___ + end + + koan "Handlers can also return error responses" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil) + assert GenServer.call(pid, {:unlock, 2017}) == ___ + end + + koan "Referencing processes by their PID gets old pretty quickly, so let's name them" do + {:ok, pid} = GenServer.start_link(BicycleLock, nil, name: :bike_lock) + assert GenServer.call(:bike_lock, :name_check) == ___ + end + + koan "Our server works but it's pretty ugly to use; so lets use a cleaner interface" do + BicycleLock.start_link + assert BicycleLock.unlock(1234) == ___ + end + + koan "Let's use the remaining functions in the external API" do + BicycleLock.start_link(31337) + {_, response} = BicycleLock.unlock(31337) + assert response == ___ + + BicycleLock.password_reset(31337, "Elixir") + {_, response} = BicycleLock.unlock(31337) + assert response == ___ + + {_, response} = BicycleLock.owner_info + assert response == ___ + end +end \ No newline at end of file diff --git a/lib/runner.ex b/lib/runner.ex index be1d0ae..5f1d98b 100644 --- a/lib/runner.ex +++ b/lib/runner.ex @@ -18,6 +18,7 @@ defmodule Runner do Tasks, Agents, Protocols, + GenServers, ] def koan?(koan), do: Enum.member?(@modules, koan) diff --git a/test/koans/genservers_koans_test.exs b/test/koans/genservers_koans_test.exs new file mode 100644 index 0000000..7175ea6 --- /dev/null +++ b/test/koans/genservers_koans_test.exs @@ -0,0 +1,21 @@ +defmodule GenServersTests do + use ExUnit.Case + import TestHarness + + test "GenServers" do + answers = [ + true, + "Hey Arnold!", + 1234, + {:multiple, ["Tribe Bicycle Co.", "CRMO Series"]}, + ["this", "is", "sparta"], + "Hello", + {:error, "Incorrect password!"}, + "Congrats! Your process was successfully named.", + {:ok, "Bicycle unlocked!"}, + {:multiple, ["Bicycle unlocked!", "Incorrect password!", "Argh...Jack Sparrow's password is: Elixir"]}, + ] + + test_all(GenServers, answers) + end +end