Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114311
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
60 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment