Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F113119
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/tesla.ex b/lib/tesla.ex
index 50f20c2..f3fbee3 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,408 +1,408 @@
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 __module__ :: atom
+ @type __client__ :: function
@type stack :: [{atom, atom, any}]
@type t :: %__MODULE__{
method: method,
query: query,
url: url,
headers: headers,
body: body,
status: status,
opts: opts,
- __module__: __module__
+ __module__: __module__,
+ __client__: __client__
}
defstruct method: nil,
url: "",
query: [],
headers: %{},
body: nil,
status: nil,
opts: [],
- __module__: nil
+ __module__: nil,
+ __client__: nil
end
defmodule Tesla.Builder do
@http_verbs ~w(head get delete trace options post put patch)a
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_http_verbs())
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
opts = Macro.escape(opts)
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_http_verbs do
Enum.map @http_verbs, &generate_api/1
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, module: __MODULE__
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 ++ [__module__: module])
+ def perform_request(module, client \\ nil, options) do
+ stack = prepare(module, List.wrap(client) ++ module.__middleware__ ++ default_middleware ++ [module.__adapter__])
+ env = struct(Tesla.Env, options ++ [__module__: module, __client__: client])
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])
# useful helper fuctions
def put_opt(env, key, value) do
Map.update!(env, :opts, &Keyword.put(&1, key, value))
end
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
diff --git a/test/tesla_test.exs b/test/tesla_test.exs
index c2b2130..5119e70 100644
--- a/test/tesla_test.exs
+++ b/test/tesla_test.exs
@@ -1,245 +1,247 @@
defmodule TeslaTest do
use ExUnit.Case
describe "Macros" do
defmodule Mc do
defmodule Basic.Middleware.Plus do
def call(env, next, opts) do
env
|> Map.put(:url, "#{env.url}/#{opts[:with]}")
|> Tesla.run(next)
end
end
defmodule Basic.Middleware.Plus1 do
def call(env, next, _opts) do
env
|> Map.put(:url, "#{env.url}/1")
|> Tesla.run(next)
end
end
defmodule Basic do
use Tesla
plug Basic.Middleware.Plus, with: "engine"
plug Basic.Middleware.Plus1
plug :some_function
adapter :some_adapter, some: "opts"
end
defmodule Empty do
use Tesla
end
defmodule Fun do
use Tesla
adapter fn env ->
Map.put(env, :url, "#{env.url}/anon")
end
end
end
test "middleware list" do
assert Mc.Basic.__middleware__ == [
{Mc.Basic.Middleware.Plus, [with: "engine"]},
{Mc.Basic.Middleware.Plus1, nil},
{:some_function, nil}
]
assert Mc.Basic.__adapter__ == {:some_adapter, some: "opts"}
end
test "defauilt adapter" do
assert Mc.Empty.__adapter__ == Tesla.default_adapter
end
test "adapter as function" do
assert is_function(Mc.Fun.__adapter__)
end
end
describe "Middleware" do
defmodule M do
defmodule Mid do
def call(env, next, opts) do
env
|> Map.update!(:url, fn url -> url <> "/module/" <> opts[:before] end)
|> Tesla.run(next)
|> Map.update!(:url, fn url -> url <> "/module/" <> opts[:after] end)
end
end
defmodule Client do
use Tesla
plug Mid, before: "A", after: "B"
plug :local_middleware
adapter fn env -> env end
def local_middleware(env, next) do
env
|> Map.put(:url, env.url <> "/local")
|> Tesla.run(next)
end
end
end
test "execute middleware top down" do
response = M.Client.request(url: "one")
assert response.url == "one/module/A/local/module/B"
end
end
describe "Adapters" do
defmodule A do
defmodule Adapter do
def call(env, opts \\ []) do
Map.put(env, :url, env.url <> "/module/" <> opts[:with])
end
end
defmodule ClientModule do
use Tesla
adapter Adapter, with: "someopt"
end
defmodule ClientLocal do
use Tesla
adapter :local_adapter
def local_adapter(env) do
Map.put(env, :url, env.url <> "/local")
end
end
defmodule ClientAnon do
use Tesla
adapter fn env ->
Map.put(env, :url, env.url <> "/anon")
end
end
end
test "execute module adapter" do
response = A.ClientModule.request(url: "test")
assert response.url == "test/module/someopt"
end
test "execute local function adapter" do
response = A.ClientLocal.request(url: "test")
assert response.url == "test/local"
end
test "execute anonymous function adapter" do
response = A.ClientAnon.request(url: "test")
assert response.url == "test/anon"
end
end
describe "request API" do
defmodule R do
defmodule Client do
use Tesla
adapter fn env ->
env
end
def new do
Tesla.build_client [
{R.Mid1, [with: "/mid1"]},
{R.Mid2, nil},
:local_middleware
]
end
def local_middleware(env, next) do
env
|> Map.put(:url, env.url <> "/local")
|> Tesla.run(next)
end
end
defmodule Mid1 do
def call(env, next, opts) do
env
|> Map.put(:url, env.url <> opts[:with])
|> Tesla.run(next)
end
end
defmodule Mid2 do
def call(env, next, _opts) do
env
|> Map.put(:url, env.url <> "/mid2")
|> Tesla.run(next)
end
end
end
test "basic request" do
response = R.Client.request(url: "/", method: :post, query: [page: 1], body: "data")
assert response.method == :post
assert response.url == "/"
assert response.query == [page: 1]
assert response.body == "data"
end
test "shortcut function" do
response = R.Client.get("/get")
assert response.method == :get
assert response.url == "/get"
end
test "request with client" do
client = fn env, next ->
env
|> Map.put(:url, "/prefix" <> env.url)
|> Tesla.run(next)
end
response = R.Client.get("/")
assert response.url == "/"
+ refute response.__client__
response = client |> R.Client.get("/")
assert response.url == "/prefix/"
+ assert response.__client__ == client
end
test "build_client helper" do
client = R.Client.new
response = client |> R.Client.get("test")
assert response.url == "test/mid1/mid2/local"
end
test "insert request middleware function at runtime" do
fun = fn env, next ->
env
|> Map.put(:url, env.url <> ".json")
|> Tesla.run(next)
end
res = fun |> R.Client.get("/foo")
assert res.url == "/foo.json"
end
test "insert response middleware function at runtime" do
fun = fn env, next ->
env
|> Tesla.run(next)
|> Map.put(:url, env.url <> ".json")
end
res = fun |> R.Client.get("/foo")
assert res.url == "/foo.json"
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 24, 9:50 AM (1 d, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39404
Default Alt Text
(19 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment