diff --git a/README.md b/README.md index 71d5e49..de7c4f1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ Each exercise is created as a standalone Mix project requiring a varying degree > Provided with a string of characters ("aabbc"), print all possible palindrome premutations ("abcba", "bacab") to IO. + - [Simple Bank](/tree/master/simple_bank) _added 2018-04-30_ + + > Implement a GenServer based bank with support for account registration, deposits, withdrawls, and account balance inquiries. + + - [Simple Bank Online](/tree/master/simple_bank_online) _added 2018-05-02_ + + > Using the implementation from [Simple Bank](/tree/master/simple_bank), complete the JSON API to bring your bank online! + ## Contributions We'd love to hear your feedback on how these exercises are working for you. diff --git a/simple_bank/.formatter.exs b/simple_bank/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/simple_bank/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/simple_bank/.gitignore b/simple_bank/.gitignore new file mode 100644 index 0000000..2490feb --- /dev/null +++ b/simple_bank/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +simple_bank-*.tar + diff --git a/simple_bank/README.md b/simple_bank/README.md new file mode 100644 index 0000000..6cba9f9 --- /dev/null +++ b/simple_bank/README.md @@ -0,0 +1,21 @@ +# SimpleBank + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `simple_bank` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:simple_bank, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/simple_bank](https://hexdocs.pm/simple_bank). + diff --git a/simple_bank/config/config.exs b/simple_bank/config/config.exs new file mode 100644 index 0000000..21632ba --- /dev/null +++ b/simple_bank/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# 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, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :simple_bank, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:simple_bank, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/simple_bank/lib/simple_bank.ex b/simple_bank/lib/simple_bank.ex new file mode 100644 index 0000000..0a689d8 --- /dev/null +++ b/simple_bank/lib/simple_bank.ex @@ -0,0 +1,32 @@ +defmodule SimpleBank do + @moduledoc """ + SimpleBank is a GenServer charading as a bank. + """ + use GenServer + + def start_link(initial_state \\ %{}) do + GenServer.start_link(__MODULE__, initial_state) + end + + def init(initial_state) do + {:ok, initial_state} + end + + @spec register(pid(), String.t()) :: {:ok, String.t()} | {:error, reason} + def register(bank_pid, name) do + end + + @spec deposit(pid(), String.t(), pos_integer} :: {:ok, pos_integer} | {:error, reason} + def deposit(bank_pid, account_id, amount) do + end + + @spec deposit(pid(), String.t()} :: {:ok, pos_integer} | {:error, reason} + def balance(bank_pid, account_id) do + end + + @spec withdrawl(pid(), String.t(), pos_integer} :: {:ok, {pos_integer, pos_integer}} | {:error, reason} + def withdrawl(bank_pid, account_id, amount) do + end + + def init(initial_state), do: initial_state +end diff --git a/simple_bank/lib/simple_bank/account.ex b/simple_bank/lib/simple_bank/account.ex new file mode 100644 index 0000000..bc91847 --- /dev/null +++ b/simple_bank/lib/simple_bank/account.ex @@ -0,0 +1,5 @@ +defmodule SimpleBank.Account do + @type t :: %__MODULE__{balance: integer(), id: String.t(), name: String.t()} + + defstruct [:balance, :id, :name] +end diff --git a/simple_bank/mix.exs b/simple_bank/mix.exs new file mode 100644 index 0000000..4bd902a --- /dev/null +++ b/simple_bank/mix.exs @@ -0,0 +1,28 @@ +defmodule SimpleBank.MixProject do + use Mix.Project + + def project do + [ + app: :simple_bank, + version: "0.1.0", + elixir: "~> 1.6", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, + ] + end +end diff --git a/simple_bank/test/simple_bank_test.exs b/simple_bank/test/simple_bank_test.exs new file mode 100644 index 0000000..55675ae --- /dev/null +++ b/simple_bank/test/simple_bank_test.exs @@ -0,0 +1,63 @@ +defmodule SimpleBankTest do + use ExUnit.Case, async: false + + alias SimpleBank.Account + + setup do + {:ok, bank_pid} = SimpleBank.start_link([%Account{balance: 100, id: "test_id", name: "Test"}]) + {:ok, bank: bank_pid} + end + + describe "register/2" do + test "creates a new account and generates an account id", %{bank: bank_pid} do + {:ok, account_id} = SimpleBank.register(bank_pid, "Another Test Account") + assert is_binary(account_id) + end + + test "raises an error for existing account names", %{bank: bank_pid} do + {:error, :existing_account} = SimpleBank.register(bank_pid, "Test") + end + end + + describe "deposit/3" do + test "increases the account balance by the deposited amount", %{bank: bank_pid} do + assert {:ok, 110} == SimpleBank.deposit(bank_pid, "test_id", 10) + end + + test "does not allow deposits of negative ammounts", %{bank: bank_pid} do + assert {:error, :pos_integer_only} == SimpleBank.deposit(bank_pid, "test_id", -1) + end + + test "raises an error if the account does not exist", %{bank: bank_pid} do + assert {:error, :missing_account} == SimpleBank.deposit(bank_pid, "doesnotexist", 10) + end + end + + describe "balance/2" do + test "returns the current account balance", %{bank: bank_pid} do + assert {:ok, 110} == SimpleBank.deposit(bank_pid, "test_id", 10) + end + + test "raises an error if the account does not exist", %{bank: bank_pid} do + assert {:error, :missing_account} == SimpleBank.balance(bank_pid, "doesnotexist") + end + end + + 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) + end + + test "does not negative ammount balances", %{bank: bank_pid} do + assert {:error, :insufficient_funds} == SimpleBank.withdrawl(bank_pid, "test_id", -1) + end + + test "does not allow withdrawls of negative ammounts", %{bank: bank_pid} do + assert {:error, :pos_integer_only} == SimpleBank.withdrawl(bank_pid, "test_id", 1000) + end + + test "raises an error if the account does not exist", %{bank: bank_pid} do + assert {:error, :missing_account} == SimpleBank.deposit(bank_pid, "doesnotexist", 10) + end + end +end diff --git a/simple_bank/test/test_helper.exs b/simple_bank/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/simple_bank/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/simple_bank_online/.formatter.exs b/simple_bank_online/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/simple_bank_online/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/simple_bank_online/.gitignore b/simple_bank_online/.gitignore new file mode 100644 index 0000000..f6463ad --- /dev/null +++ b/simple_bank_online/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +simple_bank_online-*.tar + diff --git a/simple_bank_online/README.md b/simple_bank_online/README.md new file mode 100644 index 0000000..74b3563 --- /dev/null +++ b/simple_bank_online/README.md @@ -0,0 +1,21 @@ +# SimpleBankOnline + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `simple_bank_online` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:simple_bank_online, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/simple_bank_online](https://hexdocs.pm/simple_bank_online). + diff --git a/simple_bank_online/config/config.exs b/simple_bank_online/config/config.exs new file mode 100644 index 0000000..564ea14 --- /dev/null +++ b/simple_bank_online/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# 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, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :simple_bank_online, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:simple_bank_online, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/simple_bank_online/lib/simple_bank_online.ex b/simple_bank_online/lib/simple_bank_online.ex new file mode 100644 index 0000000..b16a0ac --- /dev/null +++ b/simple_bank_online/lib/simple_bank_online.ex @@ -0,0 +1,18 @@ +defmodule SimpleBankOnline do + @moduledoc """ + Documentation for SimpleBankOnline. + """ + + @doc """ + Hello world. + + ## Examples + + iex> SimpleBankOnline.hello + :world + + """ + def hello do + :world + end +end diff --git a/simple_bank_online/lib/simple_bank_online/application.ex b/simple_bank_online/lib/simple_bank_online/application.ex new file mode 100644 index 0000000..4493d19 --- /dev/null +++ b/simple_bank_online/lib/simple_bank_online/application.ex @@ -0,0 +1,17 @@ +defmodule SimpleBankOnline.Application do + @moduledoc false + + use Application + + import Supervisor.Spec + + def start(_type, _args) do + children = [ + Plug.Adapters.Cowboy.child_spec(:http, SimpleBankOnline.Router, [], port: 8000), + worker(SimpleBank, []) + ] + + opts = [strategy: :one_for_one, name: Web.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/simple_bank_online/lib/simple_bank_online/router.ex b/simple_bank_online/lib/simple_bank_online/router.ex new file mode 100644 index 0000000..134df8b --- /dev/null +++ b/simple_bank_online/lib/simple_bank_online/router.ex @@ -0,0 +1,39 @@ +defmodule SimpleBankOnline.Router do + use Plug.Router + + plug :match + plug Plug.RequestId + plug Plug.Logger, log: :info + plug Plug.Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Poison + plug :dispatch + + get "/", do: send_resp(conn, 200, Poison.encode!(%{msg: "Welcome to Simple Bank Online!"})) + + post "/accounts" do + end + + get "/accounts/:id" do + end + + post "/accounts/:id/deposits" do + end + + post "/accounts/:id/withdrawals" do + end + + match _ do + render({:error, :not_found}, conn) + end + + defp render({:ok, result}, conn) do + send_resp(conn, 200, Poison.encode!(result)) + end + + defp render({:error, reason}, conn) when reason in [:not_found, :missing_account] do + send_resp(conn, 404, Poison.encode!(%{errors: ["We're sorry. What you're looking for cannot be found."]})) + end + + defp render({:error, reason}, conn) do + send_resp(conn, 400, Poison.encode!(%{errors: [reason]})) + end +end diff --git a/simple_bank_online/mix.exs b/simple_bank_online/mix.exs new file mode 100644 index 0000000..58192a1 --- /dev/null +++ b/simple_bank_online/mix.exs @@ -0,0 +1,31 @@ +defmodule SimpleBankOnline.MixProject do + use Mix.Project + + def project do + [ + app: :simple_bank_online, + version: "0.1.0", + elixir: "~> 1.6", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {SimpleBankOnline.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:cowboy, "~> 1.0"}, + {:plug, "~> 1.4"}, + {:poison, "~> 3.1"}, + {:simple_bank, path: "../simple_bank"}, + ] + end +end diff --git a/simple_bank_online/test/simple_bank_online_test.exs b/simple_bank_online/test/simple_bank_online_test.exs new file mode 100644 index 0000000..5e26973 --- /dev/null +++ b/simple_bank_online/test/simple_bank_online_test.exs @@ -0,0 +1,8 @@ +defmodule SimpleBankOnlineTest do + use ExUnit.Case + doctest SimpleBankOnline + + test "greets the world" do + assert SimpleBankOnline.hello() == :world + end +end diff --git a/simple_bank_online/test/test_helper.exs b/simple_bank_online/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/simple_bank_online/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()