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 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:

  1. Put query and url encode together;
  2. Add headers
  3. Add body
  4. Need encode request body with json or multipart
  5. 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
end

It 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

  1. Add maxwell to your list of dependencies in mix.exs:
    def deps do
    [{:maxwell, github: "zhongwencool/maxwell", branch: master}]
    end
  2. 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.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 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
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 format support as hackney. More example

License

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