Skip to content

Commit

Permalink
A collection of general API improvements
Browse files Browse the repository at this point in the history
- Working with Conn APIs requires calling new/0 or new/1 first
  old APIs are deprecated
- new/1 parses the provided string with URI.parse/1 and sets the Conn
  struct fields accordingly (so you can pass full or partial url
  fragments and get what you would expect, including those containing
  query strings)
- All headers are normalized to lower-case on put/get, and internally
  headers are a simple key/value map instead of the previous key =>
  key/value map, which was unnecessary.
- Implemented get_req_headers/1, deprecated get_req_header/1
- Implemented put_req_headers/2, deprecated put_req_header/2
- get_req_header/2 returns String.t instead of {String.t, String.t}
- get_req_headers/1 returns %{String.t => String.t} instead of
  %{String.t => %{String.t => String.t}}
- Added a number of typespecs, and fixed invalid ones
- Some small style fixes, documentation updates
- Fixed a bug with the Header middleware which was not overriding
  headers in the connection if already set. This one may be up for
  discussion if what's really desired.
- Added HeaderCase middleware for enforcing header casing
  • Loading branch information
bitwalker committed Jan 16, 2017
1 parent 8957e54 commit 2a0ebd4
Show file tree
Hide file tree
Showing 24 changed files with 718 additions and 594 deletions.
177 changes: 35 additions & 142 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,183 +9,71 @@ Maxwell is an HTTP client that provides a common interface over [:httpc](http://

[Documentation for Maxwell is available online](https://hexdocs.pm/maxwell).

## Usage
## Getting Started

Use `Maxwell.Builder` module to create the API wrappers. The following is a simple example:
The simplest way to use Maxwell is by creating a module which will be your API wrapper, using `Maxwell.Builder`:

```elixir
defmodule GitHubClient do
# Generates `get/1`, `get!/1`, `patch/1`, `patch!/1` public functions
# You can omit the list and functions for all HTTP methods will be generated
use Maxwell.Builder, ~w(get patch)a

# For a complete list of middlewares, see the docs
middleware Maxwell.Middleware.BaseUrl, "https://api.github.com"
middleware Maxwell.Middleware.Headers, %{'Content-Type': "application/vnd.github.v3+json", 'User-Agent': 'zhongwenool'}
middleware Maxwell.Middleware.Opts, [connect_timeout: 3000]
middleware Maxwell.Middleware.Headers, %{"content-type": "application/vnd.github.v3+json", "user-agent": "zhongwenool"}
middleware Maxwell.Middleware.Opts, connect_timeout: 3000
middleware Maxwell.Middleware.Json
middleware Maxwell.Middleware.Logger

adapter Maxwell.Adapter.Hackney # default adapter is Maxwell.Adapter.Httpc
# adapter can be omitted, and the default will be used (currently :httpc)
adapter Maxwell.Adapter.Hackney

# List public repositories for the specified user.
def user_repos(username) do
put_path("/users/" <> username <> "/repos") |> get
"/users/#{username}/repos"
|> new()
|> get()
end

# Edit owner repositories
def edit_repo_desc(owner, repo, name, desc) do
new
|> put_path("/repos/#{owner}/#{repo}")
"/repos/#{owner}/#{repo}"
|> new()
|> put_req_body(%{name: name, description: desc})
|> patch
|> patch()
end
end
```

Example usage is as follows:
`Maxwell.Builder` injects functions for all supported HTTP methods, in two flavors, the first (e.g. `get/1`) will
return `{:ok, Maxwell.Conn.t}` or `{:error, term, Maxwell.Conn.t}`. The second (e.g. `get!/1`) will return
`Maxwell.Conn.t` *only* if the request succeeds and returns a 2xx status code, otherwise it will raise `Maxwell.Error`.

```elixir
$ MIX_ENV=TEST iex -S mix
iex(1)> GitHubClient.
edit_repo_desc/4 get!/0 get!/1
get!/2 get/0 get/1
patch!/0 patch!/1 patch!/2
patch/0 patch/1 user_repos/1
iex(1)> GitHubClient.user_repos("zhongwencool")
22:23:42.307 [info] get https://api.github.com <<<200(3085.772ms)
%Maxwell.Conn{method: :get, opts: [connect_timeout: 3000, recv_timeout: 20000]
...(truncated)
```

You can also use Maxwell without defining a module:
The same functions are also exported by the `Maxwell` module, which you can use if you do not wish to define a wrapper
module for your API, as shown below:

```elixir
iex(1)> alias Maxwell.Conn
iex(2)> Conn.new("http://httpbin.org") |>
Conn.put_path("/drip") |>
iex(2)> Conn.new("http://httpbin.org/drip") |>
Conn.put_query_string(%{numbytes: 25, duration: 1, delay: 1, code: 200}) |>
Maxwell.get
{:ok,
%Maxwell.Conn{method: :get, opts: [], path: "",
%Maxwell.Conn{method: :get, opts: [], path: "/drip",
query_string: %{code: 200, delay: 1, duration: 1, numbytes: 25},
req_body: nil, req_headers: %{}, resp_body: '*************************',
resp_headers: %{"access-control-allow-credentials" => {"access-control-allow-credentials",
"true"},
"access-control-allow-origin" => {"access-control-allow-origin", "*"},
"connection" => {"connection", "keep-alive"},
"content-length" => {"content-length", "25"},
"content-type" => {"content-type", "application/octet-stream"},
"date" => {"date", "Sun, 18 Dec 2016 14:32:38 GMT"},
"server" => {"server", "nginx"}}, state: :sent, status: 200,
url: "http://httpbin.org/drip"}}
resp_headers: %{"access-control-allow-credentials" => "true",
"access-control-allow-origin" => "*",
"connection" => "keep-alive",
"content-length" => "25",
"content-type" => "application/octet-stream",
"date" => "Sun, 18 Dec 2016 14:32:38 GMT",
"server" => "nginx"}, state: :sent, status: 200,
url: "http://httpbin.org"}}
```

### Helper functions for Maxwell.Conn

```elixir
new(request_url_string)
|> put_query_string(request_query_map)
|> put_req_header(request_headers_map)
|> put_option(request_opts_keyword_list)
|> put_req_body(request_body_term)
|> YourClient.{http_method}!
|> get_resp_body
```

See the documentation of `Maxwell.Conn` for more information.

## Responses

When calling one of (non-bang versions) of the HTTP method functions on a client module or the `Maxwell` module, you
can expect either `{:ok, Maxwell.Conn.t}` or `{:error, reason, Maxwell.Conn.t}` to be returned.

When calling of the bang versions of the HTTP method functions, e.g. `get!`, you can expect `Maxwell.Conn.t` if successful,
or a `Maxwell.Error` will be raised.

## Example Client

The following is a full implementation of a client showing various features of Maxwell.

```elixir
defmodule Client do
#generate 4 function get/1, get!/1 post/1 post!/1 function
use Maxwell.Builder, ~w(get post)a

middleware Maxwell.Middleware.BaseUrl, "http://httpbin.org"
middleware Maxwell.Middleware.Headers, %{"Content-Type" => "application/json"}
middleware Maxwell.Middleware.Opts, [connect_timeout: 5000, recv_timeout: 10000]
middleware Maxwell.Middleware.Json
middleware Maxwell.Middleware.Logger

adapter Maxwell.Adapter.Hackney

@doc """
Simple get request
Get origin ip
"""
def get_ip() do
new
|> put_path("/ip")
|> get!
|> get_resp_body("origin")
end

@doc """
Post whole file once
###Example
Client.post_file_once("./mix.exs")
"""
def post_file_once(filepath) do
new
|> put_path("/post")
|> put_req_body({:file, filepath})
|> post!
|> get_resp_body("data")
end

@doc """
Post whole file by chunked
###Example
Client.post_file_chunked("./mix.exs")
"""
def post_file_chunked(filepath) do
new
|> put_path("/post")
|> put_req_header("transfer_encoding", "chunked")
|> put_req_body({:file, filepath})
|> post!
|> get_resp_body("data")
end

@doc """
Post by stream
###Example
["1", "2", "3"] |> Stream.map(fn(x) -> List.duplicate(x, 2) end) |> Client.post_stream
"""
def post_stream(stream) do
new
|> put_path("/post")
|> put_req_body(stream)
|> post!
|> get_resp_body("data")
end

@doc """
Post multipart form
###Example
Client.post_multipart_form({:multipart, [{:file, "./mix.exs"}]})
"""
def post_multipart_form(multipart) do
new
|> put_path("/post")
|> put_req_body(multipart)
|> post!
|> get_resp_body("data")
end

end

```
There are numerous helper functions for the `Maxwell.Conn` struct. See it's module docs
for a list of all functions, and detailed info about how they behave.

## Installation

Expand Down Expand Up @@ -248,6 +136,11 @@ Sets the base url for all requests.

Sets default headers for all requests.

### Maxwell.Middleware.HeaderCase

Enforces that all header keys share a specific casing style, e.g. lower-case,
upper-case, or title-case.

### Maxwell.Middleware.Opts

Sets adapter options for all requests.
Expand Down
9 changes: 3 additions & 6 deletions lib/maxwell/adapter/hackney.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ if Code.ensure_loaded?(:hackney) do
end

defp format_response({:ok, status, headers, body}, conn) when is_binary(body) do
headers = for {key, value} <- headers, into: %{} do
down_key = key |> to_string |> String.downcase
{down_key, {key, to_string(value)}}
end
headers = Enum.reduce(headers, %{}, fn {k, v}, acc ->
Map.put(acc, String.downcase(to_string(k)), to_string(v))
end)
%{conn | status: status,
resp_headers: headers,
req_body: nil,
Expand All @@ -57,8 +56,6 @@ if Code.ensure_loaded?(:hackney) do
defp format_response({:error, reason}, conn) do
{:error, reason, %{conn | state: :error}}
end

end

end

11 changes: 4 additions & 7 deletions lib/maxwell/adapter/httpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ defmodule Maxwell.Adapter.Httpc do

defp header_serialize(headers) do
{content_type, headers} = Map.pop(headers, "content-type")
headers = Enum.map(headers, fn({_, {key, value}}) -> {to_char_list(key), to_char_list(value)} end)
headers = Enum.map(headers, fn {key, value} -> {to_char_list(key), to_char_list(value)} end)
case content_type do
nil -> {nil, headers}
{_, type} -> {to_char_list(type), headers}
nil -> {nil, headers}
type -> {to_char_list(type), headers}
end
end

Expand All @@ -80,9 +80,7 @@ defmodule Maxwell.Adapter.Httpc do
defp format_response({:ok, {status_line, headers, body}}, conn) do
{_http_version, status, _reason_phrase} = status_line
headers = for {key, value} <- headers, into: %{} do
key = key |> to_string
down_key = key |> String.downcase
{down_key, {key, to_string(value)}}
{String.downcase(to_string(key)), to_string(value)}
end
%{conn | status: status,
resp_headers: headers,
Expand All @@ -107,6 +105,5 @@ defmodule Maxwell.Adapter.Httpc do
defp format_response({:error, reason}, conn) do
{:error, reason, %{conn | state: :error}}
end

end

9 changes: 3 additions & 6 deletions lib/maxwell/adapter/ibrowse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ if Code.ensure_loaded?(:ibrowse) do

defp format_response({:ok, status, headers, body}, conn) do
{status, _} = status |> to_string |> Integer.parse
headers = for {key, value} <- headers, into: %{} do
down_key = key |> to_string |> String.downcase
{down_key, {key, to_string(value)}}
end
headers = Enum.reduce(headers, %{}, fn {k, v}, acc ->
Map.put(acc, String.downcase(to_string(k)), to_string(v))
end)
%{conn | status: status,
resp_headers: headers,
resp_body: body,
Expand All @@ -77,8 +76,6 @@ if Code.ensure_loaded?(:ibrowse) do
defp format_response({:error, reason}, conn) do
{:error, reason, %{conn | state: :error}}
end

end

end

25 changes: 11 additions & 14 deletions lib/maxwell/adapter/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,17 @@ defmodule Maxwell.Adapter.Util do
end

@doc """
Make headers to key word list.
Converts the headers map to a list of tuples.
* `headers` - `Map.t`, for example: `%{"content-type" => {"Content-Type", "application/json"}}`
* `headers` - `Map.t`, for example: `%{"content-type" => "application/json"}`
### Examples
#[{"Content-Type", "application/json"}]
iex> headers_serialize(%{"content-type" => {"Content-Type", "application/json"}})
iex> headers_serialize(%{"content-type" => "application/json"})
[{"content-type", "application/json"}]
"""

def header_serialize(headers) do
headers |> Map.values
Enum.into(headers, [])
end

@doc """
Expand Down Expand Up @@ -83,9 +81,8 @@ defmodule Maxwell.Adapter.Util do
"""
def chunked?(conn) do
case Conn.get_req_header(conn, "transfer-encoding") do
{_, "chunked"} -> true
{_, type} -> "chunked" == String.downcase(type)
nil -> false
nil -> false
type -> "chunked" == String.downcase(type)
end
end

Expand Down Expand Up @@ -122,15 +119,15 @@ defmodule Maxwell.Adapter.Util do
case next_stream_fun.({:cont, nil}) do
{:suspended, item, next_stream_fun} -> {:ok, item, next_stream_fun}
{:halted, _} -> :eof
{:done, _} -> :eof
{:done, _} -> :eof
end
end
def stream_iterate(stream) do
case Enumerable.reduce(stream, {:cont, nil}, fn(item, nil)-> {:suspend, item} end) do
case Enumerable.reduce(stream, {:cont, nil}, fn(item, nil)-> {:suspend, item} end) do
{:suspended, item, next_stream} -> {:ok, item, next_stream}
{:done, _} -> :eof
{:done, _} -> :eof
{:halted, _} -> :eof
end
end
end

defp multipart_body({:start, boundary, multiparts}) do
Expand Down
Loading

0 comments on commit 2a0ebd4

Please sign in to comment.