Current section

Files

Jump to
fsdb lib server.ex
Raw

lib/server.ex

defmodule Fsdb.Server do
@moduledoc """
Server module.
"""
use GenServer
@rowre ~r/row\.[0-9]+/
@rowid "row.id"
##########################################
# Public API
##########################################
@doc """
Starts the GenServer.
`state` *must* contain a valid path in the `:path` key.
`opts` is optional and is passed verbatim to GenServer.
Returns `{:ok, pid}`.
## Example
```
Fsdb.Server.start_link([path: "./fsdb"], [name: Fsdb.Server])
```
"""
def start_link(state, opts \\ []) do
GenServer.start_link(__MODULE__, state, opts)
end
@doc """
Stops the GenServer.
Returns `:ok`.
"""
def stop(pid) do
GenServer.cast(pid, :stop)
end
##########################################
# GenServer Implementation
##########################################
def init(state) do
#path expansion should happend only once at config time
#to ensure it repeats even under current directory change
path = Keyword.fetch!(state, :path)
File.mkdir_p!(path)
{:ok, {path}}
end
def handle_cast(:stop, state={_path}) do
{:stop, :normal, state}
end
def handle_call({:create, table}, _from, state={path}) do
{:reply, create_(path, table), state}
end
def handle_call({:drop, table}, _from, state={path}) do
{:reply, drop_(path, table), state}
end
def handle_call({:list, table}, _from, state={path}) do
{:reply, list_(path, table), state}
end
def handle_call({:insert, table, row}, _from, state={path}) do
{:reply, insert_(path, table, row), state}
end
def handle_call({:save, table, id, row}, _from, state={path}) do
{:reply, save_(path, table, id, row), state}
end
def handle_call({:fetch, table, id}, _from, state={path}) do
{:reply, fetch_(path, table, id), state}
end
def handle_call({:update, table, id, row}, _from, state={path}) do
{:reply, update_(path, table, id, row), state}
end
def handle_call({:delete, table, id}, _from, state={path}) do
{:reply, delete_(path, table, id), state}
end
##########################################
# Internal Implementation
##########################################
defp create_(path, table) do
folder = Path.join(path, table)
if !File.exists?(folder) do
File.mkdir_p!(folder)
file = Path.join(folder, @rowid)
:ok = dump_(file, 0)
end
:ok
end
defp drop_(path, table) do
folder = Path.join(path, table)
File.rm_rf!(folder)
:ok #rm returns list of deleted files
end
defp list_(path, table) do
folder = Path.join(path, table)
for file <- File.ls!(folder),
Regex.match?(@rowre, file) do
parse_(Path.join(folder, file))
end
end
defp parse_(file) do
txt = File.read!(file) |> :erlang.binary_to_list
{:ok, tokens, _} = :erl_scan.string(txt)
{:ok, row} = :erl_parse.parse_term(tokens)
row
end
defp insert_(path, table, row) do
folder = Path.join(path, table)
id = next_(folder)
file = Path.join(folder, rowid(id))
:ok = dump_(file, {id, row})
{id, row}
end
defp save_(path, table, id, row) do
folder = Path.join(path, table)
file = Path.join(folder, rowid(id))
:ok = dump_(file, {id, row})
{id, row}
end
defp next_(folder) do
:ok = File.mkdir_p(folder)
file = Path.join(folder, @rowid)
id = parse_(file) + 1
:ok = dump_(file, id)
id
end
defp dump_(file, row) do
File.write! file, :io_lib.format("~p.", [row])
end
defp fetch_(path, table, id) do
folder = Path.join(path, table)
file = Path.join(folder, rowid(id))
if File.exists?(file) do
parse_(file)
else
{:not_found, table, id}
end
end
defp rowid(id) do
fid = Integer.to_string(id) |> String.rjust(6, ?0)
"row.#{fid}"
end
defp update_(path, table, id, row) do
folder = Path.join(path, table)
file = Path.join(folder, rowid(id))
if File.exists?(file) do
:ok = dump_(file, {id, row})
{id, row}
else
{:not_found, table, id}
end
end
defp delete_(path, table, id) do
folder = Path.join(path, table)
file = Path.join(folder, rowid(id))
if File.exists?(file) do
{id, row} = parse_(file)
File.rm!(file)
{id, row}
else
{:not_found, table, id}
end
end
end