Page MenuHomePhorge

No OneTemporary

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

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)

Event Timeline