defmodule Mpx do @moduledoc """ Wrapper for the (Ministry Platform)[https://www.thinkministry.com/ministryplatform/] api. For a complete reference to the MP API browse to `https://yourhost/ministryplatformapi/swagger` """ @dialyzer {:nowarn_function, from_config: 2} @dialyzer {:nowarn_function, authenticate: 0} @endpoint Application.get_env(:mpx, :mp_base_url, "") @username Application.get_env(:mpx, :mp_username) @password Application.get_env(:mpx, :mp_password) @client_id Application.get_env(:mpx, :mp_client_id) @client_secret Application.get_env(:mpx, :mp_client_secret) @type authentication_opts :: [username: String.t, password: String.t, client_id: String.t, client_secret: String.t] @type http :: {atom(), map() | list()} | {atom(), term()} require Logger @doc """ Authenticate with Ministry Platform. Takes a keyword list of options in the shape: ``` [ username: "mp-username", password: "mp-password", client_id: "mp-client-id", client_secret: "mp-client-secret" ] ``` If keyword options are not passed in, defaults to configuration parameters: ``` mpx, mp_username: "mp-username", mp_password: System.get_env("MP_PASSWORD"), mp_client_id: {:system, "MP_CLIENT_ID"}, mp_client_secret: "mp-client-secret" ``` Returns `{:ok, token}` where `token` can be used in subsequent calls to the API that need to be authenticated or `{:error, reason}` """ @spec authenticate(authentication_opts | nil) :: {:ok, String.t} | {:error, String.t} def authenticate(opts \\ []) do Logger.debug("trying to authenticate") response = HTTPoison.post( "#{from_config(:mp_base_url, @endpoint)}/ministryplatform/oauth/token", get_auth_body(opts), %{"Content-type" => "application/x-www-form-urlencoded"} ) case response do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> with {:ok, token} <- Poison.decode(body) do {:ok, Map.get(token, "access_token")} else {:error, {:invalid, line, char}} -> {:error, "unable to decode token from " <> body <> " because of " <> char <> " at line " <> line} {:error, err} -> {:error, "unable to decode token from " <> body <> " because of " <> err} end {:ok, %HTTPoison.Response{status_code: code, body: body}} -> Logger.debug(inspect(body)) {:error, body} {:error, %HTTPoison.Error{reason: reason}} -> Logger.debug(inspect(reason)) {:error, reason} _ -> {:error, :unknown} end end @doc """ Wrapper for the Ministry Platform GET Api call. Takes optional keyword list of options: `[ "$select": "The_columns_to_select", "$filter": "The filter to apply", etc... ]` See swagger for a complete list of parameters that the MP rest api takes. Returns an `HTTPoison.Response` or `HTTPoison.Error` """ @spec get(binary(), binary(), keyword(binary)) :: http def get(path, auth_token, opts \\ []) do HTTPoison.get( "#{from_config(:mp_base_url, @endpoint)}/ministryplatformapi/#{path}#{stringify_url_options(opts)}", %{"Authorization" => "Bearer #{auth_token}"}) |> map_response end @doc """ Wrapper for the Ministry Platform POST Api call Takes optional keyword list of options: `[ "$select": "The_columns_to_select", "$filter": "The filter to apply", etc... ]` see swagger for a complete list of parameters that the MP rest api takes. Returns an `HTTPoison.Response` or `HTTPoison.Error` """ @spec post(binary(), binary(), map() | list(), keyword(binary())) :: http def post(path, auth_token, data, opts \\ []) do case Poison.encode(data) do {:ok, encoded_data} -> HTTPoison.post( "#{from_config(:mp_base_url, @endpoint)}/ministryplatformapi/#{path}#{stringify_url_options(opts)}", encoded_data, %{"Authorization" => "Bearer #{auth_token}"}) |> map_response err -> err end end @doc """ Wrapper for the Ministry Platform PUT Api call Takes optional keyword list of options: `[ "$select": "The_columns_to_select", "$filter": "The filter to apply", etc... ]` see the swagger documentation for a complete list of parameters that the MP rest api takes. Returns an `HTTPoison.Response` or `HTTPoison.Error` """ @spec put(binary(), binary(), map() | list(), keyword(binary())) :: http def put(path, auth_token, data, opts \\ []) do case Poison.encode(data) do {:ok, encoded_data} -> HTTPoison.put( "#{from_config(:mp_base_url, @endpoint)}/ministryplatformapi/#{path}#{stringify_url_options(opts)}", encoded_data, %{"Authorization" => "Bearer #{auth_token}"}) |> map_response err -> err end end @doc """ Wrapper for the Ministry Platform DELETE Api call Takes optional keyword list of options: `[ "$select": "The_columns_to_select", "$filter": "The filter to apply", etc... ]` see swagger for a complete list of parameters that the MP rest api takes Returns an `HTTPoison.Response` or `HTTPoison.Error` """ @spec delete(binary(), binary(), Mpx.Tables.delete_options) :: http def delete(path, auth_token, opts \\ []) do HTTPoison.delete( "#{from_config(:mp_base_url, @endpoint)}/ministryplatformapi/#{path}#{stringify_url_options(opts)}", %{"Authorization" => "Bearer #{auth_token}"}) |> map_response end @doc false @spec stringify_url_options(keyword() | []) :: binary() defp stringify_url_options([]), do: "" defp stringify_url_options(opts) do options = opts |> unique_keys |> Enum.reduce([], fn(key, acc) -> vals = opts |> Keyword.get_values(key) |> Enum.map(fn(x) -> "#{key}=#{x}" end) acc ++ vals end) |> Enum.join("&") "?#{options}" end @spec unique_keys(Keyword.t) :: list() defp unique_keys(keyword_list) do keyword_list |> Enum.uniq_by(fn {k, _} -> k end) |> Keyword.keys end @doc false @spec map_response({:ok, HTTPoison.Response.t} | {:error, HTTPoison.Error}) :: {:ok, map() | list()} | {:error, binary()} defp map_response({:ok, %HTTPoison.Response{body: body, status_code: 200}}), do: Poison.decode(body) defp map_response({:ok, %HTTPoison.Response{body: body, status_code: bad}}), do: {:error, Poison.decode!(body)} defp map_response({:error, %HTTPoison.Error{id: id, reason: reason}}), do: {:error, Poison.decode!(reason)} @doc false @spec get_auth_body(authentication_opts | nil) :: String.t defp get_auth_body(username: username, password: password, client_id: client_id, client_secret: client_secret) do ["username=#{URI.encode(username)}", "&password=#{password}", "&client_id=#{client_id}", "&client_secret=#{client_secret}", "&grant_type=password"] |> Enum.join end defp get_auth_body([]) do get_auth_body(username: from_config(:mp_username, @username), password: from_config(:mp_password, @password), client_id: from_config(:mp_client_id, @client_id), client_secret: from_config(:mp_client_secret, @client_secret)) end defp get_auth_body(opts), do: raise "Wrong arguments passed to authenticate. Please setup your configuration correctly or pass a keyword list in the with username, password, client_id and client_secret" @doc false @spec from_config(atom(), term() | nil) :: no_return | String.t defp from_config(config_param, nil), do: raise "#{config_param} is a required configuration parameter when calling authenticate() without parameters" defp from_config(_config_param, binary) when is_binary(binary), do: binary defp from_config(_config_param, integer) when is_integer(integer), do: Integer.to_string(integer) defp from_config(config_param, {:system, env_var}), do: from_config(config_param, System.get_env(env_var)) defp from_config(config_param, bad_val), do: from_config(config_param, nil) end