defmodule ExCurl.Client do
@moduledoc """
A helper to create HTTP Clients based on ExCurl that can share common options.
## Example HTTP Client
You can `use ExCurl.Client` on any module to provide HTTP Client functionality and share default options. Below is an example based on the great
[Tesla documentation](https://hexdocs.pm/tesla/readme.html):
```elixir
defmodule GitHubClient do
use ExCurl.Client, defaults: [base_url: "https://api.github.com", headers: %{"authentication" => "Bearer some-secret-token"}]
def user_repos(username), do: get("/users/\#{username}/repos")
end
```
We can then use this client with the shared defaults and custom functions:
```elixir
# A custom GET request using our default base_url and headers
GitHubClient.get("/users/danielrudn")
# Our custom function, requiring only a username
GitHubClient.user_repos("danielrudn")
```
## Response Handling
You may define an optional `handle_response/1` callback to transform a response after a successful request.
A common example would be parsing a JSON string into a map. Below we have the same `GitHub` client as above
but we also parse the JSON response if possible using the `handle_response/1` callback:
```elixir
defmodule GitHubClient do
use ExCurl.Client, defaults: [base_url: "https://api.github.com", headers: %{"authentication" => "Bearer some-secret-token"}]
def handle_response(%ExCurl.Response{body: body}) do
case Jason.decode(body) do
{:ok, decoded_body} -> decoded_body
_ -> body
end
end
def user_repos(username), do: get("/users/\#{username}/repos")
end
```
## Options
All of the [Shared Options]() from ExCurl are available. There are also the follow additional options:
* `base_url` - The base URL used for all requests by this client module
"""
@doc """
See [Response Handling](#module-response-handling) for usage and examples.
"""
@callback handle_response(response :: %ExCurl.Response{}) :: any()
@optional_callbacks handle_response: 1
defmacro __using__(client_opts \\ []) do
quote do
def get!(url, opts \\ []), do: request!("GET", url, opts)
def post!(url, opts \\ []), do: request!("POST", url, opts)
def put!(url, opts \\ []), do: request!("PUT", url, opts)
def patch!(url, opts \\ []), do: request!("PATCH", url, opts)
def delete!(url, opts \\ []), do: request!("DELETE", url, opts)
def get(url, opts \\ []), do: request("GET", url, opts)
def post(url, opts \\ []), do: request("POST", url, opts)
def put(url, opts \\ []), do: request("PUT", url, opts)
def patch(url, opts \\ []), do: request("PATCH", url, opts)
def delete(url, opts \\ []), do: request("DELETE", url, opts)
def request(method, url, opts \\ []) do
merged_opts = Keyword.merge(default_opts(), opts)
with {:ok, %ExCurl.Response{} = resp} <-
ExCurl.request(method, request_url(url, merged_opts), merged_opts) do
if function_exported?(__MODULE__, :handle_response, 1) do
{:ok, apply(__MODULE__, :handle_response, [resp])}
else
{:ok, resp}
end
end
end
def request!(method, url, opts \\ []) do
merged_opts = Keyword.merge(default_opts(), opts)
response = ExCurl.request!(method, request_url(url, merged_opts), merged_opts)
if function_exported?(__MODULE__, :handle_response, 1) do
apply(__MODULE__, :handle_response, [response])
else
response
end
end
defp request_url(url_or_path, opts) do
case Keyword.get(opts, :base_url, nil) do
base_url when is_binary(base_url) -> base_url <> url_or_path
_ -> url_or_path
end
end
defp default_opts do
case Keyword.get(unquote(client_opts), :defaults, []) do
opts_list when is_list(opts_list) -> opts_list
opts_atom when is_atom(opts_atom) -> apply(__MODULE__, opts_atom, [])
opts_func when is_function(opts_func, 0) -> opts_func.()
end
end
end
end
end