Page MenuHomePhorge

No OneTemporary

Size
60 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla.ex b/lib/tesla.ex
index ab9aa37..2c9ddf0 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,515 +1,523 @@
defmodule Tesla.Error do
defexception message: "", reason: nil
end
defmodule Tesla.Env do
@type client :: Tesla.Client.t() | (t, stack -> t)
@type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch
@type url :: binary
@type param :: binary | [{binary | atom, param}]
@type query :: [{binary | atom, param}]
@type headers :: %{binary => binary}
#
@type body :: any
@type status :: integer
@type opts :: [any]
@type __module__ :: atom
@type __client__ :: function
@type stack :: [{atom, atom, any} | {atom, atom} | {:fn, (t -> t)} | {:fn, (t, stack -> t)}]
@type t :: %__MODULE__{
method: method,
query: query,
url: url,
headers: headers,
body: body,
status: status,
opts: opts,
__module__: __module__,
__client__: __client__
}
defstruct method: nil,
url: "",
query: [],
headers: %{},
body: nil,
status: nil,
opts: [],
__module__: nil,
__client__: nil
end
defmodule Tesla.Client do
@type t :: %__MODULE__{
fun: (Tesla.Env.t(), Tesla.Env.stack() -> Tesla.Env.t()) | nil,
pre: Tesla.Env.stack(),
post: Tesla.Env.stack()
}
defstruct fun: nil,
pre: [],
post: []
end
defmodule Tesla.Middleware do
@callback call(env :: Tesla.Env.t(), next :: Tesla.Env.stack(), options :: any) :: Tesla.Env.t()
end
defmodule Tesla.Builder do
@http_verbs ~w(head get delete trace options post put patch)a
defmacro __using__(opts \\ []) do
opts = Macro.prewalk(opts, &Macro.expand(&1, __CALLER__))
docs = Keyword.get(opts, :docs, true)
quote do
Module.register_attribute(__MODULE__, :__middleware__, accumulate: true)
Module.register_attribute(__MODULE__, :__adapter__, [])
if unquote(docs) do
@type option ::
{:method, Tesla.Env.method()}
| {:url, Tesla.Env.url()}
| {:query, Tesla.Env.query()}
| {:headers, Tesla.Env.headers()}
| {:body, Tesla.Env.body()}
| {:opts, Tesla.Env.opts()}
@doc """
Perform a request using client function
Options:
- `:method` - the request method, one of [:head, :get, :delete, :trace, :options, :post, :put, :patch]
- `:url` - either full url e.g. "http://example.com/some/path" or just "/some/path" if using `Tesla.Middleware.BaseUrl`
- `:query` - a keyword list of query params, e.g. `[page: 1, per_page: 100]`
- `:headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
- `:body` - depends on used middleware:
- by default it can be a binary
- if using e.g. JSON encoding middleware it can be a nested map
- if adapter supports it it can be a Stream with any of the above
- `:opts` - custom, per-request middleware or adapter options
Examples:
ExampleApi.request(method: :get, url: "/users/path")
You can also use shortcut methods like:
ExampleApi.get("/users/1")
or
myclient |> ExampleApi.post("/users", %{name: "Jon"})
"""
@spec request(Tesla.Env.client(), [option]) :: Tesla.Env.t()
else
@doc false
end
def request(%Tesla.Client{} = client, options) do
Tesla.perform_request(__MODULE__, client, options)
end
if unquote(docs) do
@doc """
Perform a request. See `request/2` for available options.
"""
@spec request([option]) :: Tesla.Env.t()
else
@doc false
end
def request(options) do
Tesla.perform_request(__MODULE__, options)
end
unquote(generate_http_verbs(opts))
import Tesla.Builder, only: [plug: 1, plug: 2, adapter: 1, adapter: 2]
@before_compile Tesla.Builder
end
end
@doc """
Attach middleware to your API client
```ex
defmodule ExampleApi do
use Tesla
# plug middleware module with options
plug Tesla.Middleware.BaseUrl, "http://api.example.com"
plug Tesla.Middleware.JSON, engine: Poison
# plug middleware function
plug :handle_errors
# middleware function gets two parameters: Tesla.Env and the rest of middleware call stack
# and must return Tesla.Env
def handle_errors(env, next) do
env
|> modify_env_before_request
|> Tesla.run(next) # run the rest of stack
|> modify_env_after_request
end
end
"""
defmacro plug(middleware, opts \\ nil) do
opts = Macro.escape(opts)
quote do: @__middleware__({unquote(middleware), unquote(opts)})
end
@doc """
Choose adapter for your API client
```ex
defmodule ExampleApi do
use Tesla
# set adapter as module
adapter Tesla.Adapter.Hackney
# set adapter as function
adapter :local_adapter
# set adapter as anonymous function
adapter fn env ->
...
env
end
# adapter function gets Tesla.Env as parameter and must return Tesla.Env
def local_adapter(env) do
...
env
end
end
"""
defmacro adapter({:fn, _, _} = adapter) do
adapter = Macro.escape(adapter)
quote do: @__adapter__(unquote(adapter))
end
defmacro adapter(adapter, opts \\ nil) do
quote do: @__adapter__({unquote(adapter), unquote(opts)})
end
defp generate_http_verbs(opts) do
only = Keyword.get(opts, :only, @http_verbs)
except = Keyword.get(opts, :except, [])
@http_verbs
|> Enum.filter(&(&1 in only && not &1 in except))
|> Enum.map(&generate_api(&1, Keyword.get(opts, :docs, true)))
end
defp generate_api(method, docs) when method in [:post, :put, :patch] do
quote do
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url(), Tesla.Env.body(), [option]) ::
Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url, body, options) when is_list(options) do
request(client, [method: unquote(method), url: url, body: body] ++ options)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url, body, options) when is_function(fun) and is_list(options) do
unquote(method)(%Tesla.Client{fun: fun}, url, body, options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url(), Tesla.Env.body()) ::
Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url, body) do
request(client, method: unquote(method), url: url, body: body)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url, body) when is_function(fun) do
unquote(method)(%Tesla.Client{fun: fun}, url, body)
end
if unquote(docs) do
@spec unquote(method)(Tesla.Env.url(), Tesla.Env.body(), [option]) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url, body, options) when is_list(options) do
request([method: unquote(method), url: url, body: body] ++ options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
"""
@spec unquote(method)(Tesla.Env.url(), Tesla.Env.body()) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url, body) do
request(method: unquote(method), url: url, body: body)
end
end
end
defp generate_api(method, docs) when method in [:head, :get, :delete, :trace, :options] do
quote do
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url(), [option]) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url, options) when is_list(options) do
request(client, [method: unquote(method), url: url] ++ options)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url, options) when is_function(fun) and is_list(options) do
unquote(method)(%Tesla.Client{fun: fun}, url, options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
myclient |> ExampleApi.#{unquote(method)}("/users")
ExampleApi.#{unquote(method)}("/users", query: [page: 1])
"""
@spec unquote(method)(Tesla.Env.client(), Tesla.Env.url()) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(%Tesla.Client{} = client, url) do
request(client, method: unquote(method), url: url)
end
# fallback to keep backward compatibility
def unquote(method)(fun, url) when is_function(fun) do
unquote(method)(%Tesla.Client{fun: fun}, url)
end
if unquote(docs) do
@spec unquote(method)(Tesla.Env.url(), [option]) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url, options) when is_list(options) do
request([method: unquote(method), url: url] ++ options)
end
if unquote(docs) do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `request/1` or `request/2` for options definition.
Example
ExampleApi.#{unquote(method)}("/users")
"""
@spec unquote(method)(Tesla.Env.url()) :: Tesla.Env.t()
else
@doc false
end
def unquote(method)(url) do
request(method: unquote(method), url: url)
end
end
end
defmacro __before_compile__(env) do
adapter = Module.get_attribute(env.module, :__adapter__)
middleware = Module.get_attribute(env.module, :__middleware__) |> Enum.reverse()
quote do
def __middleware__, do: unquote(middleware)
def __adapter__, do: Tesla.adapter(__MODULE__, unquote(adapter))
end
end
end
defmodule Tesla do
use Tesla.Builder
@moduledoc """
A HTTP toolkit for building API clients using middlewares
Include Tesla module in your api client:
```ex
defmodule ExampleApi do
use Tesla
plug Tesla.Middleware.BaseUrl, "http://api.example.com"
plug Tesla.Middleware.JSON
end
"""
defmacro __using__(opts \\ []) do
quote do
use Tesla.Builder, unquote(opts)
end
end
def perform_request(module, client \\ nil, options) do
%{fun: fun, pre: pre, post: post} = client || %Tesla.Client{}
stack =
pre ++
prepare(module, List.wrap(fun) ++ module.__middleware__ ++ default_middleware()) ++
post ++ prepare(module, [module.__adapter__])
env = struct(Tesla.Env, options ++ [__module__: module, __client__: client])
run(env, stack)
end
@spec prepare(atom, [any]) :: Tesla.Env.stack()
def prepare(module, stack) do
Enum.map(stack, fn
{name, opts} -> prepare_module(module, name, opts)
name when is_atom(name) -> prepare_module(module, name, nil)
fun when is_function(fun) -> {:fn, fun}
end)
end
defp prepare_module(module, name, opts) do
case Atom.to_charlist(name) do
~c"Elixir." ++ _ -> {name, :call, [opts]}
_ -> {module, name}
end
end
# empty stack case is useful for reusing/testing middlewares (just pass [] as next)
def run(env, []), do: env
# last item in stack is adapter - skip passing rest of stack
def run(env, [{:fn, f}]), do: apply(f, [env])
def run(env, [{m, f}]), do: apply(m, f, [env])
def run(env, [{m, f, a}]), do: apply(m, f, [env | a])
# for all other elements pass (env, next, opts)
def run(env, [{:fn, f} | rest]), do: apply(f, [env, rest])
def run(env, [{m, f} | rest]), do: apply(m, f, [env, rest])
def run(env, [{m, f, a} | rest]), do: apply(m, f, [env, rest | a])
# useful helper fuctions
def put_opt(env, key, value) do
Map.update!(env, :opts, &Keyword.put(&1, key, value))
end
+ def get_header(%Tesla.Env{headers: headers}, key) do
+ headers[key]
+ end
+
+ def put_headers(env, map) when is_map(map) do
+ %{env | headers: Map.merge(env.headers, map)}
+ end
+
def adapter(module, custom) do
module_adapter_from_config(module) || custom || default_adapter()
end
defp module_adapter_from_config(module) do
Application.get_env(:tesla, module, [])[:adapter]
end
def default_adapter do
Application.get_env(:tesla, :adapter, Tesla.Adapter.Httpc)
end
def run_default_adapter(env, opts \\ []) do
apply(default_adapter(), :call, [env, opts])
end
def default_middleware do
[{Tesla.Middleware.Normalize, nil}]
end
@doc """
Dynamically build client from list of middlewares.
```ex
defmodule ExampleAPI do
use Tesla
def new(token) do
Tesla.build_client([
{Tesla.Middleware.Headers, %{"Authorization" => token}}
])
end
end
client = ExampleAPI.new(token: "abc")
client |> ExampleAPI.get("/me")
```
"""
defmacro build_client(pre, post \\ []) do
quote do
%Tesla.Client{
pre: Tesla.prepare(__MODULE__, unquote(pre)),
post: Tesla.prepare(__MODULE__, unquote(post))
}
end
end
def build_adapter(fun) do
%Tesla.Client{post: [{:fn, fn env, _next -> fun.(env) end}]}
end
def build_url(url, []), do: url
def build_url(url, query) do
join = if String.contains?(url, "?"), do: "&", else: "?"
url <> join <> encode_query(query)
end
defp encode_query(query) do
query
|> Enum.flat_map(&encode_pair/1)
|> URI.encode_query()
end
defp encode_pair({key, value}) when is_list(value) do
if Keyword.keyword?(value) do
Enum.flat_map(value, fn {k, v} -> encode_pair({"#{key}[#{k}]", v}) end)
else
Enum.map(value, fn e -> {"#{key}[]", e} end)
end
end
defp encode_pair({key, value}), do: [{key, value}]
end
diff --git a/lib/tesla/adapter/httpc.ex b/lib/tesla/adapter/httpc.ex
index 4f3fe8a..3cc517f 100644
--- a/lib/tesla/adapter/httpc.ex
+++ b/lib/tesla/adapter/httpc.ex
@@ -1,73 +1,73 @@
defmodule Tesla.Adapter.Httpc do
@moduledoc """
Adapter for [httpc](http://erlang.org/doc/man/httpc.html)
This is the default adapter.
**NOTE** Tesla overrides default autoredirect value with false to ensure
consistency between adapters
"""
import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1]
alias Tesla.Multipart
@override_defaults autoredirect: false
@http_opts ~w(timeout connect_timeout ssl essl autoredirect proxy_auth version relaxed url_encode)a
def call(env, opts) do
opts = Keyword.merge(@override_defaults, opts || [])
with {:ok, {status, headers, body}} <- request(env, opts) do
format_response(env, status, headers, body)
end
end
defp format_response(env, {_, status, _}, headers, body) do
%{env | status: status, headers: headers, body: body}
end
defp request(env, opts) do
- content_type = to_charlist(env.headers["content-type"] || "")
+ content_type = to_charlist(Tesla.get_header(env, "content-type") || "")
handle(
request(
env.method || :get,
Tesla.build_url(env.url, env.query) |> to_charlist,
Enum.into(env.headers, [], fn {k, v} -> {to_charlist(k), to_charlist(v)} end),
content_type,
env.body,
Keyword.split(opts ++ env.opts, @http_opts)
)
)
end
defp request(method, url, headers, _content_type, nil, {http_opts, opts}) do
:httpc.request(method, {url, headers}, http_opts, opts)
end
defp request(method, url, headers, _content_type, %Multipart{} = mp, opts) do
headers = headers ++ Multipart.headers(mp)
headers = for {key, value} <- headers, do: {to_charlist(key), to_charlist(value)}
{content_type, headers} = Keyword.pop_first(headers, 'Content-Type', 'text/plain')
body = stream_to_fun(Multipart.body(mp))
request(method, url, headers, to_charlist(content_type), body, opts)
end
defp request(method, url, headers, content_type, %Stream{} = body, opts) do
fun = stream_to_fun(body)
request(method, url, headers, content_type, fun, opts)
end
defp request(method, url, headers, content_type, body, opts) when is_function(body) do
body = {:chunkify, &next_chunk/1, body}
request(method, url, headers, content_type, body, opts)
end
defp request(method, url, headers, content_type, body, {http_opts, opts}) do
:httpc.request(method, {url, headers, content_type, body}, http_opts, opts)
end
defp handle({:error, {:failed_connect, _}}), do: {:error, :econnrefused}
defp handle(response), do: response
end
diff --git a/lib/tesla/middleware/basic_auth.ex b/lib/tesla/middleware/basic_auth.ex
index 360e9d9..9ea8578 100644
--- a/lib/tesla/middleware/basic_auth.ex
+++ b/lib/tesla/middleware/basic_auth.ex
@@ -1,60 +1,60 @@
defmodule Tesla.Middleware.BasicAuth do
@behaviour Tesla.Middleware
@moduledoc """
Basic authentication middleware
[Wiki on the topic](https://en.wikipedia.org/wiki/Basic_access_authentication)
### Example
```
defmodule MyClient do
use Tesla
# static configuration
plug Tesla.Middleware.BasicAuth, username: "user", password: "pass"
# dynamic user & pass
def new(username, password, opts \\\\ %{}) do
Tesla.build_client [
{Tesla.Middleware.BasicAuth, Map.merge(%{username: username, password: password}, opts)}
]
end
end
```
### Options
- `:username` - username (defaults to `""`)
- `:password` - password (defaults to `""`)
"""
def call(env, next, opts) do
opts = opts || %{}
env
- |> Map.update!(:headers, &Map.merge(&1, authorization_header(opts)))
+ |> Tesla.put_headers(authorization_header(opts))
|> Tesla.run(next)
end
defp authorization_header(opts) do
opts
|> authorization_vars()
|> encode()
|> create_header()
end
defp authorization_vars(opts) do
%{
username: opts[:username] || "",
password: opts[:password] || ""
}
end
defp create_header(auth) do
%{"Authorization" => "Basic #{auth}"}
end
defp encode(%{username: username, password: password}) do
Base.encode64("#{username}:#{password}")
end
end
diff --git a/lib/tesla/middleware/compression.ex b/lib/tesla/middleware/compression.ex
index b4a6dba..4263f06 100644
--- a/lib/tesla/middleware/compression.ex
+++ b/lib/tesla/middleware/compression.ex
@@ -1,92 +1,92 @@
defmodule Tesla.Middleware.Compression do
@behaviour Tesla.Middleware
@moduledoc """
Compress requests and decompress responses.
Supports "gizp" and "deflate" encodings using erlang's built-in `:zlib` module.
### Example usage
```
defmodule MyClient do
use Tesla
plug Tesla.Middleware.Compression, format: "gzip"
end
```
### Options
- `:format` - request compression format, `"gzip"` (default) or `"defalte"`
"""
def call(env, next, opts) do
env
|> compress(opts)
|> Tesla.run(next)
|> decompress()
end
defp compressable?(body), do: is_binary(body)
@doc """
Compress request, used by `Tesla.Middleware.CompressRequest`
"""
def compress(env, opts) do
if compressable?(env.body) do
format = Keyword.get(opts || [], :format, "gzip")
env
|> Map.update!(:body, &compress_body(&1, format))
|> Tesla.Middleware.Headers.call([], %{"Content-Encoding" => format})
else
env
end
end
defp compress_body(body, "gzip"), do: :zlib.gzip(body)
defp compress_body(body, "deflate"), do: :zlib.zip(body)
@doc """
Decompress response, used by `Tesla.Middleware.DecompressResponse`
"""
def decompress(env) do
env
- |> Map.update!(:body, &decompress_body(&1, env.headers["content-encoding"]))
+ |> Map.update!(:body, &decompress_body(&1, Tesla.get_header(env, "content-encoding")))
end
defp decompress_body(<<31, 139, 8, _::binary>> = body, "gzip"), do: :zlib.gunzip(body)
defp decompress_body(body, "deflate"), do: :zlib.unzip(body)
defp decompress_body(body, _content_encoding), do: body
end
defmodule Tesla.Middleware.CompressRequest do
@behaviour Tesla.Middleware
@moduledoc """
Only compress request.
See `Tesla.Middleware.Compression` for options.
"""
def call(env, next, opts) do
env
|> Tesla.Middleware.Compression.compress(opts)
|> Tesla.run(next)
end
end
defmodule Tesla.Middleware.DecompressResponse do
@behaviour Tesla.Middleware
@moduledoc """
Only decompress response.
See `Tesla.Middleware.Compression` for options.
"""
def call(env, next, _opts) do
env
|> Tesla.run(next)
|> Tesla.Middleware.Compression.decompress()
end
end
diff --git a/lib/tesla/middleware/core.ex b/lib/tesla/middleware/core.ex
index 29f931f..080ff1a 100644
--- a/lib/tesla/middleware/core.ex
+++ b/lib/tesla/middleware/core.ex
@@ -1,171 +1,165 @@
defmodule Tesla.Middleware.Normalize do
@moduledoc false
def call(env, next, _opts) do
env
|> normalize
|> Tesla.run(next)
|> normalize
end
def normalize({:error, reason}) do
raise %Tesla.Error{message: "adapter error: #{inspect(reason)}", reason: reason}
end
def normalize(env) do
env
|> Map.update!(:status, &normalize_status/1)
|> Map.update!(:headers, &normalize_headers/1)
|> Map.update!(:body, &normalize_body/1)
end
def normalize_status(nil), do: nil
def normalize_status(status) when is_integer(status), do: status
def normalize_status(status) when is_binary(status), do: status |> String.to_integer()
def normalize_status(status) when is_list(status),
do: status |> to_string |> String.to_integer()
def normalize_headers(headers) when is_map(headers) or is_list(headers) do
Enum.into(headers, %{}, fn {k, v} ->
{k |> to_string |> String.downcase(), v |> to_string}
end)
end
def normalize_body(data) when is_list(data), do: IO.iodata_to_binary(data)
def normalize_body(data), do: data
end
defmodule Tesla.Middleware.BaseUrl do
@behaviour Tesla.Middleware
@moduledoc """
Set base URL for all requests.
The base URL will be prepended to request path/url only
if it does not include http(s).
### Example usage
```
defmodule MyClient do
use Tesla
plug Tesla.Middleware.BaseUrl, "https://api.github.com"
end
MyClient.get("/path") # equals to GET https://api.github.com/path
MyClient.get("http://example.com/path") # equals to GET http://example.com/path
```
"""
def call(env, next, base) do
env
|> apply_base(base)
|> Tesla.run(next)
end
defp apply_base(env, base) do
if Regex.match?(~r/^https?:\/\//, env.url) do
# skip if url is already with scheme
env
else
%{env | url: join(base, env.url)}
end
end
defp join(base, url) do
case {String.last(to_string(base)), url} do
{nil, url} -> url
{"/", "/" <> rest} -> base <> rest
{"/", rest} -> base <> rest
{_, "/" <> rest} -> base <> "/" <> rest
{_, rest} -> base <> "/" <> rest
end
end
end
defmodule Tesla.Middleware.Headers do
@behaviour Tesla.Middleware
@moduledoc """
Set default headers for all requests
### Example usage
```
defmodule Myclient do
use Tesla
plug Tesla.Middleware.Headers, %{"User-Agent" => "Tesla"}
end
```
"""
def call(env, next, headers) do
env
- |> merge(headers)
+ |> Tesla.put_headers(headers)
|> Tesla.run(next)
end
-
- defp merge(env, nil), do: env
-
- defp merge(env, headers) do
- Map.update!(env, :headers, &Map.merge(&1, headers))
- end
end
defmodule Tesla.Middleware.Query do
@behaviour Tesla.Middleware
@moduledoc """
Set default query params for all requests
### Example usage
```
defmodule Myclient do
use Tesla
plug Tesla.Middleware.Query, [token: "some-token"]
end
```
"""
def call(env, next, query) do
env
|> merge(query)
|> Tesla.run(next)
end
defp merge(env, nil), do: env
defp merge(env, query) do
Map.update!(env, :query, &(&1 ++ query))
end
end
defmodule Tesla.Middleware.Opts do
@behaviour Tesla.Middleware
@moduledoc """
Set default opts for all requests
### Example usage
```
defmodule Myclient do
use Tesla
plug Tesla.Middleware.Opts, [some: "option"]
end
```
"""
def call(env, next, opts) do
Tesla.run(%{env | opts: env.opts ++ opts}, next)
end
end
defmodule Tesla.Middleware.BaseUrlFromConfig do
def call(env, next, opts) do
base = config(opts)[:base_url]
Tesla.Middleware.BaseUrl.call(env, next, base)
end
defp config(opts) do
Application.get_env(Keyword.fetch!(opts, :otp_app), Keyword.fetch!(opts, :module))
end
end
diff --git a/lib/tesla/middleware/decode_rels.ex b/lib/tesla/middleware/decode_rels.ex
index 863aef7..ad55cc3 100644
--- a/lib/tesla/middleware/decode_rels.ex
+++ b/lib/tesla/middleware/decode_rels.ex
@@ -1,49 +1,49 @@
defmodule Tesla.Middleware.DecodeRels do
@behaviour Tesla.Middleware
@moduledoc """
Decode `Link` Hypermedia HTTP header into `opts[:rels]` field in response.
### Example usage
```
defmodule MyClient do
use Tesla
plug Tesla.Middleware.DecodeRels
end
env = MyClient.get("/...")
env.opts[:rels] # => %{"Next" => "http://...", "Prev" => "..."}
```
"""
def call(env, next, _opts) do
env
|> Tesla.run(next)
|> parse_rels
end
defp parse_rels(env) do
- if link = env.headers["link"] do
+ if link = Tesla.get_header(env, "link") do
Tesla.put_opt(env, :rels, rels(link))
else
env
end
end
defp rels(link) do
link
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.map(&rel/1)
|> Enum.into(%{})
end
defp rel(item) do
Regex.run(~r/\A<(.+)>; rel="(.+)"\z/, item, capture: :all_but_first)
|> Enum.reverse()
|> List.to_tuple()
end
end
diff --git a/lib/tesla/middleware/digest_auth.ex b/lib/tesla/middleware/digest_auth.ex
index f8c2f3a..be7d9eb 100644
--- a/lib/tesla/middleware/digest_auth.ex
+++ b/lib/tesla/middleware/digest_auth.ex
@@ -1,124 +1,124 @@
defmodule Tesla.Middleware.DigestAuth do
@behaviour Tesla.Middleware
@moduledoc """
Digest access authentication middleware
[Wiki on the topic](https://en.wikipedia.org/wiki/Digest_access_authentication)
**NOTE**: Currently the implementation is incomplete and works only for MD5 algorithm
and auth qop.
### Example
```
defmodule MyClient do
use Tesla
def client(username, password, opts \\ %{}) do
Tesla.build_client [
{Tesla.Middleware.DigestAuth, Map.merge(%{username: username, password: password}, opts)}
]
end
end
```
### Options
- `:username` - username (defaults to `""`)
- `:password` - password (defaults to `""`)
- `:cnonce_fn` - custom function generating client nonce (defaults to `&Tesla.Middleware.DigestAuth.cnonce/0`)
- `:nc` - nonce counter (defaults to `"00000000"`)
"""
def call(env, next, opts) do
if env.opts && Keyword.get(env.opts, :digest_auth_handshake) do
Tesla.run(env, next)
else
opts = opts || %{}
env
- |> Map.update!(:headers, &Map.merge(&1, authorization_header(env, opts)))
+ |> Tesla.put_headers(authorization_header(env, opts))
|> Tesla.run(next)
end
end
defp authorization_header(env, opts) do
env
|> authorization_vars(opts)
|> calculated_authorization_values
|> create_header
end
defp authorization_vars(env, opts) do
unauthorized_response =
env.__module__.get(
env.__client__,
env.url,
opts: Keyword.put(env.opts || [], :digest_auth_handshake, true)
)
%{
username: opts[:username] || "",
password: opts[:password] || "",
path: URI.parse(env.url).path,
- auth: unauthorized_response.headers["www-authenticate"] |> parse_www_authenticate_header,
+ auth: Tesla.get_header(unauthorized_response, "www-authenticate") |> parse_www_authenticate_header,
method: env.method |> to_string |> String.upcase(),
client_nonce: (opts[:cnonce_fn] || &cnonce/0).(),
nc: opts[:nc] || "00000000"
}
end
defp calculated_authorization_values(%{auth: auth}) when auth == %{}, do: []
defp calculated_authorization_values(auth_vars) do
[
{"username", auth_vars.username},
{"realm", auth_vars.auth["realm"]},
{"uri", auth_vars[:path]},
{"nonce", auth_vars.auth["nonce"]},
{"nc", auth_vars.nc},
{"cnonce", auth_vars.client_nonce},
{"response", response(auth_vars)},
# hard-coded, will not work for MD5-sess
{"algorithm", "MD5"},
# hard-coded, will not work for auth-int or unspecified
{"qop", "auth"}
]
end
defp single_header_val({k, v}) when k in ~w(nc qop algorithm), do: "#{k}=#{v}"
defp single_header_val({k, v}), do: "#{k}=\"#{v}\""
defp create_header([]), do: %{}
defp create_header(calculated_authorization_values) do
vals =
calculated_authorization_values
|> Enum.reduce([], fn val, acc -> [single_header_val(val) | acc] end)
|> Enum.join(", ")
%{"Authorization" => "Digest #{vals}"}
end
defp ha1(%{username: username, auth: %{"realm" => realm}, password: password}) do
md5("#{username}:#{realm}:#{password}")
end
defp ha2(%{method: method, path: path}) do
md5("#{method}:#{path}")
end
defp response(%{auth: %{"nonce" => nonce}, nc: nc, client_nonce: client_nonce} = auth_vars) do
md5("#{ha1(auth_vars)}:#{nonce}:#{nc}:#{client_nonce}:auth:#{ha2(auth_vars)}")
end
defp parse_www_authenticate_header(nil), do: %{}
defp parse_www_authenticate_header(header) do
Regex.scan(~r/(\w+?)="(.+?)"/, header)
|> Enum.reduce(%{}, fn [_, key, val], acc -> Map.merge(acc, %{key => val}) end)
end
defp md5(data), do: Base.encode16(:erlang.md5(data), case: :lower)
defp cnonce, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
diff --git a/lib/tesla/middleware/json.ex b/lib/tesla/middleware/json.ex
index 091744a..9b0656c 100644
--- a/lib/tesla/middleware/json.ex
+++ b/lib/tesla/middleware/json.ex
@@ -1,150 +1,150 @@
defmodule Tesla.Middleware.JSON do
@behaviour Tesla.Middleware
@moduledoc """
Encode requests and decode responses as JSON.
This middleware requires [poison](https://hex.pm/packages/poison) (or other engine) as dependency.
Remember to add `{:poison, ">= 1.0"}` to dependencies (and `:poison` to applications in `mix.exs`)
Also, you need to recompile tesla after adding `:poison` dependency:
```
mix deps.clean tesla
mix deps.compile tesla
```
### Example usage
```
defmodule MyClient do
use Tesla
plug Tesla.Middleware.JSON # use poison engine
# or
plug Tesla.Middleware.JSON, engine: JSX, engine_opts: [strict: [:comments]]
# or
plug Tesla.Middleware.JSON, decode: &JSX.decode/1, encode: &JSX.encode/1
end
```
### Options
- `:decode` - decoding function
- `:encode` - encoding function
- `:engine` - encode/decode engine, e.g `Poison` or `JSX` (defaults to Poison)
- `:engine_opts` - optional engine options
- `:decode_content_types` - list of additional decodable content-types
"""
# NOTE: text/javascript added to support Facebook Graph API.
# see https://github.com/teamon/tesla/pull/13
@default_content_types ["application/json", "text/javascript"]
@default_engine Poison
def call(env, next, opts) do
opts = opts || []
env
|> encode(opts)
|> Tesla.run(next)
|> decode(opts)
end
@doc """
Encode request body as JSON. Used by `Tesla.Middleware.EncodeJson`
"""
def encode(env, opts) do
if encodable?(env) do
env
|> Map.update!(:body, &encode_body(&1, opts))
|> Tesla.Middleware.Headers.call([], %{"content-type" => "application/json"})
else
env
end
end
defp encode_body(%Stream{} = body, opts), do: encode_stream(body, opts)
defp encode_body(body, opts) when is_function(body), do: encode_stream(body, opts)
defp encode_body(body, opts), do: process(body, :encode, opts)
defp encode_stream(body, opts) do
Stream.map(body, fn item -> encode_body(item, opts) <> "\n" end)
end
defp encodable?(%{body: nil}), do: false
defp encodable?(%{body: body}) when is_binary(body), do: false
defp encodable?(%{body: %Tesla.Multipart{}}), do: false
defp encodable?(_), do: true
@doc """
Decode response body as JSON. Used by `Tesla.Middleware.DecodeJson`
"""
def decode(env, opts) do
if decodable?(env, opts) do
Map.update!(env, :body, &process(&1, :decode, opts))
else
env
end
end
defp decodable?(env, opts), do: decodable_body?(env) && decodable_content_type?(env, opts)
defp decodable_body?(env) do
(is_binary(env.body) && env.body != "") || (is_list(env.body) && env.body != [])
end
defp decodable_content_type?(env, opts) do
- case env.headers["content-type"] do
+ case Tesla.get_header(env, "content-type") do
nil -> false
content_type -> Enum.any?(content_types(opts), &String.starts_with?(content_type, &1))
end
end
defp content_types(opts),
do: @default_content_types ++ Keyword.get(opts, :decode_content_types, [])
defp process(data, op, opts) do
with {:ok, value} <- do_process(data, op, opts) do
value
else
{:error, reason} ->
raise %Tesla.Error{message: "JSON #{op} error: #{inspect(reason)}", reason: reason}
{:error, msg, position} ->
reason = {msg, position}
raise %Tesla.Error{message: "JSON #{op} error: #{inspect(reason)}", reason: reason}
end
end
defp do_process(data, op, opts) do
# :encode/:decode
if fun = opts[op] do
fun.(data)
else
engine = Keyword.get(opts, :engine, @default_engine)
opts = Keyword.get(opts, :engine_opts, [])
apply(engine, op, [data, opts])
end
end
end
defmodule Tesla.Middleware.DecodeJson do
def call(env, next, opts) do
opts = opts || []
env
|> Tesla.run(next)
|> Tesla.Middleware.JSON.decode(opts)
end
end
defmodule Tesla.Middleware.EncodeJson do
def call(env, next, opts) do
opts = opts || []
env
|> Tesla.Middleware.JSON.encode(opts)
|> Tesla.run(next)
end
end
diff --git a/test/support/adapter_case/basic.ex b/test/support/adapter_case/basic.ex
index 75b4f37..3cc1567 100644
--- a/test/support/adapter_case/basic.ex
+++ b/test/support/adapter_case/basic.ex
@@ -1,104 +1,104 @@
defmodule Tesla.AdapterCase.Basic do
defmacro __using__(_) do
quote do
alias Tesla.Env
describe "Basic" do
test "HEAD request" do
request = %Env{
method: :head,
url: "#{@url}/ip"
}
assert %Env{} = response = call(request)
assert response.status == 200
end
test "GET request" do
request = %Env{
method: :get,
url: "#{@url}/ip"
}
assert %Env{} = response = call(request)
assert response.status == 200
end
test "POST request" do
request = %Env{
method: :post,
url: "#{@url}/post",
body: "some-post-data",
headers: %{"Content-Type" => "text/plain"}
}
assert %Env{} = response = call(request)
assert response.status == 200
- assert response.headers["content-type"] == "application/json"
+ assert Tesla.get_header(response, "content-type") == "application/json"
assert Regex.match?(~r/some-post-data/, response.body)
end
test "unicode" do
request = %Env{
method: :post,
url: "#{@url}/post",
body: "1 ø 2 đ 1 \u00F8 2 \u0111",
headers: %{"Content-Type" => "text/plain"}
}
assert %Env{} = response = call(request)
assert response.status == 200
- assert response.headers["content-type"] == "application/json"
+ assert Tesla.get_header(response, "content-type") == "application/json"
assert Regex.match?(~r/1 ø 2 đ 1 ø 2 đ/, response.body)
end
test "passing query params" do
request = %Env{
method: :get,
url: "#{@url}/get",
query: [
page: 1,
sort: "desc",
status: ["a", "b", "c"],
user: [name: "Jon", age: 20]
]
}
assert %Env{} = response = call(request)
assert response.status == 200
response = Tesla.Middleware.JSON.decode(response, [])
args = response.body["args"]
assert args["page"] == "1"
assert args["sort"] == "desc"
assert args["status[]"] == ["a", "b", "c"]
assert args["user[name]"] == "Jon"
assert args["user[age]"] == "20"
end
test "autoredirects disabled by default" do
request = %Env{
method: :get,
url: "#{@url}/redirect-to?url=#{@url}/status/200"
}
assert %Env{} = response = call(request)
assert response.status == 301
end
test "error: connection refused" do
request = %Env{
method: :get,
url: "http://localhost:1234"
}
assert_raise Tesla.Error, fn ->
call(request)
end
end
end
end
end
end
diff --git a/test/support/adapter_case/multipart.ex b/test/support/adapter_case/multipart.ex
index 2a3fef9..af06d06 100644
--- a/test/support/adapter_case/multipart.ex
+++ b/test/support/adapter_case/multipart.ex
@@ -1,48 +1,48 @@
defmodule Tesla.AdapterCase.Multipart do
defmacro __using__(_) do
quote do
alias Tesla.Env
alias Tesla.Multipart
describe "Multipart" do
test "POST request" do
mp =
Multipart.new()
|> Multipart.add_content_type_param("charset=utf-8")
|> Multipart.add_field("field1", "foo")
|> Multipart.add_field(
"field2",
"bar",
headers: [{:"Content-Id", 1}, {:"Content-Type", "text/plain"}]
)
|> Multipart.add_file("test/tesla/multipart_test_file.sh")
|> Multipart.add_file("test/tesla/multipart_test_file.sh", name: "foobar")
request = %Env{
method: :post,
url: "#{@url}/post",
body: mp
}
assert %Env{} = response = call(request)
assert response.status == 200
- assert response.headers["content-type"] == "application/json"
+ assert Tesla.get_header(response, "content-type") == "application/json"
response = Tesla.Middleware.JSON.decode(response, [])
assert Regex.match?(
~r[multipart/form-data; boundary=#{mp.boundary}; charset=utf-8$],
response.body["headers"]["content-type"]
)
assert response.body["form"] == %{"field1" => "foo", "field2" => "bar"}
assert response.body["files"] == %{
"file" => "#!/usr/bin/env bash\necho \"test multipart file\"\n",
"foobar" => "#!/usr/bin/env bash\necho \"test multipart file\"\n"
}
end
end
end
end
end
diff --git a/test/tesla/middleware/basic_auth_test.exs b/test/tesla/middleware/basic_auth_test.exs
index e0a8be4..d00a396 100644
--- a/test/tesla/middleware/basic_auth_test.exs
+++ b/test/tesla/middleware/basic_auth_test.exs
@@ -1,79 +1,79 @@
defmodule Tesla.Middleware.BasicAuthTest do
use ExUnit.Case
defmodule BasicClient do
use Tesla
adapter fn env ->
case env.url do
"/basic-auth" -> env
end
end
def client(username, password, opts \\ %{}) do
Tesla.build_client([
{
Tesla.Middleware.BasicAuth,
Map.merge(
%{
username: username,
password: password
},
opts
)
}
])
end
def client() do
Tesla.build_client([Tesla.Middleware.BasicAuth])
end
end
defmodule BasicClientPlugOptions do
use Tesla
plug Tesla.Middleware.BasicAuth, username: "Auth", password: "Test"
adapter fn env ->
case env.url do
"/basic-auth" -> env
end
end
end
test "sends request with proper authorization header" do
username = "Aladdin"
password = "OpenSesame"
base_64_encoded = Base.encode64("#{username}:#{password}")
assert base_64_encoded == "QWxhZGRpbjpPcGVuU2VzYW1l"
request = BasicClient.client(username, password) |> BasicClient.get("/basic-auth")
- auth_header = request.headers["authorization"]
+ auth_header = Tesla.get_header(request, "authorization")
assert auth_header == "Basic #{base_64_encoded}"
end
test "it correctly encodes a blank username and password" do
base_64_encoded = Base.encode64(":")
assert base_64_encoded == "Og=="
request = BasicClient.client() |> BasicClient.get("/basic-auth")
- auth_header = request.headers["authorization"]
+ auth_header = Tesla.get_header(request, "authorization")
assert auth_header == "Basic #{base_64_encoded}"
end
test "username and password can be passed to plug directly" do
username = "Auth"
password = "Test"
base_64_encoded = Base.encode64("#{username}:#{password}")
assert base_64_encoded == "QXV0aDpUZXN0"
request = BasicClientPlugOptions.get("/basic-auth")
- auth_header = request.headers["authorization"]
+ auth_header = Tesla.get_header(request, "authorization")
assert auth_header == "Basic #{base_64_encoded}"
end
end
diff --git a/test/tesla/middleware/digest_auth_test.exs b/test/tesla/middleware/digest_auth_test.exs
index 8aa1ebe..22302ca 100644
--- a/test/tesla/middleware/digest_auth_test.exs
+++ b/test/tesla/middleware/digest_auth_test.exs
@@ -1,101 +1,101 @@
defmodule Tesla.Middleware.DigestAuthTest do
use ExUnit.Case, async: false
defmodule DigestClient do
use Tesla
adapter fn env ->
case env do
%{url: "/no-digest-auth"} ->
env
%{headers: %{"authorization" => _}} ->
env
_ ->
%{
env
| headers: %{
"WWW-Authenticate" => """
Digest realm="testrealm@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
"""
}
}
end
end
def client(username, password, opts \\ %{}) do
Tesla.build_client([
{
Tesla.Middleware.DigestAuth,
Map.merge(
%{
username: username,
password: password,
cnonce_fn: fn -> "0a4f113b" end,
nc: "00000001"
},
opts
)
}
])
end
end
defmodule DigestClientWithDefaults do
use Tesla
def client do
Tesla.build_client([
{Tesla.Middleware.DigestAuth, nil}
])
end
end
test "sends request with proper authorization header" do
request =
DigestClient.client("Mufasa", "Circle Of Life") |> DigestClient.get("/dir/index.html")
- auth_header = request.headers["authorization"]
+ auth_header = Tesla.get_header(request, "authorization")
assert auth_header =~ ~r/^Digest /
assert auth_header =~ "username=\"Mufasa\""
assert auth_header =~ "realm=\"testrealm@host.com\""
assert auth_header =~ "algorithm=MD5"
assert auth_header =~ "qop=auth"
assert auth_header =~ "uri=\"/dir/index.html\""
assert auth_header =~ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""
assert auth_header =~ "nc=00000001"
assert auth_header =~ "cnonce=\"0a4f113b\""
assert auth_header =~ "response=\"6629fae49393a05397450978507c4ef1\""
end
test "has default values for username and cn" do
response = DigestClientWithDefaults.client() |> DigestClient.get("/")
- auth_header = response.headers["authorization"]
+ auth_header = Tesla.get_header(response, "authorization")
assert auth_header =~ "username=\"\""
assert auth_header =~ "nc=00000000"
end
test "generates different cnonce with each request by default" do
request = fn -> DigestClientWithDefaults.client() |> DigestClient.get("/") end
- cnonce_1 = Regex.run(~r/cnonce="(.*?)"/, request.().headers["authorization"]) |> Enum.at(1)
- cnonce_2 = Regex.run(~r/cnonce="(.*?)"/, request.().headers["authorization"]) |> Enum.at(1)
+ cnonce_1 = Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
+ cnonce_2 = Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
assert cnonce_1 != cnonce_2
end
test "works when passing custom opts" do
request = DigestClientWithDefaults.client() |> DigestClient.get("/", opts: [hodor: "hodor"])
assert request.opts == [hodor: "hodor"]
end
test "ignores digest auth when server doesn't respond with www-authenticate header" do
response = DigestClientWithDefaults.client() |> DigestClient.get("/no-digest-auth")
- refute response.headers["authorization"]
+ refute Tesla.get_header(response, "authorization")
end
end
diff --git a/test/tesla/middleware/form_urlencoded_test.exs b/test/tesla/middleware/form_urlencoded_test.exs
index 06e09c6..f88b384 100644
--- a/test/tesla/middleware/form_urlencoded_test.exs
+++ b/test/tesla/middleware/form_urlencoded_test.exs
@@ -1,61 +1,61 @@
defmodule Tesla.Middleware.FormUrlencodedTest do
use ExUnit.Case
defmodule Client do
use Tesla
plug Tesla.Middleware.FormUrlencoded
adapter fn env ->
{status, headers, body} =
case env.url do
"/post" ->
{201, %{'Content-Type' => 'text/html'}, env.body}
"/check_incoming_content_type" ->
- {201, %{'Content-Type' => 'text/html'}, env.headers["content-type"]}
+ {201, %{'Content-Type' => 'text/html'}, Tesla.get_header(env, "content-type")}
end
%{env | status: status, headers: headers, body: body}
end
end
test "encode body as application/x-www-form-urlencoded" do
assert URI.decode_query(Client.post("/post", %{"foo" => "%bar "}).body) == %{"foo" => "%bar "}
end
test "leave body alone if binary" do
assert Client.post("/post", "data").body == "data"
end
test "check header is set as application/x-www-form-urlencoded" do
assert Client.post("/check_incoming_content_type", %{"foo" => "%bar "}).body ==
"application/x-www-form-urlencoded"
end
defmodule MultipartClient do
use Tesla
plug Tesla.Middleware.FormUrlencoded
adapter fn %{url: url, body: %Tesla.Multipart{}} = env ->
{status, headers, body} =
case url do
"/upload" ->
{200, %{'Content-Type' => 'text/html'}, "ok"}
end
%{env | status: status, headers: headers, body: body}
end
end
test "skips encoding multipart bodies" do
alias Tesla.Multipart
mp =
Multipart.new()
|> Multipart.add_field("param", "foo")
assert MultipartClient.post("/upload", mp).body == "ok"
end
end
diff --git a/test/tesla/middleware/method_override_test.exs b/test/tesla/middleware/method_override_test.exs
index 9058f04..b75a40c 100644
--- a/test/tesla/middleware/method_override_test.exs
+++ b/test/tesla/middleware/method_override_test.exs
@@ -1,72 +1,72 @@
defmodule Tesla.Middleware.MethodOverrideTest do
use ExUnit.Case
defmodule Client do
use Tesla
plug Tesla.Middleware.MethodOverride
adapter fn env ->
status =
case env do
%{method: :get} -> 200
%{method: :post} -> 201
%{method: _} -> 400
end
%{env | status: status}
end
end
test "when method is get" do
response = Client.get("/")
assert response.status == 200
- refute response.headers["x-http-method-override"]
+ refute Tesla.get_header(response, "x-http-method-override")
end
test "when method is post" do
response = Client.post("/", "")
assert response.status == 201
- refute response.headers["x-http-method-override"]
+ refute Tesla.get_header(response, "x-http-method-override")
end
test "when method isn't get or post" do
response = Client.put("/", "")
assert response.status == 201
- assert response.headers["x-http-method-override"] == "put"
+ assert Tesla.get_header(response, "x-http-method-override") == "put"
end
defmodule CustomClient do
use Tesla
plug Tesla.Middleware.MethodOverride, override: ~w(put)a
adapter fn env ->
status =
case env do
%{method: :get} -> 200
%{method: :post} -> 201
%{method: _} -> 400
end
%{env | status: status}
end
end
test "when method in override list" do
response = CustomClient.put("/", "")
assert response.status == 201
- assert response.headers["x-http-method-override"] == "put"
+ assert Tesla.get_header(response, "x-http-method-override") == "put"
end
test "when method not in override list" do
response = CustomClient.patch("/", "")
assert response.status == 400
- refute response.headers["x-http-method-override"]
+ refute Tesla.get_header(response, "x-http-method-override")
end
end
diff --git a/test/tesla_test.exs b/test/tesla_test.exs
index af91841..89b239e 100644
--- a/test/tesla_test.exs
+++ b/test/tesla_test.exs
@@ -1,284 +1,313 @@
defmodule TeslaTest do
use ExUnit.Case
require Tesla
@url "http://localhost:#{Application.get_env(:httparrot, :http_port)}"
describe "use Tesla options" do
defmodule OnlyGetClient do
use Tesla, only: [:get]
end
defmodule ExceptDeleteClient do
use Tesla.Builder, except: ~w(delete)a
end
@http_verbs ~w(head get delete trace options post put patch)a
test "limit generated functions (only)" do
functions = OnlyGetClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
assert :get in functions
refute Enum.any?(@http_verbs -- [:get], &(&1 in functions))
end
test "limit generated functions (except)" do
functions = ExceptDeleteClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
refute :delete in functions
assert Enum.all?(@http_verbs -- [:delete], &(&1 in functions))
end
end
describe "Docs" do
# Code.get_docs/2 requires .beam file of given module to exist in file system
# See test/support/docs.ex file for definitions of TeslaDocsTest.* modules
test "generate docs by default" do
docs = Code.get_docs(TeslaDocsTest.Default, :docs)
assert {_, _, _, _, doc} = Enum.find(docs, &match?({{:get, 1}, _, :def, _, _}, &1))
assert doc != false
end
test "do not generate docs for HTTP methods when docs: false" do
docs = Code.get_docs(TeslaDocsTest.NoDocs, :docs)
assert {_, _, _, _, false} = Enum.find(docs, &match?({{:get, 1}, _, :def, _, _}, &1))
assert {_, _, _, _, doc} = Enum.find(docs, &match?({{:custom, 1}, _, :def, _, _}, &1))
assert doc =~ ~r/something/
end
end
describe "Adapters" do
defmodule ModuleAdapter do
def call(env, opts \\ []) do
Map.put(env, :url, env.url <> "/module/" <> opts[:with])
end
end
defmodule EmptyClient do
use Tesla
end
defmodule ModuleAdapterClient do
use Tesla
adapter ModuleAdapter, with: "someopt"
end
defmodule LocalAdapterClient do
use Tesla
adapter :local_adapter
def local_adapter(env) do
Map.put(env, :url, env.url <> "/local")
end
end
defmodule FunAdapterClient do
use Tesla
adapter fn env ->
Map.put(env, :url, env.url <> "/anon")
end
end
setup do
# clean config
Application.delete_env(:tesla, EmptyClient)
Application.delete_env(:tesla, ModuleAdapterClient)
:ok
end
test "defauilt adapter" do
assert EmptyClient.__adapter__() == Tesla.default_adapter()
end
test "adapter as module" do
assert ModuleAdapterClient.__adapter__() == {ModuleAdapter, [with: "someopt"]}
end
test "adapter as local function" do
assert LocalAdapterClient.__adapter__() == {:local_adapter, nil}
end
test "adapter as anonymous function" do
assert is_function(FunAdapterClient.__adapter__())
end
test "execute module adapter" do
response = ModuleAdapterClient.request(url: "test")
assert response.url == "test/module/someopt"
end
test "execute local function adapter" do
response = LocalAdapterClient.request(url: "test")
assert response.url == "test/local"
end
test "execute anonymous function adapter" do
response = FunAdapterClient.request(url: "test")
assert response.url == "test/anon"
end
test "use adapter override from config" do
Application.put_env(:tesla, EmptyClient, adapter: Tesla.Mock)
assert EmptyClient.__adapter__() == Tesla.Mock
end
test "prefer config over module setting" do
Application.put_env(:tesla, ModuleAdapterClient, adapter: Tesla.Mock)
assert ModuleAdapterClient.__adapter__() == Tesla.Mock
end
end
describe "Middleware" do
defmodule AppendOne do
@behaviour Tesla.Middleware
def call(env, next, _opts) do
env
|> Map.put(:url, "#{env.url}/1")
|> Tesla.run(next)
end
end
defmodule AppendWith do
@behaviour Tesla.Middleware
def call(env, next, opts) do
env
|> Map.update!(:url, fn url -> url <> "/MB" <> opts[:with] end)
|> Tesla.run(next)
|> Map.update!(:url, fn url -> url <> "/MA" <> opts[:with] end)
end
end
defmodule AppendClient do
use Tesla
plug AppendOne
plug AppendWith, with: "1"
plug AppendWith, with: "2"
plug :local_middleware
adapter fn env -> env end
def local_middleware(env, next) do
env
|> Map.update!(:url, fn url -> url <> "/LB" end)
|> Tesla.run(next)
|> Map.update!(:url, fn url -> url <> "/LA" end)
end
end
test "middleware list" do
assert AppendClient.__middleware__() == [
{AppendOne, nil},
{AppendWith, [with: "1"]},
{AppendWith, [with: "2"]},
{:local_middleware, nil}
]
end
test "execute middleware top down" do
response = AppendClient.get("one")
assert response.url == "one/1/MB1/MB2/LB/LA/MA2/MA1"
end
end
describe "Dynamic client" do
defmodule DynamicClient do
use Tesla
adapter fn env ->
if String.ends_with?(env.url, "/cached") do
%{env | body: "cached", status: 304}
else
Tesla.run_default_adapter(env)
end
end
def help(client \\ %Tesla.Client{}) do
get(client, "/help")
end
end
test "override adapter - Tesla.build_client" do
client =
Tesla.build_client([], [
fn env, _next ->
%{env | body: "new"}
end
])
assert %{body: "new"} = DynamicClient.help(client)
end
test "override adapter - Tesla.build_adapter" do
client =
Tesla.build_adapter(fn env ->
%{env | body: "new"}
end)
assert %{body: "new"} = DynamicClient.help(client)
end
test "statically override adapter" do
assert %{status: 200} = DynamicClient.get(@url <> "/ip")
assert %{status: 304} = DynamicClient.get(@url <> "/cached")
end
end
describe "request API" do
defmodule SimpleClient do
use Tesla
adapter fn env ->
env
end
end
test "basic request" do
response = SimpleClient.request(url: "/", method: :post, query: [page: 1], body: "data")
assert response.method == :post
assert response.url == "/"
assert response.query == [page: 1]
assert response.body == "data"
end
test "shortcut function" do
response = SimpleClient.get("/get")
assert response.method == :get
assert response.url == "/get"
end
test "shortcut function with body" do
response = SimpleClient.post("/post", "some-data")
assert response.method == :post
assert response.url == "/post"
assert response.body == "some-data"
end
test "request with client" do
client = fn env, next ->
env
|> Map.put(:url, "/prefix" <> env.url)
|> Tesla.run(next)
end
response = SimpleClient.get("/")
assert response.url == "/"
refute response.__client__
response = client |> SimpleClient.get("/")
assert response.url == "/prefix/"
assert response.__client__ == %Tesla.Client{fun: client}
end
test "better errors when given nil opts" do
assert_raise FunctionClauseError, fn ->
Tesla.get("/", nil)
end
end
end
+
+ alias Tesla.Env
+ import Tesla
+
+ describe "get_header/2" do
+ test "non existing header" do
+ assert get_header(%Env{}, "some-key") == nil
+ end
+
+ test "fetch existing header" do
+ assert get_header(%Env{headers: %{"server" => "Cowboy"}}, "server") == "Cowboy"
+ end
+ end
+
+ describe "put_headers/2" do
+ test "add headers to env existing header" do
+ env = %Env{}
+ assert get_header(env, "server") == nil
+
+ env = Tesla.put_headers(env, %{"server" => "Cowboy", "content-length" => "100"})
+ assert get_header(env, "server") == "Cowboy"
+ assert get_header(env, "content-length") == "100"
+
+ env = Tesla.put_headers(env, %{"server" => "nginx", "content-type" => "text/plain"})
+ assert get_header(env, "server") == "nginx"
+ assert get_header(env, "content-length") == "100"
+ assert get_header(env, "content-type") == "text/plain"
+ end
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Nov 26, 2:38 AM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40180
Default Alt Text
(60 KB)

Event Timeline