diff --git a/lib/koans/18_genservers.ex b/lib/koans/18_genservers.ex new file mode 100644 index 0000000..8ad4589 --- /dev/null +++ b/lib/koans/18_genservers.ex @@ -0,0 +1,153 @@ +defmodule GenServers do + use Koans + + @intro "GenServers" + + defmodule Laptop do + use GenServer + + ##### + # External API + + def start_link(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 owner_name do + GenServer.call(__MODULE__, :get_owner_name) + end + + def manufacturer do + GenServer.call(__MODULE__, :get_manufacturer) + end + + def laptop_type do + GenServer.call(__MODULE__, :get_type) + end + + def retrieve_password do + GenServer.call(__MODULE__, :get_password) + end + + def laptop_specs do + GenServer.call(__MODULE__, :get_specs) + end + + def change_password(old_password, new_password) do + GenServer.cast(__MODULE__, {:change_password, old_password, new_password}) + end + + #### + # GenServer implementation + + def handle_call(:get_password, _from, current_password) do + {:reply, current_password, current_password} + end + + def handle_call(:get_manufacturer, _from, current_state) do + {:reply, "Apple Inc.", current_state} + end + + def handle_call(:get_type, _from, current_state) do + {:reply, "MacBook Pro", current_state} + end + + def handle_call(:get_owner_name, _from, current_state) do + {:reply, {:ok, "Jack Sparrow"}, current_state} + end + + def handle_call(:get_specs, _from, current_state) do + {:reply, {:ok, ["2.9 GHz Intel Core i5"], 8192, :intel_iris_graphics}, 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, "Laptop unlocked!"}, current_password} + _ -> + {:reply, {:error, "Incorrect password!"}, current_password} + end + end + + def handle_cast({:change_password, 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(Laptop, "3kr3t!") + assert is_pid(pid) == ___ + end + + koan "When starting a GenServer you can set it's initial state" do + {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") + 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(Laptop, "3kr3t!") + 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(Laptop, "3kr3t!") + assert GenServer.call(pid, :get_manufacturer) == ___ + assert GenServer.call(pid, :get_type) == ___ + end + + koan "A handler can return multiple values and of different types" do + {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") + {:ok, processor, memory, graphics} = GenServer.call(pid, :get_specs) + assert processor == ___ + assert memory == ___ + assert graphics == ___ + end + + koan "The handle_cast callback handles asynchronous messages" do + {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") + GenServer.cast(pid, {:change_password, "3kr3t!", "73x7!n9"}) + assert GenServer.call(pid, :get_password) == ___ + end + + koan "Handlers can also return error responses" do + {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") + assert GenServer.call(pid, {:unlock, "81u3pr!n7"}) == ___ + end + + koan "Referencing processes by their PID gets old pretty quickly, so let's name them" do + {:ok, _} = GenServer.start_link(Laptop, "3kr3t!", name: :macbook) + assert GenServer.call(:macbook, :name_check) == ___ + end + + koan "Our server works but it's pretty ugly to use; so lets use a cleaner interface" do + Laptop.start_link("EL!73") + assert Laptop.unlock("EL!73") == ___ + end + + koan "Let's use the remaining functions in the external API" do + Laptop.start_link("EL!73") + {_, response} = Laptop.unlock("EL!73") + assert response == ___ + + Laptop.change_password("EL!73", "Elixir") + {_, response} = Laptop.unlock("EL!73") + assert response == ___ + + {_, response} = Laptop.owner_name + assert response == ___ + end +end \ No newline at end of file diff --git a/lib/koans/18_protocols.ex b/lib/koans/19_protocols.ex similarity index 100% rename from lib/koans/18_protocols.ex rename to lib/koans/19_protocols.ex diff --git a/lib/runner.ex b/lib/runner.ex index be1d0ae..0c7664c 100644 --- a/lib/runner.ex +++ b/lib/runner.ex @@ -17,6 +17,7 @@ defmodule Runner do Processes, Tasks, Agents, + GenServers, Protocols, ] diff --git a/test/koans/genservers_koans_test.exs b/test/koans/genservers_koans_test.exs new file mode 100644 index 0000000..b8cae92 --- /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, + "3kr3t!", + "3kr3t!", + {:multiple, ["Apple Inc.", "MacBook Pro"]}, + {:multiple, [["2.9 GHz Intel Core i5"], 8192, :intel_iris_graphics]}, + "73x7!n9", + {:error, "Incorrect password!"}, + "Congrats! Your process was successfully named.", + {:ok, "Laptop unlocked!"}, + {:multiple, ["Laptop unlocked!", "Incorrect password!", "Jack Sparrow"]}, + ] + + test_all(GenServers, answers) + end +end