defmodule Couch do
@moduledoc """
Simple, no-frills, CouchDB client
"""
@type base_error :: {:error, :bad_request} | {:error, :unauthorized} |
{:error, :server_error} | {:error, :service_unavailable} |
{:error, :bad_response} | {:error, HTTPoison.Error.t()}
def new(db, params \\ []) do
{url, headers, options} = prepare_request([db], [], params)
case HTTPoison.put(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] ->
:ok
{:ok, %HTTPoison.Response{status_code: 412}} ->
{:error, :exists}
error ->
handle_generic_response(error)
end
end
@doc """
Retrieve a document `doc` from `db`.
`params` are [documented here](https://docs.couchdb.org/en/3.2.2-docs/api/document/common.html)
"""
@spec get(String.t(), String.t() | :all_docs, Keyword.t()) :: {:ok, Map.t()} | {:error, :not_found} | {:error, any()}
def get(db, doc, params \\ [])
def get(db, :all_docs, params), do: get(db, "_all_docs", params)
def get(db, doc, params) do
{url, headers, options} = prepare_request([db, doc], [], params)
case HTTPoison.get(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Poison.decode!(body)}
error -> handle_generic_response(error)
end
end
@spec post(String.t(), Map.t(), Keyword.t()) :: {:error, :operation_failed} | {:error, :not_found} | {:error, :exists} | {:error, any()}
def post(db, data, params \\ []) do
{url, headers, options} = prepare_request([db], [{"content-type", "application/json"}], params)
with \
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] <- HTTPoison.post(url, Poison.encode!(data), headers, options),
{:json, {:ok, %{"ok" => true, "id" => id, "rev" => rev}}} <- {:json, Poison.decode(body)} do
{:ok, id, rev}
else
{:ok, %HTTPoison.Response{status_code: 409}} -> {:error, :exists}
{:json, {:ok, body}} ->
Logger.error("couch: operation failed: #{inspect body}")
{:error, :operation_failed}
error -> handle_generic_response(error)
end
end
@spec put(String.t(), Map.t(), Keyword.t()) :: {:error, :operation_failed} | {:error, :not_found} | {:error, :conflict} | {:error, any()}
def put(db, doc = %{"_id" => id, "_rev" => _}, params \\ []) do
{url, headers, options} = prepare_request([db, id], [{"content-type", "application/json"}], params)
case HTTPoison.put(url, Poison.encode!(doc), headers, options) do
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] ->
body = Poison.decode!(body)
if Map.get(body, "ok") do
{:ok, Map.get(body, "id"), Map.get(body, "rev")}
else
{:error, :operation_failed}
end
{:ok, %HTTPoison.Response{status_code: 209}} ->
{:error, :conflict}
error -> handle_generic_response(error)
end
end
defp prepare_request(path, headers \\ [], params \\ [], options \\ []) do
config = Application.get_env(:nola, :couch)
base_url = Keyword.get(config, :url, "http://localhost:5984")
path = path
|> Enum.filter(& &1)
|> Enum.map(fn(url) -> String.replace("/", "%2F") end)
|> Path.join()
url = base_url
|> URI.merge(path)
|> to_string()
headers = headers ++ [{"accept", "application/json"}, {"user-agent", "#{Nola.brand(:name)} v#{Nola.version()}"}]
params = Enum.map(params, fn({k, v}) -> {to_string(k), v} end)
client_options = Keyword.get(config, :client_options, [])
options = [params: params] ++ options ++ client_options
user = Keyword.get(config, :user)
pass = Keyword.get(config, :pass)
hackney_options = Keyword.get(options, :hackney, [])
hackney_options = if user, do: [{:basic_auth, {user, pass}} | hackney_options], else: []
options = [{:hackney, [:insecure | hackney_options]} | options]
{url, headers, options}
end
defp handle_generic_response(%HTTPoison.Response{status_code: code}), do: {:error, Plug.Conn.Status.reason_atom(code)}
defp handle_generic_response(%HTTPoison.Error{reason: reason}), do: {:error, reason}
end