Page MenuHomePhorge

No OneTemporary

Size
30 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla.ex b/lib/tesla.ex
index ad100e4..e38bc62 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,535 +1,539 @@
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.Adapter do
+ @callback call(env :: Tesla.Env.t(), 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) when is_map(headers) do
headers[key]
end
def get_header(%Tesla.Env{headers: headers}, key) when is_list(headers) do
case List.keyfind(headers, key, 0) do
{_, value} -> value
_ -> nil
end
end
def put_headers(%Tesla.Env{headers: headers} = env, map) when is_list(headers) and is_map(map) do
put_headers(env, Map.to_list(map))
end
def put_headers(env, list) when is_list(list) do
headers = Enum.reduce(list, env.headers, fn {k,v}, h -> List.keystore(h, k, 0, {k,v}) end)
%{env | headers: headers}
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/hackney.ex b/lib/tesla/adapter/hackney.ex
index 02fb832..f3be928 100644
--- a/lib/tesla/adapter/hackney.ex
+++ b/lib/tesla/adapter/hackney.ex
@@ -1,83 +1,92 @@
if Code.ensure_loaded?(:hackney) do
defmodule Tesla.Adapter.Hackney do
@moduledoc """
Adapter for [hackney](https://github.com/benoitc/hackney)
Remember to add `{:hackney, "~> 1.6"}` to dependencies (and `:hackney` to applications in `mix.exs`)
Also, you need to recompile tesla after adding `:hackney` dependency:
```
mix deps.clean tesla
mix deps.compile tesla
```
### Example usage
```
# set globally in config/config.exs
config :tesla, :adapter, :hackney
# set per module
defmodule MyClient do
use Tesla
adapter :hackney
end
```
"""
-
+ @behaviour Tesla.Adapter
alias Tesla.Multipart
def call(env, opts) do
with {:ok, status, headers, body} <- request(env, opts || []) do
- %{env | status: status, headers: headers, body: body}
+ %{env | status: status, headers: format_headers(headers), body: format_body(body)}
end
end
+ defp format_headers(headers) do
+ for {key, value} <- headers do
+ {String.downcase(to_string(key)), to_string(value)}
+ end
+ end
+
+ defp format_body(data) when is_list(data), do: IO.iodata_to_binary(data)
+ defp format_body(data) when is_binary(data), do: data
+
defp request(env, opts) do
request(
env.method,
Tesla.build_url(env.url, env.query),
Enum.into(env.headers, []),
env.body,
opts ++ env.opts
)
end
defp request(method, url, headers, %Stream{} = body, opts),
do: request_stream(method, url, headers, body, opts)
defp request(method, url, headers, body, opts) when is_function(body),
do: request_stream(method, url, headers, body, opts)
defp request(method, url, headers, %Multipart{} = mp, opts) do
headers = headers ++ Multipart.headers(mp)
body = Multipart.body(mp)
request(method, url, headers, body, opts)
end
defp request(method, url, headers, body, opts) do
handle(:hackney.request(method, url, headers, body || '', opts))
end
defp request_stream(method, url, headers, body, opts) do
with {:ok, ref} <- :hackney.request(method, url, headers, :stream, opts) do
for data <- body, do: :ok = :hackney.send_body(ref, data)
handle(:hackney.start_response(ref))
else
e -> handle(e)
end
end
defp handle({:error, _} = error), do: error
defp handle({:ok, status, headers}), do: {:ok, status, headers, []}
defp handle({:ok, status, headers, ref}) when is_reference(ref) do
with {:ok, body} <- :hackney.body(ref) do
{:ok, status, headers, body}
end
end
defp handle({:ok, status, headers, body}), do: {:ok, status, headers, body}
end
end
diff --git a/lib/tesla/adapter/httpc.ex b/lib/tesla/adapter/httpc.ex
index 3cc517f..eb11908 100644
--- a/lib/tesla/adapter/httpc.ex
+++ b/lib/tesla/adapter/httpc.ex
@@ -1,73 +1,91 @@
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
"""
+ @behaviour Tesla.Adapter
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}
+ %{env | status: status, headers: format_headers(headers), body: format_body(body)}
end
+ # from http://erlang.org/doc/man/httpc.html
+ # headers() = [header()]
+ # header() = {field(), value()}
+ # field() = string()
+ # value() = string()
+ defp format_headers(headers) do
+ for {key, value} <- headers do
+ {String.downcase(to_string(key)), to_string(value)}
+ end
+ end
+
+ # from http://erlang.org/doc/man/httpc.html
+ # string() = list of ASCII characters
+ # Body = string() | binary()
+ defp format_body(data) when is_list(data), do: IO.iodata_to_binary(data)
+ defp format_body(data) when is_binary(data), do: data
+
defp request(env, opts) do
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/adapter/ibrowse.ex b/lib/tesla/adapter/ibrowse.ex
index b502199..99fe489 100644
--- a/lib/tesla/adapter/ibrowse.ex
+++ b/lib/tesla/adapter/ibrowse.ex
@@ -1,76 +1,90 @@
if Code.ensure_loaded?(:ibrowse) do
defmodule Tesla.Adapter.Ibrowse do
@moduledoc """
Adapter for [ibrowse](https://github.com/cmullaparthi/ibrowse)
Remember to add `{:ibrowse, "~> 4.2"}` to dependencies (and `:ibrowse` to applications in `mix.exs`)
Also, you need to recompile tesla after adding `:ibrowse` dependency:
```
mix deps.clean tesla
mix deps.compile tesla
```
### Example usage
```
# set globally in config/config.exs
config :tesla, :adapter, :ibrowse
# set per module
defmodule MyClient do
use Tesla
adapter :ibrowse
end
```
"""
+ @behaviour Tesla.Adapter
import Tesla.Adapter.Shared, only: [stream_to_fun: 1, next_chunk: 1]
alias Tesla.Multipart
def call(env, opts) do
with {:ok, status, headers, body} <- request(env, opts || []) do
- %{env | status: status, headers: headers, body: body}
+ %{env | status: format_status(status), headers: format_headers(headers), body: format_body(body)}
end
end
+ defp format_status(status) when is_list(status) do
+ status |> to_string() |> String.to_integer()
+ end
+
+ defp format_headers(headers) do
+ for {key, value} <- headers do
+ {String.downcase(to_string(key)), to_string(value)}
+ end
+ end
+
+ defp format_body(data) when is_list(data), do: IO.iodata_to_binary(data)
+ defp format_body(data) when is_binary(data), do: data
+
defp request(env, opts) do
body = env.body || []
handle(
request(
Tesla.build_url(env.url, env.query) |> to_charlist,
Enum.into(env.headers, []),
env.method,
body,
opts ++ env.opts
)
)
end
defp request(url, headers, method, %Multipart{} = mp, opts) do
headers = headers ++ Multipart.headers(mp)
body = stream_to_fun(Multipart.body(mp))
request(url, headers, method, body, opts)
end
defp request(url, headers, method, %Stream{} = body, opts) do
fun = stream_to_fun(body)
request(url, headers, method, fun, opts)
end
defp request(url, headers, method, body, opts) when is_function(body) do
body = {&next_chunk/1, body}
opts = Keyword.put(opts, :transfer_encoding, :chunked)
request(url, headers, method, body, opts)
end
defp request(url, headers, method, body, opts) do
:ibrowse.send_req(url, headers, method, body, opts)
end
defp handle({:error, {:conn_failed, error}}), do: error
defp handle(response), do: response
end
end
diff --git a/lib/tesla/middleware/core.ex b/lib/tesla/middleware/core.ex
index 4463bf1..d7ed605 100644
--- a/lib/tesla/middleware/core.ex
+++ b/lib/tesla/middleware/core.ex
@@ -1,166 +1,153 @@
defmodule Tesla.Middleware.Normalize do
@moduledoc false
def call(env, next, _opts) do
env
|> normalize
|> Tesla.run(next)
|> normalize
- |> Map.update!(:headers, &Map.to_list/1)
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
|> Tesla.put_headers(headers)
|> Tesla.run(next)
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/test/tesla/middleware/normalize_test.exs b/test/tesla/middleware/normalize_test.exs
deleted file mode 100644
index 2ed38fa..0000000
--- a/test/tesla/middleware/normalize_test.exs
+++ /dev/null
@@ -1,64 +0,0 @@
-defmodule Tesla.Middleware.NormalizeTest do
- use ExUnit.Case
- alias Tesla.Middleware.Normalize
-
- defp call(args) do
- Normalize.call(struct!(Tesla.Env, args), [], [])
- end
-
- describe "status" do
- test "when nil" do
- env = call(status: nil)
- assert env.status == nil
- end
-
- test "when num" do
- env = call(status: 200)
- assert env.status == 200
- end
-
- test "when string" do
- env = call(status: "200")
- assert env.status == 200
- end
-
- test "when charlist" do
- env = call(status: '200')
- assert env.status == 200
- end
- end
-
- describe "body" do
- test "when nil" do
- env = call(body: nil)
- assert env.body == nil
- end
-
- test "when string" do
- env = call(body: "some-body")
- assert env.body == "some-body"
- end
-
- test "when charlist" do
- env = call(body: 'some-body')
- assert env.body == "some-body"
- end
- end
-
- # describe "headers" do
- # test "when empty list" do
- # env = call(headers: [])
- # assert env.headers == %{}
- # end
- #
- # test "when list" do
- # env = call(headers: [a: "1", b: 2])
- # assert env.headers == %{"a" => "1", "b" => "2"}
- # end
- #
- # test "when map" do
- # env = call(headers: %{"User-Agent" => "tesla"})
- # assert env.headers == %{"user-agent" => "tesla"}
- # end
- # end
-end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 29, 11:13 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41323
Default Alt Text
(30 KB)

Event Timeline