Page MenuHomePhorge

No OneTemporary

Size
39 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla.ex b/lib/tesla.ex
index 9a07382..556bf0d 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,237 +1,237 @@
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 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__: atom,
__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) ::
{:ok, Tesla.Env.t()} | {:error, any}
end
defmodule Tesla.Adapter do
@callback call(env :: Tesla.Env.t(), options :: any) :: {:ok, Tesla.Env.t()} | {:error, any}
end
defmodule Tesla do
use Tesla.Builder
alias Tesla.Env
require Tesla.Adapter.Httpc
@default_adapter Tesla.Adapter.Httpc
@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
@doc false
def execute(module, %{fun: fun, pre: pre, post: post} = client, options) do
env = struct(Env, options ++ [__module__: module, __client__: client])
stack = pre ++ wrapfun(fun) ++ module.__middleware__ ++ post ++ [effective_adapter(module)]
run(env, stack)
end
defp wrapfun(nil), do: []
defp wrapfun(fun), do: [{:fn, fun}]
@doc false
def effective_adapter(module) do
with nil <- adapter_per_module_from_config(module),
nil <- adapter_per_module(module),
nil <- adapter_from_config() do
adapter_default()
end
end
defp adapter_per_module_from_config(module) do
case Application.get_env(:tesla, module, [])[:adapter] do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_per_module(module) do
module.__adapter__
end
defp adapter_from_config do
case Application.get_env(:tesla, :adapter) do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_default do
{@default_adapter, :call, [[]]}
end
def run_default_adapter(env, opts \\ []) do
apply(@default_adapter, :call, [env, opts])
end
# empty stack case is useful for reusing/testing middlewares (just pass [] as next)
def run(env, []), do: {:ok, 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, 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, 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
@spec get_header(Env.t(), binary) :: binary | nil
def get_header(%Env{headers: headers}, key) do
case List.keyfind(headers, key, 0) do
{_, value} -> value
_ -> nil
end
end
@spec get_headers(Env.t(), binary) :: [binary]
def get_headers(%Env{headers: headers}, key) do
for {k, v} <- headers, k == key, do: v
end
@spec put_header(Env.t(), binary, binary) :: Env.t()
def put_header(%Env{} = env, key, value) do
headers = List.keystore(env.headers, key, 0, {key, value})
%{env | headers: headers}
end
@spec put_headers(Env.t(), [{binary, binary}]) :: Env.t()
def put_headers(%Env{} = env, list) when is_list(list) do
%{env | headers: env.headers ++ list}
end
@spec delete_header(Env.t(), binary) :: Env.t()
def delete_header(%Env{} = env, key) do
headers = for {k, v} <- env.headers, k != key, do: {k, v}
%{env | headers: headers}
end
@spec put_body(Env.t(), Env.body()) :: Env.t()
def put_body(%Env{} = env, body), do: %{env | body: body}
@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}}
+ {Tesla.Middleware.Headers, [{"authorization", token}]
])
end
end
client = ExampleAPI.new(token: "abc")
client |> ExampleAPI.get("/me")
```
"""
defmacro build_client(pre, post \\ []) do
quote do
require Tesla.Builder
Tesla.Builder.client(unquote(pre), 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/builder.ex b/lib/tesla/builder.ex
index 6f4905a..22dba26 100644
--- a/lib/tesla/builder.ex
+++ b/lib/tesla/builder.ex
@@ -1,392 +1,375 @@
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.execute(__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.execute(__MODULE__, %Tesla.Client{}, 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
+ # or without options
+ plug Tesla.Middleware.JSON
- # 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
+ # or a custom middleware
+ plug MyProject.CustomMiddleware
end
"""
defmacro plug(middleware, opts) do
quote do
@__middleware__ {
{unquote(Macro.escape(middleware)), unquote(Macro.escape(opts))},
{:middleware, unquote(Macro.escape(__CALLER__))}
}
end
end
defmacro plug(middleware) do
quote do
@__middleware__ {
unquote(Macro.escape(middleware)),
{:middleware, unquote(Macro.escape(__CALLER__))}
}
end
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(name, opts) do
quote do
@__adapter__ {
{unquote(Macro.escape(name)), unquote(Macro.escape(opts))},
{:adapter, unquote(Macro.escape(__CALLER__))}
}
end
end
defmacro adapter(name) do
quote do
@__adapter__ {
unquote(Macro.escape(name)),
{:adapter, unquote(Macro.escape(__CALLER__))}
}
end
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
Tesla.Migration.breaking_alias_in_config!(env.module)
adapter =
env.module
|> Module.get_attribute(:__adapter__)
|> compile()
middleware =
env.module
|> Module.get_attribute(:__middleware__)
|> Enum.reverse()
|> compile()
quote do
def __middleware__, do: unquote(middleware)
def __adapter__, do: unquote(adapter)
end
end
defmacro client(pre, post) do
context = {:middleware, __CALLER__}
quote do
%Tesla.Client{
pre: unquote(compile_context(pre, context)),
post: unquote(compile_context(post, context))
}
end
end
defp compile(nil), do: nil
defp compile(list) when is_list(list), do: Enum.map(list, &compile/1)
# {Tesla.Middleware.Something, opts}
defp compile({{{:__aliases__, _, _} = ast_mod, ast_opts}, {_kind, caller}}) do
Tesla.Migration.breaking_headers_map!(ast_mod, ast_opts, caller)
quote do: {unquote(ast_mod), :call, [unquote(ast_opts)]}
end
# :local_middleware, opts
defp compile({{name, _opts}, {kind, caller}}) when is_atom(name) do
Tesla.Migration.breaking_alias!(kind, name, caller)
end
# Tesla.Middleware.Something
defp compile({{:__aliases__, _, _} = ast_mod, {_kind, _caller}}) do
quote do: {unquote(ast_mod), :call, [nil]}
end
# fn env -> ... end
defp compile({{:fn, _, _} = ast_fun, {_kind, _caller}}) do
quote do: {:fn, unquote(ast_fun)}
end
# :local_middleware
defp compile({name, {kind, caller}}) when is_atom(name) do
Tesla.Migration.breaking_alias!(kind, name, caller)
- quote do: {__MODULE__, unquote(name), []}
end
defp compile_context(list, context) do
list
|> Enum.map(&{&1, context})
|> compile()
end
end
diff --git a/lib/tesla/migration.ex b/lib/tesla/migration.ex
index 9fc98c8..0575e1b 100644
--- a/lib/tesla/migration.ex
+++ b/lib/tesla/migration.ex
@@ -1,104 +1,97 @@
defmodule Tesla.Migration do
## ALIASES
@breaking_alias "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#dropped-aliases-support-159"
@breaking_headers_map "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#headers-are-now-a-list-160"
def breaking_alias!(_kind, _name, nil), do: nil
def breaking_alias!(kind, name, caller) do
- arity = local_function_arity(kind)
-
- unless Module.defines?(caller.module, {name, arity}) do
- raise CompileError,
- file: caller.file,
- line: caller.line,
- description: """
+ raise CompileError,
+ file: caller.file,
+ line: caller.line,
+ description: """
- #{kind |> to_string |> String.capitalize()} aliases has been removed.
- Use full #{kind} name or define a local function #{name}/#{arity}
+ #{kind |> to_string |> String.capitalize()} aliases and local functions has been removed.
+ Use full #{kind} name or define a middleware module #{name |> to_string() |> String.capitalize()}
- #{snippet(caller)}
+ #{snippet(caller)}
- See #{@breaking_alias}
- """
- end
+ See #{@breaking_alias}
+ """
end
- defp local_function_arity(:adapter), do: 1
- defp local_function_arity(:middleware), do: 2
-
def breaking_alias_in_config!(module) do
check_config(
Application.get_env(:tesla, module, [])[:adapter],
"config :tesla, #{inspect(module)}, adapter: "
)
check_config(Application.get_env(:tesla, :adapter), "config :tesla, adapter: ")
end
defp check_config(nil, _label), do: nil
defp check_config({module, _opts}, label) do
check_config(module, label)
end
defp check_config(module, label) do
unless elixir_module?(module) do
raise CompileError,
description: """
Calling
#{label}#{inspect(module)}
with atom as argument has been deprecated
Use
#{label}Tesla.Adapter.Name
instead
See #{@breaking_alias}
"""
end
end
## HEADERS AS LIST
def breaking_headers_map!(
{:__aliases__, _, [:Tesla, :Middleware, :Headers]},
{:%{}, _, _},
caller
) do
raise CompileError,
file: caller.file,
line: caller.line,
description: """
Headers are now a list instead of a map.
#{snippet(caller)}
See #{@breaking_headers_map}
"""
end
def breaking_headers_map!(_middleware, _opts, _caller), do: nil
## UTILS
defp elixir_module?(atom) do
atom |> Atom.to_string() |> String.starts_with?("Elixir.")
end
defp snippet(caller) do
caller.file
|> File.read!()
|> String.split("\n")
|> Enum.at(caller.line - 1)
rescue
_ in File.Error -> ""
end
end
diff --git a/test/tesla/0.x_to_1.0_migration_test.exs b/test/tesla/0.x_to_1.0_migration_test.exs
index 70573a8..527361d 100644
--- a/test/tesla/0.x_to_1.0_migration_test.exs
+++ b/test/tesla/0.x_to_1.0_migration_test.exs
@@ -1,108 +1,112 @@
defmodule MigrationTest do
use ExUnit.Case
describe "Drop aliases #159" do
test "compile error when using atom as plug" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client1 do
use Tesla
plug :json
end
end
)
end
end
- test "no error when using atom as plug and there is a local function with that name" do
- Code.compile_quoted(
- quote do
- defmodule Client2 do
- use Tesla
- plug :json
- def json(env, next), do: Tesla.run(env, next)
+ test "compile error when using atom as plug even if there is a local function with that name" do
+ assert_raise CompileError, fn ->
+ Code.compile_quoted(
+ quote do
+ defmodule Client2 do
+ use Tesla
+ plug :json
+ def json(env, next), do: Tesla.run(env, next)
+ end
end
- end
- )
+ )
+ end
end
test "compile error when using atom as adapter" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client3 do
use Tesla
adapter :hackney
end
end
)
end
end
test "compile error when using atom as adapter with opts" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client4 do
use Tesla
adapter :hackney, recv_timeout: 10_000
end
end
)
end
end
- test "no error when using atom as adapter and there is a local function with that name" do
- Code.compile_quoted(
- quote do
- defmodule Client5 do
- use Tesla
- adapter :local
- def local(env), do: env
+ test "compile error when using atom as adapter even if there is a local function with that name" do
+ assert_raise CompileError, fn ->
+ Code.compile_quoted(
+ quote do
+ defmodule Client5 do
+ use Tesla
+ adapter :local
+ def local(env), do: env
+ end
end
- end
- )
+ )
+ end
end
test "compile error when using atom as adapter in config" do
assert_raise CompileError, fn ->
Application.put_env(:tesla, Client6, adapter: :mock)
Code.compile_quoted(
quote do
defmodule Client6 do
use Tesla
end
end
)
end
end
end
describe "Use keyword list to store headers #160" do
test "compile error when passing a map to Headers middleware" do
assert_raise CompileError, fn ->
Code.compile_quoted(
quote do
defmodule Client7 do
use Tesla
plug Tesla.Middleware.Headers, %{"User-Agent" => "tesla"}
end
end
)
end
end
test "no error when passing a list to Headers middleware" do
Code.compile_quoted(
quote do
defmodule Client8 do
use Tesla
plug Tesla.Middleware.Headers, [{"user-agent", "tesla"}]
end
end
)
end
end
end
diff --git a/test/tesla/builder_test.exs b/test/tesla/builder_test.exs
index 7b7ce45..78db31a 100644
--- a/test/tesla/builder_test.exs
+++ b/test/tesla/builder_test.exs
@@ -1,134 +1,117 @@
defmodule Tesla.BuilderTest do
use ExUnit.Case
alias Tesla.Builder
describe "Compilation" do
defmodule TestClientPlug do
use Tesla.Builder
@attr "value"
plug FirstMiddleware, @attr
plug SecondMiddleware, options: :are, fun: 1
plug ThirdMiddleware
- plug :local_middleware
plug fn env, _next -> env end
- def local_middleware(env, next), do: Tesla.run(env, next)
-
def new do
Tesla.Builder.client(
[
FirstMiddleware,
{SecondMiddleware, options: :are, fun: 1},
- :local_middleware,
fn env, _next -> env end
],
[]
)
end
end
defmodule TestClientModule do
use Tesla.Builder
adapter TheAdapter, hello: "world"
end
- defmodule TestClientFunction do
- use Tesla.Builder
- adapter :local_adapter
-
- def local_adapter(env), do: env
- end
-
defmodule TestClientAnon do
use Tesla.Builder
adapter fn env -> env end
end
test "generate __middleware__/0" do
assert [
{FirstMiddleware, :call, ["value"]},
{SecondMiddleware, :call, [[options: :are, fun: 1]]},
{ThirdMiddleware, :call, [nil]},
- {TestClientPlug, :local_middleware, []},
{:fn, fun}
] = TestClientPlug.__middleware__()
assert is_function(fun)
end
test "generate __adapter__/0 - adapter not set" do
assert TestClientPlug.__adapter__() == nil
end
test "generate __adapter__/0 - adapter as module" do
assert TestClientModule.__adapter__() == {TheAdapter, :call, [[hello: "world"]]}
end
- test "generate __adapter__/0 - adapter as module function" do
- assert TestClientFunction.__adapter__() == {TestClientFunction, :local_adapter, []}
- end
-
test "generate __adapter__/0 - adapter as anonymous function" do
assert {:fn, fun} = TestClientAnon.__adapter__()
assert is_function(fun)
end
test "dynamic client" do
client = TestClientPlug.new()
assert [
{FirstMiddleware, :call, [nil]},
{SecondMiddleware, :call, [[options: :are, fun: 1]]},
- {TestClientPlug, :local_middleware, []},
{:fn, fun}
] = client.pre
assert is_function(fun)
end
end
describe ":only/:except options" do
defmodule OnlyGetClient do
use Builder, only: [:get]
end
defmodule ExceptDeleteClient do
use 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 option" 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
end
diff --git a/test/tesla/middleware/fuse_test.exs b/test/tesla/middleware/fuse_test.exs
index 4816e4e..053b894 100644
--- a/test/tesla/middleware/fuse_test.exs
+++ b/test/tesla/middleware/fuse_test.exs
@@ -1,50 +1,52 @@
defmodule Tesla.Middleware.FuseTest do
use ExUnit.Case, async: false
+ defmodule Report do
+ def call(env, next, _) do
+ send(self(), :request_made)
+ Tesla.run(env, next)
+ end
+ end
+
defmodule Client do
use Tesla
plug Tesla.Middleware.Fuse
- plug :report
-
- def report(env, next) do
- send(self(), :request_made)
- Tesla.run(env, next)
- end
+ plug Report
adapter fn env ->
case env.url do
"/ok" ->
{:ok, env}
"/unavailable" ->
{:error, :econnrefused}
end
end
end
setup do
Application.ensure_all_started(:fuse)
:fuse.reset(Client)
:ok
end
test "regular endpoint" do
assert {:ok, %Tesla.Env{url: "/ok"}} = Client.get("/ok")
end
test "unavailable endpoint" do
assert {:error, :unavailable} = Client.get("/unavailable")
assert_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
assert_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
assert_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
refute_receive :request_made
assert {:error, :unavailable} = Client.get("/unavailable")
refute_receive :request_made
end
end
diff --git a/test/tesla_test.exs b/test/tesla_test.exs
index 697b53b..e278d12 100644
--- a/test/tesla_test.exs
+++ b/test/tesla_test.exs
@@ -1,312 +1,283 @@
defmodule TeslaTest do
use ExUnit.Case
require Tesla
@url "http://localhost:#{Application.get_env(:httparrot, :http_port)}"
describe "Adapters" do
defmodule ModuleAdapter do
def call(env, opts \\ []) do
{:ok, 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
- {:ok, Map.put(env, :url, env.url <> "/local")}
- end
- end
-
defmodule FunAdapterClient do
use Tesla
adapter fn env ->
{:ok, 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 Tesla.effective_adapter(EmptyClient) == {Tesla.Adapter.Httpc, :call, [[]]}
end
test "use adapter override from config" do
Application.put_env(:tesla, EmptyClient, adapter: Tesla.Mock)
assert Tesla.effective_adapter(EmptyClient) == {Tesla.Mock, :call, [[]]}
end
test "prefer config over module setting" do
Application.put_env(:tesla, ModuleAdapterClient, adapter: Tesla.Mock)
assert Tesla.effective_adapter(ModuleAdapterClient) == {Tesla.Mock, :call, [[]]}
end
test "execute module adapter" do
assert {:ok, response} = ModuleAdapterClient.request(url: "test")
assert response.url == "test/module/someopt"
end
- test "execute local function adapter" do
- assert {:ok, response} = LocalAdapterClient.request(url: "test")
- assert response.url == "test/local"
- end
-
test "execute anonymous function adapter" do
assert {:ok, response} = FunAdapterClient.request(url: "test")
assert response.url == "test/anon"
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)
|> case do
{:ok, env} ->
{:ok, Map.update!(env, :url, fn url -> url <> "/MA" <> opts[:with] end)}
error ->
error
end
end
end
defmodule AppendClient do
use Tesla
plug AppendOne
plug AppendWith, with: "1"
plug AppendWith, with: "2"
- plug :local_middleware
adapter fn env -> {:ok, env} end
-
- def local_middleware(env, next) do
- env
- |> Map.update!(:url, fn url -> url <> "/LB" end)
- |> Tesla.run(next)
- |> case do
- {:ok, env} ->
- {:ok, Map.update!(env, :url, fn url -> url <> "/LA" end)}
-
- error ->
- error
- end
- end
end
test "execute middleware top down" do
assert {:ok, response} = AppendClient.get("one")
- assert response.url == "one/1/MB1/MB2/LB/LA/MA2/MA1"
+ assert response.url == "one/1/MB1/MB2/MA2/MA1"
end
end
describe "Dynamic client" do
defmodule DynamicClient do
use Tesla
adapter fn env ->
if String.ends_with?(env.url, "/cached") do
{:ok, %{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 ->
{:ok, %{env | body: "new"}}
end
])
assert {:ok, %{body: "new"}} = DynamicClient.help(client)
end
test "override adapter - Tesla.build_adapter" do
client =
Tesla.build_adapter(fn env ->
{:ok, %{env | body: "new"}}
end)
assert {:ok, %{body: "new"}} = DynamicClient.help(client)
end
test "statically override adapter" do
assert {:ok, %{status: 200}} = DynamicClient.get(@url <> "/ip")
assert {:ok, %{status: 304}} = DynamicClient.get(@url <> "/cached")
end
end
describe "request API" do
defmodule SimpleClient do
use Tesla
adapter fn env ->
{:ok, env}
end
end
test "basic request" do
assert {:ok, 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
assert {:ok, response} = SimpleClient.get("/get")
assert response.method == :get
assert response.url == "/get"
end
test "shortcut function with body" do
assert {:ok, 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
assert {:ok, response} = SimpleClient.get("/")
assert response.url == "/"
assert response.__client__ == %Tesla.Client{}
assert {:ok, 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
env = %Env{headers: [{"server", "Cowboy"}]}
assert get_header(env, "some-key") == nil
end
test "existing header" do
env = %Env{headers: [{"server", "Cowboy"}]}
assert get_header(env, "server") == "Cowboy"
end
test "first of multiple headers with the same name" do
env = %Env{headers: [{"cookie", "chocolate"}, {"cookie", "biscuits"}]}
assert get_header(env, "cookie") == "chocolate"
end
end
describe "get_headers/2" do
test "none matching" do
env = %Env{headers: [{"server", "Cowboy"}]}
assert get_headers(env, "cookie") == []
end
test "multiple matches matching" do
env = %Env{headers: [{"cookie", "chocolate"}, {"cookie", "biscuits"}]}
assert get_headers(env, "cookie") == ["chocolate", "biscuits"]
end
end
describe "put_header/3" do
test "add new header" do
env = %Env{}
env = put_header(env, "server", "Cowboy")
assert get_header(env, "server") == "Cowboy"
end
test "override existing header" do
env = %Env{headers: [{"server", "Cowboy"}]}
env = put_header(env, "server", "nginx")
assert get_header(env, "server") == "nginx"
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") == "Cowboy"
assert get_header(env, "content-length") == "100"
assert get_header(env, "content-type") == "text/plain"
end
test "add multiple headers with the same name" do
env = %Env{}
env = Tesla.put_headers(env, [{"cookie", "chocolate"}, {"cookie", "biscuits"}])
assert get_headers(env, "cookie") == ["chocolate", "biscuits"]
end
end
describe "delete_header/2" do
test "delete all headers with given name" do
env = %Env{headers: [{"cookie", "chocolate"}, {"server", "Cowboy"}, {"cookie", "biscuits"}]}
env = delete_header(env, "cookie")
assert get_header(env, "cookie") == nil
assert get_header(env, "server") == "Cowboy"
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 9:25 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39752
Default Alt Text
(39 KB)

Event Timeline