Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114099
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/tesla/builder.ex b/lib/tesla/builder.ex
index 52e4f24..9f346b9 100644
--- a/lib/tesla/builder.ex
+++ b/lib/tesla/builder.ex
@@ -1,338 +1,351 @@
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.raise_if_atom!(:plug, Tesla.Middleware, middleware)
- quote do: @__middleware__({unquote(Macro.escape(middleware)), unquote(Macro.escape(opts))})
+ 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
- Tesla.Migration.raise_if_atom!(:adapter, Tesla.Adapter, adapter)
- quote do: @__adapter__({unquote(Macro.escape(adapter)), unquote(Macro.escape(opts))})
+ 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.raise_if_atom_in_config!(env.module)
+ Tesla.Migration.breaking_alias_in_config!(env.module)
adapter =
env.module
|> Module.get_attribute(:__adapter__)
- |> prepare()
+ |> prepare(:adapter)
middleware =
env.module
|> Module.get_attribute(:__middleware__)
|> Enum.reverse()
- |> prepare()
+ |> 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) when is_list(list), do: Enum.map(list, &prepare/1)
- defp prepare(nil), do: nil
- defp prepare({{:fn, _, _} = fun, nil}), do: {:fn, fun}
+ 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}),
+ defp prepare({{:__aliases__, _, _} = name, opts, _}, _),
do: quote(do: {unquote(name), :call, [unquote(opts)]})
- defp prepare({name, nil}) when is_atom(name), do: quote(do: {__MODULE__, unquote(name), []})
- defp prepare(name), do: prepare({name, nil})
+ 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 33ac8ce..fa646b2 100644
--- a/lib/tesla/migration.ex
+++ b/lib/tesla/migration.ex
@@ -1,48 +1,64 @@
defmodule Tesla.Migration do
@issue_159 "https://github.com/teamon/tesla/wiki/0.x-to-1.0-Migration-Guide#dropped-aliases-support-159"
- def raise_if_atom!(fun, scope, arg) when is_atom(arg) do
- raise CompileError, description:
- """
- Calling `#{fun}` with atom as argument has been deprecated
+ 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:
+ """
- Use `#{fun} #{inspect scope}.Name` instead
+ #{kind |> to_string |> String.capitalize} aliases has been removed.
+ Use full #{kind} name or define a local function #{name}/#{arity}
- See #{@issue_159}
- """
+ #{snippet(caller)}
+
+ See #{@issue_159}
+ """
+ end
end
- def raise_if_atom!(_fun, _scope, _arg), do: nil
+ 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 raise_if_atom_in_config!(module) do
- check_config_atom(Application.get_env(:tesla, module, [])[:adapter], "config :tesla, #{inspect module}, adapter: ")
- check_config_atom(Application.get_env(:tesla, :adapter), "config :tesla, adapter: ")
+ 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_atom(nil, _label), do: nil
- defp check_config_atom({module, _opts}, label) do
- check_config_atom(module, label)
+ defp check_config(nil, _label), do: nil
+ defp check_config({module, _opts}, label) do
+ check_config(module, label)
end
- defp check_config_atom(module, label) do
+ 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}
"""
end
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 beb08a5..c1ee583 100644
--- a/test/tesla/0.x_to_1.0_migration_test.exs
+++ b/test/tesla/0.x_to_1.0_migration_test.exs
@@ -1,38 +1,58 @@
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 Client2 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, Client3, adapter: :mock)
+ Application.put_env(:tesla, Client5, adapter: :mock)
Code.compile_quoted(quote do
- defmodule Client3 do
+ defmodule Client5 do
use Tesla
end
end)
end
end
end
end
diff --git a/test/tesla/builder_test.exs b/test/tesla/builder_test.exs
index 4478ee5..7a3e220 100644
--- a/test/tesla/builder_test.exs
+++ b/test/tesla/builder_test.exs
@@ -1,105 +1,109 @@
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)
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
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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 25, 7:48 PM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40038
Default Alt Text
(19 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment