Page MenuHomePhorge

No OneTemporary

Size
10 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 50abe39..ff781ea 100644
--- a/README.md
+++ b/README.md
@@ -1,131 +1,145 @@
# 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.
## Basic usage
```ex
# Start underlying ibrowse default adapter
Tesla.start
# 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' ...}
# Example post request
response = Tesla.post("http://httpbin.org/post", "data")
```
## Creating API clients
Use `Tesla.Builder` module to create API wrappers.
For example
```ex
defmodule GitHub do
use Tesla.Builder
with Tesla.Middleware.BaseUrl, "https://api.github.com"
with Tesla.Middleware.Headers, %{'Authorization' => 'xyz'}
with Tesla.Middleware.EncodeJson
with Tesla.Middleware.DecodeJson
adapter Tesla.Adapter.Ibrowse
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")
```
## Adapters
Tesla has support for different adapters that do the actual HTTP request processing.
### ibrowse
Tesla has built-in support for [ibrowse](https://github.com/cmullaparthi/ibrowse) Erlang HTTP client.
To use it simply include `adapter Tesla.Adapter.Ibrowse` line in your API client definition.
NOTE: Remember to start ibrowse first with `Tesla.Adapter.Ibrowse.start` before executing any requests.
ibrowse is also the default adapter when using generic `Tesla.get(...)` etc. methods.
### 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
"/" -> {200, %{}, "home"}
"/about" -> {200, %{}, "about us"}
end
end
end
```
## Middleware
### Basic
- `Tesla.Middleware.BaseUrl` - set base url for all request
- `Tesla.Middleware.Headers` - set request headers
### JSON
NOTE: requires [exjsx](https://github.com/talentdeficit/exjsx) as dependency
- `Tesla.Middleware.DecodeJson` - decode response body as JSON
- `Tesla.Middleware.EncodeJson` - endode request body as JSON
If you are using different json library writing middleware should be straightforward. See [link to json.ex] for implementation.
## Dynamic middleware
All methods can take a middleware function as the first parameter.
This allow to use convinient syntax for modyfiyng the behaviour in runtime.
Consider the following case: GitHub API can be accessed using OAuth token authorization.
We can't use `with 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 dynamid middleware function:
```ex
defmodule GitHub do
# same as above
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")
```
+
+
+## Asynchronous requests
+
+If adapter supports it, you can make asynchronous requests by passing `respond_to: pid` option:
+
+```ex
+
+Tesla.get("http://example.org", respond_to: self)
+
+receive do
+ {:tesla_response, res} -> res.status # => 200
+end
+```
diff --git a/lib/adapter/ibrowse.ex b/lib/adapter/ibrowse.ex
index 033579d..56fd41d 100644
--- a/lib/adapter/ibrowse.ex
+++ b/lib/adapter/ibrowse.ex
@@ -1,17 +1,63 @@
defmodule Tesla.Adapter.Ibrowse do
def start do
:ibrowse.start
end
def call(env) do
- {:ok, status, headers, body} = :ibrowse.send_req(
+ opts = []
+
+ if target = env.opts[:respond_to] do
+
+ gatherer = spawn_link fn -> gather_response(env, target, {:ok, nil, nil, nil}) end
+
+ opts = opts ++ [stream_to: gatherer]
+ {:ibrowse_req_id, id} = send_req(env, opts)
+ {:ok, id}
+ else
+ format_response(env, send_req(env, opts))
+ end
+ end
+
+ defp format_response(env, res) do
+ {:ok, status, headers, body} = res
+
+ {status, _} = Integer.parse(to_string(status))
+ headers = Enum.into(headers, %{})
+
+ %{env | status: status,
+ headers: headers,
+ body: body}
+ end
+
+ defp send_req(env, opts) do
+ :ibrowse.send_req(
env.url |> to_char_list,
Enum.into(env.headers, []),
- env.method
+ env.method,
+ [],
+ opts
)
+ end
- %{env | status: status,
- headers: Enum.into(headers, %{}),
- body: body}
+ defp gather_response(env, target, res) do
+ {:ok, _status, _headers, _body} = res
+
+ receive do
+ {:ibrowse_async_headers, _, status, headers} ->
+ gather_response(env, target, {:ok, status, headers, _body})
+
+ {:ibrowse_async_response, _, body} ->
+ body = if _body do
+ _body <> body
+ else
+ body
+ end
+
+ gather_response(env, target, {:ok, _status, _headers, body})
+
+ {:ibrowse_async_response_end, _} ->
+ response = format_response(env, res)
+ send target, {:tesla_response, response}
+ end
end
end
diff --git a/lib/tesla.ex b/lib/tesla.ex
index 10a5f1f..95776b2 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,131 +1,149 @@
defmodule Tesla.Env do
defstruct url: "",
method: nil,
status: nil,
headers: %{},
- body: nil
+ body: nil,
+ opts: []
end
defmodule Tesla.Builder do
defmacro __using__(_what) do
method_defs = for method <- [:get, :head, :delete, :trace, :options] do
quote do
+ def unquote(method)(fun, url, opts) when is_function(fun) do
+ request(fun, unquote(method), url, nil, opts)
+ end
+
def unquote(method)(fun, url) when is_function(fun) do
- request(fun, unquote(method), url, nil)
+ request(fun, unquote(method), url, nil, [])
+ end
+
+ def unquote(method)(url, opts) do
+ request(unquote(method), url, nil, opts)
end
def unquote(method)(url) do
- request(unquote(method), url, nil)
+ request(unquote(method), url, nil, [])
end
end
end
method_defs_with_body = for method <- [:post, :put, :patch] do
quote do
+ def unquote(method)(fun, url, body, opts) when is_function(fun) do
+ request(fun, unquote(method), url, body, opts)
+ end
+
def unquote(method)(fun, url, body) when is_function(fun) do
- request(fun, unquote(method), url, body)
+ request(fun, unquote(method), url, body, [])
+ end
+
+ def unquote(method)(url, body, opts) do
+ request(unquote(method), url, body, opts)
end
def unquote(method)(url, body) do
- request(unquote(method), url, body)
+ request(unquote(method), url, body, [])
end
end
end
quote do
unquote(method_defs)
unquote(method_defs_with_body)
- defp request(fun, method, url, body) when is_function(fun) do
+ defp request(fun, method, url, body, opts) when is_function(fun) do
env = %Tesla.Env{
method: method,
url: url,
- body: body
+ body: body,
+ opts: opts
}
fun.(env, &call_middleware/1)
end
- defp request(method, url, body) do
- request(fn (env, run) -> run.(env) end, method, url, body)
+ defp request(method, url, body, opts) do
+ request(fn (env, run) -> run.(env) end, method, url, body, opts)
end
import Tesla.Builder
Module.register_attribute(__MODULE__, :middleware, accumulate: true)
@before_compile Tesla.Builder
end
end
defmacro __before_compile__(env) do
middleware = Module.get_attribute(env.module, :middleware)
reduced = Enum.reduce(middleware, (quote do: call_adapter(env)), fn {mid, args}, acc ->
quote do
unquote(mid).call(env, fn(env) -> unquote(acc) end, unquote(args))
end
end)
quote do
def call_middleware(env) do
unquote(reduced)
end
end
end
def process_adapter_response(env, res) do
case res do
{status, headers, body} ->
%{env | status: status, headers: headers, body: body}
e -> e
end
end
defmacro adapter({:fn, _, _} = ad) do # pattern match for function
quote do
def call_adapter(env) do
Tesla.Builder.process_adapter_response(env, unquote(ad).(env))
end
end
end
defmacro adapter(ad) do
quote do
def call_adapter(env) do
Tesla.Builder.process_adapter_response(env, unquote(ad).call(env))
end
end
end
defmacro with(middleware) do
quote do
@middleware {unquote(middleware), nil}
end
end
defmacro with(middleware, args) do
quote do
@middleware {unquote(middleware), unquote(args)}
end
end
end
defmodule Tesla do
use Tesla.Builder
adapter Tesla.Adapter.Ibrowse
def build_client(stack) do
fn (env, run) ->
f = Enum.reduce(stack, run, fn ({mid, args}, acc) ->
fn (env) -> mid.call(env, fn(e) -> acc.(e) end, args) end
end)
f.(env)
end
end
def start do
Tesla.Adapter.Ibrowse.start
end
end
diff --git a/test/adapter/ibrowse_test.exs b/test/adapter/ibrowse_test.exs
new file mode 100644
index 0000000..45b813d
--- /dev/null
+++ b/test/adapter/ibrowse_test.exs
@@ -0,0 +1,29 @@
+defmodule IbrowseTest do
+ use ExUnit.Case
+
+ defmodule Client do
+ use Tesla.Builder
+
+ adapter Tesla.Adapter.Ibrowse
+ end
+
+ Tesla.Adapter.Ibrowse.start
+
+ test "async requests" do
+ {:ok, id} = Client.get("http://httpbin.org/ip", respond_to: self)
+
+ assert_receive {:tesla_response, _}, 2000
+ end
+
+ test "async requests parameters" do
+ {:ok, id} = Client.get("http://httpbin.org/ip", respond_to: self)
+
+ receive do
+ {:tesla_response, res} ->
+ assert res.status == 200
+ after
+ 2000 -> raise "Timeout"
+ end
+ end
+
+end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 24, 11:36 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
234839
Default Alt Text
(10 KB)

Event Timeline