Update: Finished persistable todo server

This commit is contained in:
Mariano Uvalle 2020-06-22 09:53:15 -05:00
parent 09665c3b88
commit 55589a963f
80 changed files with 809 additions and 0 deletions

View file

@ -0,0 +1,21 @@
# Todo
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `todo` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:todo, "~> 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/todo](https://hexdocs.pm/todo).

View file

@ -0,0 +1,7 @@
{application,todo,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"todo"},
{modules,['Elixir.Todo.Cache','Elixir.Todo.Database',
'Elixir.Todo.List','Elixir.Todo.Server']},
{registered,[]},
{vsn,"0.1.0"}]}.

View file

@ -0,0 +1,7 @@
{application,todo,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"todo"},
{modules,['Elixir.Todo.Cache','Elixir.Todo.List',
'Elixir.Todo.Server']},
{registered,[]},
{vsn,"0.1.0"}]}.

View file

@ -0,0 +1,34 @@
defmodule Todo.Cache do
use GenServer
def start() do
GenServer.start(__MODULE__, nil)
end
def server_process(cache_pid, todo_list_name) do
GenServer.call(cache_pid, {:server_process, todo_list_name})
end
@impl true
def init(_) do
Todo.Database.start()
{:ok, %{}}
end
@impl true
def handle_call({:server_process, todo_list_name}, _, todo_servers) do
case Map.fetch(todo_servers, todo_list_name) do
{:ok, todo_server} ->
{:reply, todo_server, todo_servers}
:error ->
{:ok, new_todo_server} = Todo.Server.start(todo_list_name)
{
:reply,
new_todo_server,
Map.put(todo_servers, todo_list_name, new_todo_server)
}
end
end
end

View file

@ -0,0 +1,47 @@
defmodule Todo.Database do
use GenServer
@db_folder "./persist"
def start do
GenServer.start(__MODULE__, nil, name: __MODULE__)
end
def store(key, data) do
GenServer.cast(__MODULE__, {:store, key, data})
end
def get(key) do
GenServer.call(__MODULE__, {:get, key})
end
@impl true
def init(_) do
File.mkdir_p!(@db_folder)
{:ok, nil}
end
@impl true
def handle_cast({:store, key, data}, state) do
key
|> file_name()
|> File.write!(:erlang.term_to_binary(data))
{:noreply, state}
end
@impl true
def handle_call({:get, key}, _, state) do
data =
case File.read(file_name(key)) do
{:ok, contents} -> :erlang.binary_to_term(contents)
_ -> nil
end
{:reply, data, state}
end
def file_name(key) do
Path.join(@db_folder, to_string(key))
end
end

View file

@ -0,0 +1,57 @@
defmodule Todo.List do
defstruct auto_id: 1, entries: %{}
def new(entries \\ []) do
Enum.reduce(
entries,
%Todo.List{},
&add_entry(&2, &1)
)
end
def size(todo_list) do
map_size(todo_list.entries)
end
def add_entry(todo_list, entry) do
entry = Map.put(entry, :id, todo_list.auto_id)
new_entries =
Map.put(
todo_list.entries,
todo_list.auto_id,
entry
)
%Todo.List{todo_list | entries: new_entries, auto_id: todo_list.auto_id + 1}
end
def entries(todo_list, date) do
todo_list.entries
|> Stream.filter(fn {_, entry} -> entry.date == date end)
|> Enum.map(fn {_, entry} -> entry end)
end
def update_entry(todo_list, %{} = new_entry) do
update_entry(todo_list, new_entry.id, fn _ -> new_entry end)
end
def update_entry(todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error ->
todo_list
{:ok, old_entry} ->
old_entry_id = old_entry.id
# Make sure that the result of the updater is a map and the
# id remains unchanged.
new_entry = %{id: ^old_entry_id} = updater_fun.(old_entry)
new_entries = Map.put(todo_list.entries, new_entry.id, new_entry)
%Todo.List{todo_list | entries: new_entries}
end
end
def delete_entry(todo_list, entry_id) do
%Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)}
end
end

View file

@ -0,0 +1,62 @@
defmodule Todo.Server do
use GenServer
def start(todo_list_name) do
GenServer.start(__MODULE__, todo_list_name)
end
def add_entry(pid, entry) do
GenServer.cast(pid, {:add_entry, entry})
end
def delete_entry(pid, entry_id) do
GenServer.cast(pid, {:delete_entry, entry_id})
end
def update_entry(pid, entry_id, updater_fun) do
GenServer.cast(pid, {:update_entry, entry_id, updater_fun})
end
def entries(pid, date) do
GenServer.call(pid, {:entries, date})
end
@impl true
def init(todo_list_name) do
send(self(), :init_data)
{:ok, {todo_list_name, nil}}
end
@impl true
def handle_cast({:add_entry, entry}, {name, todo_list}) do
# Get the new todo list.
new_list = Todo.List.add_entry(todo_list, entry)
# Persist the new list to disk.
Todo.Database.store(name, new_list)
{:noreply, {name, new_list}}
end
@impl true
def handle_cast({:delete_entry, entry_id}, {name, todo_list}) do
new_list = Todo.List.delete_entry(todo_list, entry_id)
Todo.Database.store(name, new_list)
{:noreply, {name, new_list}}
end
@impl true
def handle_cast({:update_entry, entry_id, updater_fun}, {name, todo_list}) do
new_list = Todo.List.update_entry(todo_list, entry_id, updater_fun)
Todo.Database.store(name, new_list)
{:noreply, {name, new_list}}
end
@impl true
def handle_call({:entries, date}, _, {_, todo_list} = state) do
{:reply, Todo.List.entries(todo_list, date), state}
end
@impl true
def handle_info(:init_data, {name, _}) do
{:noreply, {name, Todo.Database.get(name) || Todo.List.new()}}
end
end

View file

@ -0,0 +1,28 @@
defmodule Todo.MixProject do
use Mix.Project
def project do
[
app: :todo,
version: "0.1.0",
elixir: "~> 1.10",
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

Binary file not shown.

View file

@ -0,0 +1 @@
ExUnit.start()

View file

@ -0,0 +1,21 @@
defmodule TodoCacheTest do
use ExUnit.Case
test "server_process" do
{:ok, cache} = Todo.Cache.start()
bob_pid = Todo.Cache.server_process(cache, "bob")
assert bob_pid != Todo.Cache.server_process(cache, "alice")
assert bob_pid == Todo.Cache.server_process(cache, "bob")
end
test "to-do operations" do
{:ok, cache} = Todo.Cache.start()
alice = Todo.Cache.server_process(cache, "alice")
Todo.Server.add_entry(alice, %{date: ~D[2020-12-12], title: "Hello"})
entries = Todo.Server.entries(alice, ~D[2020-12-12])
assert [%{date: ~D[2020-12-12], title: "Hello"}] = entries
end
end

View file

@ -0,0 +1,64 @@
defmodule TodoListTest do
use ExUnit.Case, async: true
test "empty list" do
assert Todo.List.size(Todo.List.new()) == 0
end
test "entries" do
todo_list =
Todo.List.new([
%{date: ~D[2018-12-19], title: "Dentist"},
%{date: ~D[2018-12-20], title: "Shopping"},
%{date: ~D[2018-12-19], title: "Movies"}
])
assert Todo.List.size(todo_list) == 3
assert todo_list |> Todo.List.entries(~D[2018-12-19]) |> length() == 2
assert todo_list |> Todo.List.entries(~D[2018-12-20]) |> length() == 1
assert todo_list |> Todo.List.entries(~D[2018-12-21]) |> length() == 0
titles = todo_list |> Todo.List.entries(~D[2018-12-19]) |> Enum.map(& &1.title)
assert ["Dentist", "Movies"] = titles
end
test "add_entry" do
todo_list =
Todo.List.new()
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Dentist"})
|> Todo.List.add_entry(%{date: ~D[2018-12-20], title: "Shopping"})
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Movies"})
assert Todo.List.size(todo_list) == 3
assert todo_list |> Todo.List.entries(~D[2018-12-19]) |> length() == 2
assert todo_list |> Todo.List.entries(~D[2018-12-20]) |> length() == 1
assert todo_list |> Todo.List.entries(~D[2018-12-21]) |> length() == 0
titles = todo_list |> Todo.List.entries(~D[2018-12-19]) |> Enum.map(& &1.title)
assert ["Dentist", "Movies"] = titles
end
test "update_entry" do
todo_list =
Todo.List.new()
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Dentist"})
|> Todo.List.add_entry(%{date: ~D[2018-12-20], title: "Shopping"})
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Movies"})
|> Todo.List.update_entry(2, &Map.put(&1, :title, "Updated shopping"))
assert Todo.List.size(todo_list) == 3
assert [%{title: "Updated shopping"}] = Todo.List.entries(todo_list, ~D[2018-12-20])
end
test "delete_entry" do
todo_list =
Todo.List.new()
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Dentist"})
|> Todo.List.add_entry(%{date: ~D[2018-12-20], title: "Shopping"})
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Movies"})
|> Todo.List.delete_entry(2)
assert Todo.List.size(todo_list) == 2
assert Todo.List.entries(todo_list, ~D[2018-12-20]) == []
end
end

View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
chapter7/todo/.gitignore vendored Normal file
View file

@ -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 third-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").
todo-*.tar

21
chapter7/todo/README.md Normal file
View file

@ -0,0 +1,21 @@
# Todo
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `todo` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:todo, "~> 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/todo](https://hexdocs.pm/todo).

View file

@ -0,0 +1,47 @@
defmodule Todo.List do
defstruct auto_id: 1, entries: %{}
def new(), do: %Todo.List{}
def add_entry(todo_list, entry) do
entry = Map.put(entry, :id, todo_list.auto_id)
new_entries =
Map.put(
todo_list.entries,
todo_list.auto_id,
entry
)
%Todo.List{todo_list | entries: new_entries, auto_id: todo_list.auto_id + 1}
end
def entries(todo_list, date) do
todo_list.entries
|> Stream.filter(fn {_, entry} -> entry.date == date end)
|> Enum.map(fn {_, entry} -> entry end)
end
def update_entry(todo_list, %{} = new_entry) do
update_entry(todo_list, new_entry.id, fn _ -> new_entry end)
end
def update_entry(todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error ->
todo_list
{:ok, old_entry} ->
old_entry_id = old_entry.id
# Make sure that the result of the updater is a map and the
# id remains unchanged.
new_entry = %{id: ^old_entry_id} = updater_fun.(old_entry)
new_entries = Map.put(todo_list.entries, new_entry.id, new_entry)
%Todo.List{todo_list | entries: new_entries}
end
end
def delete_entry(todo_list, entry_id) do
%Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)}
end
end

View file

@ -0,0 +1,48 @@
defmodule Todo.Server do
use GenServer
def start do
GenServer.start(__MODULE__, nil)
end
def add_entry(pid, entry) do
GenServer.cast(pid, {:add_entry, entry})
end
def delete_entry(pid, entry_id) do
GenServer.cast(pid, {:delete_entry, entry_id})
end
def update_entry(pid, entry_id, updater_fun) do
GenServer.cast(pid, {:update_entry, entry_id, updater_fun})
end
def entries(pid, date) do
GenServer.call(pid, {:entries, date})
end
@impl true
def init(_) do
{:ok, Todo.List.new()}
end
@impl true
def handle_cast({:add_entry, entry}, state) do
{:noreply, Todo.List.add_entry(state, entry)}
end
@impl true
def handle_cast({:delete_entry, entry_id}, state) do
{:noreply, Todo.List.delete_entry(state, entry_id)}
end
@impl true
def handle_cast({:update_entry, entry_id, updater_fun}, state) do
{:noreply, Todo.List.update_entry(state, entry_id, updater_fun)}
end
@impl true
def handle_call({:entries, date}, _, state) do
{:reply, Todo.List.entries(state, date), state}
end
end

28
chapter7/todo/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule Todo.MixProject do
use Mix.Project
def project do
[
app: :todo,
version: "0.1.0",
elixir: "~> 1.10",
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

View file

@ -0,0 +1 @@
ExUnit.start()

View file

@ -0,0 +1,21 @@
# Todo
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `todo` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:todo, "~> 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/todo](https://hexdocs.pm/todo).

View file

@ -0,0 +1,7 @@
{application,todo,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"todo"},
{modules,['Elixir.Todo.Cache','Elixir.Todo.List',
'Elixir.Todo.Server']},
{registered,[]},
{vsn,"0.1.0"}]}.

View file

@ -0,0 +1,7 @@
{application,todo,
[{applications,[kernel,stdlib,elixir,logger]},
{description,"todo"},
{modules,['Elixir.Todo.Cache','Elixir.Todo.List',
'Elixir.Todo.Server']},
{registered,[]},
{vsn,"0.1.0"}]}.

View file

@ -0,0 +1,33 @@
defmodule Todo.Cache do
use GenServer
def start() do
GenServer.start(__MODULE__, nil)
end
def server_process(cache_pid, todo_list_name) do
GenServer.call(cache_pid, {:server_process, todo_list_name})
end
@impl true
def init(_) do
{:ok, %{}}
end
@impl true
def handle_call({:server_process, todo_list_name}, _, todo_servers) do
case Map.fetch(todo_servers, todo_list_name) do
{:ok, todo_server} ->
{:reply, todo_server, todo_servers}
:error ->
{:ok, new_todo_server} = Todo.Server.start()
{
:reply,
new_todo_server,
Map.put(todo_servers, todo_list_name, new_todo_server)
}
end
end
end

View file

@ -0,0 +1,57 @@
defmodule Todo.List do
defstruct auto_id: 1, entries: %{}
def new(entries \\ []) do
Enum.reduce(
entries,
%Todo.List{},
&add_entry(&2, &1)
)
end
def size(todo_list) do
Map.size(todo_list.entries)
end
def add_entry(todo_list, entry) do
entry = Map.put(entry, :id, todo_list.auto_id)
new_entries =
Map.put(
todo_list.entries,
todo_list.auto_id,
entry
)
%Todo.List{todo_list | entries: new_entries, auto_id: todo_list.auto_id + 1}
end
def entries(todo_list, date) do
todo_list.entries
|> Stream.filter(fn {_, entry} -> entry.date == date end)
|> Enum.map(fn {_, entry} -> entry end)
end
def update_entry(todo_list, %{} = new_entry) do
update_entry(todo_list, new_entry.id, fn _ -> new_entry end)
end
def update_entry(todo_list, entry_id, updater_fun) do
case Map.fetch(todo_list.entries, entry_id) do
:error ->
todo_list
{:ok, old_entry} ->
old_entry_id = old_entry.id
# Make sure that the result of the updater is a map and the
# id remains unchanged.
new_entry = %{id: ^old_entry_id} = updater_fun.(old_entry)
new_entries = Map.put(todo_list.entries, new_entry.id, new_entry)
%Todo.List{todo_list | entries: new_entries}
end
end
def delete_entry(todo_list, entry_id) do
%Todo.List{todo_list | entries: Map.delete(todo_list.entries, entry_id)}
end
end

View file

@ -0,0 +1,48 @@
defmodule Todo.Server do
use GenServer
def start do
GenServer.start(__MODULE__, nil)
end
def add_entry(pid, entry) do
GenServer.cast(pid, {:add_entry, entry})
end
def delete_entry(pid, entry_id) do
GenServer.cast(pid, {:delete_entry, entry_id})
end
def update_entry(pid, entry_id, updater_fun) do
GenServer.cast(pid, {:update_entry, entry_id, updater_fun})
end
def entries(pid, date) do
GenServer.call(pid, {:entries, date})
end
@impl true
def init(_) do
{:ok, Todo.List.new()}
end
@impl true
def handle_cast({:add_entry, entry}, state) do
{:noreply, Todo.List.add_entry(state, entry)}
end
@impl true
def handle_cast({:delete_entry, entry_id}, state) do
{:noreply, Todo.List.delete_entry(state, entry_id)}
end
@impl true
def handle_cast({:update_entry, entry_id, updater_fun}, state) do
{:noreply, Todo.List.update_entry(state, entry_id, updater_fun)}
end
@impl true
def handle_call({:entries, date}, _, state) do
{:reply, Todo.List.entries(state, date), state}
end
end

View file

@ -0,0 +1,28 @@
defmodule Todo.MixProject do
use Mix.Project
def project do
[
app: :todo,
version: "0.1.0",
elixir: "~> 1.10",
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

View file

@ -0,0 +1 @@
ExUnit.start()

View file

@ -0,0 +1,21 @@
defmodule TodoCacheTest do
use ExUnit.Case
test "server_process" do
{:ok, cache} = Todo.Cache.start()
bob_pid = Todo.Cache.server_process(cache, "bob")
assert bob_pid != Todo.Cache.server_process(cache, "alice")
assert bob_pid == Todo.Cache.server_process(cache, "bob")
end
test "to-do operations" do
{:ok, cache} = Todo.Cache.start()
alice = Todo.Cache.server_process(cache, "alice")
Todo.Server.add_entry(alice, %{date: ~D[2020-12-12], title: "Hello"})
entries = Todo.Server.entries(alice, ~D[2020-12-12])
assert [%{date: ~D[2020-12-12], title: "Hello"}] = entries
end
end

View file

@ -0,0 +1,64 @@
defmodule TodoListTest do
use ExUnit.Case, async: true
test "empty list" do
assert Todo.List.size(Todo.List.new()) == 0
end
test "entries" do
todo_list =
Todo.List.new([
%{date: ~D[2018-12-19], title: "Dentist"},
%{date: ~D[2018-12-20], title: "Shopping"},
%{date: ~D[2018-12-19], title: "Movies"}
])
assert Todo.List.size(todo_list) == 3
assert todo_list |> Todo.List.entries(~D[2018-12-19]) |> length() == 2
assert todo_list |> Todo.List.entries(~D[2018-12-20]) |> length() == 1
assert todo_list |> Todo.List.entries(~D[2018-12-21]) |> length() == 0
titles = todo_list |> Todo.List.entries(~D[2018-12-19]) |> Enum.map(& &1.title)
assert ["Dentist", "Movies"] = titles
end
test "add_entry" do
todo_list =
Todo.List.new()
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Dentist"})
|> Todo.List.add_entry(%{date: ~D[2018-12-20], title: "Shopping"})
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Movies"})
assert Todo.List.size(todo_list) == 3
assert todo_list |> Todo.List.entries(~D[2018-12-19]) |> length() == 2
assert todo_list |> Todo.List.entries(~D[2018-12-20]) |> length() == 1
assert todo_list |> Todo.List.entries(~D[2018-12-21]) |> length() == 0
titles = todo_list |> Todo.List.entries(~D[2018-12-19]) |> Enum.map(& &1.title)
assert ["Dentist", "Movies"] = titles
end
test "update_entry" do
todo_list =
Todo.List.new()
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Dentist"})
|> Todo.List.add_entry(%{date: ~D[2018-12-20], title: "Shopping"})
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Movies"})
|> Todo.List.update_entry(2, &Map.put(&1, :title, "Updated shopping"))
assert Todo.List.size(todo_list) == 3
assert [%{title: "Updated shopping"}] = Todo.List.entries(todo_list, ~D[2018-12-20])
end
test "delete_entry" do
todo_list =
Todo.List.new()
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Dentist"})
|> Todo.List.add_entry(%{date: ~D[2018-12-20], title: "Shopping"})
|> Todo.List.add_entry(%{date: ~D[2018-12-19], title: "Movies"})
|> Todo.List.delete_entry(2)
assert Todo.List.size(todo_list) == 2
assert Todo.List.entries(todo_list, ~D[2018-12-20]) == []
end
end