summaryrefslogtreecommitdiff
path: root/lib/couch.ex
blob: e33c045c5ee221d36e4074fa1c18d5e18a57094c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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(:lsg, :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