Current section

Files

Jump to
http_builder lib http_builder.ex
Raw

lib/http_builder.ex

defmodule HttpBuilder do
@moduledoc """
HttpBuilder is a library that provides a DSL for composable HTTP Requests.
Each method provided builds and updates a `HTTPBuilder.Request` object,
until passed into `send`, which calls the adapter to invoke the request.
## Example
Here's an example of a complete request chain:
HTTPBuilder.new()
|> with_adapter(HttpBuilder.Adapters.HTTPoison)
|> post("http://httparrot.com/post/1")
|> with_headers(%{"Authorization" => "Bearer token"})
|> with_json_parser(Jason)
|> with_json_body(%{"test" => "true"})
|> with_request_timeout(10_000)
|> with_receive_timeout(5_000)
|> send() # kicks off the request.
This can also be broken down into composable parts, allowing you to easily
write declarative pipelines for your API calls.
defmodule MyAPIClient do
alias HttpBuilder.Adapters
@adapter Application.get_env(:my_api_client, :client, Adapters.HTTPoison)
def client do
HTTPBuilder.new()
|> with_adapter()
|> with_host("http://httparrot.com/")
|> with_headers(%{"Authorization" => "Bearer token"})
|> with_request_timeout(10_000)
|> with_receive_timeout(5_000)
end
def create(params) do
client()
|> post("/new")
|> with_json_body(params)
|> send()
end
def update(id, params) do
client()
|> put("/item/\#{id}")
|> with_json_body(params)
|> send()
end
def list(limit, offset) do
client()
|> get("/items")
|> with_query_params(%{"limit" => limit, "offset" => offset})
|> send()
end
def delete(id) do
client()
|> delete("/item/\#{id}")
|> send()
end
end
Sometimes, you don't want to make a call against a service. By putting your
adapter in your config, you can also easily switch to a test HTTP adapter.
By pattern matching against the request object, you can handle exact request
scenarios.
defmodule MyAPIClient.TestAdapter do
@behaviour HttpBuilder.Adapter
def send(%{method: :post, path: "/new"}), do: {:ok, new_placeholder_data }
def send(%{method: :get, path: "/items"}), do: {:ok, items_placeholder_data }
# ... other request options.
end
"""
alias HttpBuilder.HttpRequest
@type request :: HttpRequest.t
@type path :: String.t
@type host :: String.t
@type adapter :: atom
@doc """
Creates a new request.
Takes in a host, providing the base path for http requests, and an adapter
module to eventually run the request.
"""
@spec new() :: request
def new(), do: %HttpRequest{}
@doc """
Casts a map of predefined options to a `HttpBuilder.Request` struct.
"""
def cast(map) when is_map(map), do: Map.merge(%HttpRequest{}, map)
@doc """
Sets the host for the request.
Can be used for host/path composition to create a client library for
an API.
"""
def with_host(request, host) when is_binary(host),
do: %{ request | host: host }
@doc """
Sets the adapter for the request.
Takes an atom, representing a module that conforms to the
`HttpBuilder.Adapter` behaviour.
"""
@spec with_adapter(request, atom) :: request
def with_adapter(request, adapter) when is_atom(adapter),
do: %{request | adapter: adapter }
@doc """
Sets the delete method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec delete(request, path) :: request
def delete(request, path \\ ""), do: %{ request | method: :delete, path: path }
@doc """
Sets the get method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec get(request, path) :: request
def get(request, path \\ ""), do: %{ request | method: :get, path: path }
@doc """
Sets the head method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec head(request, path) :: request
def head(request, path \\ ""), do: %{ request | method: :head, path: path }
@doc """
Sets the patch method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec patch(request, path) :: request
def patch(request, path \\ ""), do: %{ request | method: :patch, path: path }
@doc """
Sets the post method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec post(request, path) :: request
def post(request, path \\ ""), do: %{ request | method: :post, path: path }
@doc """
Sets the put method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec put(request, path) :: request
def put(request, path \\ ""), do: %{ request | method: :put, path: path }
@doc """
Sets the options method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec options(request, path) :: request
def options(request, path \\ ""),
do: %{ request | method: :options, path: path }
@doc """
Sets the connect method on the request.
Takes an optional path, to be added onto the host of the request.
"""
@spec options(request, path) :: request
def connect(request, path \\ ""),
do: %{ request | method: :connect, path: path }
@doc """
Sets either a list of two-item tuples, or map of query params on the request.
"""
@spec with_query_params(request, [String.t] | map) :: request
def with_query_params(request, query_params) when is_list(query_params),
do: %{ request | query_params: query_params ++ request.query_params }
def with_query_params(request, query_params) when is_map(query_params),
do: with_query_params(request, Map.to_list(query_params))
@doc """
Sets either a list of two-item tuples, or map of headers on the request.
"""
@spec with_headers(request, [String.t] | map) :: request
def with_headers(request, headers) when is_list(headers) do
%{ request | headers: headers ++ request.headers }
end
def with_headers(request, headers) when is_map(headers) do
with_headers(request, Map.to_list(headers))
end
@doc """
Adds a body to the request, with no special notation of type.
This body should be used to handle requests not explicitly covered
by HTTPBuilder, or the adapter.
"""
@spec with_body(request, term) :: request
def with_body(request, body) do
%{request | body: {:other, body } }
end
@doc """
Marks a body to be sent as JSON.
Takes the value passed in, and adds a tuple of `{:json, body}` to the request,
and adds `application/json` as `Content-Type headers`
The adapter will be responsible for encoding the value.
"""
@spec with_json_body(request, list | map) :: request
def with_json_body(request, body) do
%{ request | body: {:json, body}} |> with_headers(%{"Content-Type" => "application/json"})
end
@doc """
Sets the json parser for the request. Defaults to `Poison` if available.
Most JSON parsers can be used out of the box. See `HttpBuilder.Adapter.JSONParser` for adapter details.
"""
@spec with_json_body(request, module) :: request
def with_json_parser(request, parser) do
%{ request | json_parser: parser}
end
@doc """
Marks a body to be sent as a string.
Takes a string, and adds a tuple of `{:string, body}` to the request.
"""
@spec with_string_body(request, list | map) :: request
def with_string_body(request, body) when is_binary(body) do
%{ request | body: {:string, body}}
end
@doc """
Marks a body as a file upload.
Takes a filepath, and adds a tuple of `{:file, filepath}` to the body of the
request.
"""
@spec with_file_body(request, path) :: request
def with_file_body(request, path) do
%{request | body: {:file, path}}
end
@doc """
Marks a body as a form-encoded upload.
Takes either a list of two-item tuples, or a map of key-value pairs, and
adds a tuple of `{:form, [{"key", "value"} ...]}` to the body of the request,
and sets the Content-Type to "application/x-www-form-urlencoded".
"""
@spec with_form_encoded_body(request, [String.t] | map) :: request
def with_form_encoded_body(request, body) when is_list(body) do
%{request | body: {:form, body } }
|> with_headers([{"Content-Type", "application/x-www-form-urlencoded"}])
end
def with_form_encoded_body(request, body) when is_map(body) do
with_form_encoded_body(request, Map.to_list(body))
end
@doc """
Sets the request timeout of the request.
A request timeout is how long the overall request should take. A request
has a default value of `8000`.
"""
@spec with_request_timeout(request, integer) :: request
def with_request_timeout(request, timeout) do
%{ request | req_timeout: timeout }
end
@doc """
Sets the receive timeout of the request.
A receive timeout is how long until the request recieves a response. A request
has a default value of `5000`.
"""
@spec with_receive_timeout(request, integer) :: request
def with_receive_timeout(request, timeout) do
%{ request | rec_timeout: timeout }
end
@doc """
Sets additional options for the request that may not be handled
by this DSL.
"""
@spec with_options(request, list | term) :: request
def with_options(request, list) when is_list(list) do
%{ request | options: list ++ request.options }
end
def with_options(request, item) do
%{ request | options: [item] ++ request.options }
end
@doc """
Executes a request, with the provided adapter
"""
@spec send(HttpRequest.t) :: {:ok, term } | { :error, String.t }
def send(%{adapter: adapter} = request) do
adapter.send(request)
end
def send(_) do
raise ArgumentError, message: "No adapter set!"
end
end