Page MenuHomePhorge

No OneTemporary

Size
13 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 6856956..4fd1ef8 100644
--- a/README.md
+++ b/README.md
@@ -1,268 +1,269 @@
# 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")
```
## 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/middleware/form_urlencoded.ex b/lib/tesla/middleware/form_urlencoded.ex
new file mode 100644
index 0000000..fbb0438
--- /dev/null
+++ b/lib/tesla/middleware/form_urlencoded.ex
@@ -0,0 +1,41 @@
+defmodule Tesla.Middleware.FormUrlencoded do
+ @doc """
+ Send request body as application/x-www-form-urlencoded
+
+ Example:
+ defmodule Myclient do
+ use Tesla
+
+ plug Tesla.Middleware.FormUrlencoded
+ end
+
+ Myclient.post("/url", %{key: :value})
+ """
+ def call(env, next, opts) do
+ opts = opts || []
+
+ env
+ |> encode(opts)
+ |> Tesla.run(next)
+ end
+
+ def encode(env, opts) do
+ if encodable?(env) do
+ env
+ |> Map.update!(:body, &encode_body(&1, opts))
+ |> Tesla.Middleware.Headers.call([], %{"content-type" => "application/x-www-form-urlencoded"})
+ else
+ env
+ end
+ end
+
+ defp encode_body(body, _opts) when is_binary(body), do: body
+
+ defp encode_body(body, _opts), do: do_process(body)
+
+ def encodable?(env), do: env.body != nil
+
+ defp do_process(data) do
+ URI.encode_query(data)
+ end
+end
diff --git a/mix.lock b/mix.lock
index aed6d5f..79f18ed 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,23 +1,23 @@
%{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
"con_cache": {:hex, :con_cache, "0.11.1", "acbe5a1d10c47faba30d9629c9121e1ef9bca0aa5eddbb86f4e777bd9f9f9b6f", [:mix], [{:exactor, "~> 2.2.0", [hex: :exactor, optional: false]}]},
- "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
+ "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
"dialyxir": {:hex, :dialyxir, "0.3.5", "eaba092549e044c76f83165978979f60110dc58dd5b92fd952bf2312f64e9b14", [:mix], []},
"earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"exactor": {:hex, :exactor, "2.2.2", "90b27d72c05614801a60f8400afd4e4346dfc33ea9beffe3b98a794891d2ff96", [:mix], []},
"excoveralls": {:hex, :excoveralls, "0.5.6", "35a903f6f78619ee7f951448dddfbef094b3a0d8581657afaf66465bc930468e", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
"exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]},
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
- "fuse": {:hex, :fuse, "2.4.0", "499ab65d3b1d291909973e1c25bdc55a17379fb26899f25eb68b6437a880de1b", [:rebar, :make], []},
+ "fuse": {:hex, :fuse, "2.4.0", "499ab65d3b1d291909973e1c25bdc55a17379fb26899f25eb68b6437a880de1b", [:make, :rebar], []},
"hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]},
"httparrot": {:hex, :httparrot, "0.5.0", "17ef9ae11ec36e8a329fba30eb7b27b84848c9997d25f1bc94f27e1784370b3f", [:mix], [{:con_cache, "~> 0.11.1", [hex: :con_cache, optional: false]}, {:cowboy, "~> 1.0.0", [hex: :cowboy, optional: false]}, {:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}]},
"ibrowse": {:hex, :ibrowse, "4.2.2", "b32b5bafcc77b7277eff030ed32e1acc3f610c64e9f6aea19822abcadf681b4b", [:rebar3], []},
"idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
"jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
"mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]},
"poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []},
"ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}}
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:make, :rebar], []}}
diff --git a/test/tesla/middleware/form_urlencoded_test.exs b/test/tesla/middleware/form_urlencoded_test.exs
new file mode 100644
index 0000000..50d0d4d
--- /dev/null
+++ b/test/tesla/middleware/form_urlencoded_test.exs
@@ -0,0 +1,35 @@
+defmodule FormUrlencodedTest do
+ use ExUnit.Case
+
+ use Tesla.Middleware.TestCase, middleware: Tesla.Middleware.FormUrlencoded
+
+ defmodule Client do
+ use Tesla
+
+ plug Tesla.Middleware.FormUrlencoded
+
+ adapter fn (env) ->
+ {status, headers, body} = case env.url do
+ "/post" ->
+ {201, %{'Content-Type' => 'text/html'}, env.body}
+ "/check_incoming_content_type" ->
+ {201, %{'Content-Type' => 'text/html'}, env.headers["content-type"]}
+ end
+
+ %{env | status: status, headers: headers, body: body}
+ end
+ end
+
+ test "encode body as application/x-www-form-urlencoded" do
+ assert URI.decode_query(Client.post("/post", %{"foo" => "%bar "}).body) == %{"foo" => "%bar "}
+ end
+
+ test "leave body alone if binary" do
+ assert Client.post("/post", "data").body == "data"
+ end
+
+ test "check header is set as application/x-www-form-urlencoded" do
+ assert Client.post("/check_incoming_content_type", %{"foo" => "%bar "}).body
+ == "application/x-www-form-urlencoded"
+ end
+end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jan 21, 3:19 PM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55724
Default Alt Text
(13 KB)

Event Timeline