Last active
October 6, 2017 13:38
-
-
Save schmalz/d8acd3ba0f1c8ceacf2a39a8fa325e97 to your computer and use it in GitHub Desktop.
Elixir in Action - Chapter 7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Todo.Cache do | |
use GenServer | |
@moduledoc """ | |
A todo-list cache; maps todo-list names to their corresponding server PIDs. | |
""" | |
# Client API | |
@doc """ | |
Start a cache. | |
""" | |
def start(), do: GenServer.start(Todo.Cache, nil) | |
@doc """ | |
Retrieve the todo server PID associated with `name`, creating the server if necessary. | |
""" | |
def server_process(cache_pid, name), do: GenServer.call(cache_pid, {:server_process, name}) | |
# Server Callbacks | |
def init(_) do | |
Todo.Database.start("./persist") | |
{:ok, %{}} | |
end | |
def handle_call({:server_process, name}, _, servers) do | |
case Map.fetch(servers, name) do | |
{:ok, server} -> {:reply, server, servers} | |
:error -> | |
{:ok, server} = Todo.Server.start(name) | |
{:reply, server, Map.put(servers, name, server)} | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Todo.Database do | |
use GenServer | |
@moduledoc """ | |
Persistence for todo-lists. | |
""" | |
@server_name :database_server | |
@worker_count 3 | |
# Client API | |
@doc """ | |
Start a persistence server, storing its data in `db_folder_path`. | |
""" | |
def start(db_folder_path), do: GenServer.start(__MODULE__, db_folder_path, name: @server_name) | |
@doc """ | |
Store `data` under `key`. | |
""" | |
def store(key, data), do: GenServer.cast(@server_name, {:store, key, data}) | |
@doc """ | |
Retrieve the data stored under `key`. | |
""" | |
def get(key), do: GenServer.call(@server_name, {:get, key}) | |
# Server Callbacks | |
def init(db_folder_path) do | |
File.mkdir_p(db_folder_path) | |
workers = | |
0..(@worker_count - 1) | |
|> Stream.map(fn(i) -> | |
{:ok, worker_pid} = Todo.DatabaseWorker.start(db_folder_path) | |
{i, worker_pid} | |
end) | |
|> Enum.into(%{}) | |
{:ok, workers} | |
end | |
def handle_cast({:store, key, data}, workers) do | |
Todo.DatabaseWorker.store(worker_for_key(workers, key), key, data) | |
{:noreply, workers} | |
end | |
def handle_call({:get, key}, _from, workers) do | |
data = Todo.DatabaseWorker.get(worker_for_key(workers, key), key) | |
{:reply, data, workers} | |
end | |
defp worker_for_key(workers, key), do: workers[:erlang.phash2(key, @worker_count)] | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Todo.DatabaseWorker do | |
use GenServer | |
@moduledoc """ | |
Worker for the todo-list persistence database server. | |
""" | |
# Client API | |
@doc """ | |
Start a worker, storing its data in `db_folder_path`. | |
""" | |
def start(db_folder_path), do: GenServer.start(__MODULE__, db_folder_path) | |
@doc """ | |
Store `data` under `key`. | |
""" | |
def store(worker_pid, key, data), do: GenServer.cast(worker_pid, {:store, key, data}) | |
@doc """ | |
Retrieve the data stored under `key`. | |
""" | |
def get(worker_pid, key), do: GenServer.call(worker_pid, {:get, key}) | |
# Server Callbacks | |
def init(db_folder_path) do | |
{:ok, db_folder_path} | |
end | |
def handle_cast({:store, key, data}, db_folder_path) do | |
file_name(db_folder_path, key) | |
|> File.write!(:erlang.term_to_binary(data)) | |
{:noreply, db_folder_path} | |
end | |
def handle_call({:get, key}, _from, db_folder_path) do | |
data = | |
case File.read(file_name(db_folder_path, key)) do | |
{:ok, contents} -> :erlang.binary_to_term(contents) | |
_ -> nil | |
end | |
{:reply, data, db_folder_path} | |
end | |
defp file_name(db_folder_path, key), do: "#{db_folder_path}/#{key}" | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{entries: entries, auto_id: auto_id} = todo_list, entry) do | |
entry = Map.put(entry, :id, auto_id) | |
new_entries = Map.put(entries, auto_id, entry) | |
%Todo.List{todo_list | entries: new_entries, auto_id: auto_id + 1 } | |
end | |
def entries(%Todo.List{entries: entries}, date) do | |
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{entries: entries} = todo_list, entry_id, updater_fun) do | |
case entries[entry_id] do | |
nil -> todo_list | |
old_entry -> | |
new_entry = updater_fun.(old_entry) | |
new_entries = Map.put(entries, new_entry.id, new_entry) | |
%Todo.List{todo_list | entries: new_entries} | |
end | |
end | |
def delete_entry( %Todo.List{entries: entries} = todo_list, entry_id) do | |
%Todo.List{todo_list | entries: Map.delete(entries, entry_id)} | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Todo.Server do | |
use GenServer | |
# Client API | |
def start(name) do | |
GenServer.start(Todo.Server, name) | |
end | |
def add_entry(todo_server, new_entry) do | |
GenServer.cast(todo_server, {:add_entry, new_entry}) | |
end | |
def entries(todo_server, date) do | |
GenServer.call(todo_server, {:entries, date}) | |
end | |
# Server Callbacks | |
def init(name) do | |
send(self(), {:init, name}) | |
{:ok, nil} | |
end | |
def handle_cast({:add_entry, new_entry}, {name, todo_list}) do | |
new_state = Todo.List.add_entry(todo_list, new_entry) | |
Todo.Database.store(name, new_state) | |
{:noreply, {name, new_state}} | |
end | |
def handle_call({:entries, date}, _from, {name, todo_list}) do | |
{:reply, Todo.List.entries(todo_list, date), {name, todo_list}} | |
end | |
# Needed for testing purposes | |
def handle_info({:init, name}, _state) do | |
{:noreply, {name, Todo.Database.get(name) || Todo.List.new()}} | |
end | |
def handle_info(:stop, state), do: {:stop, :normal, state} | |
def handle_info(_, state), do: {:noreply, state} | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment