Maxwell

Build StatusCoveralls Coverage

Maxwell is an HTTP client that provides a common interface over many adapters (such as hackney, ibrowse) and embraces the concept of Rack middleware when processing the request/response cycle.

It borrows the idea from tesla(elixir) which is loosely based on Faraday(ruby)

We already have httpoison and httpotion, why do we need another wrapper?

In every application I have to define process_url/1process_request_headersprocess_response_body functions.

But these functions can be the same in most cases, which we don't need to define every time.

The operations usually follow the following steps:

  1. Put query and url encode together
  2. Add headers
  3. Add body
  4. Encode request body with json or multipart
  5. Record response's header to decode response body by self

So Maxwell makes those same steps into middlewares, without losing any flexibility.

See the specific example here.

Usage

Use Maxwell.Builder module to create the API wrappers.

defmodule GitHub do
# create get/1, get!/1
use Maxwell.Builder, ~w(get patch)a
middleware Maxwell.Middleware.BaseUrl, "https://api.github.com"
middleware Maxwell.Middleware.Opts, [connect_timeout: 3000]
middleware Maxwell.Middleware.Headers, %{'Content-Type': "application/vnd.github.v3+json", 'User-Agent': 'zhongwenool'}
middleware Maxwell.Middleware.EncodeJson
middleware Maxwell.Middleware.DecodeJson
adapter Maxwell.Adapter.Ibrowse
# List public repositories for the specified user.
# :ibrowse.send_req('https://api.github.com/users/zhongwencool/repos', [\"Content-Type\": \"application/vnd.github.v3+json\", \"User-Agent\": 'zhongwenool'], :get, [], [connect_timeout: 3000])
def user_repos(username) do
url("/users/" <> username <> "/repos") |> get!
end
# Edit owner repositories
# :ibrowse.send_req('https://api.github.com/repos/owner/repo', [\"Content-Type\": \"application/vnd.github.v3+json\", \"User-Agent\": 'zhongwenool'], :patch, \"{\\\"name\\\":\\\"name\\\",\\\"description\\\":\\\"desc\\\"}\", [connect_timeout: 3000])"
def edit_repo_desc(owner, repo, name, desc) do
url("/repos/#{owner}/#{repo}")
|> body(%{name: name, description: desc})
|> patch!
end
end

It auto-packages all middleware's params (url, query, headers, opts, encode_request_body, decode_response_body) to adapter (ibrowse)

For more examples see h Maxwell

Request helper functions

url(request_url_string_or_char_list)
|> query(request_query_map)
|> headers(request_headers_map)
|> opts(request_opts_keyword_list)
|> body(request_body_term)
|> YourClient.{http_method}!

Multipart helper function

multipart(maxwell \\ %Maxwell, request_multipart_list) -> new_maxwell # same as hackney

More info: Multipart format

Asynchronous helper function

respond_to(maxwell \\ %Maxwell, target_pid) -> new_maxwell

More info: [Asynchronous Request](#Asynchronous requests)

Response result

{:ok,
%Maxwell{
headers: reponse_headers_map,
status: reponse_http_status_integer,
body: reponse_body_term,
opts: request_opts_keyword_list,
url: request_urlwithquery_string,
}}
# or
{:error, reason_term}

Installation

  1. Add maxwell to your list of dependencies in mix.exs:
def deps do
[{:maxwell, "~> 1.0.2"}]
end
  1. Ensure maxwell has started before your application:
def application do
[applications: [:maxwell]] # **also add your adapter(ibrowse,hackney...) here **
end

Adapters

Maxwell has support for different adapters that do the actual HTTP request processing.

ibrowse

Maxwell has built-in support for ibrowse Erlang HTTP client.

To use it simply include adapter Maxwell.Adapter.Ibrowse line in your API client definition. global default adapter

config :maxwell,
default_adapter: Maxwell.Adapter.Ibrowse

NOTE: Remember to include ibrowse(adapter) in applications list.

hackney

Maxwell has built-in support for hackney Erlang HTTP client.

To use it simply include adapter Maxwell.Adapter.Hackney line in your API client definition. global default adapter

config :maxwell,
default_adapter: Maxwell.Adapter.Hackney

NOTE: Remember to include hackney (adapter) in applications list.

Middleware

Basic

JSON

NOTE: Default requires poison as dependency

Custom JSON library example

@middleware Maxwell.Middleware.EncodeJson, [encode_func: &Poison.encode/1, content_type: "text/javascript"]
@middleware Maxwell.Middleware.DecodeJson [decode_func: &Poison.decode/1, valid_types: ["text/html"] ]

Encode body by encode_func then add %{'Content-Type': content_type} to headers (default content_type is "application/json")

Decode body if 'content-type' in ["text/html","application/json", "text/javascript"]

Default only decode body when it's ["application/json", "text/javascript"]

Writing your own middleware

A Maxwell middleware is a module with call/3 function:

defmodule MyMiddleware do
def call(env = %maxwell{}, run, options) do
# ...
end
end

The arguments are:

There is no distinction between request and response middleware, it's all about executing run function at the correct time.

For example, request logger middleware could be implemented like this:

defmodule Maxwell.Middleware.RequestLogger do
def call(env, run, _) do
IO.inspect env # print request env
run.(env)
end
end

and response logger middleware like this:

defmodule Maxwell.Middleware.ResponseLogger do
def call(env, run, _) do
res = run.(env) # the adapter always return {:ok, env}/{:ok, ref_id}/{error, reason}
IO.inspect res # print response
res
end
end

Asynchronous requests

If the adapter supports it, you can make asynchronous requests by passing respond_to: pid option:

Maxwell.get(url: "http://example.org", respond_to: self)
receive do
{:maxwell_response, {:ok, res}} -> res.status # => 200
{:maxwell_response, {:error, reason, env}} -> env # the request env
end

Multipart

response =
[url: "http://httpbin.org/post", multipart: [{"name", "value"}, {:file, "test/maxwell/multipart_test_file.sh"}]]
|> Client.post!
# reponse.body["files"] is %{"file" => "#!/usr/bin/env bash\necho \"test multipart file\"\n"}

both ibrowse and hackney adapter support multipart

{:multipart: lists}, lists support:

  1. {:file, path}
  2. {:file, path, extra_headers}
  3. {:file, path, disposition, extra_headers}
  4. {:mp_mixed, name, mixed_boundary}
  5. {:mp_mixed_eof, mixed_boundary}
  6. {name, bin_data}
  7. {name, bin_data, extra_headers}
  8. {name, bin_data, disposition, extra_headers}

All formats supported by hackney. See more examples.

License

See the LICENSE file for license rights and limitations (MIT).