defmodule Mojito.Base do @moduledoc ~S""" Provides a default implementation for Mojito functions. This module is meant to be `use`'d in custom modules in order to wrap the functionalities provided by Mojiti. For example, this is very useful to build custom API clients around Mojito: defmodule CustomAPI do use Mojito.Base end """ @type method :: :head | :get | :post | :put | :patch | :delete | :options | String.t() @type header :: {String.t(), String.t()} @type headers :: [header] @type request :: %Mojito.Request{ method: method, url: String.t(), headers: headers | nil, body: String.t() | nil, opts: Keyword.t() | nil } @type request_kwlist :: [request_field] @type request_field :: {:method, method} | {:url, String.t()} | {:headers, headers} | {:body, String.t()} | {:opts, Keyword.t()} @type response :: %Mojito.Response{ status_code: pos_integer, headers: headers, body: String.t(), complete: boolean } @type error :: %Mojito.Error{ reason: any, message: String.t() | nil } @type pool_opts :: [pool_opt | {:destinations, [{atom, pool_opts}]}] @type pool_opt :: {:size, pos_integer} | {:max_overflow, non_neg_integer} | {:pools, pos_integer} | {:strategy, :lifo | :fifo} @type url :: String.t() @type body :: String.t() @type payload :: String.t() @callback request(method, url) :: {:ok, response} | {:error, error} | no_return @callback request(method, url, headers) :: {:ok, response} | {:error, error} | no_return @callback request(method, url, headers, body) :: {:ok, response} | {:error, error} | no_return @callback request(method, url, headers, body, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback request(request | request_kwlist) :: {:ok, response} | {:error, error} @callback head(url) :: {:ok, response} | {:error, error} | no_return @callback head(url, headers) :: {:ok, response} | {:error, error} | no_return @callback head(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback get(url) :: {:ok, response} | {:error, error} | no_return @callback get(url, headers) :: {:ok, response} | {:error, error} | no_return @callback get(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback post(url) :: {:ok, response} | {:error, error} | no_return @callback post(url, headers) :: {:ok, response} | {:error, error} | no_return @callback post(url, headers, payload) :: {:ok, response} | {:error, error} | no_return @callback post(url, headers, payload, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback put(url) :: {:ok, response} | {:error, error} | no_return @callback put(url, headers) :: {:ok, response} | {:error, error} | no_return @callback put(url, headers, payload) :: {:ok, response} | {:error, error} | no_return @callback put(url, headers, payload, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback patch(url) :: {:ok, response} | {:error, error} | no_return @callback patch(url, headers) :: {:ok, response} | {:error, error} | no_return @callback patch(url, headers, payload) :: {:ok, response} | {:error, error} | no_return @callback patch(url, headers, payload, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback delete(url) :: {:ok, response} | {:error, error} | no_return @callback delete(url, headers) :: {:ok, response} | {:error, error} | no_return @callback delete(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return @callback options(url) :: {:ok, response} | {:error, error} | no_return @callback options(url, headers) :: {:ok, response} | {:error, error} | no_return @callback options(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return defmacro __using__(_) do quote do @behaviour Mojito.Base @type method :: Mojito.Base.method() @type header :: Mojito.Base.header() @type headers :: Mojito.Base.headers() @type request :: Mojito.Base.request() @type request_kwlist :: Mojito.Base.request_kwlist() @type request_fields :: Mojito.Base.request_field() @type response :: Mojito.Base.response() @type error :: Mojito.Base.error() @type pool_opts :: Mojito.Base.pool_opts() @type pool_opt :: Mojito.Base.pool_opt() @type url :: Mojito.Base.url() @type body :: Mojito.Base.body() @type payload :: Mojito.Base.payload() @doc ~S""" Performs an HTTP request and returns the response. See `request/1` for details. """ @spec request(method, url, headers, body | nil, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def request(method, url, headers \\ [], body \\ "", opts \\ []) do %Mojito.Request{ method: method, url: url, headers: headers, body: body, opts: opts } |> request end @doc ~S""" Performs an HTTP request and returns the response. If the `pool: true` option is given, or `:pool` is not specified, the request will be made using Mojito's automatic connection pooling system. For more details, see `Mojito.Pool.request/1`. This is the default mode of operation, and is recommended for best performance. If `pool: false` is given as an option, the request will be made on a brand new connection. This does not spawn an additional process. Messages of the form `{:tcp, _, _}` or `{:ssl, _, _}` will be sent to and handled by the caller. If the caller process expects to receive other `:tcp` or `:ssl` messages at the same time, conflicts can occur; in this case, it is recommended to wrap `request/1` in `Task.async/1`, or use one of the pooled request modes. Options: * `:pool` - See above. * `:timeout` - Response timeout in milliseconds, or `:infinity`. Defaults to `Application.get_env(:mojito, :timeout, 5000)`. * `:raw` - Set this to `true` to prevent the decompression of `gzip` or `compress`-encoded responses. * `:transport_opts` - Options to be passed to either `:gen_tcp` or `:ssl`. Most commonly used to perform insecure HTTPS requests via `transport_opts: [verify: :verify_none]`. """ @spec request(request | request_kwlist) :: {:ok, response} | {:error, error} def request(request) do with {:ok, valid_request} <- Mojito.Request.validate_request(request), {:ok, valid_request} <- Mojito.Request.convert_headers_values_to_string(valid_request) do case Keyword.get(valid_request.opts, :pool, true) do true -> Mojito.Pool.Poolboy.request(valid_request) false -> Mojito.Request.Single.request(valid_request) pid when is_pid(pid) -> Mojito.Pool.Poolboy.Single.request(pid, valid_request) impl when is_atom(impl) -> impl.request(valid_request) end |> maybe_decompress(valid_request.opts) end end defp maybe_decompress({:ok, response}, opts) do case Keyword.get(opts, :raw) do true -> {:ok, response} _ -> case Enum.find(response.headers, fn {k, _v} -> k == "content-encoding" end) do {"content-encoding", "gzip"} -> {:ok, %Mojito.Response{response | body: :zlib.gunzip(response.body)}} {"content-encoding", "deflate"} -> {:ok, %Mojito.Response{ response | body: :zlib.uncompress(response.body) }} _ -> # we don't have a decompressor for this so just returning {:ok, response} end end end defp maybe_decompress(response, _opts) do response end @doc ~S""" Performs an HTTP HEAD request and returns the response. See `request/1` for documentation. """ @spec head(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def head(url, headers \\ [], opts \\ []) do request(:head, url, headers, nil, opts) end @doc ~S""" Performs an HTTP GET request and returns the response. ## Examples Assemble a URL with a query string params and fetch it with GET request: >>>> "https://www.google.com/search" ...> |> URI.parse() ...> |> Map.put(:query, URI.encode_query(%{"q" => "mojito elixir"})) ...> |> URI.to_string() ...> |> Mojito.get() {:ok, %Mojito.Response{ body: " ...", complete: true, headers: [ {"content-type", "text/html; charset=ISO-8859-1"}, ... ], status_code: 200 }} See `request/1` for detailed documentation. """ @spec get(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def get(url, headers \\ [], opts \\ []) do request(:get, url, headers, nil, opts) end @doc ~S""" Performs an HTTP POST request and returns the response. ## Examples Submitting a form with POST request: >>>> Mojito.post( ...> "http://localhost:4000/messages", ...> [{"content-type", "application/x-www-form-urlencoded"}], ...> URI.encode_query(%{"message[subject]" => "Contact request", "message[content]" => "data"})) {:ok, %Mojito.Response{ body: "Thank you!", complete: true, headers: [ {"server", "Cowboy"}, {"connection", "keep-alive"}, ... ], status_code: 200 }} Submitting a JSON payload as POST request body: >>>> Mojito.post( ...> "http://localhost:4000/api/messages", ...> [{"content-type", "application/json"}], ...> Jason.encode!(%{"message" => %{"subject" => "Contact request", "content" => "data"}})) {:ok, %Mojito.Response{ body: "{\"message\": \"Thank you!\"}", complete: true, headers: [ {"server", "Cowboy"}, {"connection", "keep-alive"}, ... ], status_code: 200 }} See `request/1` for detailed documentation. """ @spec post(url, headers, payload, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def post(url, headers \\ [], payload \\ "", opts \\ []) do request(:post, url, headers, payload, opts) end @doc ~S""" Performs an HTTP PUT request and returns the response. See `request/1` and `post/4` for documentation and examples. """ @spec put(url, headers, payload, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def put(url, headers \\ [], payload \\ "", opts \\ []) do request(:put, url, headers, payload, opts) end @doc ~S""" Performs an HTTP PATCH request and returns the response. See `request/1` and `post/4` for documentation and examples. """ @spec patch(url, headers, payload, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def patch(url, headers \\ [], payload \\ "", opts \\ []) do request(:patch, url, headers, payload, opts) end @doc ~S""" Performs an HTTP DELETE request and returns the response. See `request/1` for documentation and examples. """ @spec delete(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def delete(url, headers \\ [], opts \\ []) do request(:delete, url, headers, nil, opts) end @doc ~S""" Performs an HTTP OPTIONS request and returns the response. See `request/1` for documentation. """ @spec options(url, headers, Keyword.t()) :: {:ok, response} | {:error, error} | no_return def options(url, headers \\ [], opts \\ []) do request(:options, url, headers, nil, opts) end end end end