Current section

13 Versions

Jump to

Compare versions

23 files changed
+1042 additions
-471 deletions
  @@ -5,12 +5,10 @@
5 5 [![Coveralls Coverage](https://img.shields.io/coveralls/zhongwencool/maxwell.svg)](https://coveralls.io/github/zhongwencool/maxwell)
6 6 [![Hex.pm](https://img.shields.io/hexpm/v/maxwell.svg)](http://hex.pm/packages/maxwell)
7 7
8 - Maxwell is an HTTP client that provides a common interface over many adapters.
8 + Maxwell is an HTTP client that provides a common interface over [:httpc](http://erlang.org/doc/man/httpc.html), [:ibrowse](https://github.com/cmullaparthi/ibrowse), [:hackney](https://github.com/benoitc/hackney).
9 9
10 10 [Documentation for Maxwell is available online](https://hexdocs.pm/maxwell).
11 11
12 - [See the specific example here](https://gist.github.com/zhongwencool/6cd44df1acd699fc9c7159882ef3b597).
13 -
14 12 ## Usage
15 13
16 14 Use `Maxwell.Builder` module to create the API wrappers.
  @@ -26,7 +24,7 @@ defmodule GitHubClient do
26 24 middleware Maxwell.Middleware.Json
27 25 middleware Maxwell.Middleware.Logger
28 26
29 - adapter Maxwell.Adapter.Hackney # default adapter is Ibrowse
27 + adapter Maxwell.Adapter.Hackney # default adapter is Maxwell.Adapter.Httpc
30 28
31 29 #List public repositories for the specified user.
32 30 #:hackney.request(:get,
  @@ -35,7 +33,7 @@ defmodule GitHubClient do
35 33 # [],
36 34 # [connect_timeout: 3000])
37 35 def user_repos(username) do
38 - url("/users/" <> username <> "/repos") |> get
36 + put_path("/users/" <> username <> "/repos") |> get
39 37 end
40 38
41 39 # Edit owner repositories
  @@ -45,8 +43,9 @@ defmodule GitHubClient do
45 43 # "{\"name\":\"name\",\"description\":\"desc\"}",
46 44 # [connect_timeout: 3000])
47 45 def edit_repo_desc(owner, repo, name, desc) do
48 - url("/repos/#{owner}/#{repo}")
49 - |> body(%{name: name, description: desc})
46 + new
47 + |> put_path("/repos/#{owner}/#{repo}")
48 + |> put_req_body(%{name: name, description: desc})
50 49 |> patch
51 50 end
52 51 end
  @@ -54,49 +53,44 @@ end
54 53 ```ex
55 54 MIX_ENV=TEST iex -S mix
56 55 iex(1)> GitHubClient.
57 - body/1 body/2 edit_repo_desc/4
58 - get!/0 get!/1 get/0
59 - get/1 headers/1 headers/2
60 - multipart/1 multipart/2 opts/1
61 - opts/2 patch!/0 patch!/1
62 - patch/0 patch/1 query/1
63 - query/2 respond_to/1 respond_to/2
64 - url/1 url/2 user_repos/1
56 + edit_repo_desc/4 get!/0 get!/1
57 + get!/2 get/0 get/1
58 + patch!/0 patch!/1 patch!/2
59 + patch/0 patch/1 user_repos/1
65 60 iex(1)> GitHubClient.user_repos("zhongwencool")
66 - 23:34:56.632 [info] GET https://api.github.com/users/zhongwencool/repos
67 - <200(723.052ms)
68 - <
69 - Access-Control-Allow-Origin:*
70 - Access-Control-Expose-Headers:ETag, Link, X-GitHub-OTP, X-RateLimit-Limit...
71 - Cache-Control:public, max-age=60, s-maxage=60
72 - Content-Length:137695
73 - ...HEADERS...
74 - <[{"id":48745642,"name":"apns4erl","full_name":"zhongwencool/apns4erl","owner":
75 - ...BODY...
76 - ...
61 + 22:23:42.307 [info] get https://api.github.com <<<200(3085.772ms)
62 + %Maxwell.Conn{method: :get, opts: [connect_timeout: 3000, recv_timeout: 20000]
63 + ...(truncated)
64 +
77 65 ```
78 66 if you don't want to defined a client module:
79 67 ```ex
80 - iex(2)> Maxwell.new("http://httpbin.org/drip") |> Maxwell.put_querystring(%{numbytes: 25, duration: 1, delay: 1, code: 200}) |> Maxwell.get
68 + iex(2)> Maxwell.Conn.new("http://httpbin.org/drip") |> Maxwell.Conn.put_query_string(%{numbytes: 25, duration: 1, delay: 1, code: 200}) |> Maxwell.get
81 69 {:ok,
82 - %Maxwell{body: '*************************',
83 - headers: %{'Access-Control-Allow-Credentials' => 'true',
84 - 'Access-Control-Allow-Origin' => '*', 'Connection' => 'keep-alive',
85 - 'Content-Length' => '25', 'Content-Type' => 'application/octet-stream',
86 - 'Date' => 'Thu, 24 Nov 2016 16:04:21 GMT', 'Server' => 'nginx'},
87 - method: :get, opts: [], status: 200,
88 - url: "http://httpbin.org/drip?code=200&delay=1&duration=1&numbytes=25"}}
70 + %Maxwell.Conn{method: :get, opts: [], path: "",
71 + query_string: %{code: 200, delay: 1, duration: 1, numbytes: 25},
72 + req_body: nil, req_headers: %{}, resp_body: '*************************',
73 + resp_headers: %{"access-control-allow-credentials" => {"access-control-allow-credentials",
74 + "true"},
75 + "access-control-allow-origin" => {"access-control-allow-origin", "*"},
76 + "connection" => {"connection", "keep-alive"},
77 + "content-length" => {"content-length", "25"},
78 + "content-type" => {"content-type", "application/octet-stream"},
79 + "date" => {"date", "Sun, 18 Dec 2016 14:32:38 GMT"},
80 + "server" => {"server", "nginx"}}, state: :sent, status: 200,
81 + url: "http://httpbin.org/drip"}}
89 82 ```
90 - ### Request helper functions
83 + ### Maxwell.Conn helper functions
91 84 ```ex
92 85 new(request_url_string)
93 - |> put_querystring(request_query_map)
94 - |> put_req_headers(request_headers_map)
86 + |> put_query_string(request_query_map)
87 + |> put_req_header(request_headers_map)
95 88 |> put_option(request_opts_keyword_list)
96 89 |> put_req_body(request_body_term)
97 90 |> YourClient.{http_method}!
91 + |> get_resp_body
98 92 ```
99 - For more examples see `h Maxwell.Conn`
93 + For more examples see `h Maxwell.Conn.XXX`
100 94
101 95 ## Response result
102 96 ```ex
  @@ -105,12 +99,93 @@ For more examples see `h Maxwell.Conn`
105 99 resp_headers: reponse_headers_map,
106 100 status: reponse_http_status_integer,
107 101 resp_body: reponse_body_term,
108 - opts: request_opts_keyword_list,
109 102 url: request_urlwithquery_string,
110 103 }}
111 104
112 105 # or
113 - {:error, reason_term}
106 + {:error, reason_term, conn}
107 +
108 + ```
109 +
110 + ## Request Examples
111 + ```ex
112 + defmodule Client do
113 + #generate 4 function get/1, get!/1 post/1 post!/1 function
114 + use Maxwell.Builder, ~w(get post)a
115 +
116 + middleware Maxwell.Middleware.BaseUrl, "http://httpbin.org"
117 + middleware Maxwell.Middleware.Headers, %{"Content-Type" => "application/json"}
118 + middleware Maxwell.Middleware.Opts, [connect_timeout: 5000, recv_timeout: 10000]
119 + middleware Maxwell.Middleware.Json
120 + middleware Maxwell.Middleware.Logger
121 +
122 + adapter Maxwell.Adapter.Hackney
123 +
124 + @doc """
125 + Simple get request
126 + Get origin ip
127 + """
128 + def get_ip() do
129 + new
130 + |> put_path("/ip")
131 + |> get!
132 + |> get_resp_body("origin")
133 + end
134 +
135 + @doc """
136 + Post whole file once
137 + ###Example
138 + Client.post_file_once("./mix.exs")
139 + """
140 + def post_file_once(filepath) do
141 + new
142 + |> put_path("/post")
143 + |> put_req_body({:file, filepath})
144 + |> post!
145 + |> get_resp_body("data")
146 + end
147 +
148 + @doc """
149 + Post whole file by chunked
150 + ###Example
151 + Client.post_file_chunked("./mix.exs")
152 + """
153 + def post_file_chunked(filepath) do
154 + new
155 + |> put_path("/post")
156 + |> put_req_header("transfer_encoding", "chunked")
157 + |> put_req_body({:file, filepath})
158 + |> post!
159 + |> get_resp_body("data")
160 + end
161 +
162 + @doc """
163 + Post by stream
164 + ###Example
165 + ["1", "2", "3"] |> Stream.map(fn(x) -> List.duplicate(x, 2) end) |> Client.post_stream
166 + """
167 + def post_stream(stream) do
168 + new
169 + |> put_path("/post")
170 + |> put_req_body(stream)
171 + |> post!
172 + |> get_resp_body("data")
173 + end
174 +
175 + @doc """
176 + Post multipart form
177 + ###Example
178 + Client.post_multipart_form({:multipart, [{:file, "./mix.exs"}]})
179 + """
180 + def post_multipart_form(multipart) do
181 + new
182 + |> put_path("/post")
183 + |> put_req_body(multipart)
184 + |> post!
185 + |> get_resp_body("data")
186 + end
187 +
188 + end
114 189
115 190 ```
116 191
  @@ -119,25 +194,37 @@ For more examples see `h Maxwell.Conn`
119 194 1. Add maxwell to your list of dependencies in `mix.exs`:
120 195 ```ex
121 196 def deps do
122 - [{:maxwell, "~> 2.0.0"}]
197 + [{:maxwell, "~> 2.1.0"}]
123 198 end
124 199 ```
125 200 2. Ensure maxwell has started before your application:
126 201 ```ex
127 202 def application do
128 - [applications: [:maxwell]] # **also add your adapter(ibrowse,hackney...) here **
203 + [applications: [:maxwell]] # **also add your adapter(ibrowse,hackney) here **
129 204 end
130 205 ```
131 206 ## Adapters
132 207
133 208 Maxwell has support for different adapters that do the actual HTTP request processing.
134 209
210 + ### httpc
211 +
212 + Maxwell has built-in support for [httpc](http://erlang.org/doc/man/httpc.html) Erlang HTTP client.
213 +
214 + To use it simply include `adapter Maxwell.Adapter.Httpc` line in your API client definition.
215 + Setting global default adapter
216 +
217 + ```ex
218 + config :maxwell,
219 + default_adapter: Maxwell.Adapter.Httpc
220 + ```
221 +
135 222 ### ibrowse
136 223
137 224 Maxwell has built-in support for [ibrowse](https://github.com/cmullaparthi/ibrowse) Erlang HTTP client.
138 225
139 226 To use it simply include `adapter Maxwell.Adapter.Ibrowse` line in your API client definition.
140 - global default adapter
227 + Setting global default adapter
141 228
142 229 ```ex
143 230 config :maxwell,
  @@ -150,7 +237,7 @@ NOTE: Remember to include `:ibrowse` in applications list.
150 237 Maxwell has built-in support for [hackney](https://github.com/benoitc/hackney) Erlang HTTP client.
151 238
152 239 To use it simply include `adapter Maxwell.Adapter.Hackney` line in your API client definition.
153 - global default adapter
240 + Setting global default adapter
154 241
155 242 ```ex
156 243 config :maxwell,
  @@ -182,10 +269,6 @@ See [Maxwell.Middleware.BaseUrl](https://github.com/zhongwencool/maxwell/blob/ma
182 269
183 270 ## TODO
184 271
185 - * Support stream
186 - * Support multipart
187 - * More clear readme
188 -
189 272 ## Test
190 273 ```ex
191 274 mix test
  @@ -1,10 +1,11 @@
1 1 {<<"app">>,<<"maxwell">>}.
2 2 {<<"build_tools">>,[<<"mix">>]}.
3 3 {<<"description">>,<<"Maxwell is an HTTP client adapter.">>}.
4 - {<<"elixir">>,<<"~> 1.2">>}.
4 + {<<"elixir">>,<<"~> 1.3">>}.
5 5 {<<"files">>,
6 6 [<<"lib/LICENSE">>,<<"lib/maxwell.ex">>,<<"lib/maxwell/adapter/adapter.ex">>,
7 - <<"lib/maxwell/adapter/hackney.ex">>,<<"lib/maxwell/adapter/ibrowse.ex">>,
7 + <<"lib/maxwell/adapter/hackney.ex">>,<<"lib/maxwell/adapter/httpc.ex">>,
8 + <<"lib/maxwell/adapter/ibrowse.ex">>,<<"lib/maxwell/adapter/util.ex">>,
8 9 <<"lib/maxwell/builder.ex">>,<<"lib/maxwell/builder/adapter.ex">>,
9 10 <<"lib/maxwell/builder/middleware.ex">>,<<"lib/maxwell/builder/util.ex">>,
10 11 <<"lib/maxwell/conn.ex">>,<<"lib/maxwell/error.ex">>,
  @@ -13,7 +14,8 @@
13 14 <<"lib/maxwell/middleware/logger.ex">>,
14 15 <<"lib/maxwell/middleware/middleware.ex">>,
15 16 <<"lib/maxwell/middleware/opts.ex">>,<<"lib/maxwell/middleware/rels.ex">>,
16 - <<"lib/maxwell/multipart.ex">>,<<"LICENSE">>,<<"mix.exs">>,<<"README.md">>]}.
17 + <<"lib/maxwell/multipart.ex">>,<<"lib/test.ex">>,<<"LICENSE">>,
18 + <<"mix.exs">>,<<"README.md">>]}.
17 19 {<<"licenses">>,[<<"MIT">>]}.
18 20 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/zhongwencool/maxwell">>}]}.
19 21 {<<"maintainers">>,[<<"zhongwencool">>]}.
  @@ -35,4 +37,4 @@
35 37 {<<"name">>,<<"hackney">>},
36 38 {<<"optional">>,true},
37 39 {<<"requirement">>,<<"~> 1.6">>}]]}.
38 - {<<"version">>,<<"2.0.0">>}.
40 + {<<"version">>,<<"2.1.0">>}.
  @@ -5,14 +5,14 @@ defmodule Maxwell do
5 5 There are two kind of usages: Basic Usage and Advanced Middleware Usage.
6 6
7 7 ### Basic Usage
8 - ```ex
9 - ## Returns Origin IP, for example %{"origin" => "127.0.0.1"}
10 - Maxwell.Conn.new
11 - |> Maxwell.Conn.put_url("http://httpbin.org/ip")
12 - |> Maxwell.get!
13 - |> Maxwell.get_resp_body
14 - |> Poison.decode!
15 - ```
8 +
9 + ## Returns Origin IP, for example %{"origin" => "127.0.0.1"}
10 + Maxwell.Conn.new
11 + |> Maxwell.Conn.put_url("http://httpbin.org/ip")
12 + |> Maxwell.get!
13 + |> Maxwell.Conn.get_resp_body
14 + |> Poison.decode!
15 +
16 16 Find all `get_*&put_*` helper functions by `h Maxwell.Conn.xxx`
17 17
18 18 ### Advanced Middleware Usage(Create API Client).
  @@ -1,11 +1,82 @@
1 1 defmodule Maxwell.Adapter do
2 2 @moduledoc """
3 - Define adapter behaviour
4 - ## Examples
5 - See `Maxwell.Adapter.Ibrowse`
3 + Define adapter behaviour.
4 +
5 + ### Examples
6 + See `Maxwell.Adapter.Ibrowse`.
6 7 """
8 +
9 + defmacro __using__(_opts) do
10 + quote location: :keep do
11 + @behaviour Maxwell.Adapter
12 +
13 + alias Maxwell.Conn
14 + alias Maxwell.Adapter.Util
15 +
16 + @doc false
17 + def call(conn) do
18 + case conn.req_body do
19 + {:multipart, _} -> send_multipart(conn)
20 + {:file, _} -> send_file(conn)
21 + %Stream{} -> send_stream(conn)
22 + _ -> send_direct(conn)
23 + end
24 + end
25 +
26 + @doc """
27 + Send request without chang it's body formant.
28 +
29 + * `conn` - `%Maxwell.Conn{}`
30 +
31 + Returns `{:ok, %Maxwell.Conn{}}` or `{:error, reason_term, %Maxwell.Conn{}}`.
32 + """
33 + def send_direct(conn) do
34 + raise Maxwell.Error, {__MODULE__, "#{__MODULE__} Adapter doesn't implement send_direct/1", conn}
35 + end
36 +
37 + @doc """
38 + Send multipart form request.
39 +
40 + * `conn` - `%Maxwell.Conn{}`, the req_body is `{:multipart, form_list}`
41 + see `Maxwell.Multipart.encode_form/2` for form_list
42 +
43 + Returns `{:ok, %Maxwell.Conn{}}` or `{:error, reason_term, %Maxwell.Conn{}}`.
44 + """
45 + def send_multipart(conn) do
46 + raise Maxwell.Error, {__MODULE__, "#{__MODULE__} Adapter doesn't implement send_multipart/1", conn}
47 + end
48 +
49 + @doc """
50 + Send file request.
51 +
52 + * `conn` - `%Maxwell.Conn{}`, the req_body is `{:file, filepath}`.
53 + Auto change to chunked mode if req_headers has `%{"transfer-encoding" => "chunked"`
54 +
55 + Returns `{:ok, %Maxwell.Conn{}}` or `{:error, reason_term, %Maxwell.Conn{}}`.
56 + """
57 + def send_file(conn) do
58 + raise Maxwell.Error, {__MODULE__, "#{__MODULE__} Adapter doesn't implement send_file/1", conn}
59 + end
60 +
61 + @doc """
62 + Send stream request.
63 +
64 + * `conn` - `%Maxwell.Conn{}`, the req_body is `Stream`.
65 + Always chunked mode
66 +
67 + Returns `{:ok, %Maxwell.Conn{}}` or `{:error, reason_term, %Maxwell.Conn{}}`.
68 + """
69 + def send_stream(conn) do
70 + raise Maxwell.Error, {__MODULE__, "#{__MODULE__} Adapter doesn't implement send_stream/1", conn}
71 + end
72 +
73 + defoverridable [send_direct: 1, send_multipart: 1, send_file: 1, send_stream: 1] end
74 + end
75 +
7 76 @type return_t :: {:ok, Maxwell.Conn.t} | {:error, any, Maxwell.Conn.t}
8 - @callback call(Maxwell.Conn.t) :: return_t
77 + @callback send_direct(Maxwell.Conn.t) :: return_t
78 + @callback send_multipart(Maxwell.Conn.t) :: return_t
79 + @callback send_file(Maxwell.Conn.t) :: return_t
9 80
10 81 end
  @@ -1,45 +1,53 @@
1 1 if Code.ensure_loaded?(:hackney) do
2 2 defmodule Maxwell.Adapter.Hackney do
3 - @behaviour Maxwell.Adapter
4 3 @moduledoc """
5 4 [`hackney`](https://github.com/benoitc/hackney) adapter
6 5 """
6 + use Maxwell.Adapter
7 7
8 - @doc """
9 - * `conn` - `%Maxwell.Conn{}`
10 -
11 - Returns `{:ok, %Maxwell.Conn{}}` or `{:error, reason_term, %Maxwell.Conn{}}`.
12 - """
13 - def call(conn) do
14 - conn
15 - |> send_req
16 - |> format_response(conn)
8 + def send_direct(conn) do
9 + %Conn{url: url, req_headers: req_headers,
10 + path: path,method: method, query_string: query_string,
11 + opts: opts, req_body: req_body} = conn
12 + url = Util.url_serialize(url, path, query_string)
13 + req_headers = Util.header_serialize(req_headers)
14 + result = :hackney.request(method, url, req_headers, req_body || "", opts)
15 + format_response(result, conn)
17 16 end
18 17
19 - defp send_req(%Maxwell.Conn{url: url, req_headers: req_headers,
20 - path: path,method: method, query_string: query_string,
21 - opts: opts, req_body: req_body}) do
22 - req_headers = req_headers |> Map.to_list
23 - req_body = req_body || ""
24 - url = url |> Maxwell.Conn.append_query_string(path, query_string)
25 - :hackney.request(method, url, req_headers, req_body, opts)
18 + def send_multipart(conn), do: send_direct(conn)
19 +
20 + def send_file(conn), do: send_direct(conn)
21 +
22 + def send_stream(conn) do
23 + %Conn{url: url, req_headers: req_headers,
24 + path: path,method: method, query_string: query_string,
25 + opts: opts, req_body: req_body} = conn
26 + url = Util.url_serialize(url, path, query_string)
27 + req_headers = Util.header_serialize(req_headers)
28 + with {:ok, ref} <- :hackney.request(method, url, req_headers, :stream, opts) do
29 + for data <- req_body, do: :ok = :hackney.send_body(ref, data)
30 + ref |> :hackney.start_response |> format_response(conn)
31 + else
32 + error -> format_response(error, conn)
33 + end
26 34 end
27 35
28 36 defp format_response({:ok, status, headers, body}, conn) when is_binary(body) do
29 - {:ok, %{conn | status: status,
30 - resp_headers: headers|> :maps.from_list,
31 - req_body: nil,
32 - state: :sent,
37 + headers = for {key, value} <- headers, into: %{} do
38 + down_key = key |> to_string |> String.downcase
39 + {down_key, {key, to_string(value)}}
40 + end
41 + {:ok, %{conn | status: status,
42 + resp_headers: headers,
43 + req_body: nil,
44 + state: :sent,
33 45 resp_body: body}}
34 46 end
35 47 defp format_response({:ok, status, headers, body}, conn) do
36 - with {:ok, body} <- :hackney.body(body) do
37 - {:ok,
38 - %{conn |status: status,
39 - resp_headers: headers|> :maps.from_list,
40 - req_body: nil,
41 - state: :sent,
42 - resp_body: body}}
48 + case :hackney.body(body) do
49 + {:ok, body} -> format_response({:ok, status, headers, body}, conn)
50 + {:error, _reason} = error -> format_response(error, conn)
43 51 end
44 52 end
45 53 defp format_response({:ok, status, headers}, conn) do
Loading more files…