Page MenuHomePhorge

No OneTemporary

Size
33 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 2c28728..75a916d 100644
--- a/README.md
+++ b/README.md
@@ -1,269 +1,299 @@
# Tesla
[![Build Status](https://travis-ci.org/teamon/tesla.svg?branch=master)](https://travis-ci.org/teamon/tesla)
[![Hex.pm](https://img.shields.io/hexpm/v/tesla.svg)](http://hex.pm/packages/tesla)
Tesla is an HTTP client losely based on [Faraday](https://github.com/lostisland/faraday).
It embraces the concept of middleware when processing the request/response cycle.
## Direct usage
```ex
# Example get request
response = Tesla.get("http://httpbin.org/ip")
response.status # => 200
response.body # => '{\n "origin": "87.205.72.203"\n}\n'
response.headers # => %{'Content-Type' => 'application/json' ...}
response = Tesla.get("http://httpbin.org/get", query: [a: 1, b: "foo"])
response.url # => "http://httpbin.org/get?a=1&b=foo"
# Example post request
response = Tesla.post("http://httpbin.org/post", "data", headers: %{"Content-Type" => "application/json"})
```
## Installation
Add `tesla` as dependency in `mix.exs`
```ex
defp deps do
[{:tesla, "~> 0.5.0"},
{:poison, ">= 1.0.0"}] # for JSON middleware
end
```
### Adapters
When using `ibrowse` or `hackney` adapters remember to alter applications list in `mix.exs`
```ex
def application do
[applications: [:ibrowse, ...], ...] # or :hackney
end
```
and add it to the dependency list
```ex
defp deps do
[{:tesla, "~> 0.5.0"},
{:ibrowse, "~> 4.2"}, # or :hackney
{:poison, ">= 1.0.0"}] # for JSON middleware
end
```
## Creating API clients
Use `Tesla` module to create API wrappers.
For example
```ex
defmodule GitHub do
use Tesla
plug Tesla.Middleware.BaseUrl, "https://api.github.com"
plug Tesla.Middleware.Headers, %{"Authorization" => "token xyz"}
plug Tesla.Middleware.JSON
adapter Tesla.Adapter.Hackney
def user_repos(login) do
get("/user/" <> login <> "/repos")
end
end
```
Then use it like this:
```ex
GitHub.get("/user/teamon/repos")
GitHub.user_repos("teamon")
```
+### Supported options
+
+Tesla.Builder allows to pass following options
+
+#### `:only` and `:except`
+
+Useful when you don't need functions for all http verbs to be generated.
+
+```ex
+ #examples
+ use Tesla, only: ~w(get post)a
+ use Tesla, only: [:delete]
+ use Tesla, except: [:delete, :options]
+```
+
+#### `:docs`
+
+You can disable docs for tesla generated functions if you don't want them to be included in your own project docs.
+
+```ex
+ defmodule MyProject.ApiModule do
+ @moduledoc "Module that does something"
+
+ use Tesla, docs: false
+
+ @doc "Function to get something from somewhere"
+ def custom_function(), do: get(...)
+ end
+```
+
## Adapters
Tesla has support for different adapters that do the actual HTTP request processing.
### [httpc](http://erlang.org/doc/man/httpc.html)
The default adapter, available in all erlang installations
### [hackney](https://github.com/benoitc/hackney)
This adapter supports real streaming body.
To use it simply include `adapter :hackney` line in your API client definition.
NOTE: Remember to include hackney in applications list.
### [ibrowse](https://github.com/cmullaparthi/ibrowse)
Tesla has built-in support for [ibrowse](https://github.com/cmullaparthi/ibrowse) Erlang HTTP client.
To use it simply include `adapter :ibrowse` line in your API client definition.
NOTE: Remember to include ibrowse in applications list.
### Test / Mock
When testing it might be useful to use simple function as adapter:
```ex
defmodule MyApi do
use Tesla
adapter fn (env) ->
case env.url do
"/" -> %{env | status: 200, body: "home"}
"/about" -> %{env | status: 200, body: "about us"}
end
end
end
```
## Middleware
### Basic
- `Tesla.Middleware.BaseUrl` - set base url for all request
- `Tesla.Middleware.Headers` - set request headers
- `Tesla.Middleware.Query` - set query parameters
- `Tesla.Middleware.DecodeRels` - decode `Link` header into `opts[:rels]` field in response
- `Tesla.Middleware.Retry` - retry few times in case of connection refused
- `Tesla.Middleware.FormUrlencoded` - urlencode POST body parameter, useful for POSTing a map/keyword list
### JSON
NOTE: requires [poison](https://hex.pm/packages/poison) (or other engine) as dependency
- `Tesla.Middleware.JSON` - encode/decode request/response bodies as JSON
If you are using different json library it can be easily configured:
```ex
plug Tesla.Middleware.JSON, engine: JSX, engine_opts: [strict: [:comments]]
# or
plug Tesla.Middleware.JSON, decode: &JSX.decode/1, encode: &JSX.encode/1
```
See [`json.ex`](https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/json.ex) for implementation details.
### Logging
- `Tesla.Middleware.Logger` - log each request in single line including method, path, status and execution time (colored)
- `Tesla.Middleware.DebugLogger` - log full request and response (incl. headers and body)
### Authentication
- `Tesla.Middleware.DigestAuth` - [Digest access authentication](https://en.wikipedia.org/wiki/Digest_access_authentication)
## Dynamic middleware
All functions can take a middleware function as the first parameter.
This allow to use convenient syntax for modifying the behaviour in runtime.
Consider the following case: GitHub API can be accessed using OAuth token authorization.
We can't use `plug Tesla.Middleware.Headers, %{"Authorization" => "token here"}` since this would be compiled only once and there is no way to insert dynamic user token.
Instead, we can use `Tesla.build_client` to create a dynamic middleware function:
```ex
defmodule GitHub do
# same as above with a slightly change to `user_repos/1`
def user_repos(client, login) do
get(client, "/user/" <> login <> "/repos")
end
def client(token) do
Tesla.build_client [
{Tesla.Middleware.Headers, %{"Authorization" => "token: " <> token }}
]
end
end
```
and then:
```ex
client = GitHub.client(user_token)
client |> GitHub.user_repos("teamon")
client |> GitHub.get("/me")
```
## Writing your own middleware
A Tesla middleware is a module with `call/3` function, that at some point calls `Tesla.run(env, next)` to process
the rest of stack
```ex
defmodule MyMiddleware do
def call(env, next, options) do
env
|> do_something_with_request
|> Tesla.run(next)
|> do_something_with_response
end
end
```
The arguments are:
- `env` - `Tesla.Env` instance
- `next` - middleware continuation stack; to be executed with `Tesla.run(env, next)`
- `options` - arguments passed during middleware configuration (`plug MyMiddleware, options`)
There is no distinction between request and response middleware, it's all about executing `Tesla.run/2` function at the correct time.
For example, z request logger middleware could be implemented like this:
```ex
defmodule Tesla.Middleware.RequestLogger do
def call(env, next, _) do
IO.inspect env # print request env
Tesla.run(env, next)
end
end
```
and response logger middleware like this:
```ex
defmodule Tesla.Middleware.ResponseLogger do
def call(env, next, _) do
res = Tesla.run(env, next)
IO.inspect res # print response env
res
end
end
```
See [`core.ex`](https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/core.ex) and [`json.ex`](https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/json.ex) for more examples.
## Streaming body
If adapter supports it, you can pass a [Stream](http://elixir-lang.org/docs/stable/elixir/Stream.html) as body, e.g.:
```ex
defmodule ES do
use Tesla.Builder
plug Tesla.Middleware.BaseUrl, "http://localhost:9200"
plug Tesla.Middleware.DecodeJson
plug Tesla.Middleware.EncodeJson
def index(records) do
stream = records |> Stream.map(fn record -> %{index: [some, data]})
post("/_bulk", stream)
end
end
```
Each piece of stream will be encoded as json and sent as a new line (conforming to json stream format)
diff --git a/lib/tesla.ex b/lib/tesla.ex
index 6a1d22a..2ac60dd 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,408 +1,456 @@
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__,
__client__: __client__
}
defstruct method: nil,
url: "",
query: [],
headers: %{},
body: nil,
status: nil,
opts: [],
__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
+ 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__, [])
- @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}
+ 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
+ @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
+ 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:
+ Examples:
- iex> ExampleApi.request(method: :get, url: "/users/path")
+ iex> ExampleApi.request(method: :get, url: "/users/path")
- You can also use shortcut methods like:
+ You can also use shortcut methods like:
- iex> ExampleApi.get("/users/1")
+ iex> ExampleApi.get("/users/1")
- or
+ or
- iex> myclient |> ExampleApi.post("/users", %{name: "Jon"})
- """
- @spec request(Tesla.Env.client, [option]) :: Tesla.Env.t
+ iex> myclient |> ExampleApi.post("/users", %{name: "Jon"})
+ """
+ @spec request(Tesla.Env.client, [option]) :: Tesla.Env.t
+ else
+ @doc false
+ end
def request(client, options) do
Tesla.perform_request(__MODULE__, client, options)
end
- @doc """
- Perform a request. See `request/2` for available options.
- """
+ 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.perform_request(__MODULE__, options)
end
- unquote(generate_http_verbs())
+ 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"
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
+ defp generate_http_verbs(opts) do
+ selected_verbs =
+ @http_verbs
+ |> Enum.reject(&(not &1 in Keyword.get(opts, :only, @http_verbs)))
+ |> Enum.reject(&(&1 in Keyword.get(opts, :except, [])))
+
+ Enum.map selected_verbs, &generate_api(&1, Keyword.get(opts, :docs, true))
end
- defp generate_api(method) when method in [:post, :put, :patch] do
+ defp generate_api(method, docs) 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
+ if unquote(docs) 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
+ else
+ @doc false
+ end
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
+ if unquote(docs) 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"})
+ 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
+ else
+ @doc false
+ end
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
+ 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) 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
+ if unquote(docs) do
+ @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
+ 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) when method in [:head, :get, :delete, :trace, :options] do
+ defp generate_api(method, docs) 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
+ if unquote(docs) 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
+ else
+ @doc false
+ end
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
+ if unquote(docs) 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")
+ iex> 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)(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
+ if unquote(docs) do
+ @spec unquote(method)(Tesla.Env.url, [option]) :: Tesla.Env.t
+ else
+ @doc false
+ end
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
+ if unquote(docs) do
+ @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
+ else
+ @doc false
+ end
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
+ defmacro __using__(opts \\ []) do
quote do
- use Tesla.Builder, module: __MODULE__
+ use Tesla.Builder, unquote(opts)
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 \\ 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 5119e70..797675c 100644
--- a/test/tesla_test.exs
+++ b/test/tesla_test.exs
@@ -1,247 +1,279 @@
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
+
+ defmodule Only do
+ use Tesla, only: [:get]
+ end
+
+ defmodule Except do
+ use Tesla.Builder, except: ~w(delete)a
+ end
+
+ defmodule Private do
+ use Tesla.Builder, private: true
+
+ adapter fn env ->
+ Map.put(env, :status, 200)
+ end
+
+ def custom_get(url), do: get(url)
+ end
end
+ @http_verbs ~w(head get delete trace options post put patch)a
+
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
+
+ test "limit generated functions (only)" do
+ functions = Mc.Only.__info__(:functions) |> Keyword.keys() |> Enum.uniq
+ assert :get in functions
+ refute Enum.any?(@http_verbs -- [:get], &(&1 in functions))
+ end
+
+ test "limit generated functions (except)" do
+ functions = Mc.Except.__info__(:functions) |> Keyword.keys() |> Enum.uniq
+ refute :delete in functions
+ assert Enum.all?(@http_verbs -- [:delete], &(&1 in functions))
+ 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

Mime Type
text/x-diff
Expires
Tue, Jan 21, 5:17 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55742
Default Alt Text
(33 KB)

Event Timeline