diff --git a/lessons/otp-concurrency/simple_bank/config/config.exs b/lessons/otp-concurrency/simple_bank/config/config.exs index 21632ba..37f91eb 100644 --- a/lessons/otp-concurrency/simple_bank/config/config.exs +++ b/lessons/otp-concurrency/simple_bank/config/config.exs @@ -2,6 +2,7 @@ # and its dependencies with the aid of the Mix.Config module. use Mix.Config + # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this # file won't be loaded nor affect the parent project. For this reason, diff --git a/lessons/otp-concurrency/simple_bank/lib/simple_bank.ex b/lessons/otp-concurrency/simple_bank/lib/simple_bank.ex index 79c1614..7317b98 100644 --- a/lessons/otp-concurrency/simple_bank/lib/simple_bank.ex +++ b/lessons/otp-concurrency/simple_bank/lib/simple_bank.ex @@ -4,31 +4,122 @@ defmodule SimpleBank do """ use GenServer + @typedoc """ + An atom describing an error + """ + @type reason :: atom + + alias SimpleBank.Account + def start_link(initial_state \\ %{}) do GenServer.start_link(__MODULE__, initial_state) end - @spec register(pid(), String.t()) :: {:ok, String.t()} | {:error, String.t()} + @spec register(pid(), String.t()) :: {:ok, String.t()} | {:error, reason} def register(bank_pid, name) do + case get_account(bank_pid, name) do + nil -> + account = %Account{balance: 0, id: UUID.uuid4(), name: name} + GenServer.cast(bank_pid, {:register, account}) + {:ok, account.id} + %Account{} -> + {:error, :existing_account} + end + end + + def get_account(bank_pid, name) do + GenServer.call(bank_pid, {:get_account, name}) + end + + def get_account_by_id(bank_pid, account_id) do + GenServer.call(bank_pid, {:get_account_by_id, account_id}) end - @spec deposit(pid(), String.t(), pos_integer()) :: {:ok, pos_integer()} | {:error, String.t()} + @spec deposit(pid(), String.t(), pos_integer()) :: {:ok, pos_integer()} | {:error, reason} def deposit(bank_pid, account_id, amount) when is_integer(amount) and amount > 0 do + case get_account_by_id(bank_pid, account_id) do + nil -> {:error, :missing_account} + account -> + account = GenServer.call(bank_pid, {:deposit, account, amount}) + {:ok, account.balance} + end end - def deposit(_bank_pid, _account_id, _amount) do - {:error, :missing_account} + def deposit(_bank_pid, _account_id, amount) when is_integer(amount) and amount <= 0 do + {:error, :pos_integer_only} end - @spec deposit(pid(), String.t()) :: {:ok, pos_integer} | {:error, String.t()} + @spec balance(pid(), String.t()) :: {:ok, pos_integer} | {:error, reason} def balance(bank_pid, account_id) do + case get_account_by_id(bank_pid, account_id) do + nil -> {:error, :missing_account} + account -> + {:ok, account.balance} + end end - @spec withdrawl(pid(), String.t(), pos_integer()) :: {:ok, {pos_integer(), pos_integer()}} | {:error, String.t()} - def withdrawl(bank_pid, account_id, amount) do + @spec withdrawl(pid(), String.t(), pos_integer()) :: {:ok, {pos_integer(), pos_integer()}} | {:error, reason} + def withdrawl(bank_pid, account_id, amount) when is_integer(amount) and amount > 0 do + case get_account_by_id(bank_pid, account_id) do + nil -> {:error, :missing_account} + account -> + if account.balance < amount do + {:error, :insufficient_funds} + else + account = GenServer.call(bank_pid, {:withdrawal, account, amount}) + {:ok, account.balance} + end + end + end + + def withdrawl(_bank_pid, _account_id, amount) when is_integer(amount) and amount <= 0 do + {:error, :pos_integer_only} end def init(initial_state) do {:ok, initial_state} end + + def handle_cast({:register, account}, state) do + {:ok, [state | account]} + end + + def handle_call({:get_account, account_name}, _from, state) do + case Enum.find(state, fn account -> account.name == account_name end) do + nil -> {:reply, nil, state} + account -> {:reply, account, state} + end + end + + def handle_call({:get_account_by_id, account_id}, _from, state) do + case Enum.find(state, fn account -> account.id == account_id end) do + nil -> {:reply, nil, state} + account -> {:reply, account, state} + end + end + + def handle_call({:deposit, account, amount}, _from, state) do + account = do_deposit(account, amount) + new_state = update_account_in_state(account, state) + {:reply, account, new_state} + end + + def handle_call({:withdrawal, account, amount}, _from, state) do + account = do_withdrawal(account, amount) + new_state = update_account_in_state(account, state) + {:reply, account, new_state} + end + + def do_deposit(account, amount) do + Map.put(account, :balance, account.balance + amount) + end + + def do_withdrawal(account, amount) do + Map.put(account, :balance, account.balance - amount) + end + + def update_account_in_state(account, state) do + accounts = Enum.reject(state, fn a -> a.id == account.id end) + [accounts | account] + end end diff --git a/lessons/otp-concurrency/simple_bank/mix.exs b/lessons/otp-concurrency/simple_bank/mix.exs index 4bd902a..f4d0ff6 100644 --- a/lessons/otp-concurrency/simple_bank/mix.exs +++ b/lessons/otp-concurrency/simple_bank/mix.exs @@ -21,6 +21,7 @@ defmodule SimpleBank.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + { :elixir_uuid, "~> 1.2" } # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, ] diff --git a/lessons/otp-concurrency/simple_bank/mix.lock b/lessons/otp-concurrency/simple_bank/mix.lock new file mode 100644 index 0000000..39f3f8c --- /dev/null +++ b/lessons/otp-concurrency/simple_bank/mix.lock @@ -0,0 +1,3 @@ +%{ + "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, +} diff --git a/lessons/otp-concurrency/simple_bank/test/simple_bank_test.exs b/lessons/otp-concurrency/simple_bank/test/simple_bank_test.exs index 170e06e..2ff9288 100644 --- a/lessons/otp-concurrency/simple_bank/test/simple_bank_test.exs +++ b/lessons/otp-concurrency/simple_bank/test/simple_bank_test.exs @@ -9,7 +9,7 @@ defmodule SimpleBankTest do end describe "register/2" do - test "creates a new account and generates an account id", %{bank: bank_pid} do + test "creates a new account with the given name and a randomly generated id. HINT: We've included the `elixir-uuid` dependency so that you can generate a random UUID string. See docs here https://github.com/zyro/elixir-uuid", %{bank: bank_pid} do {:ok, account_id} = SimpleBank.register(bank_pid, "Another Test Account") assert is_binary(account_id) end @@ -21,7 +21,7 @@ defmodule SimpleBankTest do describe "deposit/3" do test "increases the account balance by the deposited amount", %{bank: bank_pid} do - assert {:ok, 10} == SimpleBank.deposit(bank_pid, "test_id", 10) + assert {:ok, 110} == SimpleBank.deposit(bank_pid, "test_id", 10) end test "does not allow deposits of negative amounts", %{bank: bank_pid} do @@ -35,7 +35,7 @@ defmodule SimpleBankTest do describe "balance/2" do test "returns the current account balance", %{bank: bank_pid} do - assert {:ok, 110} == SimpleBank.deposit(bank_pid, "test_id", 10) + assert {:ok, 100} == SimpleBank.balance(bank_pid, "test_id") end test "raises an error if the account does not exist", %{bank: bank_pid} do @@ -45,15 +45,15 @@ defmodule SimpleBankTest do describe "withdrawl/3" do test "decreases the account balance by the withdrawn amount", %{bank: bank_pid} do - assert {:ok, 100} == SimpleBank.withdrawl(bank_pid, "test_id", 10) + assert {:ok, 90} == SimpleBank.withdrawl(bank_pid, "test_id", 10) end test "does not negative ammount balances", %{bank: bank_pid} do - assert {:error, :insufficient_funds} == SimpleBank.withdrawl(bank_pid, "test_id", -1) + assert {:error, :insufficient_funds} == SimpleBank.withdrawl(bank_pid, "test_id", 1000) end test "does not allow withdrawls of negative amounts", %{bank: bank_pid} do - assert {:error, :pos_integer_only} == SimpleBank.withdrawl(bank_pid, "test_id", 1000) + assert {:error, :pos_integer_only} == SimpleBank.withdrawl(bank_pid, "test_id", -1) end test "raises an error if the account does not exist", %{bank: bank_pid} do diff --git a/lessons/otp-concurrency/simple_bank/test/test_helper.exs b/lessons/otp-concurrency/simple_bank/test/test_helper.exs index 869559e..a7e618d 100644 --- a/lessons/otp-concurrency/simple_bank/test/test_helper.exs +++ b/lessons/otp-concurrency/simple_bank/test/test_helper.exs @@ -1 +1,2 @@ +ExUnit.configure seed: 0 ExUnit.start() diff --git a/lessons/otp-concurrency/simple_queue/lib/simple_queue.ex b/lessons/otp-concurrency/simple_queue/lib/simple_queue.ex index 2f4460c..128ea7c 100644 --- a/lessons/otp-concurrency/simple_queue/lib/simple_queue.ex +++ b/lessons/otp-concurrency/simple_queue/lib/simple_queue.ex @@ -11,15 +11,18 @@ defmodule SimpleQueue do def handle_call(:queue, _from, state), do: {:reply, state, state} - def handle_cast({:enqueue, value}, state) do - {:noreply, state ++ [value]} + def handle_call({:enqueue, value}, _from, state) do + {:reply, value, state ++ [value]} end + def handle_call(:sum, _from, state), do: {:reply, Enum.sum(state), state} + def start_link(state \\ []) do GenServer.start_link(__MODULE__, state, name: __MODULE__) end def queue, do: GenServer.call(__MODULE__, :queue) - def enqueue(value), do: GenServer.cast(__MODULE__, {:enqueue, value}) + def enqueue(value), do: GenServer.call(__MODULE__, {:enqueue, value}) def dequeue, do: GenServer.call(__MODULE__, :dequeue) + def sum, do: GenServer.call(__MODULE__, :sum) end