farmbot_os/lib/farmbot/database/syncable.ex

160 lines
4.8 KiB
Elixir

defmodule Farmbot.Database.Syncable do
@moduledoc """
Glue between HTTP and Database.
"""
@enforce_keys [:ref_id, :body]
defstruct [:ref_id, :body]
@typedoc """
Module structs.
"""
@type body :: map
@type ref_id :: Farmbot.Database.ref_id
@type t :: %__MODULE__{ref_id: ref_id, body: body}
import Farmbot.HTTP.Helpers
alias __MODULE__.Error
@doc """
Pipe a HTTP request thru this. Trust me :tm:
"""
def parse_resp({:error, message}, _module), do: {:error, message}
def parse_resp({:ok, %{status_code: code, body: resp_body}}, module)
when is_2xx(code) do
parsed = resp_body |> String.trim() |> Poison.decode |> handle_json_output(resp_body)
cond do
is_list(parsed) -> Enum.map(parsed, fn(i) -> module.to_struct(i) end)
is_map(parsed) -> module.to_struct(parsed)
true -> raise Error,
message: "Don't know how to handle: #{inspect parsed}"
end
end
def parse_resp({:ok, bad_response}, _module), do: {:error, bad_response}
defp handle_json_output(res, resp_body) do
case res do
{:ok, body} -> body
{:error, :invalid, _pos} ->
spawn(fn() ->
IO.puts "INVALID JSON \r\n\r\n #{inspect resp_body} \r\n\r\n"
end)
raise Error, message: "Bad json"
end
end
@doc ~s"""
Builds common functionality for all Syncable Resources. `args` takes two keywords.
* `model` - a definition of the struct.
* `endpoint` - a tuple shaped like: {"/single_url", "/plural_url"}
Heres what it _WILL_ provide:
* For HTTP access we have:
* `sindular_url/0` - The single url endpoint
* `plural_url/0` - The plural url endpoint
* `fetch/1` and `fetch/2`
* `fetch/1` - takes a callback of either an anon function, or a tuple
shaped: {module, function, args} where the first arg is the result
described below.
* `fetch/2` - takes an id and a callback described above.
* For Data manipulation
* `to_struct/1` - takes a stringed map and _safely_ turns it into a stuct.
Heres what it _WILL NOT_ provide:
* local database access
* extensibility
"""
defmacro __using__(args) do
model = Keyword.get(args, :model) || raise "You need a model!"
{singular, plural} = Keyword.get(args, :endpoint) || raise Error,
message: "Syncable requires a endpoint: {singular_url, plural_url}"
quote do
alias Farmbot.HTTP
import Farmbot.Database.Syncable, only: [parse_resp: 2]
defstruct unquote(model) ++ [:id]
defimpl Inspect, for: __MODULE__ do
def inspect(syncable, _) do
"#Syncable<#{List.last(Module.split(__MODULE__))} #{syncable.id}>"
end
end
@doc "Find an item by id from the database."
def get_by_id(record_storage, id) do
Farmbot.Database.RecordStorage.get_by_id(record_storage, __MODULE__, id)
end
@doc "Get all items."
def get_all(record_storage) do
Farmbot.Database.RecordStorage.get_all(record_storage, __MODULE__)
end
@doc "Flush all items."
def flush(record_storage) do
Farmbot.Database.RecordStorage.flush(record_storage, __MODULE__)
end
@doc """
The Singular api endpoing url.
"""
def singular_url, do: unquote(singular)
@doc """
The plural api endpoint.
"""
def plural_url, do: unquote(plural)
@doc """
Fetches all `#{__MODULE__}` objects from the API.
"""
def fetch(http, then) do
url = "/api" <> plural_url()
result = http |> HTTP.get(url) |> parse_resp(__MODULE__)
if function_exported?(__MODULE__, :on_fetch, 2) do
apply __MODULE__, :on_fetch, [result]
end
case then do
{module, fun, args} -> apply(module, fun, [result | args])
anon when is_function(anon) -> anon.(result)
end
end
@doc """
Fetches a specific `#{__MODULE__}` from the API, by it's id.
"""
def fetch(http, id, then) do
url = "/api" <> unquote(singular) <> "/#{id}"
result = http |> HTTP.get(url) |> parse_resp(__MODULE__)
if function_exported?(__MODULE__, :on_fetch, 2) do
apply __MODULE__, :on_fetch, [result]
end
case then do
{module, fun, args} -> apply(module, fun, [result | args])
anon when is_function(anon) -> anon.(result)
end
end
@doc """
Changes a string map, to a struct
"""
def to_struct(item) do
module = __MODULE__
sym_keys = Map.keys(%__MODULE__{})
str_keys = Enum.map(sym_keys, fn(key) -> Atom.to_string(key) end)
next = Map.take(item, str_keys)
new = Map.new(next, fn({key, val}) -> {String.to_atom(key), val} end)
struct(module, new)
end
end
end
end