Maxwell
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:
- Put query and url encode together
- Add headers
- Add body
- Encode request body with json or multipart
- 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
- Add maxwell to your list of dependencies in
mix.exs:
def deps do
[{:maxwell, "~> 1.0.2"}]
end
- 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
Maxwell.Middleware.BaseUrl- set base url for all requestMaxwell.Middleware.Headers- set request headersMaxwell.Middleware.Opts- set options for all requestMaxwell.Middleware.DecodeRels- decode reponse rels
JSON
NOTE: Default requires poison as dependency
Maxwell.Middleware.EncodeJson- encdode request body as JSON, it will add 'Content-Type' to headersMaxwell.Middleware.DecodeJson- decode response body as JSON
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:
env-%Maxwell{}instancerun- continuation function for the rest of middleware/adapter stackoptions- arguments passed during middleware configuration (middleware MyMiddleware, options)
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:
{:file, path}{:file, path, extra_headers}{:file, path, disposition, extra_headers}{:mp_mixed, name, mixed_boundary}{:mp_mixed_eof, mixed_boundary}{name, bin_data}{name, bin_data, extra_headers}{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).