Packages
tesla
1.18.3
1.20.0
1.18.3
1.18.2
1.18.1
1.18.0
1.17.0
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.3
1.12.2
1.12.1
1.12.0
1.11.2
1.11.1
1.11.0
1.10.3
1.10.2
1.10.1
1.10.0
1.9.0
1.8.1
retired
1.8.0
1.7.0
1.6.1
1.6.0
1.5.1
1.5.0
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.0
1.0.0
1.0.0-beta.1
0.10.0
0.9.0
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.2.2
0.2.1
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
HTTP client library, with support for middleware and multiple adapters.
Current section
Files
Jump to
Current section
Files
lib/tesla/builder.ex
defmodule Tesla.Builder do
@moduledoc false
@http_verbs ~w(head get delete trace options post put patch)a
@body ~w(post put patch)a
defmacro __using__(opts \\ []) do
caller_module = __CALLER__.module
if caller_module != Tesla and
not Application.get_env(:tesla, :disable_deprecated_builder_warning, false) do
IO.warn("""
`use Tesla.Builder` and `use Tesla` in #{inspect(caller_module)} are soft-deprecated. \
It will be removed in future major version in favor of
Runtime Configuration instead. Please share your feedback at https://github.com/elixir-tesla/tesla/discussions/732
If you can turn off this warning, add the following to your config.exs:
```elixir
config :tesla, disable_deprecated_builder_warning: true
```
""")
end
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
@typedoc "Options that may be passed to a request function. See `request/2` for detailed descriptions."
else
@typedoc false
end
@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()}
| {:assigns, Tesla.Env.assigns()}
| {:private, Tesla.Env.private()}
if unquote(docs) do
@doc """
Perform a request.
## 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 or nested map of query params, e.g. `[page: 1, per_page: 100]`
- `:headers` - a keyword 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
- `:assigns` - per-request user data, see `t:Tesla.Env.assigns/0`
- `:private` - per-request data reserved for libraries and middleware,
see `t:Tesla.Env.private/0`
## Examples
ExampleApi.request(method: :get, url: "/users/path")
ExampleApi.get("/users", query: [username: "John Smith"], opts: [query_encoding: &Plug.Conn.Query.encode/1])
# use shortcut methods
ExampleApi.get("/users/1")
ExampleApi.post(client, "/users", %{name: "Jon"})
"""
else
@doc false
end
@spec request(Tesla.Env.client(), [option]) :: Tesla.Env.result()
def request(%Tesla.Client{} = client \\ %Tesla.Client{}, options) do
Tesla.execute(__MODULE__, client, options)
end
if unquote(docs) do
@doc """
Perform request and raise in case of error.
This is similar to `request/2` behaviour from Tesla 0.x
See `request/2` for list of available options.
"""
else
@doc false
end
@spec request!(Tesla.Env.client(), [option]) :: Tesla.Env.t() | no_return
def request!(%Tesla.Client{} = client \\ %Tesla.Client{}, options) do
Tesla.execute!(__MODULE__, 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.
defmodule ExampleApi do
use Tesla
# plug middleware module with options
plug Tesla.Middleware.BaseUrl, "http://api.example.com"
# or without options
plug Tesla.Middleware.JSON
# 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.
defmodule ExampleApi do
use Tesla
# set adapter as module
adapter Tesla.Adapter.Hackney
# set adapter as anonymous function
adapter fn env ->
...
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
defmacro __before_compile__(env) do
adapter =
env.module
|> Module.get_attribute(:__adapter__)
|> compile()
middleware =
env.module
|> Module.get_attribute(:__middleware__)
|> Enum.reverse()
|> compile()
quote location: :keep do
def __middleware__, do: unquote(middleware)
def __adapter__, do: unquote(adapter)
end
end
def client(pre, post, adapter \\ nil)
def client(pre, post, nil) do
%Tesla.Client{pre: runtime(pre), post: runtime(post)}
end
def client(pre, post, adapter) do
%Tesla.Client{pre: runtime(pre), post: runtime(post), adapter: runtime(adapter)}
end
@default_opts []
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
quote do: {unquote(ast_mod), :call, [unquote(ast_opts)]}
end
# Tesla.Middleware.Something
defp compile({{:__aliases__, _, _} = ast_mod, {_kind, _caller}}) do
quote do: {unquote(ast_mod), :call, [unquote(@default_opts)]}
end
# fn env -> ... end
defp compile({{:fn, _, _} = ast_fun, {_kind, _caller}}) do
quote do: {:fn, unquote(ast_fun)}
end
defp runtime(list) when is_list(list), do: Enum.map(list, &runtime/1)
defp runtime({module, opts}) when is_atom(module), do: {module, :call, [opts]}
defp runtime(fun) when is_function(fun), do: {:fn, fun}
defp runtime(module) when is_atom(module), do: {module, :call, [@default_opts]}
defp generate_http_verbs(opts) do
only = Keyword.get(opts, :only, @http_verbs)
except = Keyword.get(opts, :except, [])
docs = Keyword.get(opts, :docs, true)
for method <- @http_verbs do
for bang <- [:safe, :bang],
client <- [:client, :noclient],
opts <- [:opts, :noopts],
method in only && method not in except do
gen(method, bang, client, opts, docs)
end
end
end
defp gen(method, safe, client, opts, docs) do
quote location: :keep do
unquote(gen_doc(method, safe, client, opts, docs))
unquote(gen_spec(method, safe, client, opts))
unquote(gen_fun(method, safe, client, opts))
end
end
defp gen_doc(method, safe, :client, :opts, true) do
request = to_string(req(safe))
name = name(method, safe)
{body, body_line} =
if method in @body do
{~s|, %{name: "Jon"}|, ""}
else
{"", ~s|#{name}(client, "/users", body: %{name: "Jon"})|}
end
quote location: :keep do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase())} request.
See `#{unquote(request)}/1` or `#{unquote(request)}/2` for options definition.
#{unquote(name)}("/users"#{unquote(body)})
#{unquote(name)}("/users"#{unquote(body)}, query: [scope: "admin"])
#{unquote(name)}(client, "/users"#{unquote(body)})
#{unquote(name)}(client, "/users"#{unquote(body)}, query: [scope: "admin"])
#{unquote(body_line)}
"""
end
end
defp gen_doc(_method, _bang, _client, _opts, _) do
quote location: :keep do
@doc false
end
end
defp gen_spec(method, safe, client, opts) do
quote location: :keep do
@spec unquote(name(method, safe))(unquote_splicing(types(method, client, opts))) ::
unquote(type(safe))
end
end
defp gen_fun(method, safe, client, opts) do
quote location: :keep do
def unquote(name(method, safe))(unquote_splicing(inputs(method, client, opts))) do
unquote(req(safe))(unquote_splicing(outputs(method, client, opts)))
end
end
|> gen_guards(opts)
end
defp gen_guards({:def, _, [head, [do: body]]}, :opts) do
quote do
def unquote(head) when is_list(opts), do: unquote(body)
end
end
defp gen_guards(def, _opts), do: def
defp name(method, :safe), do: method
defp name(method, :bang), do: String.to_atom("#{method}!")
defp req(:safe), do: :request
defp req(:bang), do: :request!
defp types(method, client, opts), do: type(client) ++ type(:url) ++ type(method) ++ type(opts)
defp type(:safe), do: quote(do: Tesla.Env.result())
defp type(:bang), do: quote(do: Tesla.Env.t() | no_return)
defp type(:client), do: [quote(do: Tesla.Env.client())]
defp type(:noclient), do: []
defp type(:opts), do: [quote(do: [option])]
defp type(:noopts), do: []
defp type(:url), do: [quote(do: Tesla.Env.url())]
defp type(method) when method in @body, do: [quote(do: Tesla.Env.body())]
defp type(_method), do: []
defp inputs(method, client, opts),
do: input(client) ++ input(:url) ++ input(method) ++ input(opts)
defp input(:client), do: [quote(do: %Tesla.Client{} = client)]
defp input(:noclient), do: []
defp input(:opts), do: [quote(do: opts)]
defp input(:noopts), do: []
defp input(:url), do: [quote(do: url)]
defp input(method) when method in @body, do: [quote(do: body)]
defp input(_method), do: []
defp outputs(method, client, opts), do: output(client) ++ [output(output(method), opts)]
defp output(:client), do: [quote(do: client)]
defp output(:noclient), do: []
defp output(m) when m in @body, do: quote(do: [method: unquote(m), url: url, body: body])
defp output(m), do: quote(do: [method: unquote(m), url: url])
defp output(prev, :opts), do: quote(do: unquote(prev) ++ opts)
defp output(prev, :noopts), do: prev
end