Permalink
Cannot retrieve contributors at this time
defmodule Maxwell.Conn do | |
@moduledoc """ | |
The Maxwell connection. | |
This module defines a `Maxwell.Conn` struct and the main functions | |
for working with Maxwell connections. | |
### Request fields | |
These fields contain request information: | |
* `url` - the requested url as a binary, example: `"www.example.com:8080/path/?foo=bar"`. | |
* `method` - the request method as a atom, example: `GET`. | |
* `req_headers` - the request headers as a map, example: `%{"content-type" => "text/plain"}`. | |
* `req_body` - the request body, by default is an empty string. It is set | |
to nil after the request is set. | |
### Response fields | |
These fields contain response information: | |
* `status` - the response status | |
* `resp_headers` - the response headers as a map. | |
* `resp_body` - the response body (todo desc). | |
### Connection fields | |
* `state` - the connection state | |
The connection state is used to track the connection lifecycle. It starts | |
as `:unsent` but is changed to `:sending`, Its final result is `:sent` or `:error`. | |
### Protocols | |
`Maxwell.Conn` implements Inspect protocols out of the box. | |
The inspect protocol provides a nice representation of the connection. | |
""" | |
@type file_body_t :: {:file, Path.t()} | |
@type t :: %__MODULE__{ | |
state: :unsent | :sending | :sent | :error, | |
method: atom, | |
url: String.t(), | |
path: String.t(), | |
query_string: map, | |
opts: Keyword.t(), | |
req_headers: %{binary => binary}, | |
req_body: iodata | map | Maxwell.Multipart.t() | file_body_t | Enumerable.t(), | |
status: non_neg_integer | nil, | |
resp_headers: %{binary => binary}, | |
resp_body: iodata | map, | |
private: map | |
} | |
defstruct state: :unsent, | |
method: nil, | |
url: "", | |
path: "", | |
query_string: %{}, | |
req_headers: %{}, | |
req_body: nil, | |
opts: [], | |
status: nil, | |
resp_headers: %{}, | |
resp_body: "", | |
private: %{} | |
alias Maxwell.{Conn, Query} | |
defmodule AlreadySentError do | |
@moduledoc """ | |
Error raised when trying to modify or send an already sent request | |
""" | |
defexception message: "the request was already sent" | |
end | |
defmodule NotSentError do | |
@moduledoc """ | |
Error raised when no request is sent in a connection | |
""" | |
defexception message: "the request was not sent yet" | |
end | |
@doc """ | |
Create a new connection. | |
The url provided will be parsed by `URI.parse/1`, and the relevant connection fields will | |
be set accordingly. | |
### Examples | |
iex> new() | |
%Maxwell.Conn{} | |
iex> new("http://example.com/foo") | |
%Maxwell.Conn{url: "http://example.com", path: "/foo"} | |
iex> new("http://example.com/foo?bar=qux") | |
%Maxwell.Conn{url: "http://example.com", path: "/foo", query_string: %{"bar" => "qux"}} | |
""" | |
@spec new() :: t | |
def new(), do: %Conn{} | |
@spec new(binary) :: t | |
def new(url) when is_binary(url) do | |
%URI{scheme: scheme, path: path, query: query} = uri = URI.parse(url) | |
scheme = scheme || "http" | |
path = path || "" | |
conn = | |
case uri do | |
%URI{host: nil} -> | |
# This is a badly formed URI, so we'll do best effort: | |
cond do | |
# example.com:8080 | |
scheme != nil and Integer.parse(path) != :error -> | |
%Conn{url: "http://#{scheme}:#{path}"} | |
# example.com | |
String.contains?(path, ".") -> | |
%Conn{url: "#{scheme}://#{path}"} | |
# special case for localhost | |
path == "localhost" -> | |
%Conn{url: "#{scheme}://localhost"} | |
# /example - not a valid hostname, assume it's a path | |
String.starts_with?(path, "/") -> | |
%Conn{path: path} | |
# example - not a valid hostname, assume it's a path | |
true -> | |
%Conn{path: "/" <> path} | |
end | |
%URI{userinfo: nil, scheme: "http", port: 80, host: host} -> | |
%Conn{url: "http://#{host}", path: path} | |
%URI{userinfo: nil, scheme: "https", port: 443, host: host} -> | |
%Conn{url: "https://#{host}", path: path} | |
%URI{userinfo: nil, port: port, host: host} -> | |
%Conn{url: "#{scheme}://#{host}:#{port}", path: path} | |
%URI{userinfo: userinfo, port: port, host: host} -> | |
%Conn{url: "#{scheme}://#{userinfo}@#{host}:#{port}", path: path} | |
end | |
case is_nil(query) do | |
true -> conn | |
false -> put_query_string(conn, Query.decode(query)) | |
end | |
end | |
@doc """ | |
Set the path of the request. | |
### Examples | |
iex> put_path(new(), "delete") | |
%Maxwell.Conn{path: "delete"} | |
""" | |
@spec put_path(t, String.t()) :: t | no_return | |
def put_path(%Conn{state: :unsent} = conn, path), do: %{conn | path: path} | |
def put_path(_conn, _path), do: raise(AlreadySentError) | |
@doc false | |
def put_path(path) when is_binary(path) do | |
IO.warn("put_path/1 is deprecated, use new/1 or new/2 followed by put_path/2 instead") | |
put_path(new(), path) | |
end | |
@doc """ | |
Add query string to `conn.query_string`. | |
* `conn` - `%Conn{}` | |
* `query_map` - as map, for example `%{foo => bar}` | |
### Examples | |
# %Conn{query_string: %{name: "zhong wen"}} | |
put_query_string(%Conn{}, %{name: "zhong wen"}) | |
""" | |
@spec put_query_string(t, map()) :: t | no_return | |
def put_query_string(%Conn{state: :unsent, query_string: qs} = conn, query) do | |
%{conn | query_string: Map.merge(qs, query)} | |
end | |
def put_query_string(_conn, _query_map), do: raise(AlreadySentError) | |
@doc false | |
def put_query_string(query) when is_map(query) do | |
IO.warn( | |
"put_query_string/1 is deprecated, use new/1 or new/2 followed by put_query_string/2 instead" | |
) | |
put_query_string(new(), query) | |
end | |
@doc """ | |
Set a query string value for the request. | |
### Examples | |
iex> put_query_string(new(), :name, "zhong wen") | |
%Maxwell.Conn{query_string: %{:name => "zhong wen"}} | |
""" | |
def put_query_string(%Conn{state: :unsent, query_string: qs} = conn, key, value) do | |
%{conn | query_string: Map.put(qs, key, value)} | |
end | |
def put_query_string(_conn, _key, _value), do: raise(AlreadySentError) | |
@doc """ | |
Merge a map of headers into the existing headers of the connection. | |
### Examples | |
iex> %Maxwell.Conn{headers: %{"content-type" => "text/javascript"} | |
|> put_req_headers(%{"Accept" => "application/json"}) | |
%Maxwell.Conn{req_headers: %{"accept" => "application/json", "content-type" => "text/javascript"}} | |
""" | |
@spec put_req_headers(t, map()) :: t | no_return | |
def put_req_headers(%Conn{state: :unsent, req_headers: headers} = conn, extra_headers) | |
when is_map(extra_headers) do | |
new_headers = | |
extra_headers | |
|> Enum.reduce(headers, fn {header_name, header_value}, acc -> | |
Map.put(acc, String.downcase(header_name), header_value) | |
end) | |
%{conn | req_headers: new_headers} | |
end | |
def put_req_headers(_conn, _headers), do: raise(AlreadySentError) | |
# TODO: Remove | |
@doc false | |
def put_req_header(headers) do | |
IO.warn( | |
"put_req_header/1 is deprecated, use new/1 or new/2 followed by put_req_headers/2 instead" | |
) | |
put_req_headers(new(), headers) | |
end | |
# TODO: Remove | |
@doc false | |
def put_req_header(conn, headers) when is_map(headers) do | |
IO.warn("put_req_header/2 is deprecated, use put_req_headers/1 instead") | |
put_req_headers(conn, headers) | |
end | |
@doc """ | |
Set a request header. If it already exists, it is updated. | |
### Examples | |
iex> %Maxwell.Conn{req_headers: %{"content-type" => "text/javascript"}} | |
|> put_req_header("Content-Type", "application/json") | |
|> put_req_header("User-Agent", "zhongwencool") | |
%Maxwell.Conn{req_headers: %{"content-type" => "application/json", "user-agent" => "zhongwenool"} | |
""" | |
def put_req_header(%Conn{state: :unsent, req_headers: headers} = conn, key, value) do | |
new_headers = Map.put(headers, String.downcase(key), value) | |
%{conn | req_headers: new_headers} | |
end | |
def put_req_header(_conn, _key, _value), do: raise(AlreadySentError) | |
@doc """ | |
Get all request headers as a map | |
### Examples | |
iex> %Maxwell.Conn{req_headers: %{"cookie" => "xyz"} |> get_req_header | |
%{"cookie" => "xyz"} | |
""" | |
@spec get_req_header(t) :: %{String.t() => String.t()} | |
def get_req_headers(%Conn{req_headers: headers}), do: headers | |
# TODO: Remove | |
@doc false | |
def get_req_header(conn) do | |
IO.warn("get_req_header/1 is deprecated, use get_req_headers/1 instead") | |
get_req_headers(conn) | |
end | |
@doc """ | |
Get a request header by key. The key lookup is case-insensitive. | |
Returns the value as a string, or nil if it doesn't exist. | |
### Examples | |
iex> %Maxwell.Conn{req_headers: %{"cookie" => "xyz"} |> get_req_header("cookie") | |
"xyz" | |
""" | |
@spec get_req_header(t, String.t()) :: String.t() | nil | |
def get_req_header(conn, nil) do | |
IO.warn("get_req_header/2 with a nil key is deprecated, use get_req_headers/2 instead") | |
get_req_headers(conn) | |
end | |
def get_req_header(%Conn{req_headers: headers}, key), do: Map.get(headers, String.downcase(key)) | |
@doc """ | |
Set adapter options for the request. | |
### Examples | |
iex> put_options(new(), connect_timeout: 4000) | |
%Maxwell.Conn{opts: [connect_timeout: 4000]} | |
""" | |
@spec put_options(t, Keyword.t()) :: t | no_return | |
def put_options(%Conn{state: :unsent, opts: opts} = conn, extra_opts) | |
when is_list(extra_opts) do | |
%{conn | opts: Keyword.merge(opts, extra_opts)} | |
end | |
def put_options(_conn, extra_opts) when is_list(extra_opts), do: raise(AlreadySentError) | |
@doc """ | |
Set an adapter option for the request. | |
### Examples | |
iex> put_option(new(), :connect_timeout, 5000) | |
%Maxwell.Conn{opts: [connect_timeout: 5000]} | |
""" | |
@spec put_option(t, atom(), term()) :: t | no_return | |
def put_option(%Conn{state: :unsent, opts: opts} = conn, key, value) when is_atom(key) do | |
%{conn | opts: [{key, value} | opts]} | |
end | |
def put_option(%Conn{}, key, _value) when is_atom(key), do: raise(AlreadySentError) | |
# TODO: remove | |
@doc false | |
def put_option(opts) when is_list(opts) do | |
IO.warn("put_option/1 is deprecated, use new/1 or new/2 followed by put_options/2 instead") | |
put_options(new(), opts) | |
end | |
# TODO: remove | |
@doc false | |
def put_option(conn, opts) when is_list(opts) do | |
IO.warn("put_option/2 is deprecated, use put_options/2 instead") | |
put_options(conn, opts) | |
end | |
@doc """ | |
Set the request body. | |
### Examples | |
iex> put_req_body(new(), "new body") | |
%Maxwell.Conn{req_body: "new_body"} | |
""" | |
@spec put_req_body(t, Enumerable.t() | binary()) :: t | no_return | |
def put_req_body(%Conn{state: :unsent} = conn, req_body) do | |
%{conn | req_body: req_body} | |
end | |
def put_req_body(_conn, _req_body), do: raise(AlreadySentError) | |
# TODO: remove | |
@doc false | |
def put_req_body(body) do | |
IO.warn("put_req_body/1 is deprecated, use new/1 or new/2 followed by put_req_body/2 instead") | |
put_req_body(new(), body) | |
end | |
@doc """ | |
Get response status. | |
Raises `Maxwell.Conn.NotSentError` when the request is unsent. | |
### Examples | |
iex> get_status(%Maxwell.Conn{status: 200}) | |
200 | |
""" | |
@spec get_status(t) :: pos_integer | no_return | |
def get_status(%Conn{status: status, state: state}) when state !== :unsent, do: status | |
def get_status(_conn), do: raise(NotSentError) | |
@doc """ | |
Get all response headers as a map. | |
### Examples | |
iex> %Maxwell.Conn{resp_headers: %{"cookie" => "xyz"} |> get_resp_header | |
%{"cookie" => "xyz"} | |
""" | |
@spec get_resp_headers(t) :: %{String.t() => String.t()} | no_return | |
def get_resp_headers(%Conn{state: :unsent}), do: raise(NotSentError) | |
def get_resp_headers(%Conn{resp_headers: headers}), do: headers | |
# TODO: remove | |
@doc false | |
def get_resp_header(conn) do | |
IO.warn("get_resp_header/1 is deprecated, use get_resp_headers/1 instead") | |
get_resp_headers(conn) | |
end | |
@doc """ | |
Get a response header by key. | |
The value is returned as a string, or nil if the header is not set. | |
### Examples | |
iex> %Maxwell.Conn{resp_headers: %{"cookie" => "xyz"}} |> get_resp_header("cookie") | |
"xyz" | |
""" | |
@spec get_resp_header(t, String.t()) :: String.t() | nil | no_return | |
def get_resp_header(%Conn{state: :unsent}, _key), do: raise(NotSentError) | |
# TODO: remove | |
def get_resp_header(conn, nil) do | |
IO.warn("get_resp_header/2 with a nil key is deprecated, use get_resp_headers/1 instead") | |
get_resp_headers(conn) | |
end | |
def get_resp_header(%Conn{resp_headers: headers}, key), | |
do: Map.get(headers, String.downcase(key)) | |
@doc """ | |
Return the response body. | |
### Examples | |
iex> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: "best http client"}) | |
"best http client" | |
""" | |
@spec get_resp_body(t) :: binary() | map() | no_return | |
def get_resp_body(%Conn{state: :sent, resp_body: body}), do: body | |
def get_resp_body(_conn), do: raise(NotSentError) | |
@doc """ | |
Return a value from the response body by key or with a parsing function. | |
### Examples | |
iex> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: %{"name" => "xyz"}}, "name") | |
"xyz" | |
iex> func = fn(x) -> | |
...> [key, value] = String.split(x, ":") | |
...> value | |
...> end | |
...> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: "name:xyz"}, func) | |
"xyz" | |
""" | |
def get_resp_body(%Conn{state: state}, _) when state != :sent, do: raise(NotSentError) | |
def get_resp_body(%Conn{resp_body: body}, func) when is_function(func, 1), do: func.(body) | |
def get_resp_body(%Conn{resp_body: body}, keys) when is_list(keys), do: get_in(body, keys) | |
def get_resp_body(%Conn{resp_body: body}, key), do: body[key] | |
@doc """ | |
Set a private value. If it already exists, it is updated. | |
### Examples | |
iex> %Maxwell.Conn{private: %{}} | |
|> put_private(:user_id, "zhongwencool") | |
%Maxwell.Conn{private: %{user_id: "zhongwencool"}} | |
""" | |
@spec put_private(t, atom, term()) :: t | |
def put_private(%Conn{private: private} = conn, key, value) do | |
new_private = Map.put(private, key, value) | |
%{conn | private: new_private} | |
end | |
@doc """ | |
Get a private value | |
### Examples | |
iex> %Maxwell.Conn{private: %{user_id: "zhongwencool"}} | |
|> get_private(:user_id) | |
"zhongwencool" | |
""" | |
@spec get_private(t, atom) :: term() | |
def get_private(%Conn{private: private}, key) do | |
Map.get(private, key) | |
end | |
defimpl Inspect, for: Conn do | |
def inspect(conn, opts) do | |
Inspect.Any.inspect(conn, opts) | |
end | |
end | |
end |