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