Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F112409
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/tesla/builder.ex b/lib/tesla/builder.ex
index 9f346b9..7ef7aa5 100644
--- a/lib/tesla/builder.ex
+++ b/lib/tesla/builder.ex
@@ -1,351 +1,352 @@
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
# 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
+ Tesla.Migration.breaking_headers_map!(middleware, opts, __CALLER__)
quote do: @__middleware__({
unquote(Macro.escape(middleware)),
unquote(Macro.escape(opts)),
unquote(Macro.escape(__CALLER__))
})
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(adapter, opts \\ nil) do
quote do: @__adapter__({
unquote(Macro.escape(adapter)),
unquote(Macro.escape(opts)),
unquote(Macro.escape(__CALLER__))
})
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__)
|> prepare(:adapter)
middleware =
env.module
|> Module.get_attribute(:__middleware__)
|> Enum.reverse()
|> prepare(:middleware)
quote do
def __middleware__, do: unquote(middleware)
def __adapter__, do: unquote(adapter)
end
end
defmacro client(pre, post) do
quote do
%Tesla.Client{
pre: unquote(prepare(pre)),
post: unquote(prepare(post))
}
end
end
defp prepare(list, kind \\ :middleware)
defp prepare(list, kind) when is_list(list), do: Enum.map(list, &prepare(&1, kind))
defp prepare(nil, _), do: nil
defp prepare({{:fn, _, _} = fun, nil, _}, _), do: {:fn, fun}
defp prepare({{:__aliases__, _, _} = name, opts, _}, _),
do: quote(do: {unquote(name), :call, [unquote(opts)]})
defp prepare({name, nil, nil}, _) when is_atom(name), do: quote(do: {__MODULE__, unquote(name), []})
defp prepare({name, nil, caller}, kind) when is_atom(name) do
Tesla.Migration.breaking_alias!(kind, name, caller)
quote(do: {__MODULE__, unquote(name), []})
end
defp prepare({name, opts}, kind), do: prepare({name, opts, nil}, kind)
defp prepare(name, kind), do: prepare({name, nil, nil}, kind)
end
diff --git a/lib/tesla/migration.ex b/lib/tesla/migration.ex
index fa646b2..29719f4 100644
--- a/lib/tesla/migration.ex
+++ b/lib/tesla/migration.ex
@@ -1,64 +1,86 @@
defmodule Tesla.Migration do
- @issue_159 "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#dropped-aliases-support-159"
+ ## 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:
"""
#{kind |> to_string |> String.capitalize} aliases has been removed.
Use full #{kind} name or define a local function #{name}/#{arity}
#{snippet(caller)}
- See #{@issue_159}
+ See #{@breaking_alias}
"""
end
end
defp local_function_arity(:adapter), do: 1
defp local_function_arity(:middleware), do: 2
- defp snippet(caller) do
- caller.file
- |> File.read!()
- |> String.split("\n")
- |> Enum.at(caller.line - 1)
- rescue
- _ in File.Error -> ""
- end
-
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
Code.ensure_loaded(module)
unless function_exported?(module, :call, 2) do
raise CompileError, description:
"""
Calling
#{label}#{inspect module}
with atom as argument has been deprecated
Use
#{label}Tesla.Adapter.Name
instead
- See #{@issue_159}
+ 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 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 c1ee583..e674198 100644
--- a/test/tesla/0.x_to_1.0_migration_test.exs
+++ b/test/tesla/0.x_to_1.0_migration_test.exs
@@ -1,58 +1,80 @@
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)
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 "no error when using atom as adapter and there is a local function with that name" do
Code.compile_quoted(quote do
defmodule Client4 do
use Tesla
adapter :local
def local(env), do: env
end
end)
end
test "compile error when using atom as adapter in config" do
assert_raise CompileError, fn ->
Application.put_env(:tesla, Client5, adapter: :mock)
Code.compile_quoted(quote do
defmodule Client5 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 Client6 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 Client7 do
+ use Tesla
+ plug Tesla.Middleware.Headers, [{"user-agent", "tesla"}]
+ end
+ end)
+ end
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 12:58 PM (23 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38980
Default Alt Text
(16 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment