Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F113082
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 2bbdfe1..b1aa64c 100644
--- a/lib/tesla.ex
+++ b/lib/tesla.ex
@@ -1,233 +1,236 @@
defmodule Tesla.Error do
defexception message: "", reason: nil
end
defmodule Tesla.Env do
@type client :: Tesla.Client.t() | (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 stack :: [{atom, atom, any} | {atom, atom} | {:fn, (t -> t)} | {:fn, (t, stack -> t)}]
@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__{
fun: (Tesla.Env.t(), Tesla.Env.stack() -> Tesla.Env.t()) | nil,
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) :: Tesla.Env.t()
end
defmodule Tesla.Adapter do
@callback call(env :: Tesla.Env.t(), options :: any) :: Tesla.Env.t()
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, %{fun: fun, pre: pre, post: post} = client, options) do
env = struct(Env, options ++ [__module__: module, __client__: client])
stack = pre ++ wrapfun(fun) ++ module.__middleware__ ++ post ++ [effective_adapter(module)]
run(env, stack)
end
defp wrapfun(nil), do: []
defp wrapfun(fun), do: [{:fn, fun}]
@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")
```
"""
defmacro build_client(pre, post \\ []) do
quote do
require Tesla.Builder
Tesla.Builder.client(unquote(pre), unquote(post))
end
end
def build_adapter(fun) do
%Tesla.Client{post: [{:fn, fn env, _next -> fun.(env) 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/lib/tesla/middleware/json.ex b/lib/tesla/middleware/json.ex
index f844d63..9920ea7 100644
--- a/lib/tesla/middleware/json.ex
+++ b/lib/tesla/middleware/json.ex
@@ -1,150 +1,155 @@
defmodule Tesla.Middleware.JSON do
@behaviour Tesla.Middleware
@moduledoc """
Encode requests and decode responses as JSON.
This middleware requires [poison](https://hex.pm/packages/poison) (or other engine) as dependency.
Remember to add `{:poison, ">= 1.0"}` to dependencies (and `:poison` to applications in `mix.exs`)
Also, you need to recompile tesla after adding `:poison` dependency:
```
mix deps.clean tesla
mix deps.compile tesla
```
### Example usage
```
defmodule MyClient do
use Tesla
plug Tesla.Middleware.JSON # use poison engine
# or
plug Tesla.Middleware.JSON, engine: JSX, engine_opts: [strict: [:comments]]
# or
plug Tesla.Middleware.JSON, decode: &JSX.decode/1, encode: &JSX.encode/1
end
```
### Options
- `:decode` - decoding function
- `:encode` - encoding function
- `:engine` - encode/decode engine, e.g `Poison` or `JSX` (defaults to Poison)
- `:engine_opts` - optional engine options
- `:decode_content_types` - list of additional decodable content-types
"""
# NOTE: text/javascript added to support Facebook Graph API.
# see https://github.com/teamon/tesla/pull/13
@default_content_types ["application/json", "text/javascript"]
@default_engine Poison
def call(env, next, opts) do
opts = opts || []
- env
- |> encode(opts)
- |> Tesla.run(next)
- |> decode(opts)
+ with {:ok, env} <- encode(env, opts),
+ {:ok, env} <- Tesla.run(env, next) do
+ decode(env, opts)
+ end
end
@doc """
Encode request body as JSON. Used by `Tesla.Middleware.EncodeJson`
"""
def encode(env, opts) do
- if encodable?(env) do
- env
- |> Map.update!(:body, &encode_body(&1, opts))
- |> Tesla.put_headers([{"content-type", "application/json"}])
+ with true <- encodable?(env),
+ {:ok, body} <- encode_body(env.body, opts) do
+ {:ok,
+ env
+ |> Tesla.put_body(body)
+ |> Tesla.put_headers([{"content-type", "application/json"}])}
else
- env
+ false -> {:ok, env}
+ error -> error
end
end
- defp encode_body(%Stream{} = body, opts), do: encode_stream(body, opts)
- defp encode_body(body, opts) when is_function(body), do: encode_stream(body, opts)
+ defp encode_body(%Stream{} = body, opts), do: {:ok, encode_stream(body, opts)}
+ defp encode_body(body, opts) when is_function(body), do: {:ok, encode_stream(body, opts)}
defp encode_body(body, opts), do: process(body, :encode, opts)
defp encode_stream(body, opts) do
- Stream.map(body, fn item -> encode_body(item, opts) <> "\n" end)
+ Stream.map(body, fn item ->
+ {:ok, body} = encode_body(item, opts)
+ body <> "\n"
+ end)
end
defp encodable?(%{body: nil}), do: false
defp encodable?(%{body: body}) when is_binary(body), do: false
defp encodable?(%{body: %Tesla.Multipart{}}), do: false
defp encodable?(_), do: true
@doc """
Decode response body as JSON. Used by `Tesla.Middleware.DecodeJson`
"""
def decode(env, opts) do
- if decodable?(env, opts) do
- Map.update!(env, :body, &process(&1, :decode, opts))
+ with true <- decodable?(env, opts),
+ {:ok, body} <- decode_body(env.body, opts) do
+ {:ok, %{env | body: body}}
else
- env
+ false -> {:ok, env}
+ error -> error
end
end
+ defp decode_body(body, opts), do: process(body, :decode, opts)
+
defp decodable?(env, opts), do: decodable_body?(env) && decodable_content_type?(env, opts)
defp decodable_body?(env) do
(is_binary(env.body) && env.body != "") || (is_list(env.body) && env.body != [])
end
defp decodable_content_type?(env, opts) do
case Tesla.get_header(env, "content-type") do
nil -> false
content_type -> Enum.any?(content_types(opts), &String.starts_with?(content_type, &1))
end
end
defp content_types(opts),
do: @default_content_types ++ Keyword.get(opts, :decode_content_types, [])
defp process(data, op, opts) do
- with {:ok, value} <- do_process(data, op, opts) do
- value
- else
- {:error, reason} ->
- raise %Tesla.Error{message: "JSON #{op} error: #{inspect(reason)}", reason: reason}
-
- {:error, msg, position} ->
- reason = {msg, position}
- raise %Tesla.Error{message: "JSON #{op} error: #{inspect(reason)}", reason: reason}
+ case do_process(data, op, opts) do
+ {:ok, data} -> {:ok, data}
+ {:error, reason} -> {:error, {__MODULE__, op, reason}}
+ {:error, reason, _pos} -> {:error, {__MODULE__, op, reason}}
end
end
defp do_process(data, op, opts) do
# :encode/:decode
if fun = opts[op] do
fun.(data)
else
engine = Keyword.get(opts, :engine, @default_engine)
opts = Keyword.get(opts, :engine_opts, [])
apply(engine, op, [data, opts])
end
end
end
defmodule Tesla.Middleware.DecodeJson do
def call(env, next, opts) do
opts = opts || []
- env
- |> Tesla.run(next)
- |> Tesla.Middleware.JSON.decode(opts)
+ with {:ok, env} <- Tesla.run(env, next) do
+ Tesla.Middleware.JSON.decode(env, opts)
+ end
end
end
defmodule Tesla.Middleware.EncodeJson do
def call(env, next, opts) do
opts = opts || []
- env
- |> Tesla.Middleware.JSON.encode(opts)
- |> Tesla.run(next)
+ with {:ok, env} <- Tesla.Middleware.JSON.encode(env, opts) do
+ Tesla.run(env, next)
+ end
end
end
diff --git a/test/tesla/middleware/json_test.exs b/test/tesla/middleware/json_test.exs
index eff5aa0..1bd92b7 100644
--- a/test/tesla/middleware/json_test.exs
+++ b/test/tesla/middleware/json_test.exs
@@ -1,189 +1,199 @@
defmodule Tesla.Middleware.JsonTest do
use ExUnit.Case
defmodule Client do
use Tesla
plug Tesla.Middleware.JSON
adapter fn env ->
{status, headers, body} =
case env.url do
"/decode" ->
{200, [{"content-type", "application/json"}], "{\"value\": 123}"}
"/encode" ->
{200, [{"content-type", "application/json"}],
env.body |> String.replace("foo", "baz")}
"/empty" ->
{200, [{"content-type", "application/json"}], nil}
"/empty-string" ->
{200, [{"content-type", "application/json"}], ""}
"/invalid-content-type" ->
{200, [{"content-type", "text/plain"}], "hello"}
"/invalid-json-format" ->
{200, [{"content-type", "application/json"}], "{\"foo\": bar}"}
"/invalid-json-encoding" ->
{200, [{"content-type", "application/json"}],
<<123, 34, 102, 111, 111, 34, 58, 32, 34, 98, 225, 114, 34, 125>>}
"/facebook" ->
{200, [{"content-type", "text/javascript"}], "{\"friends\": 1000000}"}
"/raw" ->
{200, [], env.body}
"/stream" ->
list = env.body |> Enum.to_list() |> Enum.join("---")
{200, [], list}
end
- %{env | status: status, headers: headers, body: body}
+ {:ok, %{env | status: status, headers: headers, body: body}}
end
end
test "decode JSON body" do
- assert Client.get("/decode").body == %{"value" => 123}
+ assert {:ok, env} = Client.get("/decode")
+ assert env.body == %{"value" => 123}
end
test "do not decode empty body" do
- assert Client.get("/empty").body == nil
+ assert {:ok, env} = Client.get("/empty")
+ assert env.body == nil
end
test "do not decode empty string body" do
- assert Client.get("/empty-string").body == ""
+ assert {:ok, env} = Client.get("/empty-string")
+ assert env.body == ""
end
test "decode only if Content-Type is application/json or test/json" do
- assert Client.get("/invalid-content-type").body == "hello"
+ assert {:ok, env} = Client.get("/invalid-content-type")
+ assert env.body == "hello"
end
test "encode body as JSON" do
- assert Client.post("/encode", %{"foo" => "bar"}).body == %{"baz" => "bar"}
+ assert {:ok, env} = Client.post("/encode", %{"foo" => "bar"})
+ assert env.body == %{"baz" => "bar"}
end
test "do not encode nil body" do
- assert Client.post("/raw", nil).body == nil
+ assert {:ok, env} = Client.post("/raw", nil)
+ assert env.body == nil
end
test "do not encode binary body" do
- assert Client.post("/raw", "raw-string").body == "raw-string"
+ assert {:ok, env} = Client.post("/raw", "raw-string")
+ assert env.body == "raw-string"
+ end
+
+ test "return error on encoding error" do
+ assert {:error, {Tesla.Middleware.JSON, :encode, _}} = Client.post("/encode", %{pid: self()})
end
test "decode if Content-Type is text/javascript" do
- assert Client.get("/facebook").body == %{"friends" => 1_000_000}
+ assert {:ok, env} = Client.get("/facebook")
+ assert env.body == %{"friends" => 1_000_000}
end
test "post json stream" do
stream = Stream.map(1..3, fn i -> %{id: i} end)
- assert env = Client.post("/stream", stream)
+ assert {:ok, env} = Client.post("/stream", stream)
assert env.body == ~s|{"id":1}\n---{"id":2}\n---{"id":3}\n|
end
- test "raise error when decoding invalid json format" do
- assert_raise Tesla.Error, ~r/JSON decode error:/, fn ->
- Client.get("/invalid-json-format")
- end
+ test "return error when decoding invalid json format" do
+ assert {:error, {Tesla.Middleware.JSON, :decode, _}} = Client.get("/invalid-json-format")
end
test "raise error when decoding non-utf8 json" do
- assert_raise Tesla.Error, ~r/JSON decode error:/, fn ->
- Client.get("/invalid-json-encoding")
- end
+ assert {:error, {Tesla.Middleware.JSON, :decode, _}} = Client.get("/invalid-json-encoding")
end
defmodule CustomClient do
use Tesla
plug Tesla.Middleware.DecodeJson, engine: Poison, engine_opts: [keys: :atoms]
adapter fn env ->
{status, headers, body} =
case env.url do
"/decode" ->
{200, [{"content-type", "application/json"}], "{\"value\": 123}"}
end
- %{env | status: status, headers: headers, body: body}
+ {:ok, %{env | status: status, headers: headers, body: body}}
end
end
test "decode with custom engine options" do
- assert CustomClient.get("/decode").body == %{value: 123}
+ assert {:ok, env} = CustomClient.get("/decode")
+ assert env.body == %{value: 123}
end
defmodule CustomContentTypeClient do
use Tesla
plug Tesla.Middleware.JSON, decode_content_types: ["application/x-custom-json"]
adapter fn env ->
{status, headers, body} =
case env.url do
"/decode" ->
{200, [{"content-type", "application/x-custom-json"}], "{\"value\": 123}"}
end
- %{env | status: status, headers: headers, body: body}
+ {:ok, %{env | status: status, headers: headers, body: body}}
end
end
test "decode if Content-Type specified in :decode_content_types" do
- alias CustomContentTypeClient, as: CCTClient
- assert CCTClient.get("/decode").body == %{"value" => 123}
+ assert {:ok, env} = CustomContentTypeClient.get("/decode")
+ assert env.body == %{"value" => 123}
end
defmodule EncodeDecodeJsonClient do
use Tesla
plug Tesla.Middleware.DecodeJson
plug Tesla.Middleware.EncodeJson
adapter fn env ->
{status, headers, body} =
case env.url do
"/foo2baz" ->
{200, [{"content-type", "application/json"}],
env.body |> String.replace("foo", "baz")}
end
- %{env | status: status, headers: headers, body: body}
+ {:ok, %{env | status: status, headers: headers, body: body}}
end
end
test "EncodeJson / DecodeJson work without options" do
- alias EncodeDecodeJsonClient, as: EDJClient
- assert EDJClient.post("/foo2baz", %{"foo" => "bar"}).body == %{"baz" => "bar"}
+ assert {:ok, env} = EncodeDecodeJsonClient.post("/foo2baz", %{"foo" => "bar"})
+ assert env.body == %{"baz" => "bar"}
end
defmodule MultipartClient do
use Tesla
plug Tesla.Middleware.JSON
adapter fn %{url: url, body: %Tesla.Multipart{}} = env ->
{status, headers, body} =
case url do
"/upload" ->
{200, [{"content-type", "application/json"}], "{\"status\": \"ok\"}"}
end
- %{env | status: status, headers: headers, body: body}
+ {:ok, %{env | status: status, headers: headers, body: body}}
end
end
test "skips encoding multipart bodies" do
alias Tesla.Multipart
mp =
Multipart.new()
|> Multipart.add_field("param", "foo")
- assert MultipartClient.post("/upload", mp).body == %{"status" => "ok"}
+ assert {:ok, env} = MultipartClient.post("/upload", mp)
+ assert env.body == %{"status" => "ok"}
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 24, 8:22 AM (22 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39389
Default Alt Text
(19 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment