Skip to content

Commit

Permalink
Add Mox support
Browse files Browse the repository at this point in the history
This PR does not change the public API at all but will reorganize the primary Mojito base
module functions into a Mojito.Base module meants to be pulled in via `use`.

This PR also includes behaviour and callbacks for all Mojito functions for the purpose
of compatibility with Mox.

The implementation was borrowed from HTTPoison: edgurgel/httpoison#330

Some additional types were added for readability.
  • Loading branch information
bcardarella committed Jun 19, 2020
1 parent 7c4c30d commit af8013f
Show file tree
Hide file tree
Showing 2 changed files with 379 additions and 274 deletions.
275 changes: 1 addition & 274 deletions lib/mojito.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,278 +189,5 @@ defmodule Mojito do
This software is released under the MIT License.
"""

@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}

@doc ~S"""
Performs an HTTP request and returns the response.
See `request/1` for details.
"""
@spec request(method, String.t(), headers, String.t() | 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(String.t(), 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: "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"> ...",
complete: true,
headers: [
{"content-type", "text/html; charset=ISO-8859-1"},
...
],
status_code: 200
}}
See `request/1` for detailed documentation.
"""
@spec get(String.t(), 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(String.t(), headers, String.t(), 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(String.t(), headers, String.t(), 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(String.t(), headers, String.t(), 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(String.t(), 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(String.t(), headers, Keyword.t()) ::
{:ok, response} | {:error, error} | no_return
def options(url, headers \\ [], opts \\ []) do
request(:options, url, headers, nil, opts)
end
use Mojito.Base
end
Loading

0 comments on commit af8013f

Please sign in to comment.