Page MenuHomePhorge

No OneTemporary

Size
19 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Sun, Nov 24, 8:22 AM (20 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39389
Default Alt Text
(19 KB)

Event Timeline