Update: Finished persistable todo server
This commit is contained in:
parent
09665c3b88
commit
55589a963f
80 changed files with 809 additions and 0 deletions
21
chapter7/persistable_todo_cache/README.md
Normal file
21
chapter7/persistable_todo_cache/README.md
Normal 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).
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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"}]}.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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"}]}.
|
||||||
34
chapter7/persistable_todo_cache/lib/todo/cache.ex
Normal file
34
chapter7/persistable_todo_cache/lib/todo/cache.ex
Normal 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
|
||||||
47
chapter7/persistable_todo_cache/lib/todo/database.ex
Normal file
47
chapter7/persistable_todo_cache/lib/todo/database.ex
Normal 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
|
||||||
57
chapter7/persistable_todo_cache/lib/todo/list.ex
Normal file
57
chapter7/persistable_todo_cache/lib/todo/list.ex
Normal 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
|
||||||
62
chapter7/persistable_todo_cache/lib/todo/server.ex
Normal file
62
chapter7/persistable_todo_cache/lib/todo/server.ex
Normal 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
|
||||||
28
chapter7/persistable_todo_cache/mix.exs
Normal file
28
chapter7/persistable_todo_cache/mix.exs
Normal 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
|
||||||
BIN
chapter7/persistable_todo_cache/persist/marianos list
Normal file
BIN
chapter7/persistable_todo_cache/persist/marianos list
Normal file
Binary file not shown.
1
chapter7/persistable_todo_cache/test/test_helper.exs
Normal file
1
chapter7/persistable_todo_cache/test/test_helper.exs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
||||||
21
chapter7/persistable_todo_cache/test/todo_cache_test.exs
Normal file
21
chapter7/persistable_todo_cache/test/todo_cache_test.exs
Normal 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
|
||||||
64
chapter7/persistable_todo_cache/test/todo_list_test.exs
Normal file
64
chapter7/persistable_todo_cache/test/todo_list_test.exs
Normal 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
|
||||||
4
chapter7/todo/.formatter.exs
Normal file
4
chapter7/todo/.formatter.exs
Normal 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
24
chapter7/todo/.gitignore
vendored
Normal 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
21
chapter7/todo/README.md
Normal 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).
|
||||||
|
|
||||||
47
chapter7/todo/lib/todo/list.ex
Normal file
47
chapter7/todo/lib/todo/list.ex
Normal 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
|
||||||
48
chapter7/todo/lib/todo/server.ex
Normal file
48
chapter7/todo/lib/todo/server.ex
Normal 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
28
chapter7/todo/mix.exs
Normal 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
|
||||||
1
chapter7/todo/test/test_helper.exs
Normal file
1
chapter7/todo/test/test_helper.exs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
||||||
21
chapter7/todo_cache/README.md
Normal file
21
chapter7/todo_cache/README.md
Normal 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).
|
||||||
|
|
||||||
BIN
chapter7/todo_cache/_build/dev/lib/todo/.mix/compile.elixir
Normal file
BIN
chapter7/todo_cache/_build/dev/lib/todo/.mix/compile.elixir
Normal file
Binary file not shown.
BIN
chapter7/todo_cache/_build/dev/lib/todo/.mix/compile.elixir_scm
Normal file
BIN
chapter7/todo_cache/_build/dev/lib/todo/.mix/compile.elixir_scm
Normal file
Binary file not shown.
BIN
chapter7/todo_cache/_build/dev/lib/todo/.mix/compile.protocols
Normal file
BIN
chapter7/todo_cache/_build/dev/lib/todo/.mix/compile.protocols
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7
chapter7/todo_cache/_build/dev/lib/todo/ebin/todo.app
Normal file
7
chapter7/todo_cache/_build/dev/lib/todo/ebin/todo.app
Normal 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"}]}.
|
||||||
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/.mix_test_failures
Normal file
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/.mix_test_failures
Normal file
Binary file not shown.
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/compile.elixir
Normal file
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/compile.elixir
Normal file
Binary file not shown.
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/compile.elixir_scm
Normal file
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/compile.elixir_scm
Normal file
Binary file not shown.
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/compile.protocols
Normal file
BIN
chapter7/todo_cache/_build/test/lib/todo/.mix/compile.protocols
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7
chapter7/todo_cache/_build/test/lib/todo/ebin/todo.app
Normal file
7
chapter7/todo_cache/_build/test/lib/todo/ebin/todo.app
Normal 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"}]}.
|
||||||
33
chapter7/todo_cache/lib/todo/cache.ex
Normal file
33
chapter7/todo_cache/lib/todo/cache.ex
Normal 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
|
||||||
57
chapter7/todo_cache/lib/todo/list.ex
Normal file
57
chapter7/todo_cache/lib/todo/list.ex
Normal 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
|
||||||
48
chapter7/todo_cache/lib/todo/server.ex
Normal file
48
chapter7/todo_cache/lib/todo/server.ex
Normal 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_cache/mix.exs
Normal file
28
chapter7/todo_cache/mix.exs
Normal 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
|
||||||
1
chapter7/todo_cache/test/test_helper.exs
Normal file
1
chapter7/todo_cache/test/test_helper.exs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
||||||
21
chapter7/todo_cache/test/todo_cache_test.exs
Normal file
21
chapter7/todo_cache/test/todo_cache_test.exs
Normal 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
|
||||||
64
chapter7/todo_cache/test/todo_list_test.exs
Normal file
64
chapter7/todo_cache/test/todo_list_test.exs
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue