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 borrow idea from tesla(elixir) which losely base on Faraday(ruby)
We already have httpoison and httpotion, why we need another wrapper?
I am trouble with define a lot of process_url/1process_request_headersprocess_response_body functions,
but those functions almost the same in most cases, we don't need define then every time.
the same operation steps:
- Put query and url encode together;
- Add headers
- Add body
- Need encode request body with json or multipart
- Accord reponse's header to decode response body by self
So Maxwell make those same steps into middlewares, it don't lose any flexible.
See the specific example here.
Usage
Use Maxwell.Builder module to create 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
endIt auto package all middleware's params(url, query, headers, opts, encode_requet_body, decode_response_body) to adapter(ibrowse)
More example 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}! Mulipart helper function
multipart(maxwell \\ %Maxwell, request_multipart_list) -> new_maxwell # same as hackney More info: Mulipart format
Asynchronous helper function
respond_to(maxwell \\ %Maxwell, target_pid) -> new_maxwell More info: Asynchronous Request
Reponse 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, github: "zhongwencool/maxwell", branch: master}] end -
Ensure maxwell is 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.IbrowseNOTE: 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.HackneyNOTE: 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- endode 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
endThe 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
endand 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
endAsynchronous requests
If 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, res} -> res.status # => 200
endMultipart
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 format support as hackney. More example
License
See the LICENSE file for license rights and limitations (MIT).