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. """ 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. """ @spec with_body(request, term) :: request def with_body(request, body) do %{request | body: body } end @doc """ Marks a body as a streaming upload. Takes a list of enumerables, and adds a tuple of `{:stream, enumerable}` to the body of the request. """ @spec with_stream_body(request, Enumerable.t) :: request def with_stream_body(request, enumerable) when is_list(enumerable) do %{ request | body: {:stream, enumerable}} 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. """ @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 } } 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. """ @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. """ @spec with_receive_timeout(request, integer) :: request def with_receive_timeout(request, timeout) do %{ request | rec_timeout: timeout } end @doc """ Sets the number of times to retry the request on failure. """ @spec with_retry(request, integer) :: request def with_retry(request, retry_amount) do %{ request | retry: retry_amount} 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