aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkballou <kballou@devnulllabs.io>2016-11-10 14:48:34 -0700
committerkballou <kballou@devnulllabs.io>2016-11-14 11:19:49 -0700
commit011abb47be01bce1bce599e0c265c079fa55fe86 (patch)
tree44b946164673693c84c6bc12ea71d4a5b4dc7a8c
parent1f6773180528f1f7eb9aec881d4b911964558e50 (diff)
downloadzendex-011abb47be01bce1bce599e0c265c079fa55fe86.tar.gz
zendex-011abb47be01bce1bce599e0c265c079fa55fe86.tar.xz
adding zendex httpoison.base client
This change is largely inspired by the client module of [tentacat][1]. This also adds the ability for Zendex modules (and later, users of Zendex) to pass a `:pagination` parameter to automatically pull all pages of results. [1]: https://github.com/edgurgel/tentacat/blob/master/lib/tentacat.ex
-rw-r--r--lib/zendex.ex224
-rw-r--r--lib/zendex/connection.ex2
-rw-r--r--test/zendex_test.exs49
3 files changed, 275 insertions, 0 deletions
diff --git a/lib/zendex.ex b/lib/zendex.ex
new file mode 100644
index 0000000..2fbde39
--- /dev/null
+++ b/lib/zendex.ex
@@ -0,0 +1,224 @@
+defmodule Zendex do
+ @moduledoc """
+ HTTPoison Zendesk Client
+ """
+ use HTTPoison.Base
+ alias Zendex.Connection
+ alias HTTPoison.Response
+
+ @user_agent [{"user-agent", "zendex"}]
+
+ @type response :: {integer, any} | map
+
+ @spec process_response_body(binary) :: term
+ def process_response_body(""), do: nil
+ def process_response_body(body), do: Poison.decode!(body)
+
+ @spec process_response(HTTPoison.Response.t) :: response
+ def process_response(%Response{status_code: 200, body: body}) do
+ body
+ end
+ def process_response(%Response{status_code: status_code, body: body}) do
+ {status_code, body}
+ end
+
+ @spec delete!(binary, Zendex.Connection.t, binary) :: any | {integer, any}
+ def delete!(path, connection, body \\ "") do
+ _request(:delete, url(connection, path), connection.authentication, body)
+ end
+
+ @spec post(binary, Zendex.Connection.t, binary) :: any | {integer, any}
+ def post(path, connection, body \\ "") do
+ _request(:post, url(connection, path), connection.authentication, body)
+ end
+
+ @spec post!(binary, Zendex.Connection.t, binary) :: any | {integer, any}
+ def post!(path, connection, body \\ "") do
+ {201, resp} = post(path, connection, body)
+ resp
+ end
+
+ def patch(path, connection, body \\ "") do
+ _request(:patch, url(connection, path), connection.authentication, body)
+ end
+
+ def patch!(path, connection, body \\ "") do
+ {_, resp} = patch(path, connection, body)
+ resp
+ end
+
+ @doc """
+ Underlying utility retrieval function
+
+ The options passed affect both the return value and, ultimately, the number
+ of requests made to Zendesk.
+
+ Options:
+ * `pagination` - can be `:none`, or `:auto`. Defaults to `:none`.
+ """
+ @spec get!(binary, Zendex.Connection.t, Keyword.t, Keyword.t) :: term
+ def get!(path, connection, params \\ [], options \\ []) do
+ url =
+ connection
+ |> url(path)
+ |> add_params_to_url(params)
+
+ {auth, _} = Map.split(connection, [:authentication])
+
+ case pagination(options) do
+ nil -> request_stream(:get, url, auth, "", :one_page)
+ :none -> request_stream(:get, url, auth, "", :one_page)
+ :auto ->
+ :get
+ |> request_stream(url, auth)
+ |> realize_if_needed
+ end
+ end
+
+ def _request(method, url, auth, body \\ "") do
+ json_request(method, url, body, authorization_header(auth, @user_agent))
+ end
+
+ def json_request(method, url, body \\ "", headers \\ [], options \\ []) do
+ raw_request(method, url, Poison.encode!(body), headers, options)
+ end
+
+ defp pagination(options) do
+ options
+ |> Keyword.get(:pagination)
+ |> case do
+ nil -> Application.get_env(:zendex, :pagination, nil)
+ x -> x end
+ end
+
+ def raw_request(method, url, body \\ "", headers \\ [], options \\ []) do
+ method
+ |> request!(url, body, headers, options)
+ |> process_response
+ end
+
+ def request_stream(method, url, auth, body \\ "", override \\ nil) do
+ method
+ |> request_with_pagination(url, auth, Poison.encode!(body))
+ |> stream_if_needed(override)
+ end
+ defp stream_if_needed(result = {status_code, _}, _)
+ when is_number(status_code), do: result
+ defp stream_if_needed({body, nil, _}, _), do: body
+ defp stream_if_needed({body, _, _}, :one_page), do: body
+ defp stream_if_needed(initial_results, _) do
+ Stream.resource(
+ fn -> initial_results end,
+ &process_stream/1,
+ fn _ -> nil end)
+ end
+
+ defp realize_if_needed(x)
+ when is_tuple(x) or is_binary(x) or is_list(x) or is_map(x), do: x
+ defp realize_if_needed(stream), do: Enum.to_list(stream)
+
+ defp process_stream({[], nil, _}), do: {:halt, nil}
+ defp process_stream({[], next, auth}) do
+ :get
+ |> request_with_pagination(next, auth, "")
+ |> process_stream
+ end
+ defp process_stream({items, next, auth}) when is_list(items) do
+ {items, {[], next, auth}}
+ end
+ defp process_stream({item, next, auth}) do
+ {[item], {[], next, auth}}
+ end
+
+ @spec request_with_pagination(atom, binary, Connection.auth, binary) ::
+ {binary, binary, Zendex.Connection.auth}
+ def request_with_pagination(method, url, auth, body \\ "") do
+ resp = request!(method,
+ url,
+ Poison.encode!(body),
+ authorization_header(auth, @user_agent),
+ [])
+ case process_response(resp) do
+ x when is_tuple(x) -> x
+ _ -> pagination_tuple(resp, auth)
+ end
+ end
+
+ @spec pagination_tuple(HTTPoison.Response.t, Connection.auth) ::
+ {binary, binary, Connection.auth}
+ defp pagination_tuple(%Response{body: body} = resp, auth) do
+ {process_response(resp), next_link(body), auth}
+ end
+
+ defp next_link(%{"next_page" => next}), do: next
+ defp next_link(_), do: nil
+
+ defp url(_client = %{base_url: base_url}, path = "/" <> _) do
+ base_url <> path
+ end
+
+ @doc """
+ Take an existing URI and add addition parameters, merging as necessary
+
+ ## Examples
+ iex> add_params_to_url("http://example.com/wat", [])
+ "http://example.com/wat"
+ iex> add_params_to_url("http://example.com/wat", [q: 1])
+ "http://example.com/wat?q=1"
+ iex> add_params_to_url("http://example.com/wat", [q: 1, t: 2])
+ "http://example.com/wat?q=1&t=2"
+ iex> add_params_to_url("http://example.com/wat", %{q: 1, t: 2})
+ "http://example.com/wat?q=1&t=2"
+ iex> add_params_to_url("http://example.com/wat?q=1&t=2", [])
+ "http://example.com/wat?q=1&t=2"
+ iex> add_params_to_url("http://example.com/wat?q=1", [t: 2])
+ "http://example.com/wat?q=1&t=2"
+ iex> add_params_to_url("http://example.com/wat?q=1", [q: 3, t: 2])
+ "http://example.com/wat?q=3&t=2"
+ iex> add_params_to_url("http://example.com/wat?q=1&s=4", [q: 3, t: 2])
+ "http://example.com/wat?q=3&s=4&t=2"
+ iex> add_params_to_url("http://example.com/wat?q=1&s=4", %{q: 3, t: 2})
+ "http://example.com/wat?q=3&s=4&t=2"
+
+ """
+ @spec add_params_to_url(binary, list) :: binary
+ def add_params_to_url(url, params) do
+ url
+ |> URI.parse
+ |> merge_uri_params(params)
+ |> to_string
+ end
+
+ @spec merge_uri_params(URI.t, list) :: URI.t
+ defp merge_uri_params(uri, []), do: uri
+ defp merge_uri_params(%URI{query: nil} = uri, params)
+ when is_list(params) or is_map(params) do
+ uri
+ |> Map.put(:query, URI.encode_query(params))
+ end
+ defp merge_uri_params(%URI{} = uri, params)
+ when is_list(params) or is_map(params) do
+ uri
+ |> Map.update!(:query, fn q ->
+ q
+ |> URI.decode_query
+ |> Map.merge(param_list_to_map_with_string_keys(params))
+ |> URI.encode_query
+ end)
+ end
+
+ @spec param_list_to_map_with_string_keys(list) :: map
+ defp param_list_to_map_with_string_keys(list)
+ when is_list(list) or is_map(list) do
+ for {key, value} <- list, into: Map.new do
+ {"#{key}", value}
+ end
+ end
+
+ @spec authorization_header(Connection.auth, list) :: list
+ def authorization_header(%{authentication: authentication}, headers) do
+ headers ++ [{"Authorization", "Basic #{authentication}"}]
+ end
+ def authorization_header(_, headers), do: headers
+
+end
diff --git a/lib/zendex/connection.ex b/lib/zendex/connection.ex
index 5399f29..a001976 100644
--- a/lib/zendex/connection.ex
+++ b/lib/zendex/connection.ex
@@ -4,6 +4,8 @@ defmodule Zendex.Connection do
username and password.
"""
+ @typedoc "The Authorization Header"
+ @type auth :: %{authentication: binary}
@typedoc "The connection paramters"
@type t :: %{base_url: String.t, authentication: binary}
diff --git a/test/zendex_test.exs b/test/zendex_test.exs
new file mode 100644
index 0000000..7dfaa80
--- /dev/null
+++ b/test/zendex_test.exs
@@ -0,0 +1,49 @@
+defmodule ZendexTest do
+ use ExUnit.Case
+ import Zendex
+
+ doctest Zendex
+
+ setup_all do
+ :meck.new(Poison, [:no_link])
+
+ on_exit fn ->
+ :meck.unload Poison
+ end
+ end
+
+ test "authorization_header using user and password" do
+ auth = %{authentication: "dXNlcjpwYXNzd29yZA=="}
+ expected = [{"Authorization", "Basic dXNlcjpwYXNzd29yZA=="}]
+ assert authorization_header(auth, []) == expected
+ end
+
+ test "process response on a 200 response" do
+ assert process_response(%HTTPoison.Response{status_code: 200,
+ headers: %{},
+ body: "json" }) == "json"
+ assert :meck.validate(Poison)
+ end
+
+ test "process response on a non-200 response" do
+ assert process_response(%HTTPoison.Response{status_code: 404,
+ headers: %{},
+ body: "json" }) == {404, "json"}
+ assert :meck.validate(Poison)
+ end
+
+ test "process_response_body with an empty body" do
+ assert process_response_body("") == nil
+ end
+
+ test "process_response_body with content" do
+ :meck.expect(Poison, :decode!, 1, :decoded_json)
+ assert process_response_body("json") == :decoded_json
+ end
+
+ test "process response on a non-200 response and empty body" do
+ assert process_response(%HTTPoison.Response{status_code: 404,
+ headers: %{},
+ body: nil }) == {404, nil}
+ end
+end