Packages
tesla
0.5.1
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.ex
defmodule Tesla.Error do
defexception message: "", reason: nil
end
defmodule Tesla.Env do
@type client :: (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}]
@type t :: %__MODULE__{
method: method,
query: query,
url: url,
headers: headers,
body: body,
status: status
}
defstruct method: nil,
url: "",
query: [],
headers: %{},
body: nil,
status: nil,
opts: []
end
defmodule Tesla.Builder do
defmacro __using__(_opts) do
quote do
Module.register_attribute(__MODULE__, :__middleware__, accumulate: true)
Module.register_attribute(__MODULE__, :__adapter__, [])
@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:
iex> ExampleApi.request(method: :get, url: "/users/path")
You can also use shortcut methods like:
iex> ExampleApi.get("/users/1")
or
iex> myclient |> ExampleApi.post("/users", %{name: "Jon"})
"""
@spec request(Tesla.Env.client, [option]) :: Tesla.Env.t
def request(client, options) do
Tesla.perform_request(__MODULE__, client, options)
end
@doc """
Perform a request. See `request/2` for available options.
"""
@spec request([option]) :: Tesla.Env.t
def request(options) do
Tesla.perform_request(__MODULE__, options)
end
unquote(generate_api(:head))
unquote(generate_api(:get))
unquote(generate_api(:delete))
unquote(generate_api(:trace))
unquote(generate_api(:options))
unquote(generate_api(:post))
unquote(generate_api(:put))
unquote(generate_api(:patch))
require Tesla.Builder
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
middleware = Tesla.alias(middleware)
quote do: @__middleware__ {unquote(middleware), unquote(opts)}
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({:fn, _, _} = adapter) do
adapter = Macro.escape(adapter)
quote do: @__adapter__ unquote(adapter)
end
defmacro adapter(adapter, opts \\ nil) do
adapter = Tesla.alias(adapter)
quote do: @__adapter__ {unquote(adapter), unquote(opts)}
end
defp generate_api(method) when method in [:post, :put, :patch] do
quote do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase)} request.
See `request/1` or `request/2` for options definition.
Example
iex> 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
def unquote(method)(client, url, body, options) when is_function(client) do
request(client, [method: unquote(method), url: url, body: body] ++ options)
end
@doc """
Perform a #{unquote(method |> to_string |> String.upcase)} request.
See `request/1` or `request/2` for options definition.
Example
iex> myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
iex> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
"""
@spec unquote(method)(Tesla.Env.client, Tesla.Env.url, Tesla.Env.body) :: Tesla.Env.t
def unquote(method)(client, url, body) when is_function(client) do
request(client, method: unquote(method), url: url, body: body)
end
@spec unquote(method)(Tesla.Env.url, Tesla.Env.body, [option]) :: Tesla.Env.t
def unquote(method)(url, body, options) do
request([method: unquote(method), url: url, body: body] ++ options)
end
@doc """
Perform a #{unquote(method |> to_string |> String.upcase)} request.
See `request/1` or `request/2` for options definition.
Example
iex> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
"""
@spec unquote(method)(Tesla.Env.url, Tesla.Env.body) :: Tesla.Env.t
def unquote(method)(url, body) do
request(method: unquote(method), url: url, body: body)
end
end
end
defp generate_api(method) when method in [:head, :get, :delete, :trace, :options] do
quote do
@doc """
Perform a #{unquote(method |> to_string |> String.upcase)} request.
See `request/1` or `request/2` for options definition.
Example
iex> myclient |> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
"""
@spec unquote(method)(Tesla.Env.client, Tesla.Env.url, [option]) :: Tesla.Env.t
def unquote(method)(client, url, options) when is_function(client) do
request(client, [method: unquote(method), url: url] ++ options)
end
@doc """
Perform a #{unquote(method |> to_string |> String.upcase)} request.
See `request/1` or `request/2` for options definition.
Example
iex> myclient |> ExampleApi.#{unquote(method)}("/users")
iex> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
"""
@spec unquote(method)(Tesla.Env.client, Tesla.Env.url) :: Tesla.Env.t
def unquote(method)(client, url) when is_function(client) do
request(client, method: unquote(method), url: url)
end
@spec unquote(method)(Tesla.Env.url, [option]) :: Tesla.Env.t
def unquote(method)(url, options) do
request([method: unquote(method), url: url] ++ options)
end
@doc """
Perform a #{unquote(method |> to_string |> String.upcase)} request.
See `request/1` or `request/2` for options definition.
Example
iex> ExampleApi.#{unquote(method)}("/users")
"""
@spec unquote(method)(Tesla.Env.url) :: Tesla.Env.t
def unquote(method)(url) do
request(method: unquote(method), url: url)
end
end
end
defmacro __before_compile__(env) do
adapter = Module.get_attribute(env.module, :__adapter__) || quote(do: Tesla.default_adapter)
middleware = Module.get_attribute(env.module, :__middleware__) |> Enum.reverse
quote do
def __middleware__, do: unquote(middleware)
def __adapter__, do: unquote(adapter)
end
end
end
defmodule Tesla do
use Tesla.Builder
@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
end
end
@aliases [
httpc: Tesla.Adapter.Httpc,
hackney: Tesla.Adapter.Hackney,
ibrowse: Tesla.Adapter.Ibrowse,
base_url: Tesla.Middleware.BaseUrl,
headers: Tesla.Middleware.Headers,
query: Tesla.Middleware.Query,
decode_rels: Tesla.Middleware.DecodeRels,
json: Tesla.Middleware.JSON,
logger: Tesla.Middleware.Logger,
debug_logger: Tesla.Middleware.DebugLogger
]
def alias(key) when is_atom(key), do: Keyword.get(@aliases, key, key)
def alias(key), do: key
def perform_request(module, client, options), do: do_request(module, [client], options)
def perform_request(module, options), do: do_request(module, [], options)
defp do_request(module, clients, options) do
stack = prepare(module, clients ++ module.__middleware__ ++ default_middleware ++ [module.__adapter__])
env = struct(Tesla.Env, options)
run(env, stack)
end
def prepare(module, stack) do
Enum.map stack, fn
{name, opts} -> prepare_module(module, name, opts)
name when is_atom(name) -> prepare_module(module, name, nil)
fun when is_function(fun) -> {:fn, fun}
end
end
defp prepare_module(module, name, opts) do
case Atom.to_char_list(name) do
~c"Elixir." ++ _ -> {name, :call, [opts]}
_ -> {module, name}
end
end
# empty stack case is useful for reusing/testing middlewares (just pass [] as next)
def run(env, []), do: 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}]), do: apply(m, 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} | rest]), do: apply(m, f, [env, rest])
def run(env, [{m,f,a} | rest]), do: apply(m, f, [env, rest | a])
def default_adapter do
adapter = Application.get_env(:tesla, :adapter, :httpc) |> Tesla.alias
{adapter, []}
end
def default_middleware do
[{Tesla.Middleware.Normalize, nil}]
end
@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}}
])
end
end
client = ExampleAPI.new(token: "abc")
client |> ExampleAPI.get("/me")
```
"""
defmacro build_client(stack) do
quote do
fn env,next -> Tesla.run(env, Tesla.prepare(__MODULE__, unquote(stack)) ++ next) end
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