Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F113562
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/tesla.ex b/lib/tesla.ex
index cc76382..81f97b8 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,232 +1,241 @@
defmodule Tesla.Error do
defexception message: "", reason: nil
end
defmodule Tesla.Env do
@type client :: Tesla.Client.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} | {atom, atom} | {:fn, (t -> t)} | {:fn, (t, stack -> t)}]
+ @type result :: {:ok, t()} | {:error, any}
@type t :: %__MODULE__{
method: method,
query: query,
url: url,
headers: headers,
body: body,
status: status,
opts: opts,
__module__: atom,
__client__: client
}
defstruct method: nil,
url: "",
query: [],
headers: [],
body: nil,
status: nil,
opts: [],
__module__: nil,
__client__: nil
end
defmodule Tesla.Client do
@type t :: %__MODULE__{
pre: Tesla.Env.stack(),
post: Tesla.Env.stack()
}
defstruct fun: nil,
pre: [],
post: []
end
defmodule Tesla.Middleware do
@callback call(env :: Tesla.Env.t(), next :: Tesla.Env.stack(), options :: any) ::
- {:ok, Tesla.Env.t()} | {:error, any}
+ Tesla.Env.result()
end
defmodule Tesla.Adapter do
- @callback call(env :: Tesla.Env.t(), options :: any) :: {:ok, Tesla.Env.t()} | {:error, any}
+ @callback call(env :: Tesla.Env.t(), options :: any) :: Tesla.Env.result()
def opts(defaults \\ [], env, opts) do
defaults
|> Keyword.merge(opts || [])
|> Keyword.merge(env.opts[:adapter] || [])
end
end
defmodule Tesla do
use Tesla.Builder
alias Tesla.Env
require Tesla.Adapter.Httpc
@default_adapter Tesla.Adapter.Httpc
@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, unquote(opts)
end
end
@doc false
def execute(module, %{pre: pre, post: post} = client, options) do
env = struct(Env, options ++ [__module__: module, __client__: client])
stack = pre ++ module.__middleware__ ++ post ++ [effective_adapter(module)]
run(env, stack)
end
+ @doc false
+ def execute!(module, client, options) do
+ case execute(module, client, options) do
+ {:ok, env} -> env
+ error -> raise error
+ end
+ end
+
@doc false
def effective_adapter(module) do
with nil <- adapter_per_module_from_config(module),
nil <- adapter_per_module(module),
nil <- adapter_from_config() do
adapter_default()
end
end
defp adapter_per_module_from_config(module) do
case Application.get_env(:tesla, module, [])[:adapter] do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_per_module(module) do
module.__adapter__
end
defp adapter_from_config do
case Application.get_env(:tesla, :adapter) do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_default do
{@default_adapter, :call, [[]]}
end
def run_default_adapter(env, opts \\ []) do
apply(@default_adapter, :call, [env, opts])
end
# empty stack case is useful for reusing/testing middlewares (just pass [] as next)
def run(env, []), do: {:ok, 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, 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, a} | rest]), do: apply(m, f, [env, rest | a])
# useful helper fuctions
def put_opt(env, key, value) do
Map.update!(env, :opts, &Keyword.put(&1, key, value))
end
@spec get_header(Env.t(), binary) :: binary | nil
def get_header(%Env{headers: headers}, key) do
case List.keyfind(headers, key, 0) do
{_, value} -> value
_ -> nil
end
end
@spec get_headers(Env.t(), binary) :: [binary]
def get_headers(%Env{headers: headers}, key) do
for {k, v} <- headers, k == key, do: v
end
@spec put_header(Env.t(), binary, binary) :: Env.t()
def put_header(%Env{} = env, key, value) do
headers = List.keystore(env.headers, key, 0, {key, value})
%{env | headers: headers}
end
@spec put_headers(Env.t(), [{binary, binary}]) :: Env.t()
def put_headers(%Env{} = env, list) when is_list(list) do
%{env | headers: env.headers ++ list}
end
@spec delete_header(Env.t(), binary) :: Env.t()
def delete_header(%Env{} = env, key) do
headers = for {k, v} <- env.headers, k != key, do: {k, v}
%{env | headers: headers}
end
@spec put_body(Env.t(), Env.body()) :: Env.t()
def put_body(%Env{} = env, body), do: %{env | body: body}
@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")
```
"""
def build_client(pre, post \\ []), do: Tesla.Builder.client(pre, post)
def build_adapter(fun), do: Tesla.Builder.client([], [fn env, _next -> fun.(env) 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
diff --git a/lib/tesla/builder.ex b/lib/tesla/builder.ex
index b3fd67f..32e9c0d 100644
--- a/lib/tesla/builder.ex
+++ b/lib/tesla/builder.ex
@@ -1,367 +1,325 @@
defmodule Tesla.Builder do
@http_verbs ~w(head get delete trace options post put patch)a
+ @body ~w(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()
+ @spec request(Tesla.Env.client(), [option]) :: Tesla.Env.result()
else
@doc false
end
- def request(%Tesla.Client{} = client, options) do
+ def request(%Tesla.Client{} = client \\ %Tesla.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)
+ 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
```ex
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
```ex
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
- 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
- Tesla.Migration.client_function!()
- 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
- Tesla.Migration.client_function!()
- 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
- Tesla.Migration.client_function!()
- 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
- Tesla.Migration.client_function!()
- 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__)
|> compile()
middleware =
env.module
|> Module.get_attribute(:__middleware__)
|> Enum.reverse()
|> compile()
- quote do
+ quote location: :keep do
def __middleware__, do: unquote(middleware)
def __adapter__, do: unquote(adapter)
end
end
def client(pre, post), do: %Tesla.Client{pre: runtime(pre), post: runtime(post)}
@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
Tesla.Migration.breaking_headers_map!(ast_mod, ast_opts, caller)
quote do: {unquote(ast_mod), :call, [unquote(ast_opts)]}
end
# :local_middleware, opts
defp compile({{name, _opts}, {kind, caller}}) when is_atom(name) do
Tesla.Migration.breaking_alias!(kind, name, caller)
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
# :local_middleware
defp compile({name, {kind, caller}}) when is_atom(name) do
Tesla.Migration.breaking_alias!(kind, name, caller)
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_fun(method, safe, client, opts))
+ unquote(gen_deprecated(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 = if method in @body, do: ~s|, %{name: "Jon"}|, else: ""
+
+ 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"])
+ """
+ @spec unquote(name)(unquote_splicing(input_types(method, :client, :opts))) ::
+ unquote(output_type(safe))
+ end
+ end
+
+ defp gen_doc(method, safe, client, opts, true) do
+ name = name(method, safe)
+
+ quote location: :keep do
+ @doc false
+ @spec unquote(name)(unquote_splicing(input_types(method, client, opts))) ::
+ unquote(output_type(safe))
+ end
+ end
+
+ defp gen_doc(_method, _bang, _client, _opts, false) do
+ quote location: :keep do
+ @doc false
+ end
+ end
+
+ defp input_types(method, client, opts) do
+ type(client) ++ type(:url) ++ type(method) ++ type(opts)
+ end
+
+ 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 output_type(:safe), do: quote(do: Tesla.Env.result())
+ defp output_type(:bang), do: quote(do: Tesla.Env.t() | no_return)
+
+ 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 gen_deprecated(method, safe, :client, opts) do
+ inputs = [quote(do: client) | inputs(method, :noclient, opts)]
+
+ quote location: :keep do
+ def unquote(name(method, safe))(unquote_splicing(inputs)) when is_function(client) do
+ Tesla.Migration.client_function!()
+ end
+ end
+ end
+
+ defp gen_deprecated(_method, _safe, _, _opts), do: nil
+
+ 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 inputs(method, client, opts) do
+ input(client) ++ input(:url) ++ input(method) ++ input(opts)
+ end
+
+ 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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 25, 9:31 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39757
Default Alt Text
(22 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment