Page MenuHomePhorge

No OneTemporary

Size
19 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Mon, Nov 25, 7:48 PM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40038
Default Alt Text
(19 KB)

Event Timeline